@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 +291 -69
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +291 -69
- package/dist/bin.js.map +1 -1
- package/dist/core/lintEngine.d.ts +11 -1
- package/dist/core/lintEngine.d.ts.map +1 -1
- package/dist/index.cjs +291 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +291 -69
- 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/")) {
|
|
@@ -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+(
|
|
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
|
|
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
|
|
5444
|
-
if (
|
|
5445
|
-
|
|
5446
|
-
|
|
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
|
|
5509
|
+
for (const mod of allModules) {
|
|
5456
5510
|
results.push({
|
|
5457
|
-
|
|
5458
|
-
|
|
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
|
|
5466
|
-
const
|
|
5467
|
-
const allFiles = scanDirFiles(
|
|
5468
|
-
|
|
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
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
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
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
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
|
}
|