@solid-stack/mason 1.0.31 → 1.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -219,6 +219,16 @@ function getConfiguredSharedDir(projectRoot, flags) {
219
219
  }
220
220
  return path2__default.default.join(projectRoot, "shared");
221
221
  }
222
+ function loadLintConfig(projectRoot) {
223
+ const configPath = path2__default.default.join(projectRoot, "mason-lint.config.json");
224
+ if (fs4__default.default.existsSync(configPath)) {
225
+ try {
226
+ return JSON.parse(fs4__default.default.readFileSync(configPath, "utf-8"));
227
+ } catch {
228
+ }
229
+ }
230
+ return { baseErrorPath: "src/errors/AppError.ts" };
231
+ }
222
232
 
223
233
  // src/utils/project.ts
224
234
  function findProjectRoot(startDir = process.cwd()) {
@@ -4915,22 +4925,47 @@ function scanDirFiles(dir, projectRoot) {
4915
4925
  function classifyFile(fullPath, projectRoot) {
4916
4926
  const rel = path2__default.default.relative(projectRoot, fullPath).replace(/\\/g, "/");
4917
4927
  const featureMatch = rel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/.*)?$/);
4918
- if (!featureMatch || !featureMatch[1] || !featureMatch[2]) return {};
4919
- const feature = featureMatch[1];
4920
- const subfolder = featureMatch[2];
4921
- let layer;
4922
- if (subfolder === "domain" || subfolder === "errors") {
4923
- layer = "domain";
4924
- } else if (subfolder === "useCases" || subfolder === "usecases") {
4925
- layer = "useCases";
4926
- } else if (subfolder === "services") {
4927
- layer = "services";
4928
- } else if (subfolder === "infrastructure") {
4929
- layer = "infrastructure";
4930
- } else if (subfolder === "pxExpress" || subfolder === "presentation" || subfolder === "handlers" || subfolder.startsWith("px")) {
4931
- layer = "presentation";
4932
- }
4933
- return { feature, layer };
4928
+ if (featureMatch && featureMatch[1] && featureMatch[2]) {
4929
+ const feature = featureMatch[1];
4930
+ const subfolder = featureMatch[2];
4931
+ let layer;
4932
+ if (subfolder === "domain" || subfolder === "errors") {
4933
+ layer = "domain";
4934
+ } else if (subfolder === "useCases" || subfolder === "usecases") {
4935
+ layer = "useCases";
4936
+ } else if (subfolder === "services") {
4937
+ layer = "services";
4938
+ } else if (subfolder === "infrastructure") {
4939
+ layer = "infrastructure";
4940
+ } else if (subfolder === "pxExpress" || subfolder === "presentation" || subfolder === "handlers" || subfolder.startsWith("px")) {
4941
+ layer = "presentation";
4942
+ }
4943
+ return { moduleType: "feature", moduleName: feature, feature, layer };
4944
+ }
4945
+ const sharedMatch = rel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
4946
+ if (sharedMatch && sharedMatch[1]) {
4947
+ const moduleName = sharedMatch[1];
4948
+ const rest = sharedMatch[2] || "";
4949
+ let layer = "root";
4950
+ if (rest.startsWith("domain/") || rest === "domain" || rest.startsWith("errors/") || rest === "errors") {
4951
+ layer = "domain";
4952
+ } else if (rest.startsWith("ports/") || rest === "ports") {
4953
+ layer = "ports";
4954
+ } else if (rest.startsWith("infrastructure/") || rest === "infrastructure") {
4955
+ layer = "infrastructure";
4956
+ }
4957
+ return { moduleType: "shared", moduleName, feature: moduleName, layer };
4958
+ }
4959
+ const errorMatch = rel.match(/(?:^|\/)(?:src\/)?errors(?:\/(.*))?$/);
4960
+ if (errorMatch) {
4961
+ return {
4962
+ moduleType: "error",
4963
+ moduleName: "global-errors",
4964
+ feature: "global-errors",
4965
+ layer: "domain"
4966
+ };
4967
+ }
4968
+ return {};
4934
4969
  }
4935
4970
  function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4936
4971
  if (specifier === "@solid-stack/di" || specifier.startsWith("@solid-stack/di/")) {
@@ -4955,15 +4990,28 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4955
4990
  resolvedRel = specifier.replace(/^[@~]\//, "src/");
4956
4991
  } else if (specifier === "@configs" || specifier.startsWith("@configs/")) {
4957
4992
  resolvedRel = "src/" + specifier.slice(1);
4993
+ } else if (specifier.startsWith("@/errors/") || specifier.startsWith("~/errors/") || specifier === "@/errors" || specifier === "~/errors") {
4994
+ resolvedRel = specifier.replace(/^[@~]\//, "src/");
4995
+ } else if (specifier === "@errors" || specifier.startsWith("@errors/")) {
4996
+ resolvedRel = "src/" + specifier.slice(1);
4958
4997
  } else if (specifier.startsWith("@/") || specifier.startsWith("~/")) {
4959
4998
  resolvedRel = "src/" + specifier.slice(2);
4960
- } else if (specifier.startsWith("features/") || specifier.startsWith("shared/") || specifier.startsWith("configs/") || specifier === "configs") {
4999
+ } else if (specifier.startsWith("features/") || specifier.startsWith("shared/") || specifier.startsWith("configs/") || specifier === "configs" || specifier.startsWith("errors/") || specifier === "errors") {
4961
5000
  resolvedRel = specifier;
4962
5001
  }
4963
5002
  if (!resolvedRel) {
4964
5003
  return { isExternal: true, isDiFramework: false };
4965
5004
  }
4966
5005
  const isGlobalConfig = resolvedRel.startsWith("src/configs/") || resolvedRel === "src/configs" || resolvedRel.startsWith("configs/") || resolvedRel === "configs";
5006
+ const isGlobalError = resolvedRel.startsWith("src/errors/") || resolvedRel === "src/errors" || resolvedRel.startsWith("errors/") || resolvedRel === "errors";
5007
+ if (isGlobalError) {
5008
+ return {
5009
+ isExternal: false,
5010
+ isDiFramework: false,
5011
+ isGlobalError: true,
5012
+ isTargetError: true
5013
+ };
5014
+ }
4967
5015
  const featMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/(.*))?$/);
4968
5016
  if (featMatch && featMatch[1] && featMatch[2]) {
4969
5017
  const targetFeature = featMatch[1];
@@ -4985,6 +5033,7 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4985
5033
  const targetFileName = basePart.replace(/\.(js|ts|mjs|cjs)$/, "");
4986
5034
  const isTargetPort = /^I[A-Z]/.test(targetFileName);
4987
5035
  const isTargetRepo = isTargetPort && targetFileName.includes("Repo");
5036
+ const isTargetError = targetSubfolder === "errors" || rest.startsWith("errors/") || rest.includes("/errors/") || targetFileName.endsWith("Error") || targetFileName.endsWith("Errors");
4988
5037
  return {
4989
5038
  isExternal: false,
4990
5039
  isDiFramework: false,
@@ -4993,7 +5042,8 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4993
5042
  targetSubfolder,
4994
5043
  targetFileName,
4995
5044
  isTargetPort,
4996
- isTargetRepo
5045
+ isTargetRepo,
5046
+ isTargetError
4997
5047
  };
4998
5048
  }
4999
5049
  const sharedMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
@@ -5019,17 +5069,34 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
5019
5069
  function extractImportStatements(content) {
5020
5070
  const statements = [];
5021
5071
  const lines = content.split(/\r?\n/);
5022
- const regex = /(?:import\s+(?:type\s+)?(?:[\s\w*$,{}]+\s+from\s+)?|export\s+(?:type\s+)?(?:[\s\w*$,{}]+\s+from\s+)?|import\s*\(|require\s*\()\s*["']([^"']+)["']/g;
5072
+ const regex = /(?:import\s+(type\s+)?(?:([\s\w*$,{}]+)\s+from\s+)?|export\s+(type\s+)?(?:([\s\w*$,{}]+)\s+from\s+)?|import\s*\(|require\s*\()\s*["']([^"']+)["']/g;
5023
5073
  let match;
5024
5074
  while ((match = regex.exec(content)) !== null) {
5025
- const specifier = match[1];
5075
+ const isTypeOnly = Boolean(match[1] || match[3]);
5076
+ const importClause = (match[2] || match[4] || "").trim();
5077
+ const specifier = match[5];
5026
5078
  const charIndex = match.index;
5027
5079
  const lineNum = content.substring(0, charIndex).split("\n").length;
5028
5080
  const rawCode = lines[lineNum - 1]?.trim() || match[0].trim();
5081
+ const importedNames = [];
5082
+ if (importClause) {
5083
+ const cleaned = importClause.replace(/[{}]/g, "");
5084
+ const words = cleaned.split(",").map((s) => s.trim()).filter(Boolean);
5085
+ for (const w of words) {
5086
+ const parts = w.split(/\s+as\s+/);
5087
+ const name = parts[0].replace(/^type\s+/, "").trim();
5088
+ const alias = (parts[1] || name).trim();
5089
+ if (alias && alias !== "*") {
5090
+ importedNames.push(alias);
5091
+ }
5092
+ }
5093
+ }
5029
5094
  statements.push({
5030
5095
  specifier,
5031
5096
  line: lineNum,
5032
- rawCode
5097
+ rawCode,
5098
+ isTypeOnly,
5099
+ importedNames
5033
5100
  });
5034
5101
  }
5035
5102
  return statements;
@@ -5128,6 +5195,15 @@ function discoverAllFeatures(projectRoot) {
5128
5195
  dir: path2__default.default.join(featuresBase, e.name)
5129
5196
  })).sort((a, b) => a.name.localeCompare(b.name));
5130
5197
  }
5198
+ function discoverAllShared(projectRoot) {
5199
+ const sharedBase = resolveSharedDir(projectRoot);
5200
+ if (!fs4__default.default.existsSync(sharedBase)) return [];
5201
+ const entries = fs4__default.default.readdirSync(sharedBase, { withFileTypes: true });
5202
+ return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "__tests__" && e.name !== "tests").map((e) => ({
5203
+ name: e.name,
5204
+ dir: path2__default.default.join(sharedBase, e.name)
5205
+ })).sort((a, b) => a.name.localeCompare(b.name));
5206
+ }
5131
5207
  function extractClassMethods(content, className) {
5132
5208
  let clean = "";
5133
5209
  let inLineComment = false;
@@ -5476,17 +5552,56 @@ function lintCodebase(options = {}) {
5476
5552
  const startTime = Date.now();
5477
5553
  const projectRoot = options.projectRoot || findProjectRoot(options.targetDir || process.cwd());
5478
5554
  const failFast = !options.complete;
5555
+ const lintConfig = loadLintConfig(projectRoot);
5556
+ const baseErrorRelative = lintConfig.baseErrorPath || "src/errors/AppError.ts";
5557
+ const baseErrorAbs = path2__default.default.resolve(projectRoot, baseErrorRelative);
5558
+ const featuresBase = resolveFeaturesDir(projectRoot);
5559
+ const sharedBase = resolveSharedDir(projectRoot);
5560
+ let errorsBase = path2__default.default.join(projectRoot, "src/errors");
5561
+ if (!fs4__default.default.existsSync(errorsBase) && fs4__default.default.existsSync(path2__default.default.join(projectRoot, "errors"))) {
5562
+ errorsBase = path2__default.default.join(projectRoot, "errors");
5563
+ }
5564
+ const missingFoundations = [];
5565
+ if (!fs4__default.default.existsSync(featuresBase)) missingFoundations.push("features/");
5566
+ if (!fs4__default.default.existsSync(sharedBase)) missingFoundations.push("shared/");
5567
+ if (!fs4__default.default.existsSync(errorsBase)) missingFoundations.push("errors/");
5568
+ if (missingFoundations.length > 0) {
5569
+ throw new DependencyRuleError(
5570
+ `Project Structure Violation: Missing required foundational directories: ${missingFoundations.join(", ")}. Every project must contain 'features/', 'shared/', and 'errors/'.`,
5571
+ [],
5572
+ true
5573
+ );
5574
+ }
5575
+ if (!fs4__default.default.existsSync(baseErrorAbs)) {
5576
+ throw new DependencyRuleError(
5577
+ `Project Structure Violation: Base error class not found at '${baseErrorRelative}'. Create this file or update 'baseErrorPath' in mason-lint.config.json.`,
5578
+ [],
5579
+ true
5580
+ );
5581
+ }
5582
+ const baseErrorContent = fs4__default.default.readFileSync(baseErrorAbs, "utf-8");
5583
+ const baseClassMatch = baseErrorContent.match(
5584
+ /(?:export\s+(?:default\s+)?)?(?:abstract\s+)?class\s+([A-Za-z0-9_]+)/
5585
+ );
5586
+ const baseErrorClassName = baseClassMatch ? baseClassMatch[1] : "AppError";
5479
5587
  const features = discoverAllFeatures(projectRoot);
5588
+ const sharedModules = discoverAllShared(projectRoot);
5589
+ const errorModule = { name: "global-errors", dir: errorsBase, type: "error" };
5590
+ const allModules = [
5591
+ ...features.map((f) => ({ ...f, type: "feature" })),
5592
+ ...sharedModules.map((s) => ({ ...s, type: "shared" })),
5593
+ errorModule
5594
+ ];
5480
5595
  const graph = new FeatureDependencyGraph();
5481
5596
  const allViolations = [];
5482
5597
  const results = [];
5483
5598
  const detectedCycles = [];
5484
5599
  const addViolation = (v) => {
5485
5600
  allViolations.push(v);
5486
- const featureResult = results.find((r) => r.featureName === v.feature);
5487
- if (featureResult) {
5488
- featureResult.violations.push(v);
5489
- featureResult.passed = false;
5601
+ const moduleResult = results.find((r) => r.featureName === v.feature);
5602
+ if (moduleResult) {
5603
+ moduleResult.violations.push(v);
5604
+ moduleResult.passed = false;
5490
5605
  }
5491
5606
  if (failFast) {
5492
5607
  const errorMsg = `Dependency rule violation detected in ${v.relativePath}:${v.line}:
@@ -5495,28 +5610,271 @@ Code: ${v.code}`;
5495
5610
  throw new DependencyRuleError(errorMsg, [v], true);
5496
5611
  }
5497
5612
  };
5498
- for (const feat of features) {
5613
+ for (const mod of allModules) {
5499
5614
  results.push({
5500
- featureName: feat.name,
5501
- featureDir: feat.dir,
5615
+ moduleType: mod.type,
5616
+ moduleName: mod.name,
5617
+ featureName: mod.name,
5618
+ featureDir: mod.dir,
5619
+ moduleDir: mod.dir,
5502
5620
  scannedFilesCount: 0,
5503
5621
  passed: true,
5504
5622
  violations: [],
5505
- layersChecked: ["domain", "useCases", "services", "infrastructure", "presentation"]
5623
+ layersChecked: mod.type === "feature" ? ["domain", "useCases", "services", "infrastructure", "presentation"] : mod.type === "shared" ? ["domain", "ports", "infrastructure", "root"] : ["domain"]
5506
5624
  });
5507
5625
  }
5508
- for (const feat of features) {
5509
- const featResult = results.find((r) => r.featureName === feat.name);
5510
- const allFiles = scanDirFiles(feat.dir, projectRoot);
5511
- featResult.scannedFilesCount = allFiles.length;
5626
+ for (const mod of allModules) {
5627
+ const modResult = results.find((r) => r.featureName === mod.name);
5628
+ const allFiles = scanDirFiles(mod.dir, projectRoot);
5629
+ modResult.scannedFilesCount = allFiles.length;
5512
5630
  for (const file of allFiles) {
5513
5631
  const relPath = path2__default.default.relative(projectRoot, file).replace(/\\/g, "/");
5514
- const { layer } = classifyFile(file, projectRoot);
5515
- if (!layer) continue;
5632
+ const { moduleType, moduleName, layer } = classifyFile(file, projectRoot);
5633
+ if (!layer || !moduleType || !moduleName) continue;
5516
5634
  const content = fs4__default.default.readFileSync(file, "utf-8");
5517
5635
  const statements = extractImportStatements(content);
5518
5636
  const baseName = path2__default.default.basename(file);
5519
5637
  const isBarrel = baseName === "index.ts" || baseName === "index.js";
5638
+ const throwErrorRegex = /\bthrow\s+(?:new\s+)?Error\s*\(/g;
5639
+ let throwMatch;
5640
+ while ((throwMatch = throwErrorRegex.exec(content)) !== null) {
5641
+ const charIndex = throwMatch.index;
5642
+ const lineNum = content.substring(0, charIndex).split("\n").length;
5643
+ const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || throwMatch[0];
5644
+ if (lineCode.startsWith("//")) continue;
5645
+ addViolation({
5646
+ feature: moduleName,
5647
+ layer,
5648
+ file,
5649
+ relativePath: relPath,
5650
+ line: lineNum,
5651
+ category: "Plain Error Throw Violation",
5652
+ code: lineCode,
5653
+ ruleId: "E3_PLAIN_ERROR_THROW",
5654
+ severity: "error",
5655
+ advice: `Throwing plain 'Error' is prohibited. You must throw '${baseErrorClassName}' or a domain-specific error class that inherits from it.`,
5656
+ fixExample: {
5657
+ bad: lineCode,
5658
+ good: `throw new ${baseErrorClassName}("Something went wrong");`
5659
+ }
5660
+ });
5661
+ }
5662
+ const isErrorFile = relPath.includes("/errors/") || relPath.startsWith("errors/") || relPath.startsWith("src/errors/") || moduleType === "error" || baseName.endsWith("Error.ts") || baseName.endsWith("Errors.ts") || baseName.endsWith("Error.js") || baseName.endsWith("Errors.js");
5663
+ const isBaseErrorFile = path2__default.default.resolve(file) === path2__default.default.resolve(baseErrorAbs);
5664
+ if (isErrorFile && !isBaseErrorFile) {
5665
+ const classRegex = /(?:export\s+(?:default\s+)?)?(?:abstract\s+)?class\s+([A-Za-z0-9_]+)(?:<[^>]+>)?(?:\s+extends\s+([A-Za-z0-9_]+))?/g;
5666
+ let classMatch;
5667
+ while ((classMatch = classRegex.exec(content)) !== null) {
5668
+ const className = classMatch[1];
5669
+ const extendsName = classMatch[2];
5670
+ const charIndex = classMatch.index;
5671
+ const lineNum = content.substring(0, charIndex).split("\n").length;
5672
+ const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || classMatch[0];
5673
+ if (lineCode.startsWith("//")) continue;
5674
+ if (!extendsName) {
5675
+ addViolation({
5676
+ feature: moduleName,
5677
+ layer,
5678
+ file,
5679
+ relativePath: relPath,
5680
+ line: lineNum,
5681
+ category: "Error Inheritance Violation",
5682
+ code: lineCode,
5683
+ ruleId: "E4_MISSING_INHERITANCE",
5684
+ severity: "error",
5685
+ advice: `Error class '${className}' must extend '${baseErrorClassName}' (or another class that derives from it).`,
5686
+ fixExample: {
5687
+ bad: lineCode,
5688
+ good: `export class ${className} extends ${baseErrorClassName} { ... }`
5689
+ }
5690
+ });
5691
+ } else if (extendsName === "Error") {
5692
+ addViolation({
5693
+ feature: moduleName,
5694
+ layer,
5695
+ file,
5696
+ relativePath: relPath,
5697
+ line: lineNum,
5698
+ category: "Error Inheritance Violation",
5699
+ code: lineCode,
5700
+ ruleId: "E5_NATIVE_ERROR_INHERITANCE",
5701
+ severity: "error",
5702
+ advice: `Error class '${className}' extends the native 'Error'. It must extend your base error class ('${baseErrorClassName}').`,
5703
+ fixExample: {
5704
+ bad: lineCode,
5705
+ good: `export class ${className} extends ${baseErrorClassName} { ... }`
5706
+ }
5707
+ });
5708
+ }
5709
+ }
5710
+ }
5711
+ if (moduleType === "error") {
5712
+ for (const stmt of statements) {
5713
+ const target = classifyTarget(stmt.specifier, file, projectRoot);
5714
+ if (target.targetFeature) {
5715
+ addViolation({
5716
+ feature: "global-errors",
5717
+ layer: "domain",
5718
+ file,
5719
+ relativePath: relPath,
5720
+ line: stmt.line,
5721
+ category: "Global Error Purity Violation",
5722
+ code: stmt.rawCode,
5723
+ ruleId: "E1_FEATURE_DEPENDENCY",
5724
+ severity: "error",
5725
+ advice: `Global errors must not depend on features ('${target.targetFeature}'). Global errors represent pure domain or system states.`,
5726
+ fixExample: {
5727
+ bad: stmt.rawCode,
5728
+ good: "// Global errors must not reference specific features. Keep error types pure."
5729
+ }
5730
+ });
5731
+ }
5732
+ if (target.targetSharedModule && !target.isSharedDomain) {
5733
+ addViolation({
5734
+ feature: "global-errors",
5735
+ layer: "domain",
5736
+ file,
5737
+ relativePath: relPath,
5738
+ line: stmt.line,
5739
+ category: "Global Error Purity Violation",
5740
+ code: stmt.rawCode,
5741
+ ruleId: "E2_SHARED_NON_DOMAIN",
5742
+ severity: "error",
5743
+ advice: `Global errors can only depend on shared domain elements (e.g., value objects). They cannot depend on shared ports or infrastructure.`,
5744
+ fixExample: {
5745
+ bad: stmt.rawCode,
5746
+ good: "// Depend only on pure domain types from shared modules."
5747
+ }
5748
+ });
5749
+ }
5750
+ }
5751
+ continue;
5752
+ }
5753
+ if (moduleType === "shared") {
5754
+ if (layer === "domain") {
5755
+ const lines = content.split(/\r?\n/);
5756
+ for (let i = 0; i < lines.length; i++) {
5757
+ const lineText = lines[i].trim();
5758
+ if (lineText.includes("@MakeInjectable") || lineText.includes("public static deps =")) {
5759
+ addViolation({
5760
+ feature: moduleName,
5761
+ layer,
5762
+ file,
5763
+ relativePath: relPath,
5764
+ line: i + 1,
5765
+ category: "Domain Dependence Violation",
5766
+ code: lineText,
5767
+ ruleId: "D2_DI_DEPENDENCE",
5768
+ severity: "error",
5769
+ advice: "Domain layer must have zero DI dependence. Remove @MakeInjectable decorators and static deps declarations.",
5770
+ fixExample: {
5771
+ bad: lineText,
5772
+ good: "// Domain interfaces and value objects must remain plain TypeScript classes/interfaces."
5773
+ }
5774
+ });
5775
+ }
5776
+ }
5777
+ }
5778
+ for (const stmt of statements) {
5779
+ const target = classifyTarget(stmt.specifier, file, projectRoot);
5780
+ if (target.targetFeature) {
5781
+ addViolation({
5782
+ feature: moduleName,
5783
+ layer,
5784
+ file,
5785
+ relativePath: relPath,
5786
+ line: stmt.line,
5787
+ category: "Shared Architecture Violation",
5788
+ code: stmt.rawCode,
5789
+ ruleId: "SF1_FEATURE_DEPENDENCY",
5790
+ severity: "error",
5791
+ advice: `Shared modules ('${moduleName}') are foundational and must NEVER import from feature modules ('${target.targetFeature}').`,
5792
+ fixExample: {
5793
+ bad: stmt.rawCode,
5794
+ good: "// Move shared logic/types to shared/ or communicate via domain events/callbacks."
5795
+ }
5796
+ });
5797
+ }
5798
+ if (target.targetSharedModule && target.targetSharedModule !== moduleName) {
5799
+ if (target.isSharedInfra || target.isSharedPorts) {
5800
+ addViolation({
5801
+ feature: moduleName,
5802
+ layer,
5803
+ file,
5804
+ relativePath: relPath,
5805
+ line: stmt.line,
5806
+ category: "Shared Architecture Violation",
5807
+ code: stmt.rawCode,
5808
+ ruleId: "SS1_SHARED_ENCAPSULATION",
5809
+ severity: "error",
5810
+ advice: `Shared module '${moduleName}' cannot import internal details (ports/infra) of shared module '${target.targetSharedModule}'. Depend only on its root classes or domain types.`,
5811
+ fixExample: {
5812
+ bad: stmt.rawCode,
5813
+ good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
5814
+ }
5815
+ });
5816
+ }
5817
+ }
5818
+ if (layer === "domain") {
5819
+ if (target.targetSharedModule === moduleName && (target.isSharedInfra || target.isSharedPorts || target.isSharedRoot)) {
5820
+ addViolation({
5821
+ feature: moduleName,
5822
+ layer,
5823
+ file,
5824
+ relativePath: relPath,
5825
+ line: stmt.line,
5826
+ category: "Domain Purity Violation",
5827
+ code: stmt.rawCode,
5828
+ ruleId: "SD1_OUTER_IMPORT",
5829
+ severity: "error",
5830
+ advice: `Shared domain layer must not import from its own ports, infrastructure, or root classes.`,
5831
+ fixExample: {
5832
+ bad: stmt.rawCode,
5833
+ good: "// Model pure business types and value objects directly within domain/."
5834
+ }
5835
+ });
5836
+ }
5837
+ if (target.isDiFramework) {
5838
+ addViolation({
5839
+ feature: moduleName,
5840
+ layer,
5841
+ file,
5842
+ relativePath: relPath,
5843
+ line: stmt.line,
5844
+ category: "Domain Dependence Violation",
5845
+ code: stmt.rawCode,
5846
+ ruleId: "D2_DI_DEPENDENCE",
5847
+ severity: "error",
5848
+ advice: "Domain layer must have zero DI dependence. Domain interfaces and value objects are plain classes/types.",
5849
+ fixExample: {
5850
+ bad: stmt.rawCode,
5851
+ good: "// Remove @solid-stack/di import. Domain entities are pure TypeScript."
5852
+ }
5853
+ });
5854
+ }
5855
+ }
5856
+ if (layer === "ports" && target.targetSharedModule === moduleName && target.isSharedInfra) {
5857
+ addViolation({
5858
+ feature: moduleName,
5859
+ layer,
5860
+ file,
5861
+ relativePath: relPath,
5862
+ line: stmt.line,
5863
+ category: "Shared Architecture Violation",
5864
+ code: stmt.rawCode,
5865
+ ruleId: "SP1_INFRA_IN_PORT",
5866
+ severity: "error",
5867
+ advice: `Shared ports cannot depend on infrastructure. Infrastructure implements ports, not the other way around.`,
5868
+ fixExample: {
5869
+ bad: stmt.rawCode,
5870
+ good: "// Ports define abstract interfaces without concrete infrastructure dependencies."
5871
+ }
5872
+ });
5873
+ }
5874
+ }
5875
+ continue;
5876
+ }
5877
+ const feat = mod;
5520
5878
  if (layer === "useCases" && !isBarrel) {
5521
5879
  checkUseCaseShape(feat.name, file, relPath, content, addViolation);
5522
5880
  }
@@ -5585,23 +5943,33 @@ Code: ${v.code}`;
5585
5943
  }
5586
5944
  });
5587
5945
  }
5588
- if (target.targetLayer === "domain" && !target.isTargetPort) {
5589
- addViolation({
5590
- feature: feat.name,
5591
- layer,
5592
- file,
5593
- relativePath: relPath,
5594
- line: stmt.line,
5595
- category: "Cross-Feature Domain Violation",
5596
- code: stmt.rawCode,
5597
- ruleId: "F1_CROSS_FEATURE_DOMAIN",
5598
- severity: "error",
5599
- advice: `Cannot import domain entities/factories directly from foreign feature '${target.targetFeature}'. This bypasses its use cases and breaks Bounded Context encapsulation. Communicate via ports instead.`,
5600
- fixExample: {
5601
- bad: stmt.rawCode,
5602
- good: `// Define a Port in ${feat.name}/domain/ and implement the cross-feature communication in ${feat.name}/infrastructure/ (which can then call ${target.targetFeature}'s Use Case).`
5946
+ if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetError && !stmt.isTypeOnly) {
5947
+ for (const name of stmt.importedNames) {
5948
+ const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
5949
+ const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
5950
+ const execMatch = createRegex.exec(content) || newRegex.exec(content);
5951
+ if (execMatch) {
5952
+ const charIndex = execMatch.index;
5953
+ const lineNum = content.substring(0, charIndex).split("\n").length;
5954
+ const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
5955
+ addViolation({
5956
+ feature: feat.name,
5957
+ layer,
5958
+ file,
5959
+ relativePath: relPath,
5960
+ line: lineNum,
5961
+ category: "Cross-Feature Domain Violation",
5962
+ code: lineCode,
5963
+ ruleId: "F1_CROSS_FEATURE_DOMAIN",
5964
+ severity: "error",
5965
+ advice: `Cannot instantiate domain entities/factories directly from foreign feature '${target.targetFeature}' (found instantiation of '${name}'). This bypasses its use cases and breaks Bounded Context encapsulation. Importing for types or 'instanceof' checks is allowed.`,
5966
+ fixExample: {
5967
+ bad: lineCode,
5968
+ good: `// Define a Port in ${feat.name}/domain/ and implement the cross-feature communication in ${feat.name}/infrastructure/.`
5969
+ }
5970
+ });
5603
5971
  }
5604
- });
5972
+ }
5605
5973
  }
5606
5974
  }
5607
5975
  if (target.isGlobalConfig) {
@@ -5989,23 +6357,33 @@ Code: ${v.code}`;
5989
6357
  }
5990
6358
  });
5991
6359
  }
5992
- if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo) {
5993
- addViolation({
5994
- feature: feat.name,
5995
- layer,
5996
- file,
5997
- relativePath: relPath,
5998
- line: stmt.line,
5999
- category: "Presentation Layer Violation",
6000
- code: stmt.rawCode,
6001
- ruleId: "P5_DIRECT_DOMAIN_ENTITY",
6002
- severity: "error",
6003
- advice: "Presentation handlers must not directly instantiate or import domain entities/factories. They must map requests to flat DTOs and pass them to the Use Case.",
6004
- fixExample: {
6005
- bad: stmt.rawCode,
6006
- good: "// Construct a DTO and pass it into the injected Use Case's execute() method."
6360
+ if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo && !target.isTargetError && !stmt.isTypeOnly) {
6361
+ for (const name of stmt.importedNames) {
6362
+ const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
6363
+ const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
6364
+ const execMatch = createRegex.exec(content) || newRegex.exec(content);
6365
+ if (execMatch) {
6366
+ const charIndex = execMatch.index;
6367
+ const lineNum = content.substring(0, charIndex).split("\n").length;
6368
+ const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
6369
+ addViolation({
6370
+ feature: feat.name,
6371
+ layer,
6372
+ file,
6373
+ relativePath: relPath,
6374
+ line: lineNum,
6375
+ category: "Presentation Layer Violation",
6376
+ code: lineCode,
6377
+ ruleId: "P5_DIRECT_DOMAIN_ENTITY",
6378
+ severity: "error",
6379
+ advice: `Presentation handlers must not directly instantiate domain entities (found instantiation of '${name}'). They must map requests to flat DTOs and pass them to the Use Case. Importing for types or 'instanceof' checks is allowed.`,
6380
+ fixExample: {
6381
+ bad: lineCode,
6382
+ good: "// Construct a plain DTO object and pass it into the injected Use Case's execute() method."
6383
+ }
6384
+ });
6007
6385
  }
6008
- });
6386
+ }
6009
6387
  }
6010
6388
  }
6011
6389
  }
@@ -6461,6 +6839,7 @@ exports.listExistingFeatures = listExistingFeatures;
6461
6839
  exports.listExistingShared = listExistingShared;
6462
6840
  exports.listUseCases = listUseCases;
6463
6841
  exports.loadInterfaceHierarchy = loadInterfaceHierarchy;
6842
+ exports.loadLintConfig = loadLintConfig;
6464
6843
  exports.loadMasonConfig = loadMasonConfig;
6465
6844
  exports.loadTemplate = loadTemplate;
6466
6845
  exports.normalizeRegistryUrl = normalizeRegistryUrl;