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