@morriconeluca/eslint-plugin-faf 0.4.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.4.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) {
815
+ return;
816
+ }
817
+ const config = findTreeConfig(relPath, settings.faf);
818
+ if (!config) {
506
819
  return;
507
820
  }
508
- const relDir = import_path7.default.dirname(relPath);
509
- const folderName = import_path7.default.basename(relDir);
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,309 +1034,187 @@ 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) {
655
1066
  return;
656
1067
  }
657
- const rfConfig = getRootFragmentConfig(relDir, config);
658
- if (rfConfig) {
659
- const flatRootNodes = rfConfig.rootNodes.flat();
660
- const isMatchedRootNode = flatRootNodes.some((rn) => {
661
- if (rn.includes("/")) {
662
- const rfPath = rfConfig.paths[0] ?? "";
663
- const absRn = import_path7.default.resolve(getProjectRoot(), rfPath, rn);
664
- return toRelativePath(absRn) === relPath;
665
- }
666
- return rn === fileName;
667
- });
668
- if (isMatchedRootNode) {
669
- return;
670
- }
1068
+ const relDir = import_path11.default.dirname(relPath);
1069
+ const parentType = classifyFolder(relDir, config);
1070
+ if (parentType === "foreign") {
1071
+ return;
671
1072
  }
672
- if (fileName === "index.ts" || fileName === "index.js") {
673
- if (parentType !== "fragment") {
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("_")) {
674
1080
  context.report({
675
- message: `Access Nodes ("index.ts/index.js") are exclusive to Fragments. Found index file directly inside "${folderName}" (classified as ${parentType}).`,
1081
+ message: `Layer/Category directory "${currentFolderName}" must be prefixed with an underscore (e.g. "_${currentFolderName}").`,
676
1082
  node
677
1083
  });
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
1084
  }
690
- return;
691
- }
692
- if (fileName === "README.md" || fileName === "package.json" || fileName === "tsconfig.json") {
693
- return;
694
- }
695
- const role = getFileRole(fileName, config);
696
- const ext = import_path7.default.extname(fileName);
697
- const allRoleNames = new Set(config.roles.flat());
698
- const roleSegments = fileName.slice(0, fileName.length - ext.length).split(".").slice(1).filter((part) => allRoleNames.has(part));
699
- 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)) {
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) {
718
1089
  context.report({
719
- message: `Route terminal Fragment "${folderName}" must follow the "<method>-<object>" pattern with a lowercase HTTP method (${httpMethods.join(", ")}).`,
1090
+ message: `Directory "${currentFolderName}" has an Access Node (index.ts) but its name starts with "_". Fragments must not be prefixed with underscore.`,
720
1091
  node
721
1092
  });
722
1093
  }
723
1094
  }
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;
1095
+ if (currentFolderName !== "src") {
1096
+ let baseFolderName = currentFolderName.replace(/@shared$/, "");
1097
+ if (baseFolderName.startsWith("_")) {
1098
+ baseFolderName = baseFolderName.substring(1);
735
1099
  }
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)) {
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) {
744
1108
  context.report({
745
- message: `Fragment "${folderName}" inside Category "${categoryFolderName}" must contain a Master Node with role "${categoryConfig.role}" (e.g. "${folderName}.${categoryConfig.role}.ts").`,
1109
+ message: `Folder name "${currentFolderName}" must be in kebab-case (e.g. "_my-folder" or "_[my-param]").`,
746
1110
  node
747
1111
  });
748
1112
  }
749
1113
  }
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) {
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) {
769
1119
  context.report({
770
- message: `Fragment Node "${fileName}" shares Role "${role}" with sibling "${duplicate}". Each Fragment Node must have a unique Role within its Fragment.`,
1120
+ message: `Fractal Branch name "${currentFolderName}" must match its parent scope name: "${expectedName}".`,
771
1121
  node
772
1122
  });
773
1123
  }
774
1124
  }
775
1125
  }
776
- if (parentType === "category") {
777
- const catConfig = getCategoryConfig(relDir, config);
778
- if (catConfig) {
779
- const allowedExts = catConfig.allowedExtensions ?? [
780
- ".js",
781
- ".jsx",
782
- ".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
- }
1126
+ if (parentType === "layer" || parentType === "fractal-branch" || parentType === "invalid-fragment") {
1127
+ return;
1128
+ }
1129
+ const rfConfig = getRootFragmentConfig(relDir, config);
1130
+ if (rfConfig) {
1131
+ const flatRootNodes = rfConfig.rootNodes.flat();
1132
+ const isMatchedRootNode = flatRootNodes.some((rn) => {
1133
+ if (rn.includes("/")) {
1134
+ const rfPath = rfConfig.paths[0] ?? "";
1135
+ const absRn = import_path11.default.resolve(getProjectRoot(), rfPath, rn);
1136
+ return toRelativePath(absRn) === relPath;
816
1137
  }
1138
+ return rn === fileName;
1139
+ });
1140
+ if (isMatchedRootNode) {
1141
+ return;
817
1142
  }
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);
1143
+ }
1144
+ if (fileName === "index.ts" || fileName === "index.js") {
1145
+ return;
1146
+ }
1147
+ if (fileName === "README.md" || fileName === "package.json" || fileName === "tsconfig.json") {
1148
+ return;
1149
+ }
1150
+ const role = getFileRole(fileName, config);
1151
+ const ext = import_path11.default.extname(fileName);
1152
+ const allRoleNames = new Set(config.roles.flat());
1153
+ const roleSegments = fileName.slice(0, fileName.length - ext.length).split(".").slice(1).filter((part) => allRoleNames.has(part));
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.`;
1155
+ if (parentType === "fragment" && role && roleSegments.length > 1) {
1156
+ context.report({ message: multiRoleMessage, node });
1157
+ }
1158
+ if (parentType === "category") {
1159
+ const folderName = import_path11.default.basename(relDir);
1160
+ const catConfig = getCategoryConfig(relDir, config);
1161
+ if (catConfig) {
1162
+ const allowedExts = catConfig.allowedExtensions ?? [
1163
+ ".js",
1164
+ ".jsx",
1165
+ ".ts",
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,21 +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 getRootNodeIndex(relPath, rfConfig, lcaPath) {
1224
- const relToLca = import_path13.default.relative(lcaPath, relPath).replace(/\\/g, "/");
1225
- for (let i = 0; i < rfConfig.rootNodes.length; i++) {
1226
- const group = rfConfig.rootNodes[i];
1227
- if (group && group.includes(relToLca)) {
1228
- return i;
1229
- }
1230
- }
1231
- return -1;
1232
- }
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);
1233
1504
  function isInsidePrivateCategoryOfLca(relPathB, lcaPath, config) {
1234
1505
  let current = relPathB;
1235
1506
  while (current && current !== "." && current !== "/" && current !== lcaPath) {
1236
- const parent = import_path13.default.dirname(current).replace(/\\/g, "/");
1507
+ const parent = import_path17.default.dirname(current).replace(/\\/g, "/");
1237
1508
  if (parent === current) {
1238
1509
  break;
1239
1510
  }
@@ -1246,7 +1517,9 @@ function isInsidePrivateCategoryOfLca(relPathB, lcaPath, config) {
1246
1517
  }
1247
1518
  return false;
1248
1519
  }
1249
- var rule6 = {
1520
+
1521
+ // src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
1522
+ var rule9 = {
1250
1523
  create(context) {
1251
1524
  const absPath = context.filename;
1252
1525
  const relPath = toRelativePath(absPath);
@@ -1254,11 +1527,11 @@ var rule6 = {
1254
1527
  if (!settings.faf) {
1255
1528
  return {};
1256
1529
  }
1257
- const config = findTreeConfigIncludingExcluded(relPath, settings.faf);
1530
+ const config = findTreeConfig(relPath, settings.faf);
1258
1531
  if (!config) {
1259
1532
  return {};
1260
1533
  }
1261
- const currentDir = import_path13.default.dirname(relPath);
1534
+ const currentDir = import_path18.default.dirname(relPath);
1262
1535
  const isCurrentFileExcluded = config.excludes?.some(
1263
1536
  (exc) => relPath === exc || relPath.startsWith(exc + "/")
1264
1537
  ) ?? false;
@@ -1274,27 +1547,13 @@ var rule6 = {
1274
1547
  const isImportedFileExcluded = config?.excludes?.some(
1275
1548
  (exc) => resolvedRelPath === exc || resolvedRelPath.startsWith(exc + "/")
1276
1549
  ) ?? false;
1277
- if (!isCurrentFileExcluded && isImportedFileExcluded) {
1278
- context.report({
1279
- message: `Importing from Foreign Domain "${resolvedRelPath}" is forbidden for Logical Nodes.`,
1280
- node
1281
- });
1282
- return;
1283
- }
1284
- if (isCurrentFileExcluded && !isImportedFileExcluded) {
1285
- context.report({
1286
- message: `Foreign Domain files cannot import Logical Nodes from FAF tree ("${resolvedRelPath}").`,
1287
- node
1288
- });
1289
- return;
1290
- }
1291
1550
  if (isCurrentFileExcluded || isImportedFileExcluded) {
1292
1551
  return;
1293
1552
  }
1294
- const importedDir = import_path13.default.dirname(resolvedRelPath);
1553
+ const importedDir = import_path18.default.dirname(resolvedRelPath);
1295
1554
  if (currentDir === importedDir) {
1296
- const fileA = import_path13.default.basename(relPath);
1297
- const fileB = import_path13.default.basename(resolvedRelPath);
1555
+ const fileA = import_path18.default.basename(relPath);
1556
+ const fileB = import_path18.default.basename(resolvedRelPath);
1298
1557
  const roleA = getFileRole(fileA, config);
1299
1558
  const roleB = getFileRole(fileB, config);
1300
1559
  if (roleA && roleB) {
@@ -1334,22 +1593,12 @@ var rule6 = {
1334
1593
  const idxRootA = getRootNodeIndex(relPath, rfConfig, lca);
1335
1594
  const idxRootB = getRootNodeIndex(resolvedRelPath, rfConfig, lca);
1336
1595
  if (idxRootA !== -1 && idxRootB !== -1) {
1337
- if (idxRootB >= idxRootA) {
1338
- context.report({
1339
- message: `Root Node import violation: "${import_path13.default.basename(relPath)}" cannot import from "${import_path13.default.basename(resolvedRelPath)}" under Root Fragment "${lca}".`,
1340
- node
1341
- });
1342
- }
1343
1596
  return;
1344
1597
  }
1345
1598
  }
1346
- const isSubARootFragment = classifyFolder(import_path13.default.posix.join(lca, subA), config) === "root-fragment";
1347
- 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";
1348
1601
  if (isSubARootFragment || isSubBRootFragment) {
1349
- context.report({
1350
- 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.`,
1351
- node
1352
- });
1353
1602
  return;
1354
1603
  }
1355
1604
  const { allowed, defined: hasDefinedHierarchy } = resolveHorizontalHierarchy(lca, subA, subB, config);
@@ -1389,11 +1638,11 @@ var rule6 = {
1389
1638
  type: "problem"
1390
1639
  }
1391
1640
  };
1392
- var no_peer_dependency_rule_default = rule6;
1641
+ var no_peer_dependency_rule_default = rule9;
1393
1642
 
1394
1643
  // src/_rules/no-private-category-leak/no-private-category-leak.rule.ts
1395
- var import_path14 = __toESM(require("path"), 1);
1396
- var rule7 = {
1644
+ var import_path19 = __toESM(require("path"), 1);
1645
+ var rule10 = {
1397
1646
  create(context) {
1398
1647
  const absPath = context.filename;
1399
1648
  const relPath = toRelativePath(absPath);
@@ -1414,7 +1663,7 @@ var rule7 = {
1414
1663
  if (!resolvedRelPath) {
1415
1664
  return;
1416
1665
  }
1417
- const importedDir = import_path14.default.dirname(resolvedRelPath);
1666
+ const importedDir = import_path19.default.dirname(resolvedRelPath);
1418
1667
  const info = getPrivateCategoryInfo(importedDir, config);
1419
1668
  if (info) {
1420
1669
  const { owner, privateCategoryPath } = info;
@@ -1428,7 +1677,7 @@ var rule7 = {
1428
1677
  isFractalBranchAllowed = relPath === fbOwner || relPath.startsWith(fbOwner + "/");
1429
1678
  }
1430
1679
  }
1431
- const isDirectChild = import_path14.default.dirname(relPath) === owner;
1680
+ const isDirectChild = import_path19.default.dirname(relPath) === owner;
1432
1681
  const importerSubDomain = getImmediateSubDomain(
1433
1682
  relPath,
1434
1683
  privateCategoryPath
@@ -1469,17 +1718,119 @@ var rule7 = {
1469
1718
  type: "problem"
1470
1719
  }
1471
1720
  };
1472
- 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;
1473
1820
 
1474
1821
  // src/rules.ts
1475
1822
  var rules = {
1476
1823
  "category-mutually-exclusive": category_mutually_exclusive_rule_default,
1477
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,
1478
1828
  "naming-conventions": naming_conventions_rule_default,
1479
1829
  "no-direct-fragment-import": no_direct_fragment_import_rule_default,
1480
1830
  "no-fractal-branch-leak": no_fractal_branch_leak_rule_default,
1481
1831
  "no-peer-dependency": no_peer_dependency_rule_default,
1482
- "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
1483
1834
  };
1484
1835
 
1485
1836
  // src/configs.ts
@@ -1493,11 +1844,15 @@ var configs = {
1493
1844
  rules: {
1494
1845
  "faf/category-mutually-exclusive": "error",
1495
1846
  "faf/enforce-access-node": "error",
1847
+ "faf/foreign-domain-isolation": "error",
1848
+ "faf/fragment-master-node": "error",
1849
+ "faf/logical-domain-placement": "error",
1496
1850
  "faf/naming-conventions": "error",
1497
1851
  "faf/no-direct-fragment-import": "error",
1498
1852
  "faf/no-fractal-branch-leak": "error",
1499
1853
  "faf/no-peer-dependency": "error",
1500
- "faf/no-private-category-leak": "error"
1854
+ "faf/no-private-category-leak": "error",
1855
+ "faf/root-node-dependency-direction": "error"
1501
1856
  }
1502
1857
  }
1503
1858
  };