@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CircularDependencyInfo, FeatureDependencyEvidence, LayerType, LintOptions, LintSummary, Violation } from "../types/lint.js";
|
|
1
|
+
import type { CircularDependencyInfo, FeatureDependencyEvidence, LayerType, LintOptions, LintSummary, ModuleType, Violation } from "../types/lint.js";
|
|
2
2
|
export declare class DependencyRuleError extends Error {
|
|
3
3
|
readonly violations: Violation[];
|
|
4
4
|
readonly isFailFast: boolean;
|
|
@@ -7,12 +7,14 @@ export declare class DependencyRuleError extends Error {
|
|
|
7
7
|
export interface TargetInfo {
|
|
8
8
|
isExternal: boolean;
|
|
9
9
|
isDiFramework: boolean;
|
|
10
|
+
isGlobalConfig?: boolean | undefined;
|
|
10
11
|
targetFeature?: string | undefined;
|
|
11
12
|
targetLayer?: LayerType | undefined;
|
|
12
13
|
targetSubfolder?: string | undefined;
|
|
13
14
|
targetFileName?: string | undefined;
|
|
14
15
|
isTargetPort?: boolean | undefined;
|
|
15
16
|
isTargetRepo?: boolean | undefined;
|
|
17
|
+
isTargetError?: boolean | undefined;
|
|
16
18
|
targetSharedModule?: string | undefined;
|
|
17
19
|
isSharedRoot?: boolean | undefined;
|
|
18
20
|
isSharedPorts?: boolean | undefined;
|
|
@@ -22,6 +24,8 @@ export interface TargetInfo {
|
|
|
22
24
|
export declare function isTestFile(filePath: string, projectRoot?: string): boolean;
|
|
23
25
|
export declare function scanDirFiles(dir: string, projectRoot?: string): string[];
|
|
24
26
|
export declare function classifyFile(fullPath: string, projectRoot: string): {
|
|
27
|
+
moduleType?: ModuleType | undefined;
|
|
28
|
+
moduleName?: string | undefined;
|
|
25
29
|
feature?: string | undefined;
|
|
26
30
|
layer?: LayerType | undefined;
|
|
27
31
|
};
|
|
@@ -30,6 +34,8 @@ export interface ParsedStatement {
|
|
|
30
34
|
specifier: string;
|
|
31
35
|
line: number;
|
|
32
36
|
rawCode: string;
|
|
37
|
+
isTypeOnly: boolean;
|
|
38
|
+
importedNames: string[];
|
|
33
39
|
}
|
|
34
40
|
export declare function extractImportStatements(content: string): ParsedStatement[];
|
|
35
41
|
export declare class FeatureDependencyGraph {
|
|
@@ -51,6 +57,10 @@ export declare function discoverAllFeatures(projectRoot: string): Array<{
|
|
|
51
57
|
name: string;
|
|
52
58
|
dir: string;
|
|
53
59
|
}>;
|
|
60
|
+
export declare function discoverAllShared(projectRoot: string): Array<{
|
|
61
|
+
name: string;
|
|
62
|
+
dir: string;
|
|
63
|
+
}>;
|
|
54
64
|
export interface ClassMethodInfo {
|
|
55
65
|
name: string;
|
|
56
66
|
isAsync: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lintEngine.d.ts","sourceRoot":"","sources":["../../src/core/lintEngine.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,sBAAsB,EAEtB,yBAAyB,EACzB,SAAS,EACT,WAAW,EACX,WAAW,EAEX,SAAS,EACV,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"lintEngine.d.ts","sourceRoot":"","sources":["../../src/core/lintEngine.ts"],"names":[],"mappings":"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"}
|
package/dist/index.cjs
CHANGED
|
@@ -4915,22 +4915,38 @@ function scanDirFiles(dir, projectRoot) {
|
|
|
4915
4915
|
function classifyFile(fullPath, projectRoot) {
|
|
4916
4916
|
const rel = path2__default.default.relative(projectRoot, fullPath).replace(/\\/g, "/");
|
|
4917
4917
|
const featureMatch = rel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/.*)?$/);
|
|
4918
|
-
if (
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4918
|
+
if (featureMatch && featureMatch[1] && featureMatch[2]) {
|
|
4919
|
+
const feature = featureMatch[1];
|
|
4920
|
+
const subfolder = featureMatch[2];
|
|
4921
|
+
let layer;
|
|
4922
|
+
if (subfolder === "domain" || subfolder === "errors") {
|
|
4923
|
+
layer = "domain";
|
|
4924
|
+
} else if (subfolder === "useCases" || subfolder === "usecases") {
|
|
4925
|
+
layer = "useCases";
|
|
4926
|
+
} else if (subfolder === "services") {
|
|
4927
|
+
layer = "services";
|
|
4928
|
+
} else if (subfolder === "infrastructure") {
|
|
4929
|
+
layer = "infrastructure";
|
|
4930
|
+
} else if (subfolder === "pxExpress" || subfolder === "presentation" || subfolder === "handlers" || subfolder.startsWith("px")) {
|
|
4931
|
+
layer = "presentation";
|
|
4932
|
+
}
|
|
4933
|
+
return { moduleType: "feature", moduleName: feature, feature, layer };
|
|
4934
|
+
}
|
|
4935
|
+
const sharedMatch = rel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
|
|
4936
|
+
if (sharedMatch && sharedMatch[1]) {
|
|
4937
|
+
const moduleName = sharedMatch[1];
|
|
4938
|
+
const rest = sharedMatch[2] || "";
|
|
4939
|
+
let layer = "root";
|
|
4940
|
+
if (rest.startsWith("domain/") || rest === "domain" || rest.startsWith("errors/") || rest === "errors") {
|
|
4941
|
+
layer = "domain";
|
|
4942
|
+
} else if (rest.startsWith("ports/") || rest === "ports") {
|
|
4943
|
+
layer = "ports";
|
|
4944
|
+
} else if (rest.startsWith("infrastructure/") || rest === "infrastructure") {
|
|
4945
|
+
layer = "infrastructure";
|
|
4946
|
+
}
|
|
4947
|
+
return { moduleType: "shared", moduleName, feature: moduleName, layer };
|
|
4948
|
+
}
|
|
4949
|
+
return {};
|
|
4934
4950
|
}
|
|
4935
4951
|
function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
4936
4952
|
if (specifier === "@solid-stack/di" || specifier.startsWith("@solid-stack/di/")) {
|
|
@@ -4951,14 +4967,19 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4951
4967
|
resolvedRel = specifier.replace(/^[@~]\//, "src/");
|
|
4952
4968
|
} else if (specifier.startsWith("@shared/")) {
|
|
4953
4969
|
resolvedRel = "src/" + specifier.slice(1);
|
|
4970
|
+
} else if (specifier.startsWith("@/configs/") || specifier.startsWith("~/configs/") || specifier === "@/configs" || specifier === "~/configs") {
|
|
4971
|
+
resolvedRel = specifier.replace(/^[@~]\//, "src/");
|
|
4972
|
+
} else if (specifier === "@configs" || specifier.startsWith("@configs/")) {
|
|
4973
|
+
resolvedRel = "src/" + specifier.slice(1);
|
|
4954
4974
|
} else if (specifier.startsWith("@/") || specifier.startsWith("~/")) {
|
|
4955
4975
|
resolvedRel = "src/" + specifier.slice(2);
|
|
4956
|
-
} else if (specifier.startsWith("features/") || specifier.startsWith("shared/")) {
|
|
4976
|
+
} else if (specifier.startsWith("features/") || specifier.startsWith("shared/") || specifier.startsWith("configs/") || specifier === "configs") {
|
|
4957
4977
|
resolvedRel = specifier;
|
|
4958
4978
|
}
|
|
4959
4979
|
if (!resolvedRel) {
|
|
4960
4980
|
return { isExternal: true, isDiFramework: false };
|
|
4961
4981
|
}
|
|
4982
|
+
const isGlobalConfig = resolvedRel.startsWith("src/configs/") || resolvedRel === "src/configs" || resolvedRel.startsWith("configs/") || resolvedRel === "configs";
|
|
4962
4983
|
const featMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/(.*))?$/);
|
|
4963
4984
|
if (featMatch && featMatch[1] && featMatch[2]) {
|
|
4964
4985
|
const targetFeature = featMatch[1];
|
|
@@ -4980,6 +5001,7 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4980
5001
|
const targetFileName = basePart.replace(/\.(js|ts|mjs|cjs)$/, "");
|
|
4981
5002
|
const isTargetPort = /^I[A-Z]/.test(targetFileName);
|
|
4982
5003
|
const isTargetRepo = isTargetPort && targetFileName.includes("Repo");
|
|
5004
|
+
const isTargetError = targetSubfolder === "errors" || rest.startsWith("errors/") || rest.includes("/errors/") || targetFileName.endsWith("Error") || targetFileName.endsWith("Errors");
|
|
4983
5005
|
return {
|
|
4984
5006
|
isExternal: false,
|
|
4985
5007
|
isDiFramework: false,
|
|
@@ -4988,7 +5010,8 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4988
5010
|
targetSubfolder,
|
|
4989
5011
|
targetFileName,
|
|
4990
5012
|
isTargetPort,
|
|
4991
|
-
isTargetRepo
|
|
5013
|
+
isTargetRepo,
|
|
5014
|
+
isTargetError
|
|
4992
5015
|
};
|
|
4993
5016
|
}
|
|
4994
5017
|
const sharedMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
|
|
@@ -5009,22 +5032,39 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
5009
5032
|
isSharedDomain
|
|
5010
5033
|
};
|
|
5011
5034
|
}
|
|
5012
|
-
return { isExternal: true, isDiFramework: false };
|
|
5035
|
+
return { isExternal: true, isDiFramework: false, isGlobalConfig };
|
|
5013
5036
|
}
|
|
5014
5037
|
function extractImportStatements(content) {
|
|
5015
5038
|
const statements = [];
|
|
5016
5039
|
const lines = content.split(/\r?\n/);
|
|
5017
|
-
const regex = /(?:import\s+(
|
|
5040
|
+
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;
|
|
5018
5041
|
let match;
|
|
5019
5042
|
while ((match = regex.exec(content)) !== null) {
|
|
5020
|
-
const
|
|
5043
|
+
const isTypeOnly = Boolean(match[1] || match[3]);
|
|
5044
|
+
const importClause = (match[2] || match[4] || "").trim();
|
|
5045
|
+
const specifier = match[5];
|
|
5021
5046
|
const charIndex = match.index;
|
|
5022
5047
|
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
5023
5048
|
const rawCode = lines[lineNum - 1]?.trim() || match[0].trim();
|
|
5049
|
+
const importedNames = [];
|
|
5050
|
+
if (importClause) {
|
|
5051
|
+
const cleaned = importClause.replace(/[{}]/g, "");
|
|
5052
|
+
const words = cleaned.split(",").map((s) => s.trim()).filter(Boolean);
|
|
5053
|
+
for (const w of words) {
|
|
5054
|
+
const parts = w.split(/\s+as\s+/);
|
|
5055
|
+
const name = parts[0].replace(/^type\s+/, "").trim();
|
|
5056
|
+
const alias = (parts[1] || name).trim();
|
|
5057
|
+
if (alias && alias !== "*") {
|
|
5058
|
+
importedNames.push(alias);
|
|
5059
|
+
}
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5024
5062
|
statements.push({
|
|
5025
5063
|
specifier,
|
|
5026
5064
|
line: lineNum,
|
|
5027
|
-
rawCode
|
|
5065
|
+
rawCode,
|
|
5066
|
+
isTypeOnly,
|
|
5067
|
+
importedNames
|
|
5028
5068
|
});
|
|
5029
5069
|
}
|
|
5030
5070
|
return statements;
|
|
@@ -5123,6 +5163,15 @@ function discoverAllFeatures(projectRoot) {
|
|
|
5123
5163
|
dir: path2__default.default.join(featuresBase, e.name)
|
|
5124
5164
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
5125
5165
|
}
|
|
5166
|
+
function discoverAllShared(projectRoot) {
|
|
5167
|
+
const sharedBase = resolveSharedDir(projectRoot);
|
|
5168
|
+
if (!fs4__default.default.existsSync(sharedBase)) return [];
|
|
5169
|
+
const entries = fs4__default.default.readdirSync(sharedBase, { withFileTypes: true });
|
|
5170
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "__tests__" && e.name !== "tests").map((e) => ({
|
|
5171
|
+
name: e.name,
|
|
5172
|
+
dir: path2__default.default.join(sharedBase, e.name)
|
|
5173
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
5174
|
+
}
|
|
5126
5175
|
function extractClassMethods(content, className) {
|
|
5127
5176
|
let clean = "";
|
|
5128
5177
|
let inLineComment = false;
|
|
@@ -5472,16 +5521,21 @@ function lintCodebase(options = {}) {
|
|
|
5472
5521
|
const projectRoot = options.projectRoot || findProjectRoot(options.targetDir || process.cwd());
|
|
5473
5522
|
const failFast = !options.complete;
|
|
5474
5523
|
const features = discoverAllFeatures(projectRoot);
|
|
5524
|
+
const sharedModules = discoverAllShared(projectRoot);
|
|
5525
|
+
const allModules = [
|
|
5526
|
+
...features.map((f) => ({ ...f, type: "feature" })),
|
|
5527
|
+
...sharedModules.map((s) => ({ ...s, type: "shared" }))
|
|
5528
|
+
];
|
|
5475
5529
|
const graph = new FeatureDependencyGraph();
|
|
5476
5530
|
const allViolations = [];
|
|
5477
5531
|
const results = [];
|
|
5478
5532
|
const detectedCycles = [];
|
|
5479
5533
|
const addViolation = (v) => {
|
|
5480
5534
|
allViolations.push(v);
|
|
5481
|
-
const
|
|
5482
|
-
if (
|
|
5483
|
-
|
|
5484
|
-
|
|
5535
|
+
const moduleResult = results.find((r) => r.featureName === v.feature);
|
|
5536
|
+
if (moduleResult) {
|
|
5537
|
+
moduleResult.violations.push(v);
|
|
5538
|
+
moduleResult.passed = false;
|
|
5485
5539
|
}
|
|
5486
5540
|
if (failFast) {
|
|
5487
5541
|
const errorMsg = `Dependency rule violation detected in ${v.relativePath}:${v.line}:
|
|
@@ -5490,28 +5544,156 @@ Code: ${v.code}`;
|
|
|
5490
5544
|
throw new DependencyRuleError(errorMsg, [v], true);
|
|
5491
5545
|
}
|
|
5492
5546
|
};
|
|
5493
|
-
for (const
|
|
5547
|
+
for (const mod of allModules) {
|
|
5494
5548
|
results.push({
|
|
5495
|
-
|
|
5496
|
-
|
|
5549
|
+
moduleType: mod.type,
|
|
5550
|
+
moduleName: mod.name,
|
|
5551
|
+
featureName: mod.name,
|
|
5552
|
+
featureDir: mod.dir,
|
|
5553
|
+
moduleDir: mod.dir,
|
|
5497
5554
|
scannedFilesCount: 0,
|
|
5498
5555
|
passed: true,
|
|
5499
5556
|
violations: [],
|
|
5500
|
-
layersChecked: ["domain", "useCases", "services", "infrastructure", "presentation"]
|
|
5557
|
+
layersChecked: mod.type === "feature" ? ["domain", "useCases", "services", "infrastructure", "presentation"] : ["domain", "ports", "infrastructure", "root"]
|
|
5501
5558
|
});
|
|
5502
5559
|
}
|
|
5503
|
-
for (const
|
|
5504
|
-
const
|
|
5505
|
-
const allFiles = scanDirFiles(
|
|
5506
|
-
|
|
5560
|
+
for (const mod of allModules) {
|
|
5561
|
+
const modResult = results.find((r) => r.featureName === mod.name);
|
|
5562
|
+
const allFiles = scanDirFiles(mod.dir, projectRoot);
|
|
5563
|
+
modResult.scannedFilesCount = allFiles.length;
|
|
5507
5564
|
for (const file of allFiles) {
|
|
5508
5565
|
const relPath = path2__default.default.relative(projectRoot, file).replace(/\\/g, "/");
|
|
5509
|
-
const { layer } = classifyFile(file, projectRoot);
|
|
5510
|
-
if (!layer) continue;
|
|
5566
|
+
const { moduleType, moduleName, layer } = classifyFile(file, projectRoot);
|
|
5567
|
+
if (!layer || !moduleType || !moduleName) continue;
|
|
5511
5568
|
const content = fs4__default.default.readFileSync(file, "utf-8");
|
|
5512
5569
|
const statements = extractImportStatements(content);
|
|
5513
5570
|
const baseName = path2__default.default.basename(file);
|
|
5514
5571
|
const isBarrel = baseName === "index.ts" || baseName === "index.js";
|
|
5572
|
+
if (moduleType === "shared") {
|
|
5573
|
+
if (layer === "domain") {
|
|
5574
|
+
const lines = content.split(/\r?\n/);
|
|
5575
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5576
|
+
const lineText = lines[i].trim();
|
|
5577
|
+
if (lineText.includes("@MakeInjectable") || lineText.includes("public static deps =")) {
|
|
5578
|
+
addViolation({
|
|
5579
|
+
feature: moduleName,
|
|
5580
|
+
layer,
|
|
5581
|
+
file,
|
|
5582
|
+
relativePath: relPath,
|
|
5583
|
+
line: i + 1,
|
|
5584
|
+
category: "Domain Dependence Violation",
|
|
5585
|
+
code: lineText,
|
|
5586
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
5587
|
+
severity: "error",
|
|
5588
|
+
advice: "Domain layer must have zero DI dependence. Remove @MakeInjectable decorators and static deps declarations.",
|
|
5589
|
+
fixExample: {
|
|
5590
|
+
bad: lineText,
|
|
5591
|
+
good: "// Domain interfaces and value objects must remain plain TypeScript classes/interfaces."
|
|
5592
|
+
}
|
|
5593
|
+
});
|
|
5594
|
+
}
|
|
5595
|
+
}
|
|
5596
|
+
}
|
|
5597
|
+
for (const stmt of statements) {
|
|
5598
|
+
const target = classifyTarget(stmt.specifier, file, projectRoot);
|
|
5599
|
+
if (target.targetFeature) {
|
|
5600
|
+
addViolation({
|
|
5601
|
+
feature: moduleName,
|
|
5602
|
+
layer,
|
|
5603
|
+
file,
|
|
5604
|
+
relativePath: relPath,
|
|
5605
|
+
line: stmt.line,
|
|
5606
|
+
category: "Shared Architecture Violation",
|
|
5607
|
+
code: stmt.rawCode,
|
|
5608
|
+
ruleId: "SF1_FEATURE_DEPENDENCY",
|
|
5609
|
+
severity: "error",
|
|
5610
|
+
advice: `Shared modules ('${moduleName}') are foundational and must NEVER import from feature modules ('${target.targetFeature}').`,
|
|
5611
|
+
fixExample: {
|
|
5612
|
+
bad: stmt.rawCode,
|
|
5613
|
+
good: "// Move shared logic/types to shared/ or communicate via domain events/callbacks."
|
|
5614
|
+
}
|
|
5615
|
+
});
|
|
5616
|
+
}
|
|
5617
|
+
if (target.targetSharedModule && target.targetSharedModule !== moduleName) {
|
|
5618
|
+
if (target.isSharedInfra || target.isSharedPorts) {
|
|
5619
|
+
addViolation({
|
|
5620
|
+
feature: moduleName,
|
|
5621
|
+
layer,
|
|
5622
|
+
file,
|
|
5623
|
+
relativePath: relPath,
|
|
5624
|
+
line: stmt.line,
|
|
5625
|
+
category: "Shared Architecture Violation",
|
|
5626
|
+
code: stmt.rawCode,
|
|
5627
|
+
ruleId: "SS1_SHARED_ENCAPSULATION",
|
|
5628
|
+
severity: "error",
|
|
5629
|
+
advice: `Shared module '${moduleName}' cannot import internal details (ports/infra) of shared module '${target.targetSharedModule}'. Depend only on its root classes or domain types.`,
|
|
5630
|
+
fixExample: {
|
|
5631
|
+
bad: stmt.rawCode,
|
|
5632
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
5633
|
+
}
|
|
5634
|
+
});
|
|
5635
|
+
}
|
|
5636
|
+
}
|
|
5637
|
+
if (layer === "domain") {
|
|
5638
|
+
if (target.targetSharedModule === moduleName && (target.isSharedInfra || target.isSharedPorts || target.isSharedRoot)) {
|
|
5639
|
+
addViolation({
|
|
5640
|
+
feature: moduleName,
|
|
5641
|
+
layer,
|
|
5642
|
+
file,
|
|
5643
|
+
relativePath: relPath,
|
|
5644
|
+
line: stmt.line,
|
|
5645
|
+
category: "Domain Purity Violation",
|
|
5646
|
+
code: stmt.rawCode,
|
|
5647
|
+
ruleId: "SD1_OUTER_IMPORT",
|
|
5648
|
+
severity: "error",
|
|
5649
|
+
advice: `Shared domain layer must not import from its own ports, infrastructure, or root classes.`,
|
|
5650
|
+
fixExample: {
|
|
5651
|
+
bad: stmt.rawCode,
|
|
5652
|
+
good: "// Model pure business types and value objects directly within domain/."
|
|
5653
|
+
}
|
|
5654
|
+
});
|
|
5655
|
+
}
|
|
5656
|
+
if (target.isDiFramework) {
|
|
5657
|
+
addViolation({
|
|
5658
|
+
feature: moduleName,
|
|
5659
|
+
layer,
|
|
5660
|
+
file,
|
|
5661
|
+
relativePath: relPath,
|
|
5662
|
+
line: stmt.line,
|
|
5663
|
+
category: "Domain Dependence Violation",
|
|
5664
|
+
code: stmt.rawCode,
|
|
5665
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
5666
|
+
severity: "error",
|
|
5667
|
+
advice: "Domain layer must have zero DI dependence. Domain interfaces and value objects are plain classes/types.",
|
|
5668
|
+
fixExample: {
|
|
5669
|
+
bad: stmt.rawCode,
|
|
5670
|
+
good: "// Remove @solid-stack/di import. Domain entities are pure TypeScript."
|
|
5671
|
+
}
|
|
5672
|
+
});
|
|
5673
|
+
}
|
|
5674
|
+
}
|
|
5675
|
+
if (layer === "ports" && target.targetSharedModule === moduleName && target.isSharedInfra) {
|
|
5676
|
+
addViolation({
|
|
5677
|
+
feature: moduleName,
|
|
5678
|
+
layer,
|
|
5679
|
+
file,
|
|
5680
|
+
relativePath: relPath,
|
|
5681
|
+
line: stmt.line,
|
|
5682
|
+
category: "Shared Architecture Violation",
|
|
5683
|
+
code: stmt.rawCode,
|
|
5684
|
+
ruleId: "SP1_INFRA_IN_PORT",
|
|
5685
|
+
severity: "error",
|
|
5686
|
+
advice: `Shared ports cannot depend on infrastructure. Infrastructure implements ports, not the other way around.`,
|
|
5687
|
+
fixExample: {
|
|
5688
|
+
bad: stmt.rawCode,
|
|
5689
|
+
good: "// Ports define abstract interfaces without concrete infrastructure dependencies."
|
|
5690
|
+
}
|
|
5691
|
+
});
|
|
5692
|
+
}
|
|
5693
|
+
}
|
|
5694
|
+
continue;
|
|
5695
|
+
}
|
|
5696
|
+
const feat = mod;
|
|
5515
5697
|
if (layer === "useCases" && !isBarrel) {
|
|
5516
5698
|
checkUseCaseShape(feat.name, file, relPath, content, addViolation);
|
|
5517
5699
|
}
|
|
@@ -5580,25 +5762,55 @@ Code: ${v.code}`;
|
|
|
5580
5762
|
}
|
|
5581
5763
|
});
|
|
5582
5764
|
}
|
|
5583
|
-
if (target.targetLayer === "domain" && !target.isTargetPort) {
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5765
|
+
if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetError && !stmt.isTypeOnly) {
|
|
5766
|
+
for (const name of stmt.importedNames) {
|
|
5767
|
+
const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
|
|
5768
|
+
const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
|
|
5769
|
+
const execMatch = createRegex.exec(content) || newRegex.exec(content);
|
|
5770
|
+
if (execMatch) {
|
|
5771
|
+
const charIndex = execMatch.index;
|
|
5772
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
5773
|
+
const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
|
|
5774
|
+
addViolation({
|
|
5775
|
+
feature: feat.name,
|
|
5776
|
+
layer,
|
|
5777
|
+
file,
|
|
5778
|
+
relativePath: relPath,
|
|
5779
|
+
line: lineNum,
|
|
5780
|
+
category: "Cross-Feature Domain Violation",
|
|
5781
|
+
code: lineCode,
|
|
5782
|
+
ruleId: "F1_CROSS_FEATURE_DOMAIN",
|
|
5783
|
+
severity: "error",
|
|
5784
|
+
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.`,
|
|
5785
|
+
fixExample: {
|
|
5786
|
+
bad: lineCode,
|
|
5787
|
+
good: `// Define a Port in ${feat.name}/domain/ and implement the cross-feature communication in ${feat.name}/infrastructure/.`
|
|
5788
|
+
}
|
|
5789
|
+
});
|
|
5598
5790
|
}
|
|
5599
|
-
}
|
|
5791
|
+
}
|
|
5600
5792
|
}
|
|
5601
5793
|
}
|
|
5794
|
+
if (target.isGlobalConfig) {
|
|
5795
|
+
addViolation({
|
|
5796
|
+
feature: feat.name,
|
|
5797
|
+
layer,
|
|
5798
|
+
file,
|
|
5799
|
+
relativePath: relPath,
|
|
5800
|
+
line: stmt.line,
|
|
5801
|
+
category: "Global Configuration Violation",
|
|
5802
|
+
code: stmt.rawCode,
|
|
5803
|
+
ruleId: "F2_GLOBAL_CONFIG_DEPENDENCY",
|
|
5804
|
+
severity: "error",
|
|
5805
|
+
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.`,
|
|
5806
|
+
fixExample: {
|
|
5807
|
+
bad: stmt.rawCode,
|
|
5808
|
+
good: `// 1. Define I${toPascalCase(feat.name)}Config locally
|
|
5809
|
+
// 2. Inject it via static deps
|
|
5810
|
+
// 3. Import and bind global configs inside diProvider.ts`
|
|
5811
|
+
}
|
|
5812
|
+
});
|
|
5813
|
+
}
|
|
5602
5814
|
if (layer === "domain") {
|
|
5603
5815
|
if (target.targetLayer && ["useCases", "services", "infrastructure", "presentation"].includes(target.targetLayer)) {
|
|
5604
5816
|
addViolation({
|
|
@@ -5654,7 +5866,7 @@ Code: ${v.code}`;
|
|
|
5654
5866
|
}
|
|
5655
5867
|
});
|
|
5656
5868
|
}
|
|
5657
|
-
if (target.isExternal && !target.isDiFramework) {
|
|
5869
|
+
if (target.isExternal && !target.isDiFramework && !target.isGlobalConfig) {
|
|
5658
5870
|
addViolation({
|
|
5659
5871
|
feature: feat.name,
|
|
5660
5872
|
layer,
|
|
@@ -5964,23 +6176,33 @@ Code: ${v.code}`;
|
|
|
5964
6176
|
}
|
|
5965
6177
|
});
|
|
5966
6178
|
}
|
|
5967
|
-
if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo) {
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
6179
|
+
if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo && !target.isTargetError && !stmt.isTypeOnly) {
|
|
6180
|
+
for (const name of stmt.importedNames) {
|
|
6181
|
+
const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
|
|
6182
|
+
const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
|
|
6183
|
+
const execMatch = createRegex.exec(content) || newRegex.exec(content);
|
|
6184
|
+
if (execMatch) {
|
|
6185
|
+
const charIndex = execMatch.index;
|
|
6186
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
6187
|
+
const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
|
|
6188
|
+
addViolation({
|
|
6189
|
+
feature: feat.name,
|
|
6190
|
+
layer,
|
|
6191
|
+
file,
|
|
6192
|
+
relativePath: relPath,
|
|
6193
|
+
line: lineNum,
|
|
6194
|
+
category: "Presentation Layer Violation",
|
|
6195
|
+
code: lineCode,
|
|
6196
|
+
ruleId: "P5_DIRECT_DOMAIN_ENTITY",
|
|
6197
|
+
severity: "error",
|
|
6198
|
+
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.`,
|
|
6199
|
+
fixExample: {
|
|
6200
|
+
bad: lineCode,
|
|
6201
|
+
good: "// Construct a plain DTO object and pass it into the injected Use Case's execute() method."
|
|
6202
|
+
}
|
|
6203
|
+
});
|
|
5982
6204
|
}
|
|
5983
|
-
}
|
|
6205
|
+
}
|
|
5984
6206
|
}
|
|
5985
6207
|
}
|
|
5986
6208
|
}
|