@morriconeluca/eslint-plugin-faf 0.3.0 → 0.5.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/main.cjs CHANGED
@@ -39,7 +39,7 @@ module.exports = __toCommonJS(main_exports);
39
39
  // package.json
40
40
  var package_default = {
41
41
  name: "@morriconeluca/eslint-plugin-faf",
42
- version: "0.3.0",
42
+ version: "0.5.0",
43
43
  description: "An ESLint plugin for FAF projects.",
44
44
  repository: {
45
45
  type: "git",
@@ -127,6 +127,7 @@ var import_path3 = __toESM(require("path"), 1);
127
127
 
128
128
  // src/_rules/_rules@shared/_utils/_utils@shared/_stores/cache/cache.store.ts
129
129
  var state = {
130
+ ancestorChainCache: /* @__PURE__ */ new Map(),
130
131
  categoryConfigCache: /* @__PURE__ */ new Map(),
131
132
  classifyCache: /* @__PURE__ */ new Map(),
132
133
  dirCache: {},
@@ -383,27 +384,139 @@ var rule = {
383
384
  var category_mutually_exclusive_rule_default = rule;
384
385
 
385
386
  // src/_rules/enforce-access-node/enforce-access-node.rule.ts
387
+ var import_path7 = __toESM(require("path"), 1);
388
+
389
+ // src/_rules/_rules@shared/_utils/_primitives/get-project-root/get-project-root.util.ts
390
+ function getProjectRoot() {
391
+ return state.projectRoot;
392
+ }
393
+
394
+ // src/_rules/_rules@shared/_utils/_systems/walk-ancestors/walk-ancestors.util.ts
386
395
  var import_path6 = __toESM(require("path"), 1);
396
+ function walkAncestors(relDir, config) {
397
+ const cached = state.ancestorChainCache.get(relDir);
398
+ if (cached !== void 0) {
399
+ return cached;
400
+ }
401
+ const result = [];
402
+ let currentDir = relDir;
403
+ while (currentDir && currentDir !== "." && currentDir !== "/") {
404
+ const isInTree = config.includes.some(
405
+ (inc) => currentDir === inc || currentDir.startsWith(inc + "/")
406
+ );
407
+ const isExcluded = config.excludes?.some(
408
+ (exc) => currentDir === exc || currentDir.startsWith(exc + "/")
409
+ ) ?? false;
410
+ if (!isInTree || isExcluded) {
411
+ break;
412
+ }
413
+ const folderName = import_path6.default.basename(currentDir);
414
+ const type = classifyFolder(currentDir, config);
415
+ const parentDir = import_path6.default.dirname(currentDir).replace(/\\/g, "/");
416
+ const parentFolderName = import_path6.default.basename(parentDir);
417
+ const parentType = parentDir === "." || parentDir === "/" ? "unknown" : classifyFolder(parentDir, config);
418
+ result.push({
419
+ dir: currentDir,
420
+ folderName,
421
+ parentDir,
422
+ parentFolderName,
423
+ parentType,
424
+ type
425
+ });
426
+ currentDir = parentDir;
427
+ }
428
+ state.ancestorChainCache.set(relDir, result);
429
+ return result;
430
+ }
431
+
432
+ // src/_rules/enforce-access-node/enforce-access-node.rule.ts
387
433
  var rule2 = {
388
434
  create(context) {
389
435
  const absPath = context.filename;
390
436
  const relPath = toRelativePath(absPath);
391
- const fileName = import_path6.default.basename(relPath);
437
+ const settings = context.settings;
438
+ const listeners = {
439
+ Program(node) {
440
+ if (!settings.faf) {
441
+ return;
442
+ }
443
+ const config2 = findTreeConfig(relPath, settings.faf);
444
+ if (!config2) {
445
+ return;
446
+ }
447
+ const relDir2 = import_path7.default.dirname(relPath);
448
+ const folderName = import_path7.default.basename(relDir2);
449
+ const parentType2 = classifyFolder(relDir2, config2);
450
+ if (parentType2 === "foreign") {
451
+ return;
452
+ }
453
+ const fileName2 = import_path7.default.basename(relPath);
454
+ for (const {
455
+ folderName: currentFolderName,
456
+ type: currentType
457
+ } of walkAncestors(relDir2, config2)) {
458
+ if (currentType === "invalid-fragment") {
459
+ context.report({
460
+ message: `Fragment directory "${currentFolderName}" is missing an Access Node (index.ts/index.js).`,
461
+ node
462
+ });
463
+ }
464
+ }
465
+ if (parentType2 === "layer" || parentType2 === "fractal-branch" || parentType2 === "invalid-fragment") {
466
+ return;
467
+ }
468
+ const rfConfig = getRootFragmentConfig(relDir2, config2);
469
+ if (rfConfig) {
470
+ const flatRootNodes = rfConfig.rootNodes.flat();
471
+ const isMatchedRootNode = flatRootNodes.some((rn) => {
472
+ if (rn.includes("/")) {
473
+ const rfPath = rfConfig.paths[0] ?? "";
474
+ const absRn = import_path7.default.resolve(getProjectRoot(), rfPath, rn);
475
+ return toRelativePath(absRn) === relPath;
476
+ }
477
+ return rn === fileName2;
478
+ });
479
+ if (isMatchedRootNode) {
480
+ return;
481
+ }
482
+ }
483
+ if (fileName2 !== "index.ts" && fileName2 !== "index.js") {
484
+ return;
485
+ }
486
+ if (parentType2 !== "fragment") {
487
+ context.report({
488
+ message: `Access Nodes ("index.ts/index.js") are exclusive to Fragments. Found index file directly inside "${folderName}" (classified as ${parentType2}).`,
489
+ node
490
+ });
491
+ return;
492
+ }
493
+ const contents = readDirCached(relDir2);
494
+ const hasFragmentNode = contents.files.some(
495
+ (f) => f !== "index.ts" && f !== "index.js" && f !== "README.md" && f !== "package.json" && f !== "tsconfig.json"
496
+ );
497
+ if (!hasFragmentNode) {
498
+ context.report({
499
+ message: `Fragment "${folderName}" has no Master Node. Every Fragment must contain at least one Fragment Node establishing its Role.`,
500
+ node
501
+ });
502
+ }
503
+ }
504
+ };
505
+ const fileName = import_path7.default.basename(relPath);
392
506
  if (fileName !== "index.ts" && fileName !== "index.js") {
393
- return {};
507
+ return listeners;
394
508
  }
395
- const settings = context.settings;
396
509
  if (!settings.faf) {
397
- return {};
510
+ return listeners;
398
511
  }
399
512
  const config = findTreeConfig(relPath, settings.faf);
400
513
  if (!config) {
401
- return {};
514
+ return listeners;
402
515
  }
403
- const relDir = import_path6.default.dirname(relPath);
516
+ const relDir = import_path7.default.dirname(relPath);
404
517
  const parentType = classifyFolder(relDir, config);
405
518
  if (parentType !== "fragment") {
406
- return {};
519
+ return listeners;
407
520
  }
408
521
  function checkSource(node, sourceVal) {
409
522
  const isValidSibling = /^\.\/[^/]+$/.test(sourceVal);
@@ -414,11 +527,11 @@ var rule2 = {
414
527
  });
415
528
  return;
416
529
  }
417
- const importedFilename = import_path6.default.basename(sourceVal);
530
+ const importedFilename = import_path7.default.basename(sourceVal);
418
531
  if (importedFilename === "index" || importedFilename === "index.ts" || importedFilename === "index.js" || importedFilename === "README.md" || importedFilename === "package.json") {
419
532
  return;
420
533
  }
421
- const fragmentName = import_path6.default.basename(relDir);
534
+ const fragmentName = import_path7.default.basename(relDir);
422
535
  if (!importedFilename.startsWith(fragmentName + ".")) {
423
536
  context.report({
424
537
  message: `Access Node (index file) can only re-export its own Fragment Nodes (names starting with "${fragmentName}."). Got "${importedFilename}".`,
@@ -426,23 +539,22 @@ var rule2 = {
426
539
  });
427
540
  }
428
541
  }
429
- return {
430
- ExportAllDeclaration(node) {
431
- checkSource(node, node.source.value);
432
- },
433
- ExportNamedDeclaration(node) {
434
- if (node.source) {
435
- checkSource(node, node.source.value);
436
- }
437
- },
438
- ImportDeclaration(node) {
542
+ listeners.ExportAllDeclaration = (node) => {
543
+ checkSource(node, node.source.value);
544
+ };
545
+ listeners.ExportNamedDeclaration = (node) => {
546
+ if (node.source) {
439
547
  checkSource(node, node.source.value);
440
548
  }
441
549
  };
550
+ listeners.ImportDeclaration = (node) => {
551
+ checkSource(node, node.source.value);
552
+ };
553
+ return listeners;
442
554
  },
443
555
  meta: {
444
556
  docs: {
445
- description: "Enforce Access Node rules for Fragments"
557
+ description: "Enforce Access Node integrity for Fragments"
446
558
  },
447
559
  schema: [],
448
560
  type: "problem"
@@ -450,136 +562,410 @@ var rule2 = {
450
562
  };
451
563
  var enforce_access_node_rule_default = rule2;
452
564
 
453
- // src/_rules/naming-conventions/naming-conventions.rule.ts
454
- var import_path7 = __toESM(require("path"), 1);
455
-
456
- // src/_rules/_rules@shared/_utils/_primitives/get-file-role/get-file-role.util.ts
457
- function getFileRole(fileName, config) {
458
- if (fileName === "index.ts" || fileName === "index.js") {
459
- return "index";
565
+ // src/_rules/_rules@shared/_utils/_aggregates/resolve-import-path/resolve-import-path.util.ts
566
+ var import_fs2 = __toESM(require("fs"), 1);
567
+ var import_path8 = __toESM(require("path"), 1);
568
+ function resolveImportPath(importPath, currentFileRelative, settings) {
569
+ const cacheKey = `${currentFileRelative}::${importPath}`;
570
+ const cached = state.resolvedImportPathCache.get(cacheKey);
571
+ if (cached !== void 0) {
572
+ return cached;
460
573
  }
461
- const parts = fileName.split(".");
462
- let allRoles = state.rolesCache.get(config);
463
- if (!allRoles) {
464
- allRoles = new Set(config.roles.flat());
465
- state.rolesCache.set(config, allRoles);
574
+ const result = _resolveImportPath(importPath, currentFileRelative, settings);
575
+ state.resolvedImportPathCache.set(cacheKey, result);
576
+ return result;
577
+ }
578
+ function _resolveImportPath(importPath, currentFileRelative, settings) {
579
+ const pkgImports = loadPackageImports(state.projectRoot);
580
+ const aliases = Object.keys(pkgImports).length > 0 ? pkgImports : settings?.aliases || {};
581
+ let matchedAlias = null;
582
+ let longestMatch = 0;
583
+ for (const alias of Object.keys(aliases)) {
584
+ if (importPath === alias || importPath.startsWith(alias + "/")) {
585
+ if (alias.length > longestMatch) {
586
+ longestMatch = alias.length;
587
+ matchedAlias = alias;
588
+ }
589
+ }
466
590
  }
467
- for (let i = parts.length - 2; i >= 1; i--) {
468
- const part = parts[i];
469
- if (part && allRoles.has(part)) {
470
- return part;
591
+ let relResolved;
592
+ if (matchedAlias) {
593
+ const target = aliases[matchedAlias];
594
+ if (target) {
595
+ const remainder = importPath.substring(matchedAlias.length);
596
+ const absTarget = import_path8.default.resolve(state.projectRoot, target + remainder);
597
+ relResolved = toRelativePath(absTarget);
598
+ } else {
599
+ return "";
600
+ }
601
+ } else {
602
+ if (importPath.startsWith(".") || importPath.startsWith("/")) {
603
+ const absCurrentDir = import_path8.default.dirname(
604
+ import_path8.default.resolve(state.projectRoot, currentFileRelative)
605
+ );
606
+ const absResolved = import_path8.default.resolve(absCurrentDir, importPath);
607
+ relResolved = toRelativePath(absResolved);
608
+ } else {
609
+ return "";
471
610
  }
472
611
  }
473
- return null;
612
+ return finalizeResolvedPath(relResolved);
613
+ }
614
+ var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
615
+ "cjs",
616
+ "cts",
617
+ "js",
618
+ "jsx",
619
+ "mjs",
620
+ "mts",
621
+ "ts",
622
+ "tsx"
623
+ ]);
624
+ function finalizeResolvedPath(relResolved) {
625
+ const resolvedDir = import_path8.default.dirname(relResolved).replace(/\\/g, "/");
626
+ const resolvedBase = import_path8.default.basename(relResolved);
627
+ const dirContents = readDirCached(relResolved);
628
+ if (dirContents.files.includes("index.ts")) {
629
+ return (relResolved === "." ? "" : relResolved + "/") + "index.ts";
630
+ }
631
+ if (dirContents.files.includes("index.js")) {
632
+ return (relResolved === "." ? "" : relResolved + "/") + "index.js";
633
+ }
634
+ const parentContents = readDirCached(resolvedDir);
635
+ const exactMatch = parentContents.files.find((f) => f === resolvedBase);
636
+ if (!exactMatch) {
637
+ const candidates = parentContents.files.filter(
638
+ (f) => f.startsWith(resolvedBase + ".")
639
+ );
640
+ const matchWithExt = candidates.find(
641
+ (f) => SCRIPT_EXTENSIONS.has(f.slice(resolvedBase.length + 1))
642
+ ) ?? candidates[0];
643
+ if (matchWithExt) {
644
+ return (resolvedDir === "." ? "" : resolvedDir + "/") + matchWithExt;
645
+ }
646
+ }
647
+ return relResolved;
648
+ }
649
+ function loadPackageImports(projectRootPath) {
650
+ if (state.packageImports !== null) {
651
+ return state.packageImports;
652
+ }
653
+ state.packageImports = {};
654
+ const isTest = typeof process !== "undefined" && (process.env.NODE_ENV === "test" || process.env.VITEST === "true");
655
+ if (isTest) {
656
+ return state.packageImports;
657
+ }
658
+ const pkgPath = import_path8.default.resolve(projectRootPath, "package.json");
659
+ if (import_fs2.default.existsSync(pkgPath)) {
660
+ try {
661
+ const pkgContent = import_fs2.default.readFileSync(pkgPath, "utf8");
662
+ const pkg = JSON.parse(pkgContent);
663
+ if (pkg.imports) {
664
+ for (const [key, val] of Object.entries(pkg.imports)) {
665
+ if (typeof val === "string") {
666
+ const cleanKey = key.endsWith("/*") ? key.slice(0, -2) : key.endsWith("*") ? key.slice(0, -1) : key;
667
+ let cleanVal = val.endsWith("/*") ? val.slice(0, -2) : val.endsWith("*") ? val.slice(0, -1) : val;
668
+ if (cleanVal.startsWith("./")) {
669
+ cleanVal = cleanVal.substring(2);
670
+ } else if (cleanVal.startsWith("/")) {
671
+ cleanVal = cleanVal.substring(1);
672
+ }
673
+ state.packageImports[cleanKey] = cleanVal;
674
+ }
675
+ }
676
+ }
677
+ } catch {
678
+ }
679
+ }
680
+ return state.packageImports;
474
681
  }
475
682
 
476
- // src/_rules/_rules@shared/_utils/_primitives/get-project-root/get-project-root.util.ts
477
- function getProjectRoot() {
478
- return state.projectRoot;
683
+ // src/_rules/_rules@shared/_utils/_primitives/find-tree-config-including-excluded/find-tree-config-including-excluded.util.ts
684
+ function findTreeConfigIncludingExcluded(relPath, settings) {
685
+ if (!settings || !settings.trees) {
686
+ return null;
687
+ }
688
+ const cached = state.treeConfigIncludingExcludedCache.get(relPath);
689
+ if (cached !== void 0) {
690
+ return cached;
691
+ }
692
+ let result = null;
693
+ for (const tree of settings.trees) {
694
+ const isIncluded = tree.includes.some(
695
+ (inc) => relPath === inc || relPath.startsWith(inc + "/")
696
+ );
697
+ if (isIncluded) {
698
+ result = tree;
699
+ break;
700
+ }
701
+ }
702
+ state.treeConfigIncludingExcludedCache.set(relPath, result);
703
+ return result;
479
704
  }
480
705
 
481
- // src/_rules/naming-conventions/naming-conventions.rule.ts
482
- var KEBAB_CASE_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
483
- var DEFAULT_HTTP_METHODS = [
484
- "connect",
485
- "delete",
486
- "get",
487
- "head",
488
- "options",
489
- "patch",
490
- "post",
491
- "put",
492
- "trace"
493
- ];
706
+ // src/_rules/foreign-domain-isolation/foreign-domain-isolation.rule.ts
494
707
  var rule3 = {
495
708
  create(context) {
496
- return {
497
- Program(node) {
498
- const absPath = context.filename;
499
- const relPath = toRelativePath(absPath);
500
- const settings = context.settings;
501
- if (!settings.faf) {
502
- return;
503
- }
504
- const config = findTreeConfig(relPath, settings.faf);
505
- if (!config) {
709
+ const absPath = context.filename;
710
+ const relPath = toRelativePath(absPath);
711
+ const settings = context.settings;
712
+ if (!settings.faf) {
713
+ return {};
714
+ }
715
+ const config = findTreeConfigIncludingExcluded(relPath, settings.faf);
716
+ if (!config) {
717
+ return {};
718
+ }
719
+ const isCurrentFileExcluded = config.excludes?.some(
720
+ (exc) => relPath === exc || relPath.startsWith(exc + "/")
721
+ ) ?? false;
722
+ function checkImport(node, importValue) {
723
+ const resolvedRelPath = resolveImportPath(
724
+ importValue,
725
+ relPath,
726
+ settings.faf
727
+ );
728
+ if (!resolvedRelPath) {
729
+ return;
730
+ }
731
+ const isImportedFileExcluded = config?.excludes?.some(
732
+ (exc) => resolvedRelPath === exc || resolvedRelPath.startsWith(exc + "/")
733
+ ) ?? false;
734
+ if (!isCurrentFileExcluded && isImportedFileExcluded) {
735
+ context.report({
736
+ message: `Importing from Foreign Domain "${resolvedRelPath}" is forbidden for Logical Nodes.`,
737
+ node
738
+ });
739
+ return;
740
+ }
741
+ if (isCurrentFileExcluded && !isImportedFileExcluded) {
742
+ context.report({
743
+ message: `Foreign Domain files cannot import Logical Nodes from FAF tree ("${resolvedRelPath}").`,
744
+ node
745
+ });
746
+ }
747
+ }
748
+ return {
749
+ ExportAllDeclaration(node) {
750
+ checkImport(node, node.source.value);
751
+ },
752
+ ExportNamedDeclaration(node) {
753
+ if (node.source) {
754
+ checkImport(node, node.source.value);
755
+ }
756
+ },
757
+ ImportDeclaration(node) {
758
+ checkImport(node, node.source.value);
759
+ }
760
+ };
761
+ },
762
+ meta: {
763
+ docs: {
764
+ description: "Enforce isolation between Foreign Domains and Logical Nodes"
765
+ },
766
+ schema: [],
767
+ type: "problem"
768
+ }
769
+ };
770
+ var foreign_domain_isolation_rule_default = rule3;
771
+
772
+ // src/_rules/fragment-master-node/fragment-master-node.rule.ts
773
+ var import_path9 = __toESM(require("path"), 1);
774
+
775
+ // src/_rules/_rules@shared/_utils/_primitives/get-file-role/get-file-role.util.ts
776
+ function getFileRole(fileName, config) {
777
+ if (fileName === "index.ts" || fileName === "index.js") {
778
+ return "index";
779
+ }
780
+ const parts = fileName.split(".");
781
+ let allRoles = state.rolesCache.get(config);
782
+ if (!allRoles) {
783
+ allRoles = new Set(config.roles.flat());
784
+ state.rolesCache.set(config, allRoles);
785
+ }
786
+ for (let i = parts.length - 2; i >= 1; i--) {
787
+ const part = parts[i];
788
+ if (part && allRoles.has(part)) {
789
+ return part;
790
+ }
791
+ }
792
+ return null;
793
+ }
794
+
795
+ // src/_rules/fragment-master-node/fragment-master-node.rule.ts
796
+ var DEFAULT_HTTP_METHODS = [
797
+ "connect",
798
+ "delete",
799
+ "get",
800
+ "head",
801
+ "options",
802
+ "patch",
803
+ "post",
804
+ "put",
805
+ "trace"
806
+ ];
807
+ var rule4 = {
808
+ create(context) {
809
+ return {
810
+ Program(node) {
811
+ const absPath = context.filename;
812
+ const relPath = toRelativePath(absPath);
813
+ const settings = context.settings;
814
+ if (!settings.faf) {
506
815
  return;
507
816
  }
508
- const relDir = import_path7.default.dirname(relPath);
509
- const folderName = import_path7.default.basename(relDir);
817
+ const config = findTreeConfig(relPath, settings.faf);
818
+ if (!config) {
819
+ return;
820
+ }
821
+ const relDir = import_path9.default.dirname(relPath);
822
+ const folderName = import_path9.default.basename(relDir);
510
823
  const parentType = classifyFolder(relDir, config);
511
- if (parentType === "foreign") {
824
+ if (parentType !== "fragment") {
512
825
  return;
513
826
  }
514
- const fileName = import_path7.default.basename(relPath);
515
- let currentDir = relDir;
516
- while (currentDir && currentDir !== "." && currentDir !== "/") {
517
- const isInTree = config.includes.some(
518
- (inc) => currentDir === inc || currentDir.startsWith(inc + "/")
519
- );
520
- const isExcluded = config.excludes?.some(
521
- (exc) => currentDir === exc || currentDir.startsWith(exc + "/")
522
- ) ?? false;
523
- if (!isInTree || isExcluded) {
524
- break;
827
+ const fileName = import_path9.default.basename(relPath);
828
+ const rfConfig = getRootFragmentConfig(relDir, config);
829
+ if (rfConfig) {
830
+ const flatRootNodes = rfConfig.rootNodes.flat();
831
+ const isMatchedRootNode = flatRootNodes.some((rn) => {
832
+ if (rn.includes("/")) {
833
+ const rfPath = rfConfig.paths[0] ?? "";
834
+ const absRn = import_path9.default.resolve(getProjectRoot(), rfPath, rn);
835
+ return toRelativePath(absRn) === relPath;
836
+ }
837
+ return rn === fileName;
838
+ });
839
+ if (isMatchedRootNode) {
840
+ return;
525
841
  }
526
- const currentFolderName = import_path7.default.basename(currentDir);
527
- const currentType = classifyFolder(currentDir, config);
528
- if ((currentType === "layer" || currentType === "category") && !currentFolderName.startsWith("_")) {
842
+ }
843
+ if (fileName === "index.ts" || fileName === "index.js") {
844
+ return;
845
+ }
846
+ if (fileName === "README.md" || fileName === "package.json" || fileName === "tsconfig.json") {
847
+ return;
848
+ }
849
+ const role = getFileRole(fileName, config);
850
+ const ext = import_path9.default.extname(fileName);
851
+ const allRoleNames = new Set(config.roles.flat());
852
+ const roleSegments = fileName.slice(0, fileName.length - ext.length).split(".").slice(1).filter((part) => allRoleNames.has(part));
853
+ const matchedRoute = config.routeHierarchies?.find(
854
+ (rh) => rh.paths.some((rp) => relDir === rp || relDir.startsWith(rp + "/"))
855
+ );
856
+ if (matchedRoute) {
857
+ const contents = readDirCached(relDir);
858
+ const fileRoles = contents.files.map((f) => getFileRole(f, config));
859
+ if (!fileRoles.includes(matchedRoute.role)) {
529
860
  context.report({
530
- message: `Layer/Category directory "${currentFolderName}" must be prefixed with an underscore (e.g. "_${currentFolderName}").`,
861
+ message: `Route terminal Fragment "${folderName}" must contain a Master Node with role "${matchedRoute.role}" (e.g. "${folderName}.${matchedRoute.role}.ts").`,
531
862
  node
532
863
  });
533
864
  }
534
- if (currentFolderName.startsWith("_") && !currentFolderName.includes("@shared")) {
535
- const contents = readDirCached(currentDir);
536
- const hasIndex = contents.files.includes("index.ts") || contents.files.includes("index.js");
537
- if (hasIndex) {
538
- context.report({
539
- message: `Directory "${currentFolderName}" has an Access Node (index.ts) but its name starts with "_". Fragments must not be prefixed with underscore.`,
540
- node
541
- });
542
- }
865
+ const httpMethods = matchedRoute.httpMethods ?? DEFAULT_HTTP_METHODS;
866
+ const routeNameRegex = new RegExp(
867
+ `^(${httpMethods.join("|")})-[a-z0-9]+(-[a-z0-9]+)*$`
868
+ );
869
+ if (!routeNameRegex.test(folderName)) {
870
+ context.report({
871
+ message: `Route terminal Fragment "${folderName}" must follow the "<method>-<object>" pattern with a lowercase HTTP method (${httpMethods.join(", ")}).`,
872
+ node
873
+ });
543
874
  }
544
- if (currentFolderName !== "src") {
545
- let baseFolderName = currentFolderName.replace(/@shared$/, "");
546
- if (baseFolderName.startsWith("_")) {
547
- baseFolderName = baseFolderName.substring(1);
548
- }
549
- if (baseFolderName.startsWith("[") && baseFolderName.endsWith("]")) {
550
- baseFolderName = baseFolderName.substring(
551
- 1,
552
- baseFolderName.length - 1
553
- );
554
- }
555
- const isKebab = KEBAB_CASE_REGEX.test(baseFolderName);
556
- if (!isKebab) {
557
- context.report({
558
- message: `Folder name "${currentFolderName}" must be in kebab-case (e.g. "_my-folder" or "_[my-param]").`,
559
- node
560
- });
561
- }
875
+ }
876
+ let currentParent = import_path9.default.dirname(relDir).replace(/\\/g, "/");
877
+ let categoryConfig = null;
878
+ let categoryFolderName = "";
879
+ while (currentParent && currentParent !== "." && currentParent !== "/") {
880
+ const currentParentType = classifyFolder(currentParent, config);
881
+ if (currentParentType === "category") {
882
+ categoryConfig = getCategoryConfig(currentParent, config);
883
+ categoryFolderName = import_path9.default.basename(currentParent);
884
+ break;
885
+ } else if (currentParentType === "fragment" || currentParentType === "root-fragment" || currentParentType === "layer") {
886
+ break;
562
887
  }
563
- if (currentType === "fractal-branch") {
564
- const expectedScope = import_path7.default.basename(import_path7.default.dirname(currentDir));
565
- const normalizedExpectedScope = expectedScope.startsWith("_") ? expectedScope.substring(1) : expectedScope;
566
- const expectedName = `_${normalizedExpectedScope}@shared`;
567
- if (currentFolderName !== expectedName) {
568
- context.report({
569
- message: `Fractal Branch name "${currentFolderName}" must match its parent scope name: "${expectedName}".`,
570
- node
571
- });
572
- }
888
+ const nextParent = import_path9.default.dirname(currentParent).replace(/\\/g, "/");
889
+ if (nextParent === currentParent) break;
890
+ currentParent = nextParent;
891
+ }
892
+ if (categoryConfig && categoryConfig.role) {
893
+ const contents = readDirCached(relDir);
894
+ const fileRoles = contents.files.map((f) => getFileRole(f, config));
895
+ if (!fileRoles.includes(categoryConfig.role)) {
896
+ context.report({
897
+ message: `Fragment "${folderName}" inside Category "${categoryFolderName}" must contain a Master Node with role "${categoryConfig.role}" (e.g. "${folderName}.${categoryConfig.role}.ts").`,
898
+ node
899
+ });
573
900
  }
574
- if (currentType === "invalid-fragment") {
901
+ }
902
+ if (!fileName.startsWith(folderName + ".")) {
903
+ context.report({
904
+ message: `Fragment Node "${fileName}" must share the parent Fragment name: "${folderName}.<role>${ext}".`,
905
+ node
906
+ });
907
+ }
908
+ if (!role) {
909
+ context.report({
910
+ message: `File "${fileName}" inside Fragment "${folderName}" must have an explicit role suffix (e.g. "${folderName}.style${ext}").`,
911
+ node
912
+ });
913
+ } else if (roleSegments.length > 1) {
914
+ } else {
915
+ const contents = readDirCached(relDir);
916
+ const duplicate = contents.files.find(
917
+ (f) => f !== fileName && f !== "index.ts" && f !== "index.js" && getFileRole(f, config) === role
918
+ );
919
+ if (duplicate) {
575
920
  context.report({
576
- message: `Fragment directory "${currentFolderName}" is missing an Access Node (index.ts/index.js).`,
921
+ message: `Fragment Node "${fileName}" shares Role "${role}" with sibling "${duplicate}". Each Fragment Node must have a unique Role within its Fragment.`,
577
922
  node
578
923
  });
579
924
  }
580
- const relParentDir = import_path7.default.dirname(currentDir).replace(/\\/g, "/");
581
- const parentFolderName = import_path7.default.basename(relParentDir);
582
- const relParentType = relParentDir === "." || relParentDir === "/" ? "unknown" : classifyFolder(relParentDir, config);
925
+ }
926
+ }
927
+ };
928
+ },
929
+ meta: {
930
+ docs: {
931
+ description: "Enforce Fragment Node identity and Master Node integrity in FAF Fragments"
932
+ },
933
+ schema: [],
934
+ type: "problem"
935
+ }
936
+ };
937
+ var fragment_master_node_rule_default = rule4;
938
+
939
+ // src/_rules/logical-domain-placement/logical-domain-placement.rule.ts
940
+ var import_path10 = __toESM(require("path"), 1);
941
+ var rule5 = {
942
+ create(context) {
943
+ return {
944
+ Program(node) {
945
+ const absPath = context.filename;
946
+ const relPath = toRelativePath(absPath);
947
+ const settings = context.settings;
948
+ if (!settings.faf) {
949
+ return;
950
+ }
951
+ const config = findTreeConfig(relPath, settings.faf);
952
+ if (!config) {
953
+ return;
954
+ }
955
+ const relDir = import_path10.default.dirname(relPath);
956
+ const folderName = import_path10.default.basename(relDir);
957
+ const parentType = classifyFolder(relDir, config);
958
+ if (parentType === "foreign") {
959
+ return;
960
+ }
961
+ const fileName = import_path10.default.basename(relPath);
962
+ for (const {
963
+ folderName: currentFolderName,
964
+ parentDir: relParentDir,
965
+ parentFolderName,
966
+ parentType: relParentType,
967
+ type: currentType
968
+ } of walkAncestors(relDir, config)) {
583
969
  if (currentType === "root-fragment") {
584
970
  if (relParentType !== "root-fragment" && relParentType !== "unknown") {
585
971
  context.report({
@@ -635,7 +1021,6 @@ var rule3 = {
635
1021
  });
636
1022
  }
637
1023
  }
638
- currentDir = import_path7.default.dirname(currentDir).replace(/\\/g, "/");
639
1024
  }
640
1025
  if (parentType === "layer") {
641
1026
  context.report({
@@ -649,9 +1034,96 @@ var rule3 = {
649
1034
  message: `Fractal Branches cannot contain files directly. File "${fileName}" is placed directly inside Fractal Branch "${folderName}".`,
650
1035
  node
651
1036
  });
1037
+ }
1038
+ }
1039
+ };
1040
+ },
1041
+ meta: {
1042
+ docs: {
1043
+ description: "Enforce topological placement constraints between FAF Logical Domains"
1044
+ },
1045
+ schema: [],
1046
+ type: "problem"
1047
+ }
1048
+ };
1049
+ var logical_domain_placement_rule_default = rule5;
1050
+
1051
+ // src/_rules/naming-conventions/naming-conventions.rule.ts
1052
+ var import_path11 = __toESM(require("path"), 1);
1053
+ var KEBAB_CASE_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
1054
+ var rule6 = {
1055
+ create(context) {
1056
+ return {
1057
+ Program(node) {
1058
+ const absPath = context.filename;
1059
+ const relPath = toRelativePath(absPath);
1060
+ const settings = context.settings;
1061
+ if (!settings.faf) {
652
1062
  return;
653
1063
  }
654
- if (parentType === "invalid-fragment") {
1064
+ const config = findTreeConfig(relPath, settings.faf);
1065
+ if (!config) {
1066
+ return;
1067
+ }
1068
+ const relDir = import_path11.default.dirname(relPath);
1069
+ const parentType = classifyFolder(relDir, config);
1070
+ if (parentType === "foreign") {
1071
+ return;
1072
+ }
1073
+ const fileName = import_path11.default.basename(relPath);
1074
+ for (const {
1075
+ dir: currentDir,
1076
+ folderName: currentFolderName,
1077
+ type: currentType
1078
+ } of walkAncestors(relDir, config)) {
1079
+ if ((currentType === "layer" || currentType === "category") && !currentFolderName.startsWith("_")) {
1080
+ context.report({
1081
+ message: `Layer/Category directory "${currentFolderName}" must be prefixed with an underscore (e.g. "_${currentFolderName}").`,
1082
+ node
1083
+ });
1084
+ }
1085
+ if (currentFolderName.startsWith("_") && !currentFolderName.includes("@shared")) {
1086
+ const contents = readDirCached(currentDir);
1087
+ const hasIndex = contents.files.includes("index.ts") || contents.files.includes("index.js");
1088
+ if (hasIndex) {
1089
+ context.report({
1090
+ message: `Directory "${currentFolderName}" has an Access Node (index.ts) but its name starts with "_". Fragments must not be prefixed with underscore.`,
1091
+ node
1092
+ });
1093
+ }
1094
+ }
1095
+ if (currentFolderName !== "src") {
1096
+ let baseFolderName = currentFolderName.replace(/@shared$/, "");
1097
+ if (baseFolderName.startsWith("_")) {
1098
+ baseFolderName = baseFolderName.substring(1);
1099
+ }
1100
+ if (baseFolderName.startsWith("[") && baseFolderName.endsWith("]")) {
1101
+ baseFolderName = baseFolderName.substring(
1102
+ 1,
1103
+ baseFolderName.length - 1
1104
+ );
1105
+ }
1106
+ const isKebab = KEBAB_CASE_REGEX.test(baseFolderName);
1107
+ if (!isKebab) {
1108
+ context.report({
1109
+ message: `Folder name "${currentFolderName}" must be in kebab-case (e.g. "_my-folder" or "_[my-param]").`,
1110
+ node
1111
+ });
1112
+ }
1113
+ }
1114
+ if (currentType === "fractal-branch") {
1115
+ const expectedScope = import_path11.default.basename(import_path11.default.dirname(currentDir));
1116
+ const normalizedExpectedScope = expectedScope.startsWith("_") ? expectedScope.substring(1) : expectedScope;
1117
+ const expectedName = `_${normalizedExpectedScope}@shared`;
1118
+ if (currentFolderName !== expectedName) {
1119
+ context.report({
1120
+ message: `Fractal Branch name "${currentFolderName}" must match its parent scope name: "${expectedName}".`,
1121
+ node
1122
+ });
1123
+ }
1124
+ }
1125
+ }
1126
+ if (parentType === "layer" || parentType === "fractal-branch" || parentType === "invalid-fragment") {
655
1127
  return;
656
1128
  }
657
1129
  const rfConfig = getRootFragmentConfig(relDir, config);
@@ -660,7 +1132,7 @@ var rule3 = {
660
1132
  const isMatchedRootNode = flatRootNodes.some((rn) => {
661
1133
  if (rn.includes("/")) {
662
1134
  const rfPath = rfConfig.paths[0] ?? "";
663
- const absRn = import_path7.default.resolve(getProjectRoot(), rfPath, rn);
1135
+ const absRn = import_path11.default.resolve(getProjectRoot(), rfPath, rn);
664
1136
  return toRelativePath(absRn) === relPath;
665
1137
  }
666
1138
  return rn === fileName;
@@ -670,288 +1142,79 @@ var rule3 = {
670
1142
  }
671
1143
  }
672
1144
  if (fileName === "index.ts" || fileName === "index.js") {
673
- if (parentType !== "fragment") {
674
- context.report({
675
- message: `Access Nodes ("index.ts/index.js") are exclusive to Fragments. Found index file directly inside "${folderName}" (classified as ${parentType}).`,
676
- node
677
- });
678
- } else {
679
- const contents = readDirCached(relDir);
680
- const hasFragmentNode = contents.files.some(
681
- (f) => f !== "index.ts" && f !== "index.js" && f !== "README.md" && f !== "package.json" && f !== "tsconfig.json"
682
- );
683
- if (!hasFragmentNode) {
684
- context.report({
685
- message: `Fragment "${folderName}" has no Master Node. Every Fragment must contain at least one Fragment Node establishing its Role.`,
686
- node
687
- });
688
- }
689
- }
690
1145
  return;
691
1146
  }
692
1147
  if (fileName === "README.md" || fileName === "package.json" || fileName === "tsconfig.json") {
693
1148
  return;
694
1149
  }
695
1150
  const role = getFileRole(fileName, config);
696
- const ext = import_path7.default.extname(fileName);
1151
+ const ext = import_path11.default.extname(fileName);
697
1152
  const allRoleNames = new Set(config.roles.flat());
698
1153
  const roleSegments = fileName.slice(0, fileName.length - ext.length).split(".").slice(1).filter((part) => allRoleNames.has(part));
699
1154
  const multiRoleMessage = `File "${fileName}" composes multiple Roles ("${roleSegments.join('", "')}") in its name. A Logical Node may declare only one Role; promote the detail that needs its own Role to an autonomous Sub-Fragment.`;
700
- if (parentType === "fragment") {
701
- const matchedRoute = config.routeHierarchies?.find(
702
- (rh) => rh.paths.some((rp) => relDir === rp || relDir.startsWith(rp + "/"))
703
- );
704
- if (matchedRoute) {
705
- const contents = readDirCached(relDir);
706
- const fileRoles = contents.files.map((f) => getFileRole(f, config));
707
- if (!fileRoles.includes(matchedRoute.role)) {
708
- context.report({
709
- message: `Route terminal Fragment "${folderName}" must contain a Master Node with role "${matchedRoute.role}" (e.g. "${folderName}.${matchedRoute.role}.ts").`,
710
- node
711
- });
712
- }
713
- const httpMethods = matchedRoute.httpMethods ?? DEFAULT_HTTP_METHODS;
714
- const routeNameRegex = new RegExp(
715
- `^(${httpMethods.join("|")})-[a-z0-9]+(-[a-z0-9]+)*$`
716
- );
717
- if (!routeNameRegex.test(folderName)) {
718
- context.report({
719
- message: `Route terminal Fragment "${folderName}" must follow the "<method>-<object>" pattern with a lowercase HTTP method (${httpMethods.join(", ")}).`,
720
- node
721
- });
722
- }
723
- }
724
- let currentParent = import_path7.default.dirname(relDir).replace(/\\/g, "/");
725
- let categoryConfig = null;
726
- let categoryFolderName = "";
727
- while (currentParent && currentParent !== "." && currentParent !== "/") {
728
- const parentType2 = classifyFolder(currentParent, config);
729
- if (parentType2 === "category") {
730
- categoryConfig = getCategoryConfig(currentParent, config);
731
- categoryFolderName = import_path7.default.basename(currentParent);
732
- break;
733
- } else if (parentType2 === "fragment" || parentType2 === "root-fragment" || parentType2 === "layer") {
734
- break;
735
- }
736
- const nextParent = import_path7.default.dirname(currentParent).replace(/\\/g, "/");
737
- if (nextParent === currentParent) break;
738
- currentParent = nextParent;
739
- }
740
- if (categoryConfig && categoryConfig.role) {
741
- const contents = readDirCached(relDir);
742
- const fileRoles = contents.files.map((f) => getFileRole(f, config));
743
- if (!fileRoles.includes(categoryConfig.role)) {
744
- context.report({
745
- message: `Fragment "${folderName}" inside Category "${categoryFolderName}" must contain a Master Node with role "${categoryConfig.role}" (e.g. "${folderName}.${categoryConfig.role}.ts").`,
746
- node
747
- });
748
- }
749
- }
750
- if (!fileName.startsWith(folderName + ".")) {
751
- context.report({
752
- message: `Fragment Node "${fileName}" must share the parent Fragment name: "${folderName}.<role>${ext}".`,
753
- node
754
- });
755
- }
756
- if (!role) {
757
- context.report({
758
- message: `File "${fileName}" inside Fragment "${folderName}" must have an explicit role suffix (e.g. "${folderName}.style${ext}").`,
759
- node
760
- });
761
- } else if (roleSegments.length > 1) {
762
- context.report({ message: multiRoleMessage, node });
763
- } else {
764
- const contents = readDirCached(relDir);
765
- const duplicate = contents.files.find(
766
- (f) => f !== fileName && f !== "index.ts" && f !== "index.js" && getFileRole(f, config) === role
767
- );
768
- if (duplicate) {
769
- context.report({
770
- message: `Fragment Node "${fileName}" shares Role "${role}" with sibling "${duplicate}". Each Fragment Node must have a unique Role within its Fragment.`,
771
- node
772
- });
773
- }
774
- }
1155
+ if (parentType === "fragment" && role && roleSegments.length > 1) {
1156
+ context.report({ message: multiRoleMessage, node });
775
1157
  }
776
1158
  if (parentType === "category") {
1159
+ const folderName = import_path11.default.basename(relDir);
777
1160
  const catConfig = getCategoryConfig(relDir, config);
778
1161
  if (catConfig) {
779
1162
  const allowedExts = catConfig.allowedExtensions ?? [
780
1163
  ".js",
781
1164
  ".jsx",
782
1165
  ".ts",
783
- ".tsx"
784
- ];
785
- if (!allowedExts.includes(ext)) {
786
- context.report({
787
- message: `File extension "${ext}" is not allowed in Category "${folderName}". Allowed extensions: ${allowedExts.join(", ")}.`,
788
- node
789
- });
790
- return;
791
- }
792
- const isCodeExt = [".js", ".jsx", ".ts", ".tsx"].includes(ext);
793
- if (isCodeExt) {
794
- if (!role) {
795
- context.report({
796
- message: `Logical Node "${fileName}" inside Category "${folderName}" must have a role suffix.`,
797
- node
798
- });
799
- } else if (role !== catConfig.role) {
800
- context.report({
801
- message: `Role "${role}" for file "${fileName}" does not match the expected Category role "${catConfig.role}".`,
802
- node
803
- });
804
- } else if (roleSegments.length > 1) {
805
- context.report({ message: multiRoleMessage, node });
806
- }
807
- } else {
808
- if (role && role !== catConfig.role) {
809
- context.report({
810
- message: `Role "${role}" for asset "${fileName}" does not match the expected Category role "${catConfig.role}".`,
811
- node
812
- });
813
- } else if (role && roleSegments.length > 1) {
814
- context.report({ message: multiRoleMessage, node });
815
- }
816
- }
817
- }
818
- }
819
- }
820
- };
821
- },
822
- meta: {
823
- docs: {
824
- description: "Enforce naming conventions and folder taxonomy in FAF architecture"
825
- },
826
- schema: [],
827
- type: "problem"
828
- }
829
- };
830
- var naming_conventions_rule_default = rule3;
831
-
832
- // src/_rules/no-direct-fragment-import/no-direct-fragment-import.rule.ts
833
- var import_path9 = __toESM(require("path"), 1);
834
-
835
- // src/_rules/_rules@shared/_utils/_aggregates/resolve-import-path/resolve-import-path.util.ts
836
- var import_fs2 = __toESM(require("fs"), 1);
837
- var import_path8 = __toESM(require("path"), 1);
838
- function resolveImportPath(importPath, currentFileRelative, settings) {
839
- const cacheKey = `${currentFileRelative}::${importPath}`;
840
- const cached = state.resolvedImportPathCache.get(cacheKey);
841
- if (cached !== void 0) {
842
- return cached;
843
- }
844
- const result = _resolveImportPath(importPath, currentFileRelative, settings);
845
- state.resolvedImportPathCache.set(cacheKey, result);
846
- return result;
847
- }
848
- function _resolveImportPath(importPath, currentFileRelative, settings) {
849
- const pkgImports = loadPackageImports(state.projectRoot);
850
- const aliases = Object.keys(pkgImports).length > 0 ? pkgImports : settings?.aliases || {};
851
- let matchedAlias = null;
852
- let longestMatch = 0;
853
- for (const alias of Object.keys(aliases)) {
854
- if (importPath === alias || importPath.startsWith(alias + "/")) {
855
- if (alias.length > longestMatch) {
856
- longestMatch = alias.length;
857
- matchedAlias = alias;
858
- }
859
- }
860
- }
861
- let relResolved;
862
- if (matchedAlias) {
863
- const target = aliases[matchedAlias];
864
- if (target) {
865
- const remainder = importPath.substring(matchedAlias.length);
866
- const absTarget = import_path8.default.resolve(state.projectRoot, target + remainder);
867
- relResolved = toRelativePath(absTarget);
868
- } else {
869
- return "";
870
- }
871
- } else {
872
- if (importPath.startsWith(".") || importPath.startsWith("/")) {
873
- const absCurrentDir = import_path8.default.dirname(
874
- import_path8.default.resolve(state.projectRoot, currentFileRelative)
875
- );
876
- const absResolved = import_path8.default.resolve(absCurrentDir, importPath);
877
- relResolved = toRelativePath(absResolved);
878
- } else {
879
- return "";
880
- }
881
- }
882
- return finalizeResolvedPath(relResolved);
883
- }
884
- var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
885
- "cjs",
886
- "cts",
887
- "js",
888
- "jsx",
889
- "mjs",
890
- "mts",
891
- "ts",
892
- "tsx"
893
- ]);
894
- function finalizeResolvedPath(relResolved) {
895
- const resolvedDir = import_path8.default.dirname(relResolved).replace(/\\/g, "/");
896
- const resolvedBase = import_path8.default.basename(relResolved);
897
- const dirContents = readDirCached(relResolved);
898
- if (dirContents.files.includes("index.ts")) {
899
- return (relResolved === "." ? "" : relResolved + "/") + "index.ts";
900
- }
901
- if (dirContents.files.includes("index.js")) {
902
- return (relResolved === "." ? "" : relResolved + "/") + "index.js";
903
- }
904
- const parentContents = readDirCached(resolvedDir);
905
- const exactMatch = parentContents.files.find((f) => f === resolvedBase);
906
- if (!exactMatch) {
907
- const candidates = parentContents.files.filter(
908
- (f) => f.startsWith(resolvedBase + ".")
909
- );
910
- const matchWithExt = candidates.find(
911
- (f) => SCRIPT_EXTENSIONS.has(f.slice(resolvedBase.length + 1))
912
- ) ?? candidates[0];
913
- if (matchWithExt) {
914
- return (resolvedDir === "." ? "" : resolvedDir + "/") + matchWithExt;
915
- }
916
- }
917
- return relResolved;
918
- }
919
- function loadPackageImports(projectRootPath) {
920
- if (state.packageImports !== null) {
921
- return state.packageImports;
922
- }
923
- state.packageImports = {};
924
- const isTest = typeof process !== "undefined" && (process.env.NODE_ENV === "test" || process.env.VITEST === "true");
925
- if (isTest) {
926
- return state.packageImports;
927
- }
928
- const pkgPath = import_path8.default.resolve(projectRootPath, "package.json");
929
- if (import_fs2.default.existsSync(pkgPath)) {
930
- try {
931
- const pkgContent = import_fs2.default.readFileSync(pkgPath, "utf8");
932
- const pkg = JSON.parse(pkgContent);
933
- if (pkg.imports) {
934
- for (const [key, val] of Object.entries(pkg.imports)) {
935
- if (typeof val === "string") {
936
- const cleanKey = key.endsWith("/*") ? key.slice(0, -2) : key.endsWith("*") ? key.slice(0, -1) : key;
937
- let cleanVal = val.endsWith("/*") ? val.slice(0, -2) : val.endsWith("*") ? val.slice(0, -1) : val;
938
- if (cleanVal.startsWith("./")) {
939
- cleanVal = cleanVal.substring(2);
940
- } else if (cleanVal.startsWith("/")) {
941
- cleanVal = cleanVal.substring(1);
1166
+ ".tsx"
1167
+ ];
1168
+ if (!allowedExts.includes(ext)) {
1169
+ context.report({
1170
+ message: `File extension "${ext}" is not allowed in Category "${folderName}". Allowed extensions: ${allowedExts.join(", ")}.`,
1171
+ node
1172
+ });
1173
+ return;
1174
+ }
1175
+ const isCodeExt = [".js", ".jsx", ".ts", ".tsx"].includes(ext);
1176
+ if (isCodeExt) {
1177
+ if (!role) {
1178
+ context.report({
1179
+ message: `Logical Node "${fileName}" inside Category "${folderName}" must have a role suffix.`,
1180
+ node
1181
+ });
1182
+ } else if (role !== catConfig.role) {
1183
+ context.report({
1184
+ message: `Role "${role}" for file "${fileName}" does not match the expected Category role "${catConfig.role}".`,
1185
+ node
1186
+ });
1187
+ } else if (roleSegments.length > 1) {
1188
+ context.report({ message: multiRoleMessage, node });
1189
+ }
1190
+ } else {
1191
+ if (role && role !== catConfig.role) {
1192
+ context.report({
1193
+ message: `Role "${role}" for asset "${fileName}" does not match the expected Category role "${catConfig.role}".`,
1194
+ node
1195
+ });
1196
+ } else if (role && roleSegments.length > 1) {
1197
+ context.report({ message: multiRoleMessage, node });
1198
+ }
942
1199
  }
943
- state.packageImports[cleanKey] = cleanVal;
944
1200
  }
945
1201
  }
946
1202
  }
947
- } catch {
948
- }
1203
+ };
1204
+ },
1205
+ meta: {
1206
+ docs: {
1207
+ description: "Enforce lexical naming conventions for FAF folders and Logical Nodes"
1208
+ },
1209
+ schema: [],
1210
+ type: "problem"
949
1211
  }
950
- return state.packageImports;
951
- }
1212
+ };
1213
+ var naming_conventions_rule_default = rule6;
952
1214
 
953
1215
  // src/_rules/no-direct-fragment-import/no-direct-fragment-import.rule.ts
954
- var rule4 = {
1216
+ var import_path12 = __toESM(require("path"), 1);
1217
+ var rule7 = {
955
1218
  create(context) {
956
1219
  const absPath = context.filename;
957
1220
  const relPath = toRelativePath(absPath);
@@ -972,13 +1235,13 @@ var rule4 = {
972
1235
  if (!resolvedRelPath) {
973
1236
  return;
974
1237
  }
975
- const importedDir = import_path9.default.dirname(resolvedRelPath);
976
- const importedFile = import_path9.default.basename(resolvedRelPath);
1238
+ const importedDir = import_path12.default.dirname(resolvedRelPath);
1239
+ const importedFile = import_path12.default.basename(resolvedRelPath);
977
1240
  const importedFolderType = classifyFolder(importedDir, config);
978
1241
  if (importedFolderType === "fragment") {
979
1242
  const isAccessNode = importedFile === "index.ts" || importedFile === "index.js";
980
1243
  if (!isAccessNode) {
981
- const isInside = import_path9.default.dirname(relPath) === importedDir;
1244
+ const isInside = import_path12.default.dirname(relPath) === importedDir;
982
1245
  if (!isInside) {
983
1246
  context.report({
984
1247
  message: `Direct import of Fragment internal file "${importedFile}" is forbidden. You must import through its Access Node (index file) in "${importedDir}".`,
@@ -1010,21 +1273,21 @@ var rule4 = {
1010
1273
  type: "problem"
1011
1274
  }
1012
1275
  };
1013
- var no_direct_fragment_import_rule_default = rule4;
1276
+ var no_direct_fragment_import_rule_default = rule7;
1014
1277
 
1015
1278
  // src/_rules/no-fractal-branch-leak/no-fractal-branch-leak.rule.ts
1016
- var import_path12 = __toESM(require("path"), 1);
1279
+ var import_path15 = __toESM(require("path"), 1);
1017
1280
 
1018
1281
  // src/_rules/_rules@shared/_utils/_primitives/get-fractal-branch-owner/get-fractal-branch-owner.util.ts
1019
- var import_path10 = __toESM(require("path"), 1);
1282
+ var import_path13 = __toESM(require("path"), 1);
1020
1283
  function getFractalBranchOwner(relPath) {
1021
1284
  let current = relPath;
1022
1285
  while (current && current !== "." && current !== "/") {
1023
- const folderName = import_path10.default.basename(current);
1286
+ const folderName = import_path13.default.basename(current);
1024
1287
  if (folderName.startsWith("_") && folderName.includes("@shared")) {
1025
- return import_path10.default.dirname(current).replace(/\\/g, "/");
1288
+ return import_path13.default.dirname(current).replace(/\\/g, "/");
1026
1289
  }
1027
- const parent = import_path10.default.dirname(current).replace(/\\/g, "/");
1290
+ const parent = import_path13.default.dirname(current).replace(/\\/g, "/");
1028
1291
  if (parent === current) {
1029
1292
  break;
1030
1293
  }
@@ -1044,11 +1307,11 @@ function getImmediateSubDomain(relPath, privateCategoryPath) {
1044
1307
  }
1045
1308
 
1046
1309
  // src/_rules/_rules@shared/_utils/_systems/get-private-category-info/get-private-category-info.util.ts
1047
- var import_path11 = __toESM(require("path"), 1);
1310
+ var import_path14 = __toESM(require("path"), 1);
1048
1311
  function getPrivateCategoryInfo(relPath, config) {
1049
1312
  let current = relPath;
1050
1313
  while (current && current !== "." && current !== "/") {
1051
- const parent = import_path11.default.dirname(current).replace(/\\/g, "/");
1314
+ const parent = import_path14.default.dirname(current).replace(/\\/g, "/");
1052
1315
  if (parent === current) {
1053
1316
  break;
1054
1317
  }
@@ -1063,7 +1326,22 @@ function getPrivateCategoryInfo(relPath, config) {
1063
1326
  }
1064
1327
 
1065
1328
  // src/_rules/no-fractal-branch-leak/no-fractal-branch-leak.rule.ts
1066
- var rule5 = {
1329
+ function getFractalBranchPath(relPath) {
1330
+ let current = relPath;
1331
+ while (current && current !== "." && current !== "/") {
1332
+ const folderName = import_path15.default.basename(current);
1333
+ if (folderName.startsWith("_") && folderName.includes("@shared")) {
1334
+ return current;
1335
+ }
1336
+ const parent = import_path15.default.dirname(current).replace(/\\/g, "/");
1337
+ if (parent === current) {
1338
+ break;
1339
+ }
1340
+ current = parent;
1341
+ }
1342
+ return null;
1343
+ }
1344
+ var rule8 = {
1067
1345
  create(context) {
1068
1346
  const absPath = context.filename;
1069
1347
  const relPath = toRelativePath(absPath);
@@ -1084,7 +1362,7 @@ var rule5 = {
1084
1362
  if (!resolvedRelPath) {
1085
1363
  return;
1086
1364
  }
1087
- const importedDir = import_path12.default.dirname(resolvedRelPath);
1365
+ const importedDir = import_path15.default.dirname(resolvedRelPath);
1088
1366
  const fractalBranchOwner = getFractalBranchOwner(importedDir);
1089
1367
  if (fractalBranchOwner) {
1090
1368
  const isDescendant = relPath === fractalBranchOwner || relPath.startsWith(fractalBranchOwner + "/");
@@ -1097,7 +1375,7 @@ var rule5 = {
1097
1375
  const info = getPrivateCategoryInfo(importedDir, config);
1098
1376
  if (info) {
1099
1377
  const { owner, privateCategoryPath } = info;
1100
- const isPrivateDirectChild = import_path12.default.dirname(relPath) === owner;
1378
+ const isPrivateDirectChild = import_path15.default.dirname(relPath) === owner;
1101
1379
  const importerSubDomain = getImmediateSubDomain(
1102
1380
  relPath,
1103
1381
  privateCategoryPath
@@ -1117,6 +1395,18 @@ var rule5 = {
1117
1395
  }
1118
1396
  }
1119
1397
  }
1398
+ const importerBranchPath = getFractalBranchPath(import_path15.default.dirname(relPath));
1399
+ if (importerBranchPath) {
1400
+ const importerBranchOwner = import_path15.default.dirname(importerBranchPath).replace(/\\/g, "/");
1401
+ const isImportedInsideOwnBranch = resolvedRelPath === importerBranchPath || resolvedRelPath.startsWith(importerBranchPath + "/");
1402
+ const isImportedInsideServedSubtree = resolvedRelPath === importerBranchOwner || resolvedRelPath.startsWith(importerBranchOwner + "/");
1403
+ if (isImportedInsideServedSubtree && !isImportedInsideOwnBranch) {
1404
+ context.report({
1405
+ message: `Fractal Branch "${importerBranchPath}" cannot depend on "${resolvedRelPath}" from the subtree it serves. A Fractal Branch may only provide implementation details to its parent scope; it must never depend on it, not even indirectly.`,
1406
+ node
1407
+ });
1408
+ }
1409
+ }
1120
1410
  }
1121
1411
  return {
1122
1412
  ExportAllDeclaration(node) {
@@ -1140,33 +1430,10 @@ var rule5 = {
1140
1430
  type: "problem"
1141
1431
  }
1142
1432
  };
1143
- var no_fractal_branch_leak_rule_default = rule5;
1433
+ var no_fractal_branch_leak_rule_default = rule8;
1144
1434
 
1145
1435
  // src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
1146
- var import_path13 = __toESM(require("path"), 1);
1147
-
1148
- // src/_rules/_rules@shared/_utils/_primitives/find-tree-config-including-excluded/find-tree-config-including-excluded.util.ts
1149
- function findTreeConfigIncludingExcluded(relPath, settings) {
1150
- if (!settings || !settings.trees) {
1151
- return null;
1152
- }
1153
- const cached = state.treeConfigIncludingExcludedCache.get(relPath);
1154
- if (cached !== void 0) {
1155
- return cached;
1156
- }
1157
- let result = null;
1158
- for (const tree of settings.trees) {
1159
- const isIncluded = tree.includes.some(
1160
- (inc) => relPath === inc || relPath.startsWith(inc + "/")
1161
- );
1162
- if (isIncluded) {
1163
- result = tree;
1164
- break;
1165
- }
1166
- }
1167
- state.treeConfigIncludingExcludedCache.set(relPath, result);
1168
- return result;
1169
- }
1436
+ var import_path18 = __toESM(require("path"), 1);
1170
1437
 
1171
1438
  // src/_rules/_rules@shared/_utils/_primitives/get-lca-and-sub-branches/get-lca-and-sub-branches.util.ts
1172
1439
  function getLcaAndSubBranches(pathA, pathB) {
@@ -1196,6 +1463,19 @@ function getRoleHierarchyIndex(role, config) {
1196
1463
  return -1;
1197
1464
  }
1198
1465
 
1466
+ // src/_rules/_rules@shared/_utils/_primitives/get-root-node-index/get-root-node-index.util.ts
1467
+ var import_path16 = __toESM(require("path"), 1);
1468
+ function getRootNodeIndex(relPath, rfConfig, lcaPath) {
1469
+ const relToLca = import_path16.default.relative(lcaPath, relPath).replace(/\\/g, "/");
1470
+ for (let i = 0; i < rfConfig.rootNodes.length; i++) {
1471
+ const group = rfConfig.rootNodes[i];
1472
+ if (group && group.includes(relToLca)) {
1473
+ return i;
1474
+ }
1475
+ }
1476
+ return -1;
1477
+ }
1478
+
1199
1479
  // src/_rules/_rules@shared/_utils/_primitives/resolve-horizontal-hierarchy/resolve-horizontal-hierarchy.util.ts
1200
1480
  function resolveHorizontalHierarchy(lcaPath, subA, subB, config) {
1201
1481
  const local = config.localHorizontalHierarchies?.find(
@@ -1219,39 +1499,12 @@ function resolveHorizontalHierarchy(lcaPath, subA, subB, config) {
1219
1499
  return { allowed: false, defined: false };
1220
1500
  }
1221
1501
 
1222
- // src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
1223
- function getDirectoryRole(dirPath, config) {
1224
- const folderName = import_path13.default.basename(dirPath);
1225
- if (folderName.startsWith("_")) {
1226
- return getRoleFromFolderName(folderName, config);
1227
- }
1228
- const contents = readDirCached(dirPath);
1229
- for (const file of contents.files) {
1230
- if (file.startsWith(folderName + ".")) {
1231
- const role = getFileRole(file, config);
1232
- if (role) return role;
1233
- }
1234
- }
1235
- return null;
1236
- }
1237
- function getRoleFromFolderName(name, config) {
1238
- const catConfig = config.categories?.find((c) => c.name === name);
1239
- return catConfig ? catConfig.role ?? null : null;
1240
- }
1241
- function getRootNodeIndex(relPath, rfConfig, lcaPath) {
1242
- const relToLca = import_path13.default.relative(lcaPath, relPath).replace(/\\/g, "/");
1243
- for (let i = 0; i < rfConfig.rootNodes.length; i++) {
1244
- const group = rfConfig.rootNodes[i];
1245
- if (group && group.includes(relToLca)) {
1246
- return i;
1247
- }
1248
- }
1249
- return -1;
1250
- }
1502
+ // src/_rules/_rules@shared/_utils/_systems/is-inside-private-category-of-lca/is-inside-private-category-of-lca.util.ts
1503
+ var import_path17 = __toESM(require("path"), 1);
1251
1504
  function isInsidePrivateCategoryOfLca(relPathB, lcaPath, config) {
1252
1505
  let current = relPathB;
1253
1506
  while (current && current !== "." && current !== "/" && current !== lcaPath) {
1254
- const parent = import_path13.default.dirname(current).replace(/\\/g, "/");
1507
+ const parent = import_path17.default.dirname(current).replace(/\\/g, "/");
1255
1508
  if (parent === current) {
1256
1509
  break;
1257
1510
  }
@@ -1264,7 +1517,9 @@ function isInsidePrivateCategoryOfLca(relPathB, lcaPath, config) {
1264
1517
  }
1265
1518
  return false;
1266
1519
  }
1267
- var rule6 = {
1520
+
1521
+ // src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
1522
+ var rule9 = {
1268
1523
  create(context) {
1269
1524
  const absPath = context.filename;
1270
1525
  const relPath = toRelativePath(absPath);
@@ -1272,11 +1527,11 @@ var rule6 = {
1272
1527
  if (!settings.faf) {
1273
1528
  return {};
1274
1529
  }
1275
- const config = findTreeConfigIncludingExcluded(relPath, settings.faf);
1530
+ const config = findTreeConfig(relPath, settings.faf);
1276
1531
  if (!config) {
1277
1532
  return {};
1278
1533
  }
1279
- const currentDir = import_path13.default.dirname(relPath);
1534
+ const currentDir = import_path18.default.dirname(relPath);
1280
1535
  const isCurrentFileExcluded = config.excludes?.some(
1281
1536
  (exc) => relPath === exc || relPath.startsWith(exc + "/")
1282
1537
  ) ?? false;
@@ -1292,27 +1547,13 @@ var rule6 = {
1292
1547
  const isImportedFileExcluded = config?.excludes?.some(
1293
1548
  (exc) => resolvedRelPath === exc || resolvedRelPath.startsWith(exc + "/")
1294
1549
  ) ?? false;
1295
- if (!isCurrentFileExcluded && isImportedFileExcluded) {
1296
- context.report({
1297
- message: `Importing from Foreign Domain "${resolvedRelPath}" is forbidden for Logical Nodes.`,
1298
- node
1299
- });
1300
- return;
1301
- }
1302
- if (isCurrentFileExcluded && !isImportedFileExcluded) {
1303
- context.report({
1304
- message: `Foreign Domain files cannot import Logical Nodes from FAF tree ("${resolvedRelPath}").`,
1305
- node
1306
- });
1307
- return;
1308
- }
1309
1550
  if (isCurrentFileExcluded || isImportedFileExcluded) {
1310
1551
  return;
1311
1552
  }
1312
- const importedDir = import_path13.default.dirname(resolvedRelPath);
1553
+ const importedDir = import_path18.default.dirname(resolvedRelPath);
1313
1554
  if (currentDir === importedDir) {
1314
- const fileA = import_path13.default.basename(relPath);
1315
- const fileB = import_path13.default.basename(resolvedRelPath);
1555
+ const fileA = import_path18.default.basename(relPath);
1556
+ const fileB = import_path18.default.basename(resolvedRelPath);
1316
1557
  const roleA = getFileRole(fileA, config);
1317
1558
  const roleB = getFileRole(fileB, config);
1318
1559
  if (roleA && roleB) {
@@ -1352,22 +1593,12 @@ var rule6 = {
1352
1593
  const idxRootA = getRootNodeIndex(relPath, rfConfig, lca);
1353
1594
  const idxRootB = getRootNodeIndex(resolvedRelPath, rfConfig, lca);
1354
1595
  if (idxRootA !== -1 && idxRootB !== -1) {
1355
- if (idxRootB >= idxRootA) {
1356
- context.report({
1357
- message: `Root Node import violation: "${import_path13.default.basename(relPath)}" cannot import from "${import_path13.default.basename(resolvedRelPath)}" under Root Fragment "${lca}".`,
1358
- node
1359
- });
1360
- }
1361
1596
  return;
1362
1597
  }
1363
1598
  }
1364
- const isSubARootFragment = classifyFolder(import_path13.default.posix.join(lca, subA), config) === "root-fragment";
1365
- const isSubBRootFragment = classifyFolder(import_path13.default.posix.join(lca, subB), config) === "root-fragment";
1599
+ const isSubARootFragment = classifyFolder(import_path18.default.posix.join(lca, subA), config) === "root-fragment";
1600
+ const isSubBRootFragment = classifyFolder(import_path18.default.posix.join(lca, subB), config) === "root-fragment";
1366
1601
  if (isSubARootFragment || isSubBRootFragment) {
1367
- context.report({
1368
- message: `Root Node import violation: "${relPath}" cannot import from "${resolvedRelPath}" because no "rootNodes" relationship is configured under Root Fragment "${lca}". Relationships between Root Nodes must always be explicitly authorized by the architect.`,
1369
- node
1370
- });
1371
1602
  return;
1372
1603
  }
1373
1604
  const { allowed, defined: hasDefinedHierarchy } = resolveHorizontalHierarchy(lca, subA, subB, config);
@@ -1379,23 +1610,10 @@ var rule6 = {
1379
1610
  });
1380
1611
  }
1381
1612
  } else {
1382
- const roleA = getDirectoryRole(import_path13.default.posix.join(lca, subA), config);
1383
- const roleB = getDirectoryRole(import_path13.default.posix.join(lca, subB), config);
1384
- const idxA = roleA ? getRoleHierarchyIndex(roleA, config) : -1;
1385
- const idxB = roleB ? getRoleHierarchyIndex(roleB, config) : -1;
1386
- if (idxA !== -1 && idxB !== -1) {
1387
- if (idxB >= idxA) {
1388
- context.report({
1389
- message: `Horizontal hierarchy violation (fallback to roles): sibling directory "${subA}" (mapped to role "${roleA}", level ${idxA}) cannot import from "${subB}" (mapped to role "${roleB}", level ${idxB}) under parent "${lca}".`,
1390
- node
1391
- });
1392
- }
1393
- } else {
1394
- context.report({
1395
- message: `Peer separation violation: sibling directories "${subA}" and "${subB}" cannot import each other because no horizontal hierarchy is defined under parent "${lca}".`,
1396
- node
1397
- });
1398
- }
1613
+ context.report({
1614
+ message: `Peer separation violation: sibling directories "${subA}" and "${subB}" cannot import each other because no horizontal hierarchy is defined under parent "${lca}".`,
1615
+ node
1616
+ });
1399
1617
  }
1400
1618
  }
1401
1619
  return {
@@ -1420,11 +1638,11 @@ var rule6 = {
1420
1638
  type: "problem"
1421
1639
  }
1422
1640
  };
1423
- var no_peer_dependency_rule_default = rule6;
1641
+ var no_peer_dependency_rule_default = rule9;
1424
1642
 
1425
1643
  // src/_rules/no-private-category-leak/no-private-category-leak.rule.ts
1426
- var import_path14 = __toESM(require("path"), 1);
1427
- var rule7 = {
1644
+ var import_path19 = __toESM(require("path"), 1);
1645
+ var rule10 = {
1428
1646
  create(context) {
1429
1647
  const absPath = context.filename;
1430
1648
  const relPath = toRelativePath(absPath);
@@ -1445,7 +1663,7 @@ var rule7 = {
1445
1663
  if (!resolvedRelPath) {
1446
1664
  return;
1447
1665
  }
1448
- const importedDir = import_path14.default.dirname(resolvedRelPath);
1666
+ const importedDir = import_path19.default.dirname(resolvedRelPath);
1449
1667
  const info = getPrivateCategoryInfo(importedDir, config);
1450
1668
  if (info) {
1451
1669
  const { owner, privateCategoryPath } = info;
@@ -1459,7 +1677,7 @@ var rule7 = {
1459
1677
  isFractalBranchAllowed = relPath === fbOwner || relPath.startsWith(fbOwner + "/");
1460
1678
  }
1461
1679
  }
1462
- const isDirectChild = import_path14.default.dirname(relPath) === owner;
1680
+ const isDirectChild = import_path19.default.dirname(relPath) === owner;
1463
1681
  const importerSubDomain = getImmediateSubDomain(
1464
1682
  relPath,
1465
1683
  privateCategoryPath
@@ -1500,17 +1718,119 @@ var rule7 = {
1500
1718
  type: "problem"
1501
1719
  }
1502
1720
  };
1503
- var no_private_category_leak_rule_default = rule7;
1721
+ var no_private_category_leak_rule_default = rule10;
1722
+
1723
+ // src/_rules/root-node-dependency-direction/root-node-dependency-direction.rule.ts
1724
+ var import_path20 = __toESM(require("path"), 1);
1725
+ var rule11 = {
1726
+ create(context) {
1727
+ const absPath = context.filename;
1728
+ const relPath = toRelativePath(absPath);
1729
+ const settings = context.settings;
1730
+ if (!settings.faf) {
1731
+ return {};
1732
+ }
1733
+ const config = findTreeConfig(relPath, settings.faf);
1734
+ if (!config) {
1735
+ return {};
1736
+ }
1737
+ const currentDir = import_path20.default.dirname(relPath);
1738
+ const isCurrentFileExcluded = config.excludes?.some(
1739
+ (exc) => relPath === exc || relPath.startsWith(exc + "/")
1740
+ ) ?? false;
1741
+ function checkImport(node, importValue) {
1742
+ const resolvedRelPath = resolveImportPath(
1743
+ importValue,
1744
+ relPath,
1745
+ settings.faf
1746
+ );
1747
+ if (!resolvedRelPath) {
1748
+ return;
1749
+ }
1750
+ const isImportedFileExcluded = config?.excludes?.some(
1751
+ (exc) => resolvedRelPath === exc || resolvedRelPath.startsWith(exc + "/")
1752
+ ) ?? false;
1753
+ if (isCurrentFileExcluded || isImportedFileExcluded) {
1754
+ return;
1755
+ }
1756
+ const importedDir = import_path20.default.dirname(resolvedRelPath);
1757
+ if (currentDir === importedDir) {
1758
+ return;
1759
+ }
1760
+ const lcaInfo = getLcaAndSubBranches(currentDir, importedDir);
1761
+ if (!lcaInfo) {
1762
+ return;
1763
+ }
1764
+ const { lca, subA, subB } = lcaInfo;
1765
+ if (subB.startsWith("_") && subB.includes("@shared")) {
1766
+ return;
1767
+ }
1768
+ if (isInsidePrivateCategoryOfLca(importedDir, lca, config)) {
1769
+ return;
1770
+ }
1771
+ if (subB === "" && classifyFolder(lca, config) === "fragment") {
1772
+ return;
1773
+ }
1774
+ const rfConfig = getRootFragmentConfig(lca, config);
1775
+ if (rfConfig) {
1776
+ const idxRootA = getRootNodeIndex(relPath, rfConfig, lca);
1777
+ const idxRootB = getRootNodeIndex(resolvedRelPath, rfConfig, lca);
1778
+ if (idxRootA !== -1 && idxRootB !== -1) {
1779
+ if (idxRootB >= idxRootA) {
1780
+ context.report({
1781
+ message: `Root Node import violation: "${import_path20.default.basename(relPath)}" cannot import from "${import_path20.default.basename(resolvedRelPath)}" under Root Fragment "${lca}".`,
1782
+ node
1783
+ });
1784
+ }
1785
+ return;
1786
+ }
1787
+ }
1788
+ const isSubARootFragment = classifyFolder(import_path20.default.posix.join(lca, subA), config) === "root-fragment";
1789
+ const isSubBRootFragment = classifyFolder(import_path20.default.posix.join(lca, subB), config) === "root-fragment";
1790
+ if (isSubARootFragment || isSubBRootFragment) {
1791
+ context.report({
1792
+ message: `Root Node import violation: "${relPath}" cannot import from "${resolvedRelPath}" because no "rootNodes" relationship is configured under Root Fragment "${lca}". Relationships between Root Nodes must always be explicitly authorized by the architect.`,
1793
+ node
1794
+ });
1795
+ }
1796
+ }
1797
+ return {
1798
+ ExportAllDeclaration(node) {
1799
+ checkImport(node, node.source.value);
1800
+ },
1801
+ ExportNamedDeclaration(node) {
1802
+ if (node.source) {
1803
+ checkImport(node, node.source.value);
1804
+ }
1805
+ },
1806
+ ImportDeclaration(node) {
1807
+ checkImport(node, node.source.value);
1808
+ }
1809
+ };
1810
+ },
1811
+ meta: {
1812
+ docs: {
1813
+ description: "Enforce explicit, non-Role-based dependency direction between Root Nodes"
1814
+ },
1815
+ schema: [],
1816
+ type: "problem"
1817
+ }
1818
+ };
1819
+ var root_node_dependency_direction_rule_default = rule11;
1504
1820
 
1505
1821
  // src/rules.ts
1506
1822
  var rules = {
1507
1823
  "category-mutually-exclusive": category_mutually_exclusive_rule_default,
1508
1824
  "enforce-access-node": enforce_access_node_rule_default,
1825
+ "foreign-domain-isolation": foreign_domain_isolation_rule_default,
1826
+ "fragment-master-node": fragment_master_node_rule_default,
1827
+ "logical-domain-placement": logical_domain_placement_rule_default,
1509
1828
  "naming-conventions": naming_conventions_rule_default,
1510
1829
  "no-direct-fragment-import": no_direct_fragment_import_rule_default,
1511
1830
  "no-fractal-branch-leak": no_fractal_branch_leak_rule_default,
1512
1831
  "no-peer-dependency": no_peer_dependency_rule_default,
1513
- "no-private-category-leak": no_private_category_leak_rule_default
1832
+ "no-private-category-leak": no_private_category_leak_rule_default,
1833
+ "root-node-dependency-direction": root_node_dependency_direction_rule_default
1514
1834
  };
1515
1835
 
1516
1836
  // src/configs.ts
@@ -1524,11 +1844,15 @@ var configs = {
1524
1844
  rules: {
1525
1845
  "faf/category-mutually-exclusive": "error",
1526
1846
  "faf/enforce-access-node": "error",
1847
+ "faf/foreign-domain-isolation": "error",
1848
+ "faf/fragment-master-node": "error",
1849
+ "faf/logical-domain-placement": "error",
1527
1850
  "faf/naming-conventions": "error",
1528
1851
  "faf/no-direct-fragment-import": "error",
1529
1852
  "faf/no-fractal-branch-leak": "error",
1530
1853
  "faf/no-peer-dependency": "error",
1531
- "faf/no-private-category-leak": "error"
1854
+ "faf/no-private-category-leak": "error",
1855
+ "faf/root-node-dependency-direction": "error"
1532
1856
  }
1533
1857
  }
1534
1858
  };