@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.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.4.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) {
777
+ return;
778
+ }
779
+ const config = findTreeConfig(relPath, settings.faf);
780
+ if (!config) {
468
781
  return;
469
782
  }
470
- const relDir = path7.dirname(relPath);
471
- const folderName = path7.basename(relDir);
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,309 +996,187 @@ 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) {
617
1028
  return;
618
1029
  }
619
- const rfConfig = getRootFragmentConfig(relDir, config);
620
- if (rfConfig) {
621
- const flatRootNodes = rfConfig.rootNodes.flat();
622
- const isMatchedRootNode = flatRootNodes.some((rn) => {
623
- if (rn.includes("/")) {
624
- const rfPath = rfConfig.paths[0] ?? "";
625
- const absRn = path7.resolve(getProjectRoot(), rfPath, rn);
626
- return toRelativePath(absRn) === relPath;
627
- }
628
- return rn === fileName;
629
- });
630
- if (isMatchedRootNode) {
631
- return;
632
- }
1030
+ const relDir = path11.dirname(relPath);
1031
+ const parentType = classifyFolder(relDir, config);
1032
+ if (parentType === "foreign") {
1033
+ return;
633
1034
  }
634
- if (fileName === "index.ts" || fileName === "index.js") {
635
- if (parentType !== "fragment") {
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("_")) {
636
1042
  context.report({
637
- message: `Access Nodes ("index.ts/index.js") are exclusive to Fragments. Found index file directly inside "${folderName}" (classified as ${parentType}).`,
1043
+ message: `Layer/Category directory "${currentFolderName}" must be prefixed with an underscore (e.g. "_${currentFolderName}").`,
638
1044
  node
639
1045
  });
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
1046
  }
652
- return;
653
- }
654
- if (fileName === "README.md" || fileName === "package.json" || fileName === "tsconfig.json") {
655
- return;
656
- }
657
- const role = getFileRole(fileName, config);
658
- const ext = path7.extname(fileName);
659
- const allRoleNames = new Set(config.roles.flat());
660
- const roleSegments = fileName.slice(0, fileName.length - ext.length).split(".").slice(1).filter((part) => allRoleNames.has(part));
661
- 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)) {
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) {
680
1051
  context.report({
681
- message: `Route terminal Fragment "${folderName}" must follow the "<method>-<object>" pattern with a lowercase HTTP method (${httpMethods.join(", ")}).`,
1052
+ message: `Directory "${currentFolderName}" has an Access Node (index.ts) but its name starts with "_". Fragments must not be prefixed with underscore.`,
682
1053
  node
683
1054
  });
684
1055
  }
685
1056
  }
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;
1057
+ if (currentFolderName !== "src") {
1058
+ let baseFolderName = currentFolderName.replace(/@shared$/, "");
1059
+ if (baseFolderName.startsWith("_")) {
1060
+ baseFolderName = baseFolderName.substring(1);
697
1061
  }
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)) {
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) {
706
1070
  context.report({
707
- message: `Fragment "${folderName}" inside Category "${categoryFolderName}" must contain a Master Node with role "${categoryConfig.role}" (e.g. "${folderName}.${categoryConfig.role}.ts").`,
1071
+ message: `Folder name "${currentFolderName}" must be in kebab-case (e.g. "_my-folder" or "_[my-param]").`,
708
1072
  node
709
1073
  });
710
1074
  }
711
1075
  }
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) {
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) {
731
1081
  context.report({
732
- message: `Fragment Node "${fileName}" shares Role "${role}" with sibling "${duplicate}". Each Fragment Node must have a unique Role within its Fragment.`,
1082
+ message: `Fractal Branch name "${currentFolderName}" must match its parent scope name: "${expectedName}".`,
733
1083
  node
734
1084
  });
735
1085
  }
736
1086
  }
737
1087
  }
738
- if (parentType === "category") {
739
- const catConfig = getCategoryConfig(relDir, config);
740
- if (catConfig) {
741
- const allowedExts = catConfig.allowedExtensions ?? [
742
- ".js",
743
- ".jsx",
744
- ".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
- }
1088
+ if (parentType === "layer" || parentType === "fractal-branch" || parentType === "invalid-fragment") {
1089
+ return;
1090
+ }
1091
+ const rfConfig = getRootFragmentConfig(relDir, config);
1092
+ if (rfConfig) {
1093
+ const flatRootNodes = rfConfig.rootNodes.flat();
1094
+ const isMatchedRootNode = flatRootNodes.some((rn) => {
1095
+ if (rn.includes("/")) {
1096
+ const rfPath = rfConfig.paths[0] ?? "";
1097
+ const absRn = path11.resolve(getProjectRoot(), rfPath, rn);
1098
+ return toRelativePath(absRn) === relPath;
778
1099
  }
1100
+ return rn === fileName;
1101
+ });
1102
+ if (isMatchedRootNode) {
1103
+ return;
779
1104
  }
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);
1105
+ }
1106
+ if (fileName === "index.ts" || fileName === "index.js") {
1107
+ return;
1108
+ }
1109
+ if (fileName === "README.md" || fileName === "package.json" || fileName === "tsconfig.json") {
1110
+ return;
1111
+ }
1112
+ const role = getFileRole(fileName, config);
1113
+ const ext = path11.extname(fileName);
1114
+ const allRoleNames = new Set(config.roles.flat());
1115
+ const roleSegments = fileName.slice(0, fileName.length - ext.length).split(".").slice(1).filter((part) => allRoleNames.has(part));
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.`;
1117
+ if (parentType === "fragment" && role && roleSegments.length > 1) {
1118
+ context.report({ message: multiRoleMessage, node });
1119
+ }
1120
+ if (parentType === "category") {
1121
+ const folderName = path11.basename(relDir);
1122
+ const catConfig = getCategoryConfig(relDir, config);
1123
+ if (catConfig) {
1124
+ const allowedExts = catConfig.allowedExtensions ?? [
1125
+ ".js",
1126
+ ".jsx",
1127
+ ".ts",
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,21 +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 getRootNodeIndex(relPath, rfConfig, lcaPath) {
1186
- const relToLca = path13.relative(lcaPath, relPath).replace(/\\/g, "/");
1187
- for (let i = 0; i < rfConfig.rootNodes.length; i++) {
1188
- const group = rfConfig.rootNodes[i];
1189
- if (group && group.includes(relToLca)) {
1190
- return i;
1191
- }
1192
- }
1193
- return -1;
1194
- }
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";
1195
1466
  function isInsidePrivateCategoryOfLca(relPathB, lcaPath, config) {
1196
1467
  let current = relPathB;
1197
1468
  while (current && current !== "." && current !== "/" && current !== lcaPath) {
1198
- const parent = path13.dirname(current).replace(/\\/g, "/");
1469
+ const parent = path17.dirname(current).replace(/\\/g, "/");
1199
1470
  if (parent === current) {
1200
1471
  break;
1201
1472
  }
@@ -1208,7 +1479,9 @@ function isInsidePrivateCategoryOfLca(relPathB, lcaPath, config) {
1208
1479
  }
1209
1480
  return false;
1210
1481
  }
1211
- var rule6 = {
1482
+
1483
+ // src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
1484
+ var rule9 = {
1212
1485
  create(context) {
1213
1486
  const absPath = context.filename;
1214
1487
  const relPath = toRelativePath(absPath);
@@ -1216,11 +1489,11 @@ var rule6 = {
1216
1489
  if (!settings.faf) {
1217
1490
  return {};
1218
1491
  }
1219
- const config = findTreeConfigIncludingExcluded(relPath, settings.faf);
1492
+ const config = findTreeConfig(relPath, settings.faf);
1220
1493
  if (!config) {
1221
1494
  return {};
1222
1495
  }
1223
- const currentDir = path13.dirname(relPath);
1496
+ const currentDir = path18.dirname(relPath);
1224
1497
  const isCurrentFileExcluded = config.excludes?.some(
1225
1498
  (exc) => relPath === exc || relPath.startsWith(exc + "/")
1226
1499
  ) ?? false;
@@ -1236,27 +1509,13 @@ var rule6 = {
1236
1509
  const isImportedFileExcluded = config?.excludes?.some(
1237
1510
  (exc) => resolvedRelPath === exc || resolvedRelPath.startsWith(exc + "/")
1238
1511
  ) ?? false;
1239
- if (!isCurrentFileExcluded && isImportedFileExcluded) {
1240
- context.report({
1241
- message: `Importing from Foreign Domain "${resolvedRelPath}" is forbidden for Logical Nodes.`,
1242
- node
1243
- });
1244
- return;
1245
- }
1246
- if (isCurrentFileExcluded && !isImportedFileExcluded) {
1247
- context.report({
1248
- message: `Foreign Domain files cannot import Logical Nodes from FAF tree ("${resolvedRelPath}").`,
1249
- node
1250
- });
1251
- return;
1252
- }
1253
1512
  if (isCurrentFileExcluded || isImportedFileExcluded) {
1254
1513
  return;
1255
1514
  }
1256
- const importedDir = path13.dirname(resolvedRelPath);
1515
+ const importedDir = path18.dirname(resolvedRelPath);
1257
1516
  if (currentDir === importedDir) {
1258
- const fileA = path13.basename(relPath);
1259
- const fileB = path13.basename(resolvedRelPath);
1517
+ const fileA = path18.basename(relPath);
1518
+ const fileB = path18.basename(resolvedRelPath);
1260
1519
  const roleA = getFileRole(fileA, config);
1261
1520
  const roleB = getFileRole(fileB, config);
1262
1521
  if (roleA && roleB) {
@@ -1296,22 +1555,12 @@ var rule6 = {
1296
1555
  const idxRootA = getRootNodeIndex(relPath, rfConfig, lca);
1297
1556
  const idxRootB = getRootNodeIndex(resolvedRelPath, rfConfig, lca);
1298
1557
  if (idxRootA !== -1 && idxRootB !== -1) {
1299
- if (idxRootB >= idxRootA) {
1300
- context.report({
1301
- message: `Root Node import violation: "${path13.basename(relPath)}" cannot import from "${path13.basename(resolvedRelPath)}" under Root Fragment "${lca}".`,
1302
- node
1303
- });
1304
- }
1305
1558
  return;
1306
1559
  }
1307
1560
  }
1308
- const isSubARootFragment = classifyFolder(path13.posix.join(lca, subA), config) === "root-fragment";
1309
- 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";
1310
1563
  if (isSubARootFragment || isSubBRootFragment) {
1311
- context.report({
1312
- 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.`,
1313
- node
1314
- });
1315
1564
  return;
1316
1565
  }
1317
1566
  const { allowed, defined: hasDefinedHierarchy } = resolveHorizontalHierarchy(lca, subA, subB, config);
@@ -1351,11 +1600,11 @@ var rule6 = {
1351
1600
  type: "problem"
1352
1601
  }
1353
1602
  };
1354
- var no_peer_dependency_rule_default = rule6;
1603
+ var no_peer_dependency_rule_default = rule9;
1355
1604
 
1356
1605
  // src/_rules/no-private-category-leak/no-private-category-leak.rule.ts
1357
- import path14 from "path";
1358
- var rule7 = {
1606
+ import path19 from "path";
1607
+ var rule10 = {
1359
1608
  create(context) {
1360
1609
  const absPath = context.filename;
1361
1610
  const relPath = toRelativePath(absPath);
@@ -1376,7 +1625,7 @@ var rule7 = {
1376
1625
  if (!resolvedRelPath) {
1377
1626
  return;
1378
1627
  }
1379
- const importedDir = path14.dirname(resolvedRelPath);
1628
+ const importedDir = path19.dirname(resolvedRelPath);
1380
1629
  const info = getPrivateCategoryInfo(importedDir, config);
1381
1630
  if (info) {
1382
1631
  const { owner, privateCategoryPath } = info;
@@ -1390,7 +1639,7 @@ var rule7 = {
1390
1639
  isFractalBranchAllowed = relPath === fbOwner || relPath.startsWith(fbOwner + "/");
1391
1640
  }
1392
1641
  }
1393
- const isDirectChild = path14.dirname(relPath) === owner;
1642
+ const isDirectChild = path19.dirname(relPath) === owner;
1394
1643
  const importerSubDomain = getImmediateSubDomain(
1395
1644
  relPath,
1396
1645
  privateCategoryPath
@@ -1431,17 +1680,119 @@ var rule7 = {
1431
1680
  type: "problem"
1432
1681
  }
1433
1682
  };
1434
- 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;
1435
1782
 
1436
1783
  // src/rules.ts
1437
1784
  var rules = {
1438
1785
  "category-mutually-exclusive": category_mutually_exclusive_rule_default,
1439
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,
1440
1790
  "naming-conventions": naming_conventions_rule_default,
1441
1791
  "no-direct-fragment-import": no_direct_fragment_import_rule_default,
1442
1792
  "no-fractal-branch-leak": no_fractal_branch_leak_rule_default,
1443
1793
  "no-peer-dependency": no_peer_dependency_rule_default,
1444
- "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
1445
1796
  };
1446
1797
 
1447
1798
  // src/configs.ts
@@ -1455,11 +1806,15 @@ var configs = {
1455
1806
  rules: {
1456
1807
  "faf/category-mutually-exclusive": "error",
1457
1808
  "faf/enforce-access-node": "error",
1809
+ "faf/foreign-domain-isolation": "error",
1810
+ "faf/fragment-master-node": "error",
1811
+ "faf/logical-domain-placement": "error",
1458
1812
  "faf/naming-conventions": "error",
1459
1813
  "faf/no-direct-fragment-import": "error",
1460
1814
  "faf/no-fractal-branch-leak": "error",
1461
1815
  "faf/no-peer-dependency": "error",
1462
- "faf/no-private-category-leak": "error"
1816
+ "faf/no-private-category-leak": "error",
1817
+ "faf/root-node-dependency-direction": "error"
1463
1818
  }
1464
1819
  }
1465
1820
  };