@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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // package.json
2
2
  var package_default = {
3
3
  name: "@morriconeluca/eslint-plugin-faf",
4
- version: "0.3.0",
4
+ version: "0.5.0",
5
5
  description: "An ESLint plugin for FAF projects.",
6
6
  repository: {
7
7
  type: "git",
@@ -89,6 +89,7 @@ import path3 from "path";
89
89
 
90
90
  // src/_rules/_rules@shared/_utils/_utils@shared/_stores/cache/cache.store.ts
91
91
  var state = {
92
+ ancestorChainCache: /* @__PURE__ */ new Map(),
92
93
  categoryConfigCache: /* @__PURE__ */ new Map(),
93
94
  classifyCache: /* @__PURE__ */ new Map(),
94
95
  dirCache: {},
@@ -345,27 +346,139 @@ var rule = {
345
346
  var category_mutually_exclusive_rule_default = rule;
346
347
 
347
348
  // src/_rules/enforce-access-node/enforce-access-node.rule.ts
349
+ import path7 from "path";
350
+
351
+ // src/_rules/_rules@shared/_utils/_primitives/get-project-root/get-project-root.util.ts
352
+ function getProjectRoot() {
353
+ return state.projectRoot;
354
+ }
355
+
356
+ // src/_rules/_rules@shared/_utils/_systems/walk-ancestors/walk-ancestors.util.ts
348
357
  import path6 from "path";
358
+ function walkAncestors(relDir, config) {
359
+ const cached = state.ancestorChainCache.get(relDir);
360
+ if (cached !== void 0) {
361
+ return cached;
362
+ }
363
+ const result = [];
364
+ let currentDir = relDir;
365
+ while (currentDir && currentDir !== "." && currentDir !== "/") {
366
+ const isInTree = config.includes.some(
367
+ (inc) => currentDir === inc || currentDir.startsWith(inc + "/")
368
+ );
369
+ const isExcluded = config.excludes?.some(
370
+ (exc) => currentDir === exc || currentDir.startsWith(exc + "/")
371
+ ) ?? false;
372
+ if (!isInTree || isExcluded) {
373
+ break;
374
+ }
375
+ const folderName = path6.basename(currentDir);
376
+ const type = classifyFolder(currentDir, config);
377
+ const parentDir = path6.dirname(currentDir).replace(/\\/g, "/");
378
+ const parentFolderName = path6.basename(parentDir);
379
+ const parentType = parentDir === "." || parentDir === "/" ? "unknown" : classifyFolder(parentDir, config);
380
+ result.push({
381
+ dir: currentDir,
382
+ folderName,
383
+ parentDir,
384
+ parentFolderName,
385
+ parentType,
386
+ type
387
+ });
388
+ currentDir = parentDir;
389
+ }
390
+ state.ancestorChainCache.set(relDir, result);
391
+ return result;
392
+ }
393
+
394
+ // src/_rules/enforce-access-node/enforce-access-node.rule.ts
349
395
  var rule2 = {
350
396
  create(context) {
351
397
  const absPath = context.filename;
352
398
  const relPath = toRelativePath(absPath);
353
- const fileName = path6.basename(relPath);
399
+ const settings = context.settings;
400
+ const listeners = {
401
+ Program(node) {
402
+ if (!settings.faf) {
403
+ return;
404
+ }
405
+ const config2 = findTreeConfig(relPath, settings.faf);
406
+ if (!config2) {
407
+ return;
408
+ }
409
+ const relDir2 = path7.dirname(relPath);
410
+ const folderName = path7.basename(relDir2);
411
+ const parentType2 = classifyFolder(relDir2, config2);
412
+ if (parentType2 === "foreign") {
413
+ return;
414
+ }
415
+ const fileName2 = path7.basename(relPath);
416
+ for (const {
417
+ folderName: currentFolderName,
418
+ type: currentType
419
+ } of walkAncestors(relDir2, config2)) {
420
+ if (currentType === "invalid-fragment") {
421
+ context.report({
422
+ message: `Fragment directory "${currentFolderName}" is missing an Access Node (index.ts/index.js).`,
423
+ node
424
+ });
425
+ }
426
+ }
427
+ if (parentType2 === "layer" || parentType2 === "fractal-branch" || parentType2 === "invalid-fragment") {
428
+ return;
429
+ }
430
+ const rfConfig = getRootFragmentConfig(relDir2, config2);
431
+ if (rfConfig) {
432
+ const flatRootNodes = rfConfig.rootNodes.flat();
433
+ const isMatchedRootNode = flatRootNodes.some((rn) => {
434
+ if (rn.includes("/")) {
435
+ const rfPath = rfConfig.paths[0] ?? "";
436
+ const absRn = path7.resolve(getProjectRoot(), rfPath, rn);
437
+ return toRelativePath(absRn) === relPath;
438
+ }
439
+ return rn === fileName2;
440
+ });
441
+ if (isMatchedRootNode) {
442
+ return;
443
+ }
444
+ }
445
+ if (fileName2 !== "index.ts" && fileName2 !== "index.js") {
446
+ return;
447
+ }
448
+ if (parentType2 !== "fragment") {
449
+ context.report({
450
+ message: `Access Nodes ("index.ts/index.js") are exclusive to Fragments. Found index file directly inside "${folderName}" (classified as ${parentType2}).`,
451
+ node
452
+ });
453
+ return;
454
+ }
455
+ const contents = readDirCached(relDir2);
456
+ const hasFragmentNode = contents.files.some(
457
+ (f) => f !== "index.ts" && f !== "index.js" && f !== "README.md" && f !== "package.json" && f !== "tsconfig.json"
458
+ );
459
+ if (!hasFragmentNode) {
460
+ context.report({
461
+ message: `Fragment "${folderName}" has no Master Node. Every Fragment must contain at least one Fragment Node establishing its Role.`,
462
+ node
463
+ });
464
+ }
465
+ }
466
+ };
467
+ const fileName = path7.basename(relPath);
354
468
  if (fileName !== "index.ts" && fileName !== "index.js") {
355
- return {};
469
+ return listeners;
356
470
  }
357
- const settings = context.settings;
358
471
  if (!settings.faf) {
359
- return {};
472
+ return listeners;
360
473
  }
361
474
  const config = findTreeConfig(relPath, settings.faf);
362
475
  if (!config) {
363
- return {};
476
+ return listeners;
364
477
  }
365
- const relDir = path6.dirname(relPath);
478
+ const relDir = path7.dirname(relPath);
366
479
  const parentType = classifyFolder(relDir, config);
367
480
  if (parentType !== "fragment") {
368
- return {};
481
+ return listeners;
369
482
  }
370
483
  function checkSource(node, sourceVal) {
371
484
  const isValidSibling = /^\.\/[^/]+$/.test(sourceVal);
@@ -376,11 +489,11 @@ var rule2 = {
376
489
  });
377
490
  return;
378
491
  }
379
- const importedFilename = path6.basename(sourceVal);
492
+ const importedFilename = path7.basename(sourceVal);
380
493
  if (importedFilename === "index" || importedFilename === "index.ts" || importedFilename === "index.js" || importedFilename === "README.md" || importedFilename === "package.json") {
381
494
  return;
382
495
  }
383
- const fragmentName = path6.basename(relDir);
496
+ const fragmentName = path7.basename(relDir);
384
497
  if (!importedFilename.startsWith(fragmentName + ".")) {
385
498
  context.report({
386
499
  message: `Access Node (index file) can only re-export its own Fragment Nodes (names starting with "${fragmentName}."). Got "${importedFilename}".`,
@@ -388,23 +501,22 @@ var rule2 = {
388
501
  });
389
502
  }
390
503
  }
391
- return {
392
- ExportAllDeclaration(node) {
393
- checkSource(node, node.source.value);
394
- },
395
- ExportNamedDeclaration(node) {
396
- if (node.source) {
397
- checkSource(node, node.source.value);
398
- }
399
- },
400
- ImportDeclaration(node) {
504
+ listeners.ExportAllDeclaration = (node) => {
505
+ checkSource(node, node.source.value);
506
+ };
507
+ listeners.ExportNamedDeclaration = (node) => {
508
+ if (node.source) {
401
509
  checkSource(node, node.source.value);
402
510
  }
403
511
  };
512
+ listeners.ImportDeclaration = (node) => {
513
+ checkSource(node, node.source.value);
514
+ };
515
+ return listeners;
404
516
  },
405
517
  meta: {
406
518
  docs: {
407
- description: "Enforce Access Node rules for Fragments"
519
+ description: "Enforce Access Node integrity for Fragments"
408
520
  },
409
521
  schema: [],
410
522
  type: "problem"
@@ -412,136 +524,410 @@ var rule2 = {
412
524
  };
413
525
  var enforce_access_node_rule_default = rule2;
414
526
 
415
- // src/_rules/naming-conventions/naming-conventions.rule.ts
416
- import path7 from "path";
417
-
418
- // src/_rules/_rules@shared/_utils/_primitives/get-file-role/get-file-role.util.ts
419
- function getFileRole(fileName, config) {
420
- if (fileName === "index.ts" || fileName === "index.js") {
421
- return "index";
527
+ // src/_rules/_rules@shared/_utils/_aggregates/resolve-import-path/resolve-import-path.util.ts
528
+ import fs2 from "fs";
529
+ import path8 from "path";
530
+ function resolveImportPath(importPath, currentFileRelative, settings) {
531
+ const cacheKey = `${currentFileRelative}::${importPath}`;
532
+ const cached = state.resolvedImportPathCache.get(cacheKey);
533
+ if (cached !== void 0) {
534
+ return cached;
422
535
  }
423
- const parts = fileName.split(".");
424
- let allRoles = state.rolesCache.get(config);
425
- if (!allRoles) {
426
- allRoles = new Set(config.roles.flat());
427
- state.rolesCache.set(config, allRoles);
536
+ const result = _resolveImportPath(importPath, currentFileRelative, settings);
537
+ state.resolvedImportPathCache.set(cacheKey, result);
538
+ return result;
539
+ }
540
+ function _resolveImportPath(importPath, currentFileRelative, settings) {
541
+ const pkgImports = loadPackageImports(state.projectRoot);
542
+ const aliases = Object.keys(pkgImports).length > 0 ? pkgImports : settings?.aliases || {};
543
+ let matchedAlias = null;
544
+ let longestMatch = 0;
545
+ for (const alias of Object.keys(aliases)) {
546
+ if (importPath === alias || importPath.startsWith(alias + "/")) {
547
+ if (alias.length > longestMatch) {
548
+ longestMatch = alias.length;
549
+ matchedAlias = alias;
550
+ }
551
+ }
428
552
  }
429
- for (let i = parts.length - 2; i >= 1; i--) {
430
- const part = parts[i];
431
- if (part && allRoles.has(part)) {
432
- return part;
553
+ let relResolved;
554
+ if (matchedAlias) {
555
+ const target = aliases[matchedAlias];
556
+ if (target) {
557
+ const remainder = importPath.substring(matchedAlias.length);
558
+ const absTarget = path8.resolve(state.projectRoot, target + remainder);
559
+ relResolved = toRelativePath(absTarget);
560
+ } else {
561
+ return "";
562
+ }
563
+ } else {
564
+ if (importPath.startsWith(".") || importPath.startsWith("/")) {
565
+ const absCurrentDir = path8.dirname(
566
+ path8.resolve(state.projectRoot, currentFileRelative)
567
+ );
568
+ const absResolved = path8.resolve(absCurrentDir, importPath);
569
+ relResolved = toRelativePath(absResolved);
570
+ } else {
571
+ return "";
433
572
  }
434
573
  }
435
- return null;
574
+ return finalizeResolvedPath(relResolved);
575
+ }
576
+ var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
577
+ "cjs",
578
+ "cts",
579
+ "js",
580
+ "jsx",
581
+ "mjs",
582
+ "mts",
583
+ "ts",
584
+ "tsx"
585
+ ]);
586
+ function finalizeResolvedPath(relResolved) {
587
+ const resolvedDir = path8.dirname(relResolved).replace(/\\/g, "/");
588
+ const resolvedBase = path8.basename(relResolved);
589
+ const dirContents = readDirCached(relResolved);
590
+ if (dirContents.files.includes("index.ts")) {
591
+ return (relResolved === "." ? "" : relResolved + "/") + "index.ts";
592
+ }
593
+ if (dirContents.files.includes("index.js")) {
594
+ return (relResolved === "." ? "" : relResolved + "/") + "index.js";
595
+ }
596
+ const parentContents = readDirCached(resolvedDir);
597
+ const exactMatch = parentContents.files.find((f) => f === resolvedBase);
598
+ if (!exactMatch) {
599
+ const candidates = parentContents.files.filter(
600
+ (f) => f.startsWith(resolvedBase + ".")
601
+ );
602
+ const matchWithExt = candidates.find(
603
+ (f) => SCRIPT_EXTENSIONS.has(f.slice(resolvedBase.length + 1))
604
+ ) ?? candidates[0];
605
+ if (matchWithExt) {
606
+ return (resolvedDir === "." ? "" : resolvedDir + "/") + matchWithExt;
607
+ }
608
+ }
609
+ return relResolved;
610
+ }
611
+ function loadPackageImports(projectRootPath) {
612
+ if (state.packageImports !== null) {
613
+ return state.packageImports;
614
+ }
615
+ state.packageImports = {};
616
+ const isTest = typeof process !== "undefined" && (process.env.NODE_ENV === "test" || process.env.VITEST === "true");
617
+ if (isTest) {
618
+ return state.packageImports;
619
+ }
620
+ const pkgPath = path8.resolve(projectRootPath, "package.json");
621
+ if (fs2.existsSync(pkgPath)) {
622
+ try {
623
+ const pkgContent = fs2.readFileSync(pkgPath, "utf8");
624
+ const pkg = JSON.parse(pkgContent);
625
+ if (pkg.imports) {
626
+ for (const [key, val] of Object.entries(pkg.imports)) {
627
+ if (typeof val === "string") {
628
+ const cleanKey = key.endsWith("/*") ? key.slice(0, -2) : key.endsWith("*") ? key.slice(0, -1) : key;
629
+ let cleanVal = val.endsWith("/*") ? val.slice(0, -2) : val.endsWith("*") ? val.slice(0, -1) : val;
630
+ if (cleanVal.startsWith("./")) {
631
+ cleanVal = cleanVal.substring(2);
632
+ } else if (cleanVal.startsWith("/")) {
633
+ cleanVal = cleanVal.substring(1);
634
+ }
635
+ state.packageImports[cleanKey] = cleanVal;
636
+ }
637
+ }
638
+ }
639
+ } catch {
640
+ }
641
+ }
642
+ return state.packageImports;
436
643
  }
437
644
 
438
- // src/_rules/_rules@shared/_utils/_primitives/get-project-root/get-project-root.util.ts
439
- function getProjectRoot() {
440
- return state.projectRoot;
645
+ // src/_rules/_rules@shared/_utils/_primitives/find-tree-config-including-excluded/find-tree-config-including-excluded.util.ts
646
+ function findTreeConfigIncludingExcluded(relPath, settings) {
647
+ if (!settings || !settings.trees) {
648
+ return null;
649
+ }
650
+ const cached = state.treeConfigIncludingExcludedCache.get(relPath);
651
+ if (cached !== void 0) {
652
+ return cached;
653
+ }
654
+ let result = null;
655
+ for (const tree of settings.trees) {
656
+ const isIncluded = tree.includes.some(
657
+ (inc) => relPath === inc || relPath.startsWith(inc + "/")
658
+ );
659
+ if (isIncluded) {
660
+ result = tree;
661
+ break;
662
+ }
663
+ }
664
+ state.treeConfigIncludingExcludedCache.set(relPath, result);
665
+ return result;
441
666
  }
442
667
 
443
- // src/_rules/naming-conventions/naming-conventions.rule.ts
444
- var KEBAB_CASE_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
445
- var DEFAULT_HTTP_METHODS = [
446
- "connect",
447
- "delete",
448
- "get",
449
- "head",
450
- "options",
451
- "patch",
452
- "post",
453
- "put",
454
- "trace"
455
- ];
668
+ // src/_rules/foreign-domain-isolation/foreign-domain-isolation.rule.ts
456
669
  var rule3 = {
457
670
  create(context) {
458
- return {
459
- Program(node) {
460
- const absPath = context.filename;
461
- const relPath = toRelativePath(absPath);
462
- const settings = context.settings;
463
- if (!settings.faf) {
464
- return;
465
- }
466
- const config = findTreeConfig(relPath, settings.faf);
467
- if (!config) {
671
+ const absPath = context.filename;
672
+ const relPath = toRelativePath(absPath);
673
+ const settings = context.settings;
674
+ if (!settings.faf) {
675
+ return {};
676
+ }
677
+ const config = findTreeConfigIncludingExcluded(relPath, settings.faf);
678
+ if (!config) {
679
+ return {};
680
+ }
681
+ const isCurrentFileExcluded = config.excludes?.some(
682
+ (exc) => relPath === exc || relPath.startsWith(exc + "/")
683
+ ) ?? false;
684
+ function checkImport(node, importValue) {
685
+ const resolvedRelPath = resolveImportPath(
686
+ importValue,
687
+ relPath,
688
+ settings.faf
689
+ );
690
+ if (!resolvedRelPath) {
691
+ return;
692
+ }
693
+ const isImportedFileExcluded = config?.excludes?.some(
694
+ (exc) => resolvedRelPath === exc || resolvedRelPath.startsWith(exc + "/")
695
+ ) ?? false;
696
+ if (!isCurrentFileExcluded && isImportedFileExcluded) {
697
+ context.report({
698
+ message: `Importing from Foreign Domain "${resolvedRelPath}" is forbidden for Logical Nodes.`,
699
+ node
700
+ });
701
+ return;
702
+ }
703
+ if (isCurrentFileExcluded && !isImportedFileExcluded) {
704
+ context.report({
705
+ message: `Foreign Domain files cannot import Logical Nodes from FAF tree ("${resolvedRelPath}").`,
706
+ node
707
+ });
708
+ }
709
+ }
710
+ return {
711
+ ExportAllDeclaration(node) {
712
+ checkImport(node, node.source.value);
713
+ },
714
+ ExportNamedDeclaration(node) {
715
+ if (node.source) {
716
+ checkImport(node, node.source.value);
717
+ }
718
+ },
719
+ ImportDeclaration(node) {
720
+ checkImport(node, node.source.value);
721
+ }
722
+ };
723
+ },
724
+ meta: {
725
+ docs: {
726
+ description: "Enforce isolation between Foreign Domains and Logical Nodes"
727
+ },
728
+ schema: [],
729
+ type: "problem"
730
+ }
731
+ };
732
+ var foreign_domain_isolation_rule_default = rule3;
733
+
734
+ // src/_rules/fragment-master-node/fragment-master-node.rule.ts
735
+ import path9 from "path";
736
+
737
+ // src/_rules/_rules@shared/_utils/_primitives/get-file-role/get-file-role.util.ts
738
+ function getFileRole(fileName, config) {
739
+ if (fileName === "index.ts" || fileName === "index.js") {
740
+ return "index";
741
+ }
742
+ const parts = fileName.split(".");
743
+ let allRoles = state.rolesCache.get(config);
744
+ if (!allRoles) {
745
+ allRoles = new Set(config.roles.flat());
746
+ state.rolesCache.set(config, allRoles);
747
+ }
748
+ for (let i = parts.length - 2; i >= 1; i--) {
749
+ const part = parts[i];
750
+ if (part && allRoles.has(part)) {
751
+ return part;
752
+ }
753
+ }
754
+ return null;
755
+ }
756
+
757
+ // src/_rules/fragment-master-node/fragment-master-node.rule.ts
758
+ var DEFAULT_HTTP_METHODS = [
759
+ "connect",
760
+ "delete",
761
+ "get",
762
+ "head",
763
+ "options",
764
+ "patch",
765
+ "post",
766
+ "put",
767
+ "trace"
768
+ ];
769
+ var rule4 = {
770
+ create(context) {
771
+ return {
772
+ Program(node) {
773
+ const absPath = context.filename;
774
+ const relPath = toRelativePath(absPath);
775
+ const settings = context.settings;
776
+ if (!settings.faf) {
468
777
  return;
469
778
  }
470
- const relDir = path7.dirname(relPath);
471
- const folderName = path7.basename(relDir);
779
+ const config = findTreeConfig(relPath, settings.faf);
780
+ if (!config) {
781
+ return;
782
+ }
783
+ const relDir = path9.dirname(relPath);
784
+ const folderName = path9.basename(relDir);
472
785
  const parentType = classifyFolder(relDir, config);
473
- if (parentType === "foreign") {
786
+ if (parentType !== "fragment") {
474
787
  return;
475
788
  }
476
- const fileName = path7.basename(relPath);
477
- let currentDir = relDir;
478
- while (currentDir && currentDir !== "." && currentDir !== "/") {
479
- const isInTree = config.includes.some(
480
- (inc) => currentDir === inc || currentDir.startsWith(inc + "/")
481
- );
482
- const isExcluded = config.excludes?.some(
483
- (exc) => currentDir === exc || currentDir.startsWith(exc + "/")
484
- ) ?? false;
485
- if (!isInTree || isExcluded) {
486
- break;
789
+ const fileName = path9.basename(relPath);
790
+ const rfConfig = getRootFragmentConfig(relDir, config);
791
+ if (rfConfig) {
792
+ const flatRootNodes = rfConfig.rootNodes.flat();
793
+ const isMatchedRootNode = flatRootNodes.some((rn) => {
794
+ if (rn.includes("/")) {
795
+ const rfPath = rfConfig.paths[0] ?? "";
796
+ const absRn = path9.resolve(getProjectRoot(), rfPath, rn);
797
+ return toRelativePath(absRn) === relPath;
798
+ }
799
+ return rn === fileName;
800
+ });
801
+ if (isMatchedRootNode) {
802
+ return;
487
803
  }
488
- const currentFolderName = path7.basename(currentDir);
489
- const currentType = classifyFolder(currentDir, config);
490
- if ((currentType === "layer" || currentType === "category") && !currentFolderName.startsWith("_")) {
804
+ }
805
+ if (fileName === "index.ts" || fileName === "index.js") {
806
+ return;
807
+ }
808
+ if (fileName === "README.md" || fileName === "package.json" || fileName === "tsconfig.json") {
809
+ return;
810
+ }
811
+ const role = getFileRole(fileName, config);
812
+ const ext = path9.extname(fileName);
813
+ const allRoleNames = new Set(config.roles.flat());
814
+ const roleSegments = fileName.slice(0, fileName.length - ext.length).split(".").slice(1).filter((part) => allRoleNames.has(part));
815
+ const matchedRoute = config.routeHierarchies?.find(
816
+ (rh) => rh.paths.some((rp) => relDir === rp || relDir.startsWith(rp + "/"))
817
+ );
818
+ if (matchedRoute) {
819
+ const contents = readDirCached(relDir);
820
+ const fileRoles = contents.files.map((f) => getFileRole(f, config));
821
+ if (!fileRoles.includes(matchedRoute.role)) {
491
822
  context.report({
492
- message: `Layer/Category directory "${currentFolderName}" must be prefixed with an underscore (e.g. "_${currentFolderName}").`,
823
+ message: `Route terminal Fragment "${folderName}" must contain a Master Node with role "${matchedRoute.role}" (e.g. "${folderName}.${matchedRoute.role}.ts").`,
493
824
  node
494
825
  });
495
826
  }
496
- if (currentFolderName.startsWith("_") && !currentFolderName.includes("@shared")) {
497
- const contents = readDirCached(currentDir);
498
- const hasIndex = contents.files.includes("index.ts") || contents.files.includes("index.js");
499
- if (hasIndex) {
500
- context.report({
501
- message: `Directory "${currentFolderName}" has an Access Node (index.ts) but its name starts with "_". Fragments must not be prefixed with underscore.`,
502
- node
503
- });
504
- }
827
+ const httpMethods = matchedRoute.httpMethods ?? DEFAULT_HTTP_METHODS;
828
+ const routeNameRegex = new RegExp(
829
+ `^(${httpMethods.join("|")})-[a-z0-9]+(-[a-z0-9]+)*$`
830
+ );
831
+ if (!routeNameRegex.test(folderName)) {
832
+ context.report({
833
+ message: `Route terminal Fragment "${folderName}" must follow the "<method>-<object>" pattern with a lowercase HTTP method (${httpMethods.join(", ")}).`,
834
+ node
835
+ });
505
836
  }
506
- if (currentFolderName !== "src") {
507
- let baseFolderName = currentFolderName.replace(/@shared$/, "");
508
- if (baseFolderName.startsWith("_")) {
509
- baseFolderName = baseFolderName.substring(1);
510
- }
511
- if (baseFolderName.startsWith("[") && baseFolderName.endsWith("]")) {
512
- baseFolderName = baseFolderName.substring(
513
- 1,
514
- baseFolderName.length - 1
515
- );
516
- }
517
- const isKebab = KEBAB_CASE_REGEX.test(baseFolderName);
518
- if (!isKebab) {
519
- context.report({
520
- message: `Folder name "${currentFolderName}" must be in kebab-case (e.g. "_my-folder" or "_[my-param]").`,
521
- node
522
- });
523
- }
837
+ }
838
+ let currentParent = path9.dirname(relDir).replace(/\\/g, "/");
839
+ let categoryConfig = null;
840
+ let categoryFolderName = "";
841
+ while (currentParent && currentParent !== "." && currentParent !== "/") {
842
+ const currentParentType = classifyFolder(currentParent, config);
843
+ if (currentParentType === "category") {
844
+ categoryConfig = getCategoryConfig(currentParent, config);
845
+ categoryFolderName = path9.basename(currentParent);
846
+ break;
847
+ } else if (currentParentType === "fragment" || currentParentType === "root-fragment" || currentParentType === "layer") {
848
+ break;
524
849
  }
525
- if (currentType === "fractal-branch") {
526
- const expectedScope = path7.basename(path7.dirname(currentDir));
527
- const normalizedExpectedScope = expectedScope.startsWith("_") ? expectedScope.substring(1) : expectedScope;
528
- const expectedName = `_${normalizedExpectedScope}@shared`;
529
- if (currentFolderName !== expectedName) {
530
- context.report({
531
- message: `Fractal Branch name "${currentFolderName}" must match its parent scope name: "${expectedName}".`,
532
- node
533
- });
534
- }
850
+ const nextParent = path9.dirname(currentParent).replace(/\\/g, "/");
851
+ if (nextParent === currentParent) break;
852
+ currentParent = nextParent;
853
+ }
854
+ if (categoryConfig && categoryConfig.role) {
855
+ const contents = readDirCached(relDir);
856
+ const fileRoles = contents.files.map((f) => getFileRole(f, config));
857
+ if (!fileRoles.includes(categoryConfig.role)) {
858
+ context.report({
859
+ message: `Fragment "${folderName}" inside Category "${categoryFolderName}" must contain a Master Node with role "${categoryConfig.role}" (e.g. "${folderName}.${categoryConfig.role}.ts").`,
860
+ node
861
+ });
535
862
  }
536
- if (currentType === "invalid-fragment") {
863
+ }
864
+ if (!fileName.startsWith(folderName + ".")) {
865
+ context.report({
866
+ message: `Fragment Node "${fileName}" must share the parent Fragment name: "${folderName}.<role>${ext}".`,
867
+ node
868
+ });
869
+ }
870
+ if (!role) {
871
+ context.report({
872
+ message: `File "${fileName}" inside Fragment "${folderName}" must have an explicit role suffix (e.g. "${folderName}.style${ext}").`,
873
+ node
874
+ });
875
+ } else if (roleSegments.length > 1) {
876
+ } else {
877
+ const contents = readDirCached(relDir);
878
+ const duplicate = contents.files.find(
879
+ (f) => f !== fileName && f !== "index.ts" && f !== "index.js" && getFileRole(f, config) === role
880
+ );
881
+ if (duplicate) {
537
882
  context.report({
538
- message: `Fragment directory "${currentFolderName}" is missing an Access Node (index.ts/index.js).`,
883
+ message: `Fragment Node "${fileName}" shares Role "${role}" with sibling "${duplicate}". Each Fragment Node must have a unique Role within its Fragment.`,
539
884
  node
540
885
  });
541
886
  }
542
- const relParentDir = path7.dirname(currentDir).replace(/\\/g, "/");
543
- const parentFolderName = path7.basename(relParentDir);
544
- const relParentType = relParentDir === "." || relParentDir === "/" ? "unknown" : classifyFolder(relParentDir, config);
887
+ }
888
+ }
889
+ };
890
+ },
891
+ meta: {
892
+ docs: {
893
+ description: "Enforce Fragment Node identity and Master Node integrity in FAF Fragments"
894
+ },
895
+ schema: [],
896
+ type: "problem"
897
+ }
898
+ };
899
+ var fragment_master_node_rule_default = rule4;
900
+
901
+ // src/_rules/logical-domain-placement/logical-domain-placement.rule.ts
902
+ import path10 from "path";
903
+ var rule5 = {
904
+ create(context) {
905
+ return {
906
+ Program(node) {
907
+ const absPath = context.filename;
908
+ const relPath = toRelativePath(absPath);
909
+ const settings = context.settings;
910
+ if (!settings.faf) {
911
+ return;
912
+ }
913
+ const config = findTreeConfig(relPath, settings.faf);
914
+ if (!config) {
915
+ return;
916
+ }
917
+ const relDir = path10.dirname(relPath);
918
+ const folderName = path10.basename(relDir);
919
+ const parentType = classifyFolder(relDir, config);
920
+ if (parentType === "foreign") {
921
+ return;
922
+ }
923
+ const fileName = path10.basename(relPath);
924
+ for (const {
925
+ folderName: currentFolderName,
926
+ parentDir: relParentDir,
927
+ parentFolderName,
928
+ parentType: relParentType,
929
+ type: currentType
930
+ } of walkAncestors(relDir, config)) {
545
931
  if (currentType === "root-fragment") {
546
932
  if (relParentType !== "root-fragment" && relParentType !== "unknown") {
547
933
  context.report({
@@ -597,7 +983,6 @@ var rule3 = {
597
983
  });
598
984
  }
599
985
  }
600
- currentDir = path7.dirname(currentDir).replace(/\\/g, "/");
601
986
  }
602
987
  if (parentType === "layer") {
603
988
  context.report({
@@ -611,9 +996,96 @@ var rule3 = {
611
996
  message: `Fractal Branches cannot contain files directly. File "${fileName}" is placed directly inside Fractal Branch "${folderName}".`,
612
997
  node
613
998
  });
999
+ }
1000
+ }
1001
+ };
1002
+ },
1003
+ meta: {
1004
+ docs: {
1005
+ description: "Enforce topological placement constraints between FAF Logical Domains"
1006
+ },
1007
+ schema: [],
1008
+ type: "problem"
1009
+ }
1010
+ };
1011
+ var logical_domain_placement_rule_default = rule5;
1012
+
1013
+ // src/_rules/naming-conventions/naming-conventions.rule.ts
1014
+ import path11 from "path";
1015
+ var KEBAB_CASE_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
1016
+ var rule6 = {
1017
+ create(context) {
1018
+ return {
1019
+ Program(node) {
1020
+ const absPath = context.filename;
1021
+ const relPath = toRelativePath(absPath);
1022
+ const settings = context.settings;
1023
+ if (!settings.faf) {
614
1024
  return;
615
1025
  }
616
- if (parentType === "invalid-fragment") {
1026
+ const config = findTreeConfig(relPath, settings.faf);
1027
+ if (!config) {
1028
+ return;
1029
+ }
1030
+ const relDir = path11.dirname(relPath);
1031
+ const parentType = classifyFolder(relDir, config);
1032
+ if (parentType === "foreign") {
1033
+ return;
1034
+ }
1035
+ const fileName = path11.basename(relPath);
1036
+ for (const {
1037
+ dir: currentDir,
1038
+ folderName: currentFolderName,
1039
+ type: currentType
1040
+ } of walkAncestors(relDir, config)) {
1041
+ if ((currentType === "layer" || currentType === "category") && !currentFolderName.startsWith("_")) {
1042
+ context.report({
1043
+ message: `Layer/Category directory "${currentFolderName}" must be prefixed with an underscore (e.g. "_${currentFolderName}").`,
1044
+ node
1045
+ });
1046
+ }
1047
+ if (currentFolderName.startsWith("_") && !currentFolderName.includes("@shared")) {
1048
+ const contents = readDirCached(currentDir);
1049
+ const hasIndex = contents.files.includes("index.ts") || contents.files.includes("index.js");
1050
+ if (hasIndex) {
1051
+ context.report({
1052
+ message: `Directory "${currentFolderName}" has an Access Node (index.ts) but its name starts with "_". Fragments must not be prefixed with underscore.`,
1053
+ node
1054
+ });
1055
+ }
1056
+ }
1057
+ if (currentFolderName !== "src") {
1058
+ let baseFolderName = currentFolderName.replace(/@shared$/, "");
1059
+ if (baseFolderName.startsWith("_")) {
1060
+ baseFolderName = baseFolderName.substring(1);
1061
+ }
1062
+ if (baseFolderName.startsWith("[") && baseFolderName.endsWith("]")) {
1063
+ baseFolderName = baseFolderName.substring(
1064
+ 1,
1065
+ baseFolderName.length - 1
1066
+ );
1067
+ }
1068
+ const isKebab = KEBAB_CASE_REGEX.test(baseFolderName);
1069
+ if (!isKebab) {
1070
+ context.report({
1071
+ message: `Folder name "${currentFolderName}" must be in kebab-case (e.g. "_my-folder" or "_[my-param]").`,
1072
+ node
1073
+ });
1074
+ }
1075
+ }
1076
+ if (currentType === "fractal-branch") {
1077
+ const expectedScope = path11.basename(path11.dirname(currentDir));
1078
+ const normalizedExpectedScope = expectedScope.startsWith("_") ? expectedScope.substring(1) : expectedScope;
1079
+ const expectedName = `_${normalizedExpectedScope}@shared`;
1080
+ if (currentFolderName !== expectedName) {
1081
+ context.report({
1082
+ message: `Fractal Branch name "${currentFolderName}" must match its parent scope name: "${expectedName}".`,
1083
+ node
1084
+ });
1085
+ }
1086
+ }
1087
+ }
1088
+ if (parentType === "layer" || parentType === "fractal-branch" || parentType === "invalid-fragment") {
617
1089
  return;
618
1090
  }
619
1091
  const rfConfig = getRootFragmentConfig(relDir, config);
@@ -622,7 +1094,7 @@ var rule3 = {
622
1094
  const isMatchedRootNode = flatRootNodes.some((rn) => {
623
1095
  if (rn.includes("/")) {
624
1096
  const rfPath = rfConfig.paths[0] ?? "";
625
- const absRn = path7.resolve(getProjectRoot(), rfPath, rn);
1097
+ const absRn = path11.resolve(getProjectRoot(), rfPath, rn);
626
1098
  return toRelativePath(absRn) === relPath;
627
1099
  }
628
1100
  return rn === fileName;
@@ -632,288 +1104,79 @@ var rule3 = {
632
1104
  }
633
1105
  }
634
1106
  if (fileName === "index.ts" || fileName === "index.js") {
635
- if (parentType !== "fragment") {
636
- context.report({
637
- message: `Access Nodes ("index.ts/index.js") are exclusive to Fragments. Found index file directly inside "${folderName}" (classified as ${parentType}).`,
638
- node
639
- });
640
- } else {
641
- const contents = readDirCached(relDir);
642
- const hasFragmentNode = contents.files.some(
643
- (f) => f !== "index.ts" && f !== "index.js" && f !== "README.md" && f !== "package.json" && f !== "tsconfig.json"
644
- );
645
- if (!hasFragmentNode) {
646
- context.report({
647
- message: `Fragment "${folderName}" has no Master Node. Every Fragment must contain at least one Fragment Node establishing its Role.`,
648
- node
649
- });
650
- }
651
- }
652
1107
  return;
653
1108
  }
654
1109
  if (fileName === "README.md" || fileName === "package.json" || fileName === "tsconfig.json") {
655
1110
  return;
656
1111
  }
657
1112
  const role = getFileRole(fileName, config);
658
- const ext = path7.extname(fileName);
1113
+ const ext = path11.extname(fileName);
659
1114
  const allRoleNames = new Set(config.roles.flat());
660
1115
  const roleSegments = fileName.slice(0, fileName.length - ext.length).split(".").slice(1).filter((part) => allRoleNames.has(part));
661
1116
  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.`;
662
- if (parentType === "fragment") {
663
- const matchedRoute = config.routeHierarchies?.find(
664
- (rh) => rh.paths.some((rp) => relDir === rp || relDir.startsWith(rp + "/"))
665
- );
666
- if (matchedRoute) {
667
- const contents = readDirCached(relDir);
668
- const fileRoles = contents.files.map((f) => getFileRole(f, config));
669
- if (!fileRoles.includes(matchedRoute.role)) {
670
- context.report({
671
- message: `Route terminal Fragment "${folderName}" must contain a Master Node with role "${matchedRoute.role}" (e.g. "${folderName}.${matchedRoute.role}.ts").`,
672
- node
673
- });
674
- }
675
- const httpMethods = matchedRoute.httpMethods ?? DEFAULT_HTTP_METHODS;
676
- const routeNameRegex = new RegExp(
677
- `^(${httpMethods.join("|")})-[a-z0-9]+(-[a-z0-9]+)*$`
678
- );
679
- if (!routeNameRegex.test(folderName)) {
680
- context.report({
681
- message: `Route terminal Fragment "${folderName}" must follow the "<method>-<object>" pattern with a lowercase HTTP method (${httpMethods.join(", ")}).`,
682
- node
683
- });
684
- }
685
- }
686
- let currentParent = path7.dirname(relDir).replace(/\\/g, "/");
687
- let categoryConfig = null;
688
- let categoryFolderName = "";
689
- while (currentParent && currentParent !== "." && currentParent !== "/") {
690
- const parentType2 = classifyFolder(currentParent, config);
691
- if (parentType2 === "category") {
692
- categoryConfig = getCategoryConfig(currentParent, config);
693
- categoryFolderName = path7.basename(currentParent);
694
- break;
695
- } else if (parentType2 === "fragment" || parentType2 === "root-fragment" || parentType2 === "layer") {
696
- break;
697
- }
698
- const nextParent = path7.dirname(currentParent).replace(/\\/g, "/");
699
- if (nextParent === currentParent) break;
700
- currentParent = nextParent;
701
- }
702
- if (categoryConfig && categoryConfig.role) {
703
- const contents = readDirCached(relDir);
704
- const fileRoles = contents.files.map((f) => getFileRole(f, config));
705
- if (!fileRoles.includes(categoryConfig.role)) {
706
- context.report({
707
- message: `Fragment "${folderName}" inside Category "${categoryFolderName}" must contain a Master Node with role "${categoryConfig.role}" (e.g. "${folderName}.${categoryConfig.role}.ts").`,
708
- node
709
- });
710
- }
711
- }
712
- if (!fileName.startsWith(folderName + ".")) {
713
- context.report({
714
- message: `Fragment Node "${fileName}" must share the parent Fragment name: "${folderName}.<role>${ext}".`,
715
- node
716
- });
717
- }
718
- if (!role) {
719
- context.report({
720
- message: `File "${fileName}" inside Fragment "${folderName}" must have an explicit role suffix (e.g. "${folderName}.style${ext}").`,
721
- node
722
- });
723
- } else if (roleSegments.length > 1) {
724
- context.report({ message: multiRoleMessage, node });
725
- } else {
726
- const contents = readDirCached(relDir);
727
- const duplicate = contents.files.find(
728
- (f) => f !== fileName && f !== "index.ts" && f !== "index.js" && getFileRole(f, config) === role
729
- );
730
- if (duplicate) {
731
- context.report({
732
- message: `Fragment Node "${fileName}" shares Role "${role}" with sibling "${duplicate}". Each Fragment Node must have a unique Role within its Fragment.`,
733
- node
734
- });
735
- }
736
- }
1117
+ if (parentType === "fragment" && role && roleSegments.length > 1) {
1118
+ context.report({ message: multiRoleMessage, node });
737
1119
  }
738
1120
  if (parentType === "category") {
1121
+ const folderName = path11.basename(relDir);
739
1122
  const catConfig = getCategoryConfig(relDir, config);
740
1123
  if (catConfig) {
741
1124
  const allowedExts = catConfig.allowedExtensions ?? [
742
1125
  ".js",
743
1126
  ".jsx",
744
1127
  ".ts",
745
- ".tsx"
746
- ];
747
- if (!allowedExts.includes(ext)) {
748
- context.report({
749
- message: `File extension "${ext}" is not allowed in Category "${folderName}". Allowed extensions: ${allowedExts.join(", ")}.`,
750
- node
751
- });
752
- return;
753
- }
754
- const isCodeExt = [".js", ".jsx", ".ts", ".tsx"].includes(ext);
755
- if (isCodeExt) {
756
- if (!role) {
757
- context.report({
758
- message: `Logical Node "${fileName}" inside Category "${folderName}" must have a role suffix.`,
759
- node
760
- });
761
- } else if (role !== catConfig.role) {
762
- context.report({
763
- message: `Role "${role}" for file "${fileName}" does not match the expected Category role "${catConfig.role}".`,
764
- node
765
- });
766
- } else if (roleSegments.length > 1) {
767
- context.report({ message: multiRoleMessage, node });
768
- }
769
- } else {
770
- if (role && role !== catConfig.role) {
771
- context.report({
772
- message: `Role "${role}" for asset "${fileName}" does not match the expected Category role "${catConfig.role}".`,
773
- node
774
- });
775
- } else if (role && roleSegments.length > 1) {
776
- context.report({ message: multiRoleMessage, node });
777
- }
778
- }
779
- }
780
- }
781
- }
782
- };
783
- },
784
- meta: {
785
- docs: {
786
- description: "Enforce naming conventions and folder taxonomy in FAF architecture"
787
- },
788
- schema: [],
789
- type: "problem"
790
- }
791
- };
792
- var naming_conventions_rule_default = rule3;
793
-
794
- // src/_rules/no-direct-fragment-import/no-direct-fragment-import.rule.ts
795
- import path9 from "path";
796
-
797
- // src/_rules/_rules@shared/_utils/_aggregates/resolve-import-path/resolve-import-path.util.ts
798
- import fs2 from "fs";
799
- import path8 from "path";
800
- function resolveImportPath(importPath, currentFileRelative, settings) {
801
- const cacheKey = `${currentFileRelative}::${importPath}`;
802
- const cached = state.resolvedImportPathCache.get(cacheKey);
803
- if (cached !== void 0) {
804
- return cached;
805
- }
806
- const result = _resolveImportPath(importPath, currentFileRelative, settings);
807
- state.resolvedImportPathCache.set(cacheKey, result);
808
- return result;
809
- }
810
- function _resolveImportPath(importPath, currentFileRelative, settings) {
811
- const pkgImports = loadPackageImports(state.projectRoot);
812
- const aliases = Object.keys(pkgImports).length > 0 ? pkgImports : settings?.aliases || {};
813
- let matchedAlias = null;
814
- let longestMatch = 0;
815
- for (const alias of Object.keys(aliases)) {
816
- if (importPath === alias || importPath.startsWith(alias + "/")) {
817
- if (alias.length > longestMatch) {
818
- longestMatch = alias.length;
819
- matchedAlias = alias;
820
- }
821
- }
822
- }
823
- let relResolved;
824
- if (matchedAlias) {
825
- const target = aliases[matchedAlias];
826
- if (target) {
827
- const remainder = importPath.substring(matchedAlias.length);
828
- const absTarget = path8.resolve(state.projectRoot, target + remainder);
829
- relResolved = toRelativePath(absTarget);
830
- } else {
831
- return "";
832
- }
833
- } else {
834
- if (importPath.startsWith(".") || importPath.startsWith("/")) {
835
- const absCurrentDir = path8.dirname(
836
- path8.resolve(state.projectRoot, currentFileRelative)
837
- );
838
- const absResolved = path8.resolve(absCurrentDir, importPath);
839
- relResolved = toRelativePath(absResolved);
840
- } else {
841
- return "";
842
- }
843
- }
844
- return finalizeResolvedPath(relResolved);
845
- }
846
- var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
847
- "cjs",
848
- "cts",
849
- "js",
850
- "jsx",
851
- "mjs",
852
- "mts",
853
- "ts",
854
- "tsx"
855
- ]);
856
- function finalizeResolvedPath(relResolved) {
857
- const resolvedDir = path8.dirname(relResolved).replace(/\\/g, "/");
858
- const resolvedBase = path8.basename(relResolved);
859
- const dirContents = readDirCached(relResolved);
860
- if (dirContents.files.includes("index.ts")) {
861
- return (relResolved === "." ? "" : relResolved + "/") + "index.ts";
862
- }
863
- if (dirContents.files.includes("index.js")) {
864
- return (relResolved === "." ? "" : relResolved + "/") + "index.js";
865
- }
866
- const parentContents = readDirCached(resolvedDir);
867
- const exactMatch = parentContents.files.find((f) => f === resolvedBase);
868
- if (!exactMatch) {
869
- const candidates = parentContents.files.filter(
870
- (f) => f.startsWith(resolvedBase + ".")
871
- );
872
- const matchWithExt = candidates.find(
873
- (f) => SCRIPT_EXTENSIONS.has(f.slice(resolvedBase.length + 1))
874
- ) ?? candidates[0];
875
- if (matchWithExt) {
876
- return (resolvedDir === "." ? "" : resolvedDir + "/") + matchWithExt;
877
- }
878
- }
879
- return relResolved;
880
- }
881
- function loadPackageImports(projectRootPath) {
882
- if (state.packageImports !== null) {
883
- return state.packageImports;
884
- }
885
- state.packageImports = {};
886
- const isTest = typeof process !== "undefined" && (process.env.NODE_ENV === "test" || process.env.VITEST === "true");
887
- if (isTest) {
888
- return state.packageImports;
889
- }
890
- const pkgPath = path8.resolve(projectRootPath, "package.json");
891
- if (fs2.existsSync(pkgPath)) {
892
- try {
893
- const pkgContent = fs2.readFileSync(pkgPath, "utf8");
894
- const pkg = JSON.parse(pkgContent);
895
- if (pkg.imports) {
896
- for (const [key, val] of Object.entries(pkg.imports)) {
897
- if (typeof val === "string") {
898
- const cleanKey = key.endsWith("/*") ? key.slice(0, -2) : key.endsWith("*") ? key.slice(0, -1) : key;
899
- let cleanVal = val.endsWith("/*") ? val.slice(0, -2) : val.endsWith("*") ? val.slice(0, -1) : val;
900
- if (cleanVal.startsWith("./")) {
901
- cleanVal = cleanVal.substring(2);
902
- } else if (cleanVal.startsWith("/")) {
903
- cleanVal = cleanVal.substring(1);
1128
+ ".tsx"
1129
+ ];
1130
+ if (!allowedExts.includes(ext)) {
1131
+ context.report({
1132
+ message: `File extension "${ext}" is not allowed in Category "${folderName}". Allowed extensions: ${allowedExts.join(", ")}.`,
1133
+ node
1134
+ });
1135
+ return;
1136
+ }
1137
+ const isCodeExt = [".js", ".jsx", ".ts", ".tsx"].includes(ext);
1138
+ if (isCodeExt) {
1139
+ if (!role) {
1140
+ context.report({
1141
+ message: `Logical Node "${fileName}" inside Category "${folderName}" must have a role suffix.`,
1142
+ node
1143
+ });
1144
+ } else if (role !== catConfig.role) {
1145
+ context.report({
1146
+ message: `Role "${role}" for file "${fileName}" does not match the expected Category role "${catConfig.role}".`,
1147
+ node
1148
+ });
1149
+ } else if (roleSegments.length > 1) {
1150
+ context.report({ message: multiRoleMessage, node });
1151
+ }
1152
+ } else {
1153
+ if (role && role !== catConfig.role) {
1154
+ context.report({
1155
+ message: `Role "${role}" for asset "${fileName}" does not match the expected Category role "${catConfig.role}".`,
1156
+ node
1157
+ });
1158
+ } else if (role && roleSegments.length > 1) {
1159
+ context.report({ message: multiRoleMessage, node });
1160
+ }
904
1161
  }
905
- state.packageImports[cleanKey] = cleanVal;
906
1162
  }
907
1163
  }
908
1164
  }
909
- } catch {
910
- }
1165
+ };
1166
+ },
1167
+ meta: {
1168
+ docs: {
1169
+ description: "Enforce lexical naming conventions for FAF folders and Logical Nodes"
1170
+ },
1171
+ schema: [],
1172
+ type: "problem"
911
1173
  }
912
- return state.packageImports;
913
- }
1174
+ };
1175
+ var naming_conventions_rule_default = rule6;
914
1176
 
915
1177
  // src/_rules/no-direct-fragment-import/no-direct-fragment-import.rule.ts
916
- var rule4 = {
1178
+ import path12 from "path";
1179
+ var rule7 = {
917
1180
  create(context) {
918
1181
  const absPath = context.filename;
919
1182
  const relPath = toRelativePath(absPath);
@@ -934,13 +1197,13 @@ var rule4 = {
934
1197
  if (!resolvedRelPath) {
935
1198
  return;
936
1199
  }
937
- const importedDir = path9.dirname(resolvedRelPath);
938
- const importedFile = path9.basename(resolvedRelPath);
1200
+ const importedDir = path12.dirname(resolvedRelPath);
1201
+ const importedFile = path12.basename(resolvedRelPath);
939
1202
  const importedFolderType = classifyFolder(importedDir, config);
940
1203
  if (importedFolderType === "fragment") {
941
1204
  const isAccessNode = importedFile === "index.ts" || importedFile === "index.js";
942
1205
  if (!isAccessNode) {
943
- const isInside = path9.dirname(relPath) === importedDir;
1206
+ const isInside = path12.dirname(relPath) === importedDir;
944
1207
  if (!isInside) {
945
1208
  context.report({
946
1209
  message: `Direct import of Fragment internal file "${importedFile}" is forbidden. You must import through its Access Node (index file) in "${importedDir}".`,
@@ -972,21 +1235,21 @@ var rule4 = {
972
1235
  type: "problem"
973
1236
  }
974
1237
  };
975
- var no_direct_fragment_import_rule_default = rule4;
1238
+ var no_direct_fragment_import_rule_default = rule7;
976
1239
 
977
1240
  // src/_rules/no-fractal-branch-leak/no-fractal-branch-leak.rule.ts
978
- import path12 from "path";
1241
+ import path15 from "path";
979
1242
 
980
1243
  // src/_rules/_rules@shared/_utils/_primitives/get-fractal-branch-owner/get-fractal-branch-owner.util.ts
981
- import path10 from "path";
1244
+ import path13 from "path";
982
1245
  function getFractalBranchOwner(relPath) {
983
1246
  let current = relPath;
984
1247
  while (current && current !== "." && current !== "/") {
985
- const folderName = path10.basename(current);
1248
+ const folderName = path13.basename(current);
986
1249
  if (folderName.startsWith("_") && folderName.includes("@shared")) {
987
- return path10.dirname(current).replace(/\\/g, "/");
1250
+ return path13.dirname(current).replace(/\\/g, "/");
988
1251
  }
989
- const parent = path10.dirname(current).replace(/\\/g, "/");
1252
+ const parent = path13.dirname(current).replace(/\\/g, "/");
990
1253
  if (parent === current) {
991
1254
  break;
992
1255
  }
@@ -1006,11 +1269,11 @@ function getImmediateSubDomain(relPath, privateCategoryPath) {
1006
1269
  }
1007
1270
 
1008
1271
  // src/_rules/_rules@shared/_utils/_systems/get-private-category-info/get-private-category-info.util.ts
1009
- import path11 from "path";
1272
+ import path14 from "path";
1010
1273
  function getPrivateCategoryInfo(relPath, config) {
1011
1274
  let current = relPath;
1012
1275
  while (current && current !== "." && current !== "/") {
1013
- const parent = path11.dirname(current).replace(/\\/g, "/");
1276
+ const parent = path14.dirname(current).replace(/\\/g, "/");
1014
1277
  if (parent === current) {
1015
1278
  break;
1016
1279
  }
@@ -1025,7 +1288,22 @@ function getPrivateCategoryInfo(relPath, config) {
1025
1288
  }
1026
1289
 
1027
1290
  // src/_rules/no-fractal-branch-leak/no-fractal-branch-leak.rule.ts
1028
- var rule5 = {
1291
+ function getFractalBranchPath(relPath) {
1292
+ let current = relPath;
1293
+ while (current && current !== "." && current !== "/") {
1294
+ const folderName = path15.basename(current);
1295
+ if (folderName.startsWith("_") && folderName.includes("@shared")) {
1296
+ return current;
1297
+ }
1298
+ const parent = path15.dirname(current).replace(/\\/g, "/");
1299
+ if (parent === current) {
1300
+ break;
1301
+ }
1302
+ current = parent;
1303
+ }
1304
+ return null;
1305
+ }
1306
+ var rule8 = {
1029
1307
  create(context) {
1030
1308
  const absPath = context.filename;
1031
1309
  const relPath = toRelativePath(absPath);
@@ -1046,7 +1324,7 @@ var rule5 = {
1046
1324
  if (!resolvedRelPath) {
1047
1325
  return;
1048
1326
  }
1049
- const importedDir = path12.dirname(resolvedRelPath);
1327
+ const importedDir = path15.dirname(resolvedRelPath);
1050
1328
  const fractalBranchOwner = getFractalBranchOwner(importedDir);
1051
1329
  if (fractalBranchOwner) {
1052
1330
  const isDescendant = relPath === fractalBranchOwner || relPath.startsWith(fractalBranchOwner + "/");
@@ -1059,7 +1337,7 @@ var rule5 = {
1059
1337
  const info = getPrivateCategoryInfo(importedDir, config);
1060
1338
  if (info) {
1061
1339
  const { owner, privateCategoryPath } = info;
1062
- const isPrivateDirectChild = path12.dirname(relPath) === owner;
1340
+ const isPrivateDirectChild = path15.dirname(relPath) === owner;
1063
1341
  const importerSubDomain = getImmediateSubDomain(
1064
1342
  relPath,
1065
1343
  privateCategoryPath
@@ -1079,6 +1357,18 @@ var rule5 = {
1079
1357
  }
1080
1358
  }
1081
1359
  }
1360
+ const importerBranchPath = getFractalBranchPath(path15.dirname(relPath));
1361
+ if (importerBranchPath) {
1362
+ const importerBranchOwner = path15.dirname(importerBranchPath).replace(/\\/g, "/");
1363
+ const isImportedInsideOwnBranch = resolvedRelPath === importerBranchPath || resolvedRelPath.startsWith(importerBranchPath + "/");
1364
+ const isImportedInsideServedSubtree = resolvedRelPath === importerBranchOwner || resolvedRelPath.startsWith(importerBranchOwner + "/");
1365
+ if (isImportedInsideServedSubtree && !isImportedInsideOwnBranch) {
1366
+ context.report({
1367
+ 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.`,
1368
+ node
1369
+ });
1370
+ }
1371
+ }
1082
1372
  }
1083
1373
  return {
1084
1374
  ExportAllDeclaration(node) {
@@ -1102,33 +1392,10 @@ var rule5 = {
1102
1392
  type: "problem"
1103
1393
  }
1104
1394
  };
1105
- var no_fractal_branch_leak_rule_default = rule5;
1395
+ var no_fractal_branch_leak_rule_default = rule8;
1106
1396
 
1107
1397
  // src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
1108
- import path13 from "path";
1109
-
1110
- // src/_rules/_rules@shared/_utils/_primitives/find-tree-config-including-excluded/find-tree-config-including-excluded.util.ts
1111
- function findTreeConfigIncludingExcluded(relPath, settings) {
1112
- if (!settings || !settings.trees) {
1113
- return null;
1114
- }
1115
- const cached = state.treeConfigIncludingExcludedCache.get(relPath);
1116
- if (cached !== void 0) {
1117
- return cached;
1118
- }
1119
- let result = null;
1120
- for (const tree of settings.trees) {
1121
- const isIncluded = tree.includes.some(
1122
- (inc) => relPath === inc || relPath.startsWith(inc + "/")
1123
- );
1124
- if (isIncluded) {
1125
- result = tree;
1126
- break;
1127
- }
1128
- }
1129
- state.treeConfigIncludingExcludedCache.set(relPath, result);
1130
- return result;
1131
- }
1398
+ import path18 from "path";
1132
1399
 
1133
1400
  // src/_rules/_rules@shared/_utils/_primitives/get-lca-and-sub-branches/get-lca-and-sub-branches.util.ts
1134
1401
  function getLcaAndSubBranches(pathA, pathB) {
@@ -1158,6 +1425,19 @@ function getRoleHierarchyIndex(role, config) {
1158
1425
  return -1;
1159
1426
  }
1160
1427
 
1428
+ // src/_rules/_rules@shared/_utils/_primitives/get-root-node-index/get-root-node-index.util.ts
1429
+ import path16 from "path";
1430
+ function getRootNodeIndex(relPath, rfConfig, lcaPath) {
1431
+ const relToLca = path16.relative(lcaPath, relPath).replace(/\\/g, "/");
1432
+ for (let i = 0; i < rfConfig.rootNodes.length; i++) {
1433
+ const group = rfConfig.rootNodes[i];
1434
+ if (group && group.includes(relToLca)) {
1435
+ return i;
1436
+ }
1437
+ }
1438
+ return -1;
1439
+ }
1440
+
1161
1441
  // src/_rules/_rules@shared/_utils/_primitives/resolve-horizontal-hierarchy/resolve-horizontal-hierarchy.util.ts
1162
1442
  function resolveHorizontalHierarchy(lcaPath, subA, subB, config) {
1163
1443
  const local = config.localHorizontalHierarchies?.find(
@@ -1181,39 +1461,12 @@ function resolveHorizontalHierarchy(lcaPath, subA, subB, config) {
1181
1461
  return { allowed: false, defined: false };
1182
1462
  }
1183
1463
 
1184
- // src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
1185
- function getDirectoryRole(dirPath, config) {
1186
- const folderName = path13.basename(dirPath);
1187
- if (folderName.startsWith("_")) {
1188
- return getRoleFromFolderName(folderName, config);
1189
- }
1190
- const contents = readDirCached(dirPath);
1191
- for (const file of contents.files) {
1192
- if (file.startsWith(folderName + ".")) {
1193
- const role = getFileRole(file, config);
1194
- if (role) return role;
1195
- }
1196
- }
1197
- return null;
1198
- }
1199
- function getRoleFromFolderName(name, config) {
1200
- const catConfig = config.categories?.find((c) => c.name === name);
1201
- return catConfig ? catConfig.role ?? null : null;
1202
- }
1203
- function getRootNodeIndex(relPath, rfConfig, lcaPath) {
1204
- const relToLca = path13.relative(lcaPath, relPath).replace(/\\/g, "/");
1205
- for (let i = 0; i < rfConfig.rootNodes.length; i++) {
1206
- const group = rfConfig.rootNodes[i];
1207
- if (group && group.includes(relToLca)) {
1208
- return i;
1209
- }
1210
- }
1211
- return -1;
1212
- }
1464
+ // src/_rules/_rules@shared/_utils/_systems/is-inside-private-category-of-lca/is-inside-private-category-of-lca.util.ts
1465
+ import path17 from "path";
1213
1466
  function isInsidePrivateCategoryOfLca(relPathB, lcaPath, config) {
1214
1467
  let current = relPathB;
1215
1468
  while (current && current !== "." && current !== "/" && current !== lcaPath) {
1216
- const parent = path13.dirname(current).replace(/\\/g, "/");
1469
+ const parent = path17.dirname(current).replace(/\\/g, "/");
1217
1470
  if (parent === current) {
1218
1471
  break;
1219
1472
  }
@@ -1226,7 +1479,9 @@ function isInsidePrivateCategoryOfLca(relPathB, lcaPath, config) {
1226
1479
  }
1227
1480
  return false;
1228
1481
  }
1229
- var rule6 = {
1482
+
1483
+ // src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
1484
+ var rule9 = {
1230
1485
  create(context) {
1231
1486
  const absPath = context.filename;
1232
1487
  const relPath = toRelativePath(absPath);
@@ -1234,11 +1489,11 @@ var rule6 = {
1234
1489
  if (!settings.faf) {
1235
1490
  return {};
1236
1491
  }
1237
- const config = findTreeConfigIncludingExcluded(relPath, settings.faf);
1492
+ const config = findTreeConfig(relPath, settings.faf);
1238
1493
  if (!config) {
1239
1494
  return {};
1240
1495
  }
1241
- const currentDir = path13.dirname(relPath);
1496
+ const currentDir = path18.dirname(relPath);
1242
1497
  const isCurrentFileExcluded = config.excludes?.some(
1243
1498
  (exc) => relPath === exc || relPath.startsWith(exc + "/")
1244
1499
  ) ?? false;
@@ -1254,27 +1509,13 @@ var rule6 = {
1254
1509
  const isImportedFileExcluded = config?.excludes?.some(
1255
1510
  (exc) => resolvedRelPath === exc || resolvedRelPath.startsWith(exc + "/")
1256
1511
  ) ?? false;
1257
- if (!isCurrentFileExcluded && isImportedFileExcluded) {
1258
- context.report({
1259
- message: `Importing from Foreign Domain "${resolvedRelPath}" is forbidden for Logical Nodes.`,
1260
- node
1261
- });
1262
- return;
1263
- }
1264
- if (isCurrentFileExcluded && !isImportedFileExcluded) {
1265
- context.report({
1266
- message: `Foreign Domain files cannot import Logical Nodes from FAF tree ("${resolvedRelPath}").`,
1267
- node
1268
- });
1269
- return;
1270
- }
1271
1512
  if (isCurrentFileExcluded || isImportedFileExcluded) {
1272
1513
  return;
1273
1514
  }
1274
- const importedDir = path13.dirname(resolvedRelPath);
1515
+ const importedDir = path18.dirname(resolvedRelPath);
1275
1516
  if (currentDir === importedDir) {
1276
- const fileA = path13.basename(relPath);
1277
- const fileB = path13.basename(resolvedRelPath);
1517
+ const fileA = path18.basename(relPath);
1518
+ const fileB = path18.basename(resolvedRelPath);
1278
1519
  const roleA = getFileRole(fileA, config);
1279
1520
  const roleB = getFileRole(fileB, config);
1280
1521
  if (roleA && roleB) {
@@ -1314,22 +1555,12 @@ var rule6 = {
1314
1555
  const idxRootA = getRootNodeIndex(relPath, rfConfig, lca);
1315
1556
  const idxRootB = getRootNodeIndex(resolvedRelPath, rfConfig, lca);
1316
1557
  if (idxRootA !== -1 && idxRootB !== -1) {
1317
- if (idxRootB >= idxRootA) {
1318
- context.report({
1319
- message: `Root Node import violation: "${path13.basename(relPath)}" cannot import from "${path13.basename(resolvedRelPath)}" under Root Fragment "${lca}".`,
1320
- node
1321
- });
1322
- }
1323
1558
  return;
1324
1559
  }
1325
1560
  }
1326
- const isSubARootFragment = classifyFolder(path13.posix.join(lca, subA), config) === "root-fragment";
1327
- const isSubBRootFragment = classifyFolder(path13.posix.join(lca, subB), config) === "root-fragment";
1561
+ const isSubARootFragment = classifyFolder(path18.posix.join(lca, subA), config) === "root-fragment";
1562
+ const isSubBRootFragment = classifyFolder(path18.posix.join(lca, subB), config) === "root-fragment";
1328
1563
  if (isSubARootFragment || isSubBRootFragment) {
1329
- context.report({
1330
- 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.`,
1331
- node
1332
- });
1333
1564
  return;
1334
1565
  }
1335
1566
  const { allowed, defined: hasDefinedHierarchy } = resolveHorizontalHierarchy(lca, subA, subB, config);
@@ -1341,23 +1572,10 @@ var rule6 = {
1341
1572
  });
1342
1573
  }
1343
1574
  } else {
1344
- const roleA = getDirectoryRole(path13.posix.join(lca, subA), config);
1345
- const roleB = getDirectoryRole(path13.posix.join(lca, subB), config);
1346
- const idxA = roleA ? getRoleHierarchyIndex(roleA, config) : -1;
1347
- const idxB = roleB ? getRoleHierarchyIndex(roleB, config) : -1;
1348
- if (idxA !== -1 && idxB !== -1) {
1349
- if (idxB >= idxA) {
1350
- context.report({
1351
- 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}".`,
1352
- node
1353
- });
1354
- }
1355
- } else {
1356
- context.report({
1357
- message: `Peer separation violation: sibling directories "${subA}" and "${subB}" cannot import each other because no horizontal hierarchy is defined under parent "${lca}".`,
1358
- node
1359
- });
1360
- }
1575
+ context.report({
1576
+ message: `Peer separation violation: sibling directories "${subA}" and "${subB}" cannot import each other because no horizontal hierarchy is defined under parent "${lca}".`,
1577
+ node
1578
+ });
1361
1579
  }
1362
1580
  }
1363
1581
  return {
@@ -1382,11 +1600,11 @@ var rule6 = {
1382
1600
  type: "problem"
1383
1601
  }
1384
1602
  };
1385
- var no_peer_dependency_rule_default = rule6;
1603
+ var no_peer_dependency_rule_default = rule9;
1386
1604
 
1387
1605
  // src/_rules/no-private-category-leak/no-private-category-leak.rule.ts
1388
- import path14 from "path";
1389
- var rule7 = {
1606
+ import path19 from "path";
1607
+ var rule10 = {
1390
1608
  create(context) {
1391
1609
  const absPath = context.filename;
1392
1610
  const relPath = toRelativePath(absPath);
@@ -1407,7 +1625,7 @@ var rule7 = {
1407
1625
  if (!resolvedRelPath) {
1408
1626
  return;
1409
1627
  }
1410
- const importedDir = path14.dirname(resolvedRelPath);
1628
+ const importedDir = path19.dirname(resolvedRelPath);
1411
1629
  const info = getPrivateCategoryInfo(importedDir, config);
1412
1630
  if (info) {
1413
1631
  const { owner, privateCategoryPath } = info;
@@ -1421,7 +1639,7 @@ var rule7 = {
1421
1639
  isFractalBranchAllowed = relPath === fbOwner || relPath.startsWith(fbOwner + "/");
1422
1640
  }
1423
1641
  }
1424
- const isDirectChild = path14.dirname(relPath) === owner;
1642
+ const isDirectChild = path19.dirname(relPath) === owner;
1425
1643
  const importerSubDomain = getImmediateSubDomain(
1426
1644
  relPath,
1427
1645
  privateCategoryPath
@@ -1462,17 +1680,119 @@ var rule7 = {
1462
1680
  type: "problem"
1463
1681
  }
1464
1682
  };
1465
- var no_private_category_leak_rule_default = rule7;
1683
+ var no_private_category_leak_rule_default = rule10;
1684
+
1685
+ // src/_rules/root-node-dependency-direction/root-node-dependency-direction.rule.ts
1686
+ import path20 from "path";
1687
+ var rule11 = {
1688
+ create(context) {
1689
+ const absPath = context.filename;
1690
+ const relPath = toRelativePath(absPath);
1691
+ const settings = context.settings;
1692
+ if (!settings.faf) {
1693
+ return {};
1694
+ }
1695
+ const config = findTreeConfig(relPath, settings.faf);
1696
+ if (!config) {
1697
+ return {};
1698
+ }
1699
+ const currentDir = path20.dirname(relPath);
1700
+ const isCurrentFileExcluded = config.excludes?.some(
1701
+ (exc) => relPath === exc || relPath.startsWith(exc + "/")
1702
+ ) ?? false;
1703
+ function checkImport(node, importValue) {
1704
+ const resolvedRelPath = resolveImportPath(
1705
+ importValue,
1706
+ relPath,
1707
+ settings.faf
1708
+ );
1709
+ if (!resolvedRelPath) {
1710
+ return;
1711
+ }
1712
+ const isImportedFileExcluded = config?.excludes?.some(
1713
+ (exc) => resolvedRelPath === exc || resolvedRelPath.startsWith(exc + "/")
1714
+ ) ?? false;
1715
+ if (isCurrentFileExcluded || isImportedFileExcluded) {
1716
+ return;
1717
+ }
1718
+ const importedDir = path20.dirname(resolvedRelPath);
1719
+ if (currentDir === importedDir) {
1720
+ return;
1721
+ }
1722
+ const lcaInfo = getLcaAndSubBranches(currentDir, importedDir);
1723
+ if (!lcaInfo) {
1724
+ return;
1725
+ }
1726
+ const { lca, subA, subB } = lcaInfo;
1727
+ if (subB.startsWith("_") && subB.includes("@shared")) {
1728
+ return;
1729
+ }
1730
+ if (isInsidePrivateCategoryOfLca(importedDir, lca, config)) {
1731
+ return;
1732
+ }
1733
+ if (subB === "" && classifyFolder(lca, config) === "fragment") {
1734
+ return;
1735
+ }
1736
+ const rfConfig = getRootFragmentConfig(lca, config);
1737
+ if (rfConfig) {
1738
+ const idxRootA = getRootNodeIndex(relPath, rfConfig, lca);
1739
+ const idxRootB = getRootNodeIndex(resolvedRelPath, rfConfig, lca);
1740
+ if (idxRootA !== -1 && idxRootB !== -1) {
1741
+ if (idxRootB >= idxRootA) {
1742
+ context.report({
1743
+ message: `Root Node import violation: "${path20.basename(relPath)}" cannot import from "${path20.basename(resolvedRelPath)}" under Root Fragment "${lca}".`,
1744
+ node
1745
+ });
1746
+ }
1747
+ return;
1748
+ }
1749
+ }
1750
+ const isSubARootFragment = classifyFolder(path20.posix.join(lca, subA), config) === "root-fragment";
1751
+ const isSubBRootFragment = classifyFolder(path20.posix.join(lca, subB), config) === "root-fragment";
1752
+ if (isSubARootFragment || isSubBRootFragment) {
1753
+ context.report({
1754
+ 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.`,
1755
+ node
1756
+ });
1757
+ }
1758
+ }
1759
+ return {
1760
+ ExportAllDeclaration(node) {
1761
+ checkImport(node, node.source.value);
1762
+ },
1763
+ ExportNamedDeclaration(node) {
1764
+ if (node.source) {
1765
+ checkImport(node, node.source.value);
1766
+ }
1767
+ },
1768
+ ImportDeclaration(node) {
1769
+ checkImport(node, node.source.value);
1770
+ }
1771
+ };
1772
+ },
1773
+ meta: {
1774
+ docs: {
1775
+ description: "Enforce explicit, non-Role-based dependency direction between Root Nodes"
1776
+ },
1777
+ schema: [],
1778
+ type: "problem"
1779
+ }
1780
+ };
1781
+ var root_node_dependency_direction_rule_default = rule11;
1466
1782
 
1467
1783
  // src/rules.ts
1468
1784
  var rules = {
1469
1785
  "category-mutually-exclusive": category_mutually_exclusive_rule_default,
1470
1786
  "enforce-access-node": enforce_access_node_rule_default,
1787
+ "foreign-domain-isolation": foreign_domain_isolation_rule_default,
1788
+ "fragment-master-node": fragment_master_node_rule_default,
1789
+ "logical-domain-placement": logical_domain_placement_rule_default,
1471
1790
  "naming-conventions": naming_conventions_rule_default,
1472
1791
  "no-direct-fragment-import": no_direct_fragment_import_rule_default,
1473
1792
  "no-fractal-branch-leak": no_fractal_branch_leak_rule_default,
1474
1793
  "no-peer-dependency": no_peer_dependency_rule_default,
1475
- "no-private-category-leak": no_private_category_leak_rule_default
1794
+ "no-private-category-leak": no_private_category_leak_rule_default,
1795
+ "root-node-dependency-direction": root_node_dependency_direction_rule_default
1476
1796
  };
1477
1797
 
1478
1798
  // src/configs.ts
@@ -1486,11 +1806,15 @@ var configs = {
1486
1806
  rules: {
1487
1807
  "faf/category-mutually-exclusive": "error",
1488
1808
  "faf/enforce-access-node": "error",
1809
+ "faf/foreign-domain-isolation": "error",
1810
+ "faf/fragment-master-node": "error",
1811
+ "faf/logical-domain-placement": "error",
1489
1812
  "faf/naming-conventions": "error",
1490
1813
  "faf/no-direct-fragment-import": "error",
1491
1814
  "faf/no-fractal-branch-leak": "error",
1492
1815
  "faf/no-peer-dependency": "error",
1493
- "faf/no-private-category-leak": "error"
1816
+ "faf/no-private-category-leak": "error",
1817
+ "faf/root-node-dependency-direction": "error"
1494
1818
  }
1495
1819
  }
1496
1820
  };