@solid-stack/mason 1.0.30 → 1.0.32

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.js CHANGED
@@ -4886,22 +4886,38 @@ function scanDirFiles(dir, projectRoot) {
4886
4886
  function classifyFile(fullPath, projectRoot) {
4887
4887
  const rel = path2.relative(projectRoot, fullPath).replace(/\\/g, "/");
4888
4888
  const featureMatch = rel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/.*)?$/);
4889
- if (!featureMatch || !featureMatch[1] || !featureMatch[2]) return {};
4890
- const feature = featureMatch[1];
4891
- const subfolder = featureMatch[2];
4892
- let layer;
4893
- if (subfolder === "domain" || subfolder === "errors") {
4894
- layer = "domain";
4895
- } else if (subfolder === "useCases" || subfolder === "usecases") {
4896
- layer = "useCases";
4897
- } else if (subfolder === "services") {
4898
- layer = "services";
4899
- } else if (subfolder === "infrastructure") {
4900
- layer = "infrastructure";
4901
- } else if (subfolder === "pxExpress" || subfolder === "presentation" || subfolder === "handlers" || subfolder.startsWith("px")) {
4902
- layer = "presentation";
4903
- }
4904
- return { feature, layer };
4889
+ if (featureMatch && featureMatch[1] && featureMatch[2]) {
4890
+ const feature = featureMatch[1];
4891
+ const subfolder = featureMatch[2];
4892
+ let layer;
4893
+ if (subfolder === "domain" || subfolder === "errors") {
4894
+ layer = "domain";
4895
+ } else if (subfolder === "useCases" || subfolder === "usecases") {
4896
+ layer = "useCases";
4897
+ } else if (subfolder === "services") {
4898
+ layer = "services";
4899
+ } else if (subfolder === "infrastructure") {
4900
+ layer = "infrastructure";
4901
+ } else if (subfolder === "pxExpress" || subfolder === "presentation" || subfolder === "handlers" || subfolder.startsWith("px")) {
4902
+ layer = "presentation";
4903
+ }
4904
+ return { moduleType: "feature", moduleName: feature, feature, layer };
4905
+ }
4906
+ const sharedMatch = rel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
4907
+ if (sharedMatch && sharedMatch[1]) {
4908
+ const moduleName = sharedMatch[1];
4909
+ const rest = sharedMatch[2] || "";
4910
+ let layer = "root";
4911
+ if (rest.startsWith("domain/") || rest === "domain" || rest.startsWith("errors/") || rest === "errors") {
4912
+ layer = "domain";
4913
+ } else if (rest.startsWith("ports/") || rest === "ports") {
4914
+ layer = "ports";
4915
+ } else if (rest.startsWith("infrastructure/") || rest === "infrastructure") {
4916
+ layer = "infrastructure";
4917
+ }
4918
+ return { moduleType: "shared", moduleName, feature: moduleName, layer };
4919
+ }
4920
+ return {};
4905
4921
  }
4906
4922
  function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4907
4923
  if (specifier === "@solid-stack/di" || specifier.startsWith("@solid-stack/di/")) {
@@ -4922,14 +4938,19 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4922
4938
  resolvedRel = specifier.replace(/^[@~]\//, "src/");
4923
4939
  } else if (specifier.startsWith("@shared/")) {
4924
4940
  resolvedRel = "src/" + specifier.slice(1);
4941
+ } else if (specifier.startsWith("@/configs/") || specifier.startsWith("~/configs/") || specifier === "@/configs" || specifier === "~/configs") {
4942
+ resolvedRel = specifier.replace(/^[@~]\//, "src/");
4943
+ } else if (specifier === "@configs" || specifier.startsWith("@configs/")) {
4944
+ resolvedRel = "src/" + specifier.slice(1);
4925
4945
  } else if (specifier.startsWith("@/") || specifier.startsWith("~/")) {
4926
4946
  resolvedRel = "src/" + specifier.slice(2);
4927
- } else if (specifier.startsWith("features/") || specifier.startsWith("shared/")) {
4947
+ } else if (specifier.startsWith("features/") || specifier.startsWith("shared/") || specifier.startsWith("configs/") || specifier === "configs") {
4928
4948
  resolvedRel = specifier;
4929
4949
  }
4930
4950
  if (!resolvedRel) {
4931
4951
  return { isExternal: true, isDiFramework: false };
4932
4952
  }
4953
+ const isGlobalConfig = resolvedRel.startsWith("src/configs/") || resolvedRel === "src/configs" || resolvedRel.startsWith("configs/") || resolvedRel === "configs";
4933
4954
  const featMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/(.*))?$/);
4934
4955
  if (featMatch && featMatch[1] && featMatch[2]) {
4935
4956
  const targetFeature = featMatch[1];
@@ -4951,6 +4972,7 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4951
4972
  const targetFileName = basePart.replace(/\.(js|ts|mjs|cjs)$/, "");
4952
4973
  const isTargetPort = /^I[A-Z]/.test(targetFileName);
4953
4974
  const isTargetRepo = isTargetPort && targetFileName.includes("Repo");
4975
+ const isTargetError = targetSubfolder === "errors" || rest.startsWith("errors/") || rest.includes("/errors/") || targetFileName.endsWith("Error") || targetFileName.endsWith("Errors");
4954
4976
  return {
4955
4977
  isExternal: false,
4956
4978
  isDiFramework: false,
@@ -4959,7 +4981,8 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4959
4981
  targetSubfolder,
4960
4982
  targetFileName,
4961
4983
  isTargetPort,
4962
- isTargetRepo
4984
+ isTargetRepo,
4985
+ isTargetError
4963
4986
  };
4964
4987
  }
4965
4988
  const sharedMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
@@ -4980,22 +5003,39 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4980
5003
  isSharedDomain
4981
5004
  };
4982
5005
  }
4983
- return { isExternal: true, isDiFramework: false };
5006
+ return { isExternal: true, isDiFramework: false, isGlobalConfig };
4984
5007
  }
4985
5008
  function extractImportStatements(content) {
4986
5009
  const statements = [];
4987
5010
  const lines = content.split(/\r?\n/);
4988
- 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;
5011
+ 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;
4989
5012
  let match;
4990
5013
  while ((match = regex.exec(content)) !== null) {
4991
- const specifier = match[1];
5014
+ const isTypeOnly = Boolean(match[1] || match[3]);
5015
+ const importClause = (match[2] || match[4] || "").trim();
5016
+ const specifier = match[5];
4992
5017
  const charIndex = match.index;
4993
5018
  const lineNum = content.substring(0, charIndex).split("\n").length;
4994
5019
  const rawCode = lines[lineNum - 1]?.trim() || match[0].trim();
5020
+ const importedNames = [];
5021
+ if (importClause) {
5022
+ const cleaned = importClause.replace(/[{}]/g, "");
5023
+ const words = cleaned.split(",").map((s) => s.trim()).filter(Boolean);
5024
+ for (const w of words) {
5025
+ const parts = w.split(/\s+as\s+/);
5026
+ const name = parts[0].replace(/^type\s+/, "").trim();
5027
+ const alias = (parts[1] || name).trim();
5028
+ if (alias && alias !== "*") {
5029
+ importedNames.push(alias);
5030
+ }
5031
+ }
5032
+ }
4995
5033
  statements.push({
4996
5034
  specifier,
4997
5035
  line: lineNum,
4998
- rawCode
5036
+ rawCode,
5037
+ isTypeOnly,
5038
+ importedNames
4999
5039
  });
5000
5040
  }
5001
5041
  return statements;
@@ -5094,6 +5134,15 @@ function discoverAllFeatures(projectRoot) {
5094
5134
  dir: path2.join(featuresBase, e.name)
5095
5135
  })).sort((a, b) => a.name.localeCompare(b.name));
5096
5136
  }
5137
+ function discoverAllShared(projectRoot) {
5138
+ const sharedBase = resolveSharedDir(projectRoot);
5139
+ if (!fs4.existsSync(sharedBase)) return [];
5140
+ const entries = fs4.readdirSync(sharedBase, { withFileTypes: true });
5141
+ return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "__tests__" && e.name !== "tests").map((e) => ({
5142
+ name: e.name,
5143
+ dir: path2.join(sharedBase, e.name)
5144
+ })).sort((a, b) => a.name.localeCompare(b.name));
5145
+ }
5097
5146
  function extractClassMethods(content, className) {
5098
5147
  let clean = "";
5099
5148
  let inLineComment = false;
@@ -5443,16 +5492,21 @@ function lintCodebase(options = {}) {
5443
5492
  const projectRoot = options.projectRoot || findProjectRoot(options.targetDir || process.cwd());
5444
5493
  const failFast = !options.complete;
5445
5494
  const features = discoverAllFeatures(projectRoot);
5495
+ const sharedModules = discoverAllShared(projectRoot);
5496
+ const allModules = [
5497
+ ...features.map((f) => ({ ...f, type: "feature" })),
5498
+ ...sharedModules.map((s) => ({ ...s, type: "shared" }))
5499
+ ];
5446
5500
  const graph = new FeatureDependencyGraph();
5447
5501
  const allViolations = [];
5448
5502
  const results = [];
5449
5503
  const detectedCycles = [];
5450
5504
  const addViolation = (v) => {
5451
5505
  allViolations.push(v);
5452
- const featureResult = results.find((r) => r.featureName === v.feature);
5453
- if (featureResult) {
5454
- featureResult.violations.push(v);
5455
- featureResult.passed = false;
5506
+ const moduleResult = results.find((r) => r.featureName === v.feature);
5507
+ if (moduleResult) {
5508
+ moduleResult.violations.push(v);
5509
+ moduleResult.passed = false;
5456
5510
  }
5457
5511
  if (failFast) {
5458
5512
  const errorMsg = `Dependency rule violation detected in ${v.relativePath}:${v.line}:
@@ -5461,28 +5515,156 @@ Code: ${v.code}`;
5461
5515
  throw new DependencyRuleError(errorMsg, [v], true);
5462
5516
  }
5463
5517
  };
5464
- for (const feat of features) {
5518
+ for (const mod of allModules) {
5465
5519
  results.push({
5466
- featureName: feat.name,
5467
- featureDir: feat.dir,
5520
+ moduleType: mod.type,
5521
+ moduleName: mod.name,
5522
+ featureName: mod.name,
5523
+ featureDir: mod.dir,
5524
+ moduleDir: mod.dir,
5468
5525
  scannedFilesCount: 0,
5469
5526
  passed: true,
5470
5527
  violations: [],
5471
- layersChecked: ["domain", "useCases", "services", "infrastructure", "presentation"]
5528
+ layersChecked: mod.type === "feature" ? ["domain", "useCases", "services", "infrastructure", "presentation"] : ["domain", "ports", "infrastructure", "root"]
5472
5529
  });
5473
5530
  }
5474
- for (const feat of features) {
5475
- const featResult = results.find((r) => r.featureName === feat.name);
5476
- const allFiles = scanDirFiles(feat.dir, projectRoot);
5477
- featResult.scannedFilesCount = allFiles.length;
5531
+ for (const mod of allModules) {
5532
+ const modResult = results.find((r) => r.featureName === mod.name);
5533
+ const allFiles = scanDirFiles(mod.dir, projectRoot);
5534
+ modResult.scannedFilesCount = allFiles.length;
5478
5535
  for (const file of allFiles) {
5479
5536
  const relPath = path2.relative(projectRoot, file).replace(/\\/g, "/");
5480
- const { layer } = classifyFile(file, projectRoot);
5481
- if (!layer) continue;
5537
+ const { moduleType, moduleName, layer } = classifyFile(file, projectRoot);
5538
+ if (!layer || !moduleType || !moduleName) continue;
5482
5539
  const content = fs4.readFileSync(file, "utf-8");
5483
5540
  const statements = extractImportStatements(content);
5484
5541
  const baseName = path2.basename(file);
5485
5542
  const isBarrel = baseName === "index.ts" || baseName === "index.js";
5543
+ if (moduleType === "shared") {
5544
+ if (layer === "domain") {
5545
+ const lines = content.split(/\r?\n/);
5546
+ for (let i = 0; i < lines.length; i++) {
5547
+ const lineText = lines[i].trim();
5548
+ if (lineText.includes("@MakeInjectable") || lineText.includes("public static deps =")) {
5549
+ addViolation({
5550
+ feature: moduleName,
5551
+ layer,
5552
+ file,
5553
+ relativePath: relPath,
5554
+ line: i + 1,
5555
+ category: "Domain Dependence Violation",
5556
+ code: lineText,
5557
+ ruleId: "D2_DI_DEPENDENCE",
5558
+ severity: "error",
5559
+ advice: "Domain layer must have zero DI dependence. Remove @MakeInjectable decorators and static deps declarations.",
5560
+ fixExample: {
5561
+ bad: lineText,
5562
+ good: "// Domain interfaces and value objects must remain plain TypeScript classes/interfaces."
5563
+ }
5564
+ });
5565
+ }
5566
+ }
5567
+ }
5568
+ for (const stmt of statements) {
5569
+ const target = classifyTarget(stmt.specifier, file, projectRoot);
5570
+ if (target.targetFeature) {
5571
+ addViolation({
5572
+ feature: moduleName,
5573
+ layer,
5574
+ file,
5575
+ relativePath: relPath,
5576
+ line: stmt.line,
5577
+ category: "Shared Architecture Violation",
5578
+ code: stmt.rawCode,
5579
+ ruleId: "SF1_FEATURE_DEPENDENCY",
5580
+ severity: "error",
5581
+ advice: `Shared modules ('${moduleName}') are foundational and must NEVER import from feature modules ('${target.targetFeature}').`,
5582
+ fixExample: {
5583
+ bad: stmt.rawCode,
5584
+ good: "// Move shared logic/types to shared/ or communicate via domain events/callbacks."
5585
+ }
5586
+ });
5587
+ }
5588
+ if (target.targetSharedModule && target.targetSharedModule !== moduleName) {
5589
+ if (target.isSharedInfra || target.isSharedPorts) {
5590
+ addViolation({
5591
+ feature: moduleName,
5592
+ layer,
5593
+ file,
5594
+ relativePath: relPath,
5595
+ line: stmt.line,
5596
+ category: "Shared Architecture Violation",
5597
+ code: stmt.rawCode,
5598
+ ruleId: "SS1_SHARED_ENCAPSULATION",
5599
+ severity: "error",
5600
+ advice: `Shared module '${moduleName}' cannot import internal details (ports/infra) of shared module '${target.targetSharedModule}'. Depend only on its root classes or domain types.`,
5601
+ fixExample: {
5602
+ bad: stmt.rawCode,
5603
+ good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
5604
+ }
5605
+ });
5606
+ }
5607
+ }
5608
+ if (layer === "domain") {
5609
+ if (target.targetSharedModule === moduleName && (target.isSharedInfra || target.isSharedPorts || target.isSharedRoot)) {
5610
+ addViolation({
5611
+ feature: moduleName,
5612
+ layer,
5613
+ file,
5614
+ relativePath: relPath,
5615
+ line: stmt.line,
5616
+ category: "Domain Purity Violation",
5617
+ code: stmt.rawCode,
5618
+ ruleId: "SD1_OUTER_IMPORT",
5619
+ severity: "error",
5620
+ advice: `Shared domain layer must not import from its own ports, infrastructure, or root classes.`,
5621
+ fixExample: {
5622
+ bad: stmt.rawCode,
5623
+ good: "// Model pure business types and value objects directly within domain/."
5624
+ }
5625
+ });
5626
+ }
5627
+ if (target.isDiFramework) {
5628
+ addViolation({
5629
+ feature: moduleName,
5630
+ layer,
5631
+ file,
5632
+ relativePath: relPath,
5633
+ line: stmt.line,
5634
+ category: "Domain Dependence Violation",
5635
+ code: stmt.rawCode,
5636
+ ruleId: "D2_DI_DEPENDENCE",
5637
+ severity: "error",
5638
+ advice: "Domain layer must have zero DI dependence. Domain interfaces and value objects are plain classes/types.",
5639
+ fixExample: {
5640
+ bad: stmt.rawCode,
5641
+ good: "// Remove @solid-stack/di import. Domain entities are pure TypeScript."
5642
+ }
5643
+ });
5644
+ }
5645
+ }
5646
+ if (layer === "ports" && target.targetSharedModule === moduleName && target.isSharedInfra) {
5647
+ addViolation({
5648
+ feature: moduleName,
5649
+ layer,
5650
+ file,
5651
+ relativePath: relPath,
5652
+ line: stmt.line,
5653
+ category: "Shared Architecture Violation",
5654
+ code: stmt.rawCode,
5655
+ ruleId: "SP1_INFRA_IN_PORT",
5656
+ severity: "error",
5657
+ advice: `Shared ports cannot depend on infrastructure. Infrastructure implements ports, not the other way around.`,
5658
+ fixExample: {
5659
+ bad: stmt.rawCode,
5660
+ good: "// Ports define abstract interfaces without concrete infrastructure dependencies."
5661
+ }
5662
+ });
5663
+ }
5664
+ }
5665
+ continue;
5666
+ }
5667
+ const feat = mod;
5486
5668
  if (layer === "useCases" && !isBarrel) {
5487
5669
  checkUseCaseShape(feat.name, file, relPath, content, addViolation);
5488
5670
  }
@@ -5551,25 +5733,55 @@ Code: ${v.code}`;
5551
5733
  }
5552
5734
  });
5553
5735
  }
5554
- if (target.targetLayer === "domain" && !target.isTargetPort) {
5555
- addViolation({
5556
- feature: feat.name,
5557
- layer,
5558
- file,
5559
- relativePath: relPath,
5560
- line: stmt.line,
5561
- category: "Cross-Feature Domain Violation",
5562
- code: stmt.rawCode,
5563
- ruleId: "F1_CROSS_FEATURE_DOMAIN",
5564
- severity: "error",
5565
- 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.`,
5566
- fixExample: {
5567
- bad: stmt.rawCode,
5568
- 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).`
5736
+ if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetError && !stmt.isTypeOnly) {
5737
+ for (const name of stmt.importedNames) {
5738
+ const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
5739
+ const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
5740
+ const execMatch = createRegex.exec(content) || newRegex.exec(content);
5741
+ if (execMatch) {
5742
+ const charIndex = execMatch.index;
5743
+ const lineNum = content.substring(0, charIndex).split("\n").length;
5744
+ const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
5745
+ addViolation({
5746
+ feature: feat.name,
5747
+ layer,
5748
+ file,
5749
+ relativePath: relPath,
5750
+ line: lineNum,
5751
+ category: "Cross-Feature Domain Violation",
5752
+ code: lineCode,
5753
+ ruleId: "F1_CROSS_FEATURE_DOMAIN",
5754
+ severity: "error",
5755
+ 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.`,
5756
+ fixExample: {
5757
+ bad: lineCode,
5758
+ good: `// Define a Port in ${feat.name}/domain/ and implement the cross-feature communication in ${feat.name}/infrastructure/.`
5759
+ }
5760
+ });
5569
5761
  }
5570
- });
5762
+ }
5571
5763
  }
5572
5764
  }
5765
+ if (target.isGlobalConfig) {
5766
+ addViolation({
5767
+ feature: feat.name,
5768
+ layer,
5769
+ file,
5770
+ relativePath: relPath,
5771
+ line: stmt.line,
5772
+ category: "Global Configuration Violation",
5773
+ code: stmt.rawCode,
5774
+ ruleId: "F2_GLOBAL_CONFIG_DEPENDENCY",
5775
+ severity: "error",
5776
+ advice: `Feature layers (${layer}) must not depend directly on global configurations. Define a configuration interface/token within the feature (e.g., '${feat.name}/domain/I${toPascalCase(feat.name)}Config') and inject it. The feature's diProvider.ts will handle mapping the global config to this token.`,
5777
+ fixExample: {
5778
+ bad: stmt.rawCode,
5779
+ good: `// 1. Define I${toPascalCase(feat.name)}Config locally
5780
+ // 2. Inject it via static deps
5781
+ // 3. Import and bind global configs inside diProvider.ts`
5782
+ }
5783
+ });
5784
+ }
5573
5785
  if (layer === "domain") {
5574
5786
  if (target.targetLayer && ["useCases", "services", "infrastructure", "presentation"].includes(target.targetLayer)) {
5575
5787
  addViolation({
@@ -5625,7 +5837,7 @@ Code: ${v.code}`;
5625
5837
  }
5626
5838
  });
5627
5839
  }
5628
- if (target.isExternal && !target.isDiFramework) {
5840
+ if (target.isExternal && !target.isDiFramework && !target.isGlobalConfig) {
5629
5841
  addViolation({
5630
5842
  feature: feat.name,
5631
5843
  layer,
@@ -5935,23 +6147,33 @@ Code: ${v.code}`;
5935
6147
  }
5936
6148
  });
5937
6149
  }
5938
- if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo) {
5939
- addViolation({
5940
- feature: feat.name,
5941
- layer,
5942
- file,
5943
- relativePath: relPath,
5944
- line: stmt.line,
5945
- category: "Presentation Layer Violation",
5946
- code: stmt.rawCode,
5947
- ruleId: "P5_DIRECT_DOMAIN_ENTITY",
5948
- severity: "error",
5949
- 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.",
5950
- fixExample: {
5951
- bad: stmt.rawCode,
5952
- good: "// Construct a DTO and pass it into the injected Use Case's execute() method."
6150
+ if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo && !target.isTargetError && !stmt.isTypeOnly) {
6151
+ for (const name of stmt.importedNames) {
6152
+ const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
6153
+ const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
6154
+ const execMatch = createRegex.exec(content) || newRegex.exec(content);
6155
+ if (execMatch) {
6156
+ const charIndex = execMatch.index;
6157
+ const lineNum = content.substring(0, charIndex).split("\n").length;
6158
+ const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
6159
+ addViolation({
6160
+ feature: feat.name,
6161
+ layer,
6162
+ file,
6163
+ relativePath: relPath,
6164
+ line: lineNum,
6165
+ category: "Presentation Layer Violation",
6166
+ code: lineCode,
6167
+ ruleId: "P5_DIRECT_DOMAIN_ENTITY",
6168
+ severity: "error",
6169
+ 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.`,
6170
+ fixExample: {
6171
+ bad: lineCode,
6172
+ good: "// Construct a plain DTO object and pass it into the injected Use Case's execute() method."
6173
+ }
6174
+ });
5953
6175
  }
5954
- });
6176
+ }
5955
6177
  }
5956
6178
  }
5957
6179
  }