@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.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 (
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
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+(
|
|
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
|
|
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
|
|
5473
|
-
if (
|
|
5474
|
-
|
|
5475
|
-
|
|
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
|
|
5538
|
+
for (const mod of allModules) {
|
|
5485
5539
|
results.push({
|
|
5486
|
-
|
|
5487
|
-
|
|
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
|
|
5495
|
-
const
|
|
5496
|
-
const allFiles = scanDirFiles(
|
|
5497
|
-
|
|
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
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
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
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
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
|
}
|