@solid-stack/mason 1.0.31 → 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 +263 -66
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +263 -66
- package/dist/bin.js.map +1 -1
- package/dist/core/lintEngine.d.ts +10 -1
- package/dist/core/lintEngine.d.ts.map +1 -1
- package/dist/index.cjs +263 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +263 -66
- package/dist/index.js.map +1 -1
- package/dist/types/lint.d.ts +8 -3
- package/dist/types/lint.d.ts.map +1 -1
- package/package.json +1 -1
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 (
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
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/")) {
|
|
@@ -4947,6 +4963,7 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4947
4963
|
const targetFileName = basePart.replace(/\.(js|ts|mjs|cjs)$/, "");
|
|
4948
4964
|
const isTargetPort = /^I[A-Z]/.test(targetFileName);
|
|
4949
4965
|
const isTargetRepo = isTargetPort && targetFileName.includes("Repo");
|
|
4966
|
+
const isTargetError = targetSubfolder === "errors" || rest.startsWith("errors/") || rest.includes("/errors/") || targetFileName.endsWith("Error") || targetFileName.endsWith("Errors");
|
|
4950
4967
|
return {
|
|
4951
4968
|
isExternal: false,
|
|
4952
4969
|
isDiFramework: false,
|
|
@@ -4955,7 +4972,8 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4955
4972
|
targetSubfolder,
|
|
4956
4973
|
targetFileName,
|
|
4957
4974
|
isTargetPort,
|
|
4958
|
-
isTargetRepo
|
|
4975
|
+
isTargetRepo,
|
|
4976
|
+
isTargetError
|
|
4959
4977
|
};
|
|
4960
4978
|
}
|
|
4961
4979
|
const sharedMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
|
|
@@ -4981,17 +4999,34 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4981
4999
|
function extractImportStatements(content) {
|
|
4982
5000
|
const statements = [];
|
|
4983
5001
|
const lines = content.split(/\r?\n/);
|
|
4984
|
-
const regex = /(?:import\s+(
|
|
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;
|
|
4985
5003
|
let match;
|
|
4986
5004
|
while ((match = regex.exec(content)) !== null) {
|
|
4987
|
-
const
|
|
5005
|
+
const isTypeOnly = Boolean(match[1] || match[3]);
|
|
5006
|
+
const importClause = (match[2] || match[4] || "").trim();
|
|
5007
|
+
const specifier = match[5];
|
|
4988
5008
|
const charIndex = match.index;
|
|
4989
5009
|
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
4990
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
|
+
}
|
|
4991
5024
|
statements.push({
|
|
4992
5025
|
specifier,
|
|
4993
5026
|
line: lineNum,
|
|
4994
|
-
rawCode
|
|
5027
|
+
rawCode,
|
|
5028
|
+
isTypeOnly,
|
|
5029
|
+
importedNames
|
|
4995
5030
|
});
|
|
4996
5031
|
}
|
|
4997
5032
|
return statements;
|
|
@@ -5090,6 +5125,15 @@ function discoverAllFeatures(projectRoot) {
|
|
|
5090
5125
|
dir: path2.join(featuresBase, e.name)
|
|
5091
5126
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
5092
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
|
+
}
|
|
5093
5137
|
function extractClassMethods(content, className) {
|
|
5094
5138
|
let clean = "";
|
|
5095
5139
|
let inLineComment = false;
|
|
@@ -5439,16 +5483,21 @@ function lintCodebase(options = {}) {
|
|
|
5439
5483
|
const projectRoot = options.projectRoot || findProjectRoot(options.targetDir || process.cwd());
|
|
5440
5484
|
const failFast = !options.complete;
|
|
5441
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
|
+
];
|
|
5442
5491
|
const graph = new FeatureDependencyGraph();
|
|
5443
5492
|
const allViolations = [];
|
|
5444
5493
|
const results = [];
|
|
5445
5494
|
const detectedCycles = [];
|
|
5446
5495
|
const addViolation = (v) => {
|
|
5447
5496
|
allViolations.push(v);
|
|
5448
|
-
const
|
|
5449
|
-
if (
|
|
5450
|
-
|
|
5451
|
-
|
|
5497
|
+
const moduleResult = results.find((r) => r.featureName === v.feature);
|
|
5498
|
+
if (moduleResult) {
|
|
5499
|
+
moduleResult.violations.push(v);
|
|
5500
|
+
moduleResult.passed = false;
|
|
5452
5501
|
}
|
|
5453
5502
|
if (failFast) {
|
|
5454
5503
|
const errorMsg = `Dependency rule violation detected in ${v.relativePath}:${v.line}:
|
|
@@ -5457,28 +5506,156 @@ Code: ${v.code}`;
|
|
|
5457
5506
|
throw new DependencyRuleError(errorMsg, [v], true);
|
|
5458
5507
|
}
|
|
5459
5508
|
};
|
|
5460
|
-
for (const
|
|
5509
|
+
for (const mod of allModules) {
|
|
5461
5510
|
results.push({
|
|
5462
|
-
|
|
5463
|
-
|
|
5511
|
+
moduleType: mod.type,
|
|
5512
|
+
moduleName: mod.name,
|
|
5513
|
+
featureName: mod.name,
|
|
5514
|
+
featureDir: mod.dir,
|
|
5515
|
+
moduleDir: mod.dir,
|
|
5464
5516
|
scannedFilesCount: 0,
|
|
5465
5517
|
passed: true,
|
|
5466
5518
|
violations: [],
|
|
5467
|
-
layersChecked: ["domain", "useCases", "services", "infrastructure", "presentation"]
|
|
5519
|
+
layersChecked: mod.type === "feature" ? ["domain", "useCases", "services", "infrastructure", "presentation"] : ["domain", "ports", "infrastructure", "root"]
|
|
5468
5520
|
});
|
|
5469
5521
|
}
|
|
5470
|
-
for (const
|
|
5471
|
-
const
|
|
5472
|
-
const allFiles = scanDirFiles(
|
|
5473
|
-
|
|
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;
|
|
5474
5526
|
for (const file of allFiles) {
|
|
5475
5527
|
const relPath = path2.relative(projectRoot, file).replace(/\\/g, "/");
|
|
5476
|
-
const { layer } = classifyFile(file, projectRoot);
|
|
5477
|
-
if (!layer) continue;
|
|
5528
|
+
const { moduleType, moduleName, layer } = classifyFile(file, projectRoot);
|
|
5529
|
+
if (!layer || !moduleType || !moduleName) continue;
|
|
5478
5530
|
const content = fs14.readFileSync(file, "utf-8");
|
|
5479
5531
|
const statements = extractImportStatements(content);
|
|
5480
5532
|
const baseName = path2.basename(file);
|
|
5481
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;
|
|
5482
5659
|
if (layer === "useCases" && !isBarrel) {
|
|
5483
5660
|
checkUseCaseShape(feat.name, file, relPath, content, addViolation);
|
|
5484
5661
|
}
|
|
@@ -5547,23 +5724,33 @@ Code: ${v.code}`;
|
|
|
5547
5724
|
}
|
|
5548
5725
|
});
|
|
5549
5726
|
}
|
|
5550
|
-
if (target.targetLayer === "domain" && !target.isTargetPort) {
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
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
|
+
});
|
|
5565
5752
|
}
|
|
5566
|
-
}
|
|
5753
|
+
}
|
|
5567
5754
|
}
|
|
5568
5755
|
}
|
|
5569
5756
|
if (target.isGlobalConfig) {
|
|
@@ -5951,23 +6138,33 @@ Code: ${v.code}`;
|
|
|
5951
6138
|
}
|
|
5952
6139
|
});
|
|
5953
6140
|
}
|
|
5954
|
-
if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo) {
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
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
|
+
});
|
|
5969
6166
|
}
|
|
5970
|
-
}
|
|
6167
|
+
}
|
|
5971
6168
|
}
|
|
5972
6169
|
}
|
|
5973
6170
|
}
|