@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.cjs CHANGED
@@ -4906,22 +4906,38 @@ function scanDirFiles(dir, projectRoot) {
4906
4906
  function classifyFile(fullPath, projectRoot) {
4907
4907
  const rel = path2__default.default.relative(projectRoot, fullPath).replace(/\\/g, "/");
4908
4908
  const featureMatch = rel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/.*)?$/);
4909
- if (!featureMatch || !featureMatch[1] || !featureMatch[2]) return {};
4910
- const feature = featureMatch[1];
4911
- const subfolder = featureMatch[2];
4912
- let layer;
4913
- if (subfolder === "domain" || subfolder === "errors") {
4914
- layer = "domain";
4915
- } else if (subfolder === "useCases" || subfolder === "usecases") {
4916
- layer = "useCases";
4917
- } else if (subfolder === "services") {
4918
- layer = "services";
4919
- } else if (subfolder === "infrastructure") {
4920
- layer = "infrastructure";
4921
- } else if (subfolder === "pxExpress" || subfolder === "presentation" || subfolder === "handlers" || subfolder.startsWith("px")) {
4922
- layer = "presentation";
4923
- }
4924
- return { feature, layer };
4909
+ if (featureMatch && featureMatch[1] && featureMatch[2]) {
4910
+ const feature = featureMatch[1];
4911
+ const subfolder = featureMatch[2];
4912
+ let layer;
4913
+ if (subfolder === "domain" || subfolder === "errors") {
4914
+ layer = "domain";
4915
+ } else if (subfolder === "useCases" || subfolder === "usecases") {
4916
+ layer = "useCases";
4917
+ } else if (subfolder === "services") {
4918
+ layer = "services";
4919
+ } else if (subfolder === "infrastructure") {
4920
+ layer = "infrastructure";
4921
+ } else if (subfolder === "pxExpress" || subfolder === "presentation" || subfolder === "handlers" || subfolder.startsWith("px")) {
4922
+ layer = "presentation";
4923
+ }
4924
+ return { moduleType: "feature", moduleName: feature, feature, layer };
4925
+ }
4926
+ const sharedMatch = rel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
4927
+ if (sharedMatch && sharedMatch[1]) {
4928
+ const moduleName = sharedMatch[1];
4929
+ const rest = sharedMatch[2] || "";
4930
+ let layer = "root";
4931
+ if (rest.startsWith("domain/") || rest === "domain" || rest.startsWith("errors/") || rest === "errors") {
4932
+ layer = "domain";
4933
+ } else if (rest.startsWith("ports/") || rest === "ports") {
4934
+ layer = "ports";
4935
+ } else if (rest.startsWith("infrastructure/") || rest === "infrastructure") {
4936
+ layer = "infrastructure";
4937
+ }
4938
+ return { moduleType: "shared", moduleName, feature: moduleName, layer };
4939
+ }
4940
+ return {};
4925
4941
  }
4926
4942
  function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4927
4943
  if (specifier === "@solid-stack/di" || specifier.startsWith("@solid-stack/di/")) {
@@ -4942,14 +4958,19 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4942
4958
  resolvedRel = specifier.replace(/^[@~]\//, "src/");
4943
4959
  } else if (specifier.startsWith("@shared/")) {
4944
4960
  resolvedRel = "src/" + specifier.slice(1);
4961
+ } else if (specifier.startsWith("@/configs/") || specifier.startsWith("~/configs/") || specifier === "@/configs" || specifier === "~/configs") {
4962
+ resolvedRel = specifier.replace(/^[@~]\//, "src/");
4963
+ } else if (specifier === "@configs" || specifier.startsWith("@configs/")) {
4964
+ resolvedRel = "src/" + specifier.slice(1);
4945
4965
  } else if (specifier.startsWith("@/") || specifier.startsWith("~/")) {
4946
4966
  resolvedRel = "src/" + specifier.slice(2);
4947
- } else if (specifier.startsWith("features/") || specifier.startsWith("shared/")) {
4967
+ } else if (specifier.startsWith("features/") || specifier.startsWith("shared/") || specifier.startsWith("configs/") || specifier === "configs") {
4948
4968
  resolvedRel = specifier;
4949
4969
  }
4950
4970
  if (!resolvedRel) {
4951
4971
  return { isExternal: true, isDiFramework: false };
4952
4972
  }
4973
+ const isGlobalConfig = resolvedRel.startsWith("src/configs/") || resolvedRel === "src/configs" || resolvedRel.startsWith("configs/") || resolvedRel === "configs";
4953
4974
  const featMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/(.*))?$/);
4954
4975
  if (featMatch && featMatch[1] && featMatch[2]) {
4955
4976
  const targetFeature = featMatch[1];
@@ -4971,6 +4992,7 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4971
4992
  const targetFileName = basePart.replace(/\.(js|ts|mjs|cjs)$/, "");
4972
4993
  const isTargetPort = /^I[A-Z]/.test(targetFileName);
4973
4994
  const isTargetRepo = isTargetPort && targetFileName.includes("Repo");
4995
+ const isTargetError = targetSubfolder === "errors" || rest.startsWith("errors/") || rest.includes("/errors/") || targetFileName.endsWith("Error") || targetFileName.endsWith("Errors");
4974
4996
  return {
4975
4997
  isExternal: false,
4976
4998
  isDiFramework: false,
@@ -4979,7 +5001,8 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4979
5001
  targetSubfolder,
4980
5002
  targetFileName,
4981
5003
  isTargetPort,
4982
- isTargetRepo
5004
+ isTargetRepo,
5005
+ isTargetError
4983
5006
  };
4984
5007
  }
4985
5008
  const sharedMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
@@ -5000,22 +5023,39 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
5000
5023
  isSharedDomain
5001
5024
  };
5002
5025
  }
5003
- return { isExternal: true, isDiFramework: false };
5026
+ return { isExternal: true, isDiFramework: false, isGlobalConfig };
5004
5027
  }
5005
5028
  function extractImportStatements(content) {
5006
5029
  const statements = [];
5007
5030
  const lines = content.split(/\r?\n/);
5008
- 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;
5031
+ 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;
5009
5032
  let match;
5010
5033
  while ((match = regex.exec(content)) !== null) {
5011
- const specifier = match[1];
5034
+ const isTypeOnly = Boolean(match[1] || match[3]);
5035
+ const importClause = (match[2] || match[4] || "").trim();
5036
+ const specifier = match[5];
5012
5037
  const charIndex = match.index;
5013
5038
  const lineNum = content.substring(0, charIndex).split("\n").length;
5014
5039
  const rawCode = lines[lineNum - 1]?.trim() || match[0].trim();
5040
+ const importedNames = [];
5041
+ if (importClause) {
5042
+ const cleaned = importClause.replace(/[{}]/g, "");
5043
+ const words = cleaned.split(",").map((s) => s.trim()).filter(Boolean);
5044
+ for (const w of words) {
5045
+ const parts = w.split(/\s+as\s+/);
5046
+ const name = parts[0].replace(/^type\s+/, "").trim();
5047
+ const alias = (parts[1] || name).trim();
5048
+ if (alias && alias !== "*") {
5049
+ importedNames.push(alias);
5050
+ }
5051
+ }
5052
+ }
5015
5053
  statements.push({
5016
5054
  specifier,
5017
5055
  line: lineNum,
5018
- rawCode
5056
+ rawCode,
5057
+ isTypeOnly,
5058
+ importedNames
5019
5059
  });
5020
5060
  }
5021
5061
  return statements;
@@ -5114,6 +5154,15 @@ function discoverAllFeatures(projectRoot) {
5114
5154
  dir: path2__default.default.join(featuresBase, e.name)
5115
5155
  })).sort((a, b) => a.name.localeCompare(b.name));
5116
5156
  }
5157
+ function discoverAllShared(projectRoot) {
5158
+ const sharedBase = resolveSharedDir(projectRoot);
5159
+ if (!fs14__default.default.existsSync(sharedBase)) return [];
5160
+ const entries = fs14__default.default.readdirSync(sharedBase, { withFileTypes: true });
5161
+ return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "__tests__" && e.name !== "tests").map((e) => ({
5162
+ name: e.name,
5163
+ dir: path2__default.default.join(sharedBase, e.name)
5164
+ })).sort((a, b) => a.name.localeCompare(b.name));
5165
+ }
5117
5166
  function extractClassMethods(content, className) {
5118
5167
  let clean = "";
5119
5168
  let inLineComment = false;
@@ -5463,16 +5512,21 @@ function lintCodebase(options = {}) {
5463
5512
  const projectRoot = options.projectRoot || findProjectRoot(options.targetDir || process.cwd());
5464
5513
  const failFast = !options.complete;
5465
5514
  const features = discoverAllFeatures(projectRoot);
5515
+ const sharedModules = discoverAllShared(projectRoot);
5516
+ const allModules = [
5517
+ ...features.map((f) => ({ ...f, type: "feature" })),
5518
+ ...sharedModules.map((s) => ({ ...s, type: "shared" }))
5519
+ ];
5466
5520
  const graph = new FeatureDependencyGraph();
5467
5521
  const allViolations = [];
5468
5522
  const results = [];
5469
5523
  const detectedCycles = [];
5470
5524
  const addViolation = (v) => {
5471
5525
  allViolations.push(v);
5472
- const featureResult = results.find((r) => r.featureName === v.feature);
5473
- if (featureResult) {
5474
- featureResult.violations.push(v);
5475
- featureResult.passed = false;
5526
+ const moduleResult = results.find((r) => r.featureName === v.feature);
5527
+ if (moduleResult) {
5528
+ moduleResult.violations.push(v);
5529
+ moduleResult.passed = false;
5476
5530
  }
5477
5531
  if (failFast) {
5478
5532
  const errorMsg = `Dependency rule violation detected in ${v.relativePath}:${v.line}:
@@ -5481,28 +5535,156 @@ Code: ${v.code}`;
5481
5535
  throw new DependencyRuleError(errorMsg, [v], true);
5482
5536
  }
5483
5537
  };
5484
- for (const feat of features) {
5538
+ for (const mod of allModules) {
5485
5539
  results.push({
5486
- featureName: feat.name,
5487
- featureDir: feat.dir,
5540
+ moduleType: mod.type,
5541
+ moduleName: mod.name,
5542
+ featureName: mod.name,
5543
+ featureDir: mod.dir,
5544
+ moduleDir: mod.dir,
5488
5545
  scannedFilesCount: 0,
5489
5546
  passed: true,
5490
5547
  violations: [],
5491
- layersChecked: ["domain", "useCases", "services", "infrastructure", "presentation"]
5548
+ layersChecked: mod.type === "feature" ? ["domain", "useCases", "services", "infrastructure", "presentation"] : ["domain", "ports", "infrastructure", "root"]
5492
5549
  });
5493
5550
  }
5494
- for (const feat of features) {
5495
- const featResult = results.find((r) => r.featureName === feat.name);
5496
- const allFiles = scanDirFiles(feat.dir, projectRoot);
5497
- featResult.scannedFilesCount = allFiles.length;
5551
+ for (const mod of allModules) {
5552
+ const modResult = results.find((r) => r.featureName === mod.name);
5553
+ const allFiles = scanDirFiles(mod.dir, projectRoot);
5554
+ modResult.scannedFilesCount = allFiles.length;
5498
5555
  for (const file of allFiles) {
5499
5556
  const relPath = path2__default.default.relative(projectRoot, file).replace(/\\/g, "/");
5500
- const { layer } = classifyFile(file, projectRoot);
5501
- if (!layer) continue;
5557
+ const { moduleType, moduleName, layer } = classifyFile(file, projectRoot);
5558
+ if (!layer || !moduleType || !moduleName) continue;
5502
5559
  const content = fs14__default.default.readFileSync(file, "utf-8");
5503
5560
  const statements = extractImportStatements(content);
5504
5561
  const baseName = path2__default.default.basename(file);
5505
5562
  const isBarrel = baseName === "index.ts" || baseName === "index.js";
5563
+ if (moduleType === "shared") {
5564
+ if (layer === "domain") {
5565
+ const lines = content.split(/\r?\n/);
5566
+ for (let i = 0; i < lines.length; i++) {
5567
+ const lineText = lines[i].trim();
5568
+ if (lineText.includes("@MakeInjectable") || lineText.includes("public static deps =")) {
5569
+ addViolation({
5570
+ feature: moduleName,
5571
+ layer,
5572
+ file,
5573
+ relativePath: relPath,
5574
+ line: i + 1,
5575
+ category: "Domain Dependence Violation",
5576
+ code: lineText,
5577
+ ruleId: "D2_DI_DEPENDENCE",
5578
+ severity: "error",
5579
+ advice: "Domain layer must have zero DI dependence. Remove @MakeInjectable decorators and static deps declarations.",
5580
+ fixExample: {
5581
+ bad: lineText,
5582
+ good: "// Domain interfaces and value objects must remain plain TypeScript classes/interfaces."
5583
+ }
5584
+ });
5585
+ }
5586
+ }
5587
+ }
5588
+ for (const stmt of statements) {
5589
+ const target = classifyTarget(stmt.specifier, file, projectRoot);
5590
+ if (target.targetFeature) {
5591
+ addViolation({
5592
+ feature: moduleName,
5593
+ layer,
5594
+ file,
5595
+ relativePath: relPath,
5596
+ line: stmt.line,
5597
+ category: "Shared Architecture Violation",
5598
+ code: stmt.rawCode,
5599
+ ruleId: "SF1_FEATURE_DEPENDENCY",
5600
+ severity: "error",
5601
+ advice: `Shared modules ('${moduleName}') are foundational and must NEVER import from feature modules ('${target.targetFeature}').`,
5602
+ fixExample: {
5603
+ bad: stmt.rawCode,
5604
+ good: "// Move shared logic/types to shared/ or communicate via domain events/callbacks."
5605
+ }
5606
+ });
5607
+ }
5608
+ if (target.targetSharedModule && target.targetSharedModule !== moduleName) {
5609
+ if (target.isSharedInfra || target.isSharedPorts) {
5610
+ addViolation({
5611
+ feature: moduleName,
5612
+ layer,
5613
+ file,
5614
+ relativePath: relPath,
5615
+ line: stmt.line,
5616
+ category: "Shared Architecture Violation",
5617
+ code: stmt.rawCode,
5618
+ ruleId: "SS1_SHARED_ENCAPSULATION",
5619
+ severity: "error",
5620
+ advice: `Shared module '${moduleName}' cannot import internal details (ports/infra) of shared module '${target.targetSharedModule}'. Depend only on its root classes or domain types.`,
5621
+ fixExample: {
5622
+ bad: stmt.rawCode,
5623
+ good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
5624
+ }
5625
+ });
5626
+ }
5627
+ }
5628
+ if (layer === "domain") {
5629
+ if (target.targetSharedModule === moduleName && (target.isSharedInfra || target.isSharedPorts || target.isSharedRoot)) {
5630
+ addViolation({
5631
+ feature: moduleName,
5632
+ layer,
5633
+ file,
5634
+ relativePath: relPath,
5635
+ line: stmt.line,
5636
+ category: "Domain Purity Violation",
5637
+ code: stmt.rawCode,
5638
+ ruleId: "SD1_OUTER_IMPORT",
5639
+ severity: "error",
5640
+ advice: `Shared domain layer must not import from its own ports, infrastructure, or root classes.`,
5641
+ fixExample: {
5642
+ bad: stmt.rawCode,
5643
+ good: "// Model pure business types and value objects directly within domain/."
5644
+ }
5645
+ });
5646
+ }
5647
+ if (target.isDiFramework) {
5648
+ addViolation({
5649
+ feature: moduleName,
5650
+ layer,
5651
+ file,
5652
+ relativePath: relPath,
5653
+ line: stmt.line,
5654
+ category: "Domain Dependence Violation",
5655
+ code: stmt.rawCode,
5656
+ ruleId: "D2_DI_DEPENDENCE",
5657
+ severity: "error",
5658
+ advice: "Domain layer must have zero DI dependence. Domain interfaces and value objects are plain classes/types.",
5659
+ fixExample: {
5660
+ bad: stmt.rawCode,
5661
+ good: "// Remove @solid-stack/di import. Domain entities are pure TypeScript."
5662
+ }
5663
+ });
5664
+ }
5665
+ }
5666
+ if (layer === "ports" && target.targetSharedModule === moduleName && target.isSharedInfra) {
5667
+ addViolation({
5668
+ feature: moduleName,
5669
+ layer,
5670
+ file,
5671
+ relativePath: relPath,
5672
+ line: stmt.line,
5673
+ category: "Shared Architecture Violation",
5674
+ code: stmt.rawCode,
5675
+ ruleId: "SP1_INFRA_IN_PORT",
5676
+ severity: "error",
5677
+ advice: `Shared ports cannot depend on infrastructure. Infrastructure implements ports, not the other way around.`,
5678
+ fixExample: {
5679
+ bad: stmt.rawCode,
5680
+ good: "// Ports define abstract interfaces without concrete infrastructure dependencies."
5681
+ }
5682
+ });
5683
+ }
5684
+ }
5685
+ continue;
5686
+ }
5687
+ const feat = mod;
5506
5688
  if (layer === "useCases" && !isBarrel) {
5507
5689
  checkUseCaseShape(feat.name, file, relPath, content, addViolation);
5508
5690
  }
@@ -5571,25 +5753,55 @@ Code: ${v.code}`;
5571
5753
  }
5572
5754
  });
5573
5755
  }
5574
- if (target.targetLayer === "domain" && !target.isTargetPort) {
5575
- addViolation({
5576
- feature: feat.name,
5577
- layer,
5578
- file,
5579
- relativePath: relPath,
5580
- line: stmt.line,
5581
- category: "Cross-Feature Domain Violation",
5582
- code: stmt.rawCode,
5583
- ruleId: "F1_CROSS_FEATURE_DOMAIN",
5584
- severity: "error",
5585
- 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.`,
5586
- fixExample: {
5587
- bad: stmt.rawCode,
5588
- 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).`
5756
+ if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetError && !stmt.isTypeOnly) {
5757
+ for (const name of stmt.importedNames) {
5758
+ const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
5759
+ const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
5760
+ const execMatch = createRegex.exec(content) || newRegex.exec(content);
5761
+ if (execMatch) {
5762
+ const charIndex = execMatch.index;
5763
+ const lineNum = content.substring(0, charIndex).split("\n").length;
5764
+ const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
5765
+ addViolation({
5766
+ feature: feat.name,
5767
+ layer,
5768
+ file,
5769
+ relativePath: relPath,
5770
+ line: lineNum,
5771
+ category: "Cross-Feature Domain Violation",
5772
+ code: lineCode,
5773
+ ruleId: "F1_CROSS_FEATURE_DOMAIN",
5774
+ severity: "error",
5775
+ 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.`,
5776
+ fixExample: {
5777
+ bad: lineCode,
5778
+ good: `// Define a Port in ${feat.name}/domain/ and implement the cross-feature communication in ${feat.name}/infrastructure/.`
5779
+ }
5780
+ });
5589
5781
  }
5590
- });
5782
+ }
5591
5783
  }
5592
5784
  }
5785
+ if (target.isGlobalConfig) {
5786
+ addViolation({
5787
+ feature: feat.name,
5788
+ layer,
5789
+ file,
5790
+ relativePath: relPath,
5791
+ line: stmt.line,
5792
+ category: "Global Configuration Violation",
5793
+ code: stmt.rawCode,
5794
+ ruleId: "F2_GLOBAL_CONFIG_DEPENDENCY",
5795
+ severity: "error",
5796
+ 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.`,
5797
+ fixExample: {
5798
+ bad: stmt.rawCode,
5799
+ good: `// 1. Define I${toPascalCase(feat.name)}Config locally
5800
+ // 2. Inject it via static deps
5801
+ // 3. Import and bind global configs inside diProvider.ts`
5802
+ }
5803
+ });
5804
+ }
5593
5805
  if (layer === "domain") {
5594
5806
  if (target.targetLayer && ["useCases", "services", "infrastructure", "presentation"].includes(target.targetLayer)) {
5595
5807
  addViolation({
@@ -5645,7 +5857,7 @@ Code: ${v.code}`;
5645
5857
  }
5646
5858
  });
5647
5859
  }
5648
- if (target.isExternal && !target.isDiFramework) {
5860
+ if (target.isExternal && !target.isDiFramework && !target.isGlobalConfig) {
5649
5861
  addViolation({
5650
5862
  feature: feat.name,
5651
5863
  layer,
@@ -5955,23 +6167,33 @@ Code: ${v.code}`;
5955
6167
  }
5956
6168
  });
5957
6169
  }
5958
- if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo) {
5959
- addViolation({
5960
- feature: feat.name,
5961
- layer,
5962
- file,
5963
- relativePath: relPath,
5964
- line: stmt.line,
5965
- category: "Presentation Layer Violation",
5966
- code: stmt.rawCode,
5967
- ruleId: "P5_DIRECT_DOMAIN_ENTITY",
5968
- severity: "error",
5969
- 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.",
5970
- fixExample: {
5971
- bad: stmt.rawCode,
5972
- good: "// Construct a DTO and pass it into the injected Use Case's execute() method."
6170
+ if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo && !target.isTargetError && !stmt.isTypeOnly) {
6171
+ for (const name of stmt.importedNames) {
6172
+ const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
6173
+ const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
6174
+ const execMatch = createRegex.exec(content) || newRegex.exec(content);
6175
+ if (execMatch) {
6176
+ const charIndex = execMatch.index;
6177
+ const lineNum = content.substring(0, charIndex).split("\n").length;
6178
+ const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
6179
+ addViolation({
6180
+ feature: feat.name,
6181
+ layer,
6182
+ file,
6183
+ relativePath: relPath,
6184
+ line: lineNum,
6185
+ category: "Presentation Layer Violation",
6186
+ code: lineCode,
6187
+ ruleId: "P5_DIRECT_DOMAIN_ENTITY",
6188
+ severity: "error",
6189
+ 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.`,
6190
+ fixExample: {
6191
+ bad: lineCode,
6192
+ good: "// Construct a plain DTO object and pass it into the injected Use Case's execute() method."
6193
+ }
6194
+ });
5973
6195
  }
5974
- });
6196
+ }
5975
6197
  }
5976
6198
  }
5977
6199
  }