@solid-stack/mason 1.0.21 → 1.0.23
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 +859 -1
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +859 -1
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/commands/lint.d.ts +4 -0
- package/dist/commands/lint.d.ts.map +1 -0
- package/dist/core/lintEngine.d.ts +55 -0
- package/dist/core/lintEngine.d.ts.map +1 -0
- package/dist/index.cjs +863 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +860 -2
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lint.d.ts +60 -0
- package/dist/types/lint.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -4160,6 +4160,832 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
4160
4160
|
);
|
|
4161
4161
|
}
|
|
4162
4162
|
}
|
|
4163
|
+
var DependencyRuleError = class extends Error {
|
|
4164
|
+
violations;
|
|
4165
|
+
isFailFast;
|
|
4166
|
+
constructor(message, violations, isFailFast = false) {
|
|
4167
|
+
super(message);
|
|
4168
|
+
this.name = "DependencyRuleError";
|
|
4169
|
+
this.violations = violations;
|
|
4170
|
+
this.isFailFast = isFailFast;
|
|
4171
|
+
}
|
|
4172
|
+
};
|
|
4173
|
+
function isTestFile2(filePath, projectRoot) {
|
|
4174
|
+
const norm = (projectRoot ? path2.relative(projectRoot, filePath) : filePath).replace(/\\/g, "/");
|
|
4175
|
+
const base = path2.basename(norm);
|
|
4176
|
+
return base.endsWith(".test.ts") || base.endsWith(".test.js") || base.endsWith(".spec.ts") || base.endsWith(".spec.js") || base.endsWith(".d.ts") || norm.includes("/__tests__/") || norm.startsWith("__tests__/") || /(?:^|\/)tests?\//.test(norm);
|
|
4177
|
+
}
|
|
4178
|
+
function scanDirFiles(dir, projectRoot) {
|
|
4179
|
+
if (!fs9.existsSync(dir)) return [];
|
|
4180
|
+
const entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
4181
|
+
const files = [];
|
|
4182
|
+
for (const entry of entries) {
|
|
4183
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "__tests__" || entry.name.startsWith(".")) {
|
|
4184
|
+
continue;
|
|
4185
|
+
}
|
|
4186
|
+
const fullPath = path2.join(dir, entry.name);
|
|
4187
|
+
if (entry.isDirectory()) {
|
|
4188
|
+
files.push(...scanDirFiles(fullPath, projectRoot));
|
|
4189
|
+
} else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".js")) && !isTestFile2(fullPath, projectRoot)) {
|
|
4190
|
+
files.push(fullPath);
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
return files;
|
|
4194
|
+
}
|
|
4195
|
+
function classifyFile(fullPath, projectRoot) {
|
|
4196
|
+
const rel = path2.relative(projectRoot, fullPath).replace(/\\/g, "/");
|
|
4197
|
+
const featureMatch = rel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/.*)?$/);
|
|
4198
|
+
if (!featureMatch || !featureMatch[1] || !featureMatch[2]) return {};
|
|
4199
|
+
const feature = featureMatch[1];
|
|
4200
|
+
const subfolder = featureMatch[2];
|
|
4201
|
+
let layer;
|
|
4202
|
+
if (subfolder === "domain" || subfolder === "errors") {
|
|
4203
|
+
layer = "domain";
|
|
4204
|
+
} else if (subfolder === "useCases" || subfolder === "usecases") {
|
|
4205
|
+
layer = "useCases";
|
|
4206
|
+
} else if (subfolder === "services") {
|
|
4207
|
+
layer = "services";
|
|
4208
|
+
} else if (subfolder === "infrastructure") {
|
|
4209
|
+
layer = "infrastructure";
|
|
4210
|
+
} else if (subfolder === "pxExpress" || subfolder === "presentation" || subfolder === "handlers" || subfolder.startsWith("px")) {
|
|
4211
|
+
layer = "presentation";
|
|
4212
|
+
}
|
|
4213
|
+
return { feature, layer };
|
|
4214
|
+
}
|
|
4215
|
+
function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
4216
|
+
if (specifier === "@solid-stack/di" || specifier.startsWith("@solid-stack/di/")) {
|
|
4217
|
+
return { isExternal: true, isDiFramework: true };
|
|
4218
|
+
}
|
|
4219
|
+
if (specifier.startsWith("node:") || ["crypto", "fs", "path", "http", "https", "url", "os", "child_process", "stream", "express", "cors", "dotenv"].includes(specifier)) {
|
|
4220
|
+
return { isExternal: true, isDiFramework: false };
|
|
4221
|
+
}
|
|
4222
|
+
let resolvedRel = null;
|
|
4223
|
+
if (specifier.startsWith(".")) {
|
|
4224
|
+
const abs = path2.resolve(path2.dirname(sourceFileAbs), specifier);
|
|
4225
|
+
resolvedRel = path2.relative(projectRoot, abs).replace(/\\/g, "/");
|
|
4226
|
+
} else if (specifier.startsWith("@/features/") || specifier.startsWith("~/features/")) {
|
|
4227
|
+
resolvedRel = specifier.replace(/^[@~]\//, "src/");
|
|
4228
|
+
} else if (specifier.startsWith("@features/")) {
|
|
4229
|
+
resolvedRel = "src/" + specifier.slice(1);
|
|
4230
|
+
} else if (specifier.startsWith("@/shared/") || specifier.startsWith("~/shared/")) {
|
|
4231
|
+
resolvedRel = specifier.replace(/^[@~]\//, "src/");
|
|
4232
|
+
} else if (specifier.startsWith("@shared/")) {
|
|
4233
|
+
resolvedRel = "src/" + specifier.slice(1);
|
|
4234
|
+
} else if (specifier.startsWith("@/") || specifier.startsWith("~/")) {
|
|
4235
|
+
resolvedRel = "src/" + specifier.slice(2);
|
|
4236
|
+
} else if (specifier.startsWith("features/") || specifier.startsWith("shared/")) {
|
|
4237
|
+
resolvedRel = specifier;
|
|
4238
|
+
}
|
|
4239
|
+
if (!resolvedRel) {
|
|
4240
|
+
return { isExternal: true, isDiFramework: false };
|
|
4241
|
+
}
|
|
4242
|
+
const featMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/(.*))?$/);
|
|
4243
|
+
if (featMatch && featMatch[1] && featMatch[2]) {
|
|
4244
|
+
const targetFeature = featMatch[1];
|
|
4245
|
+
const targetSubfolder = featMatch[2];
|
|
4246
|
+
const rest = featMatch[3] || "";
|
|
4247
|
+
let targetLayer;
|
|
4248
|
+
if (targetSubfolder === "domain" || targetSubfolder === "errors") {
|
|
4249
|
+
targetLayer = "domain";
|
|
4250
|
+
} else if (targetSubfolder === "useCases" || targetSubfolder === "usecases") {
|
|
4251
|
+
targetLayer = "useCases";
|
|
4252
|
+
} else if (targetSubfolder === "services") {
|
|
4253
|
+
targetLayer = "services";
|
|
4254
|
+
} else if (targetSubfolder === "infrastructure") {
|
|
4255
|
+
targetLayer = "infrastructure";
|
|
4256
|
+
} else if (targetSubfolder === "pxExpress" || targetSubfolder === "presentation" || targetSubfolder === "handlers" || targetSubfolder.startsWith("px")) {
|
|
4257
|
+
targetLayer = "presentation";
|
|
4258
|
+
}
|
|
4259
|
+
const basePart = rest ? path2.basename(rest) : targetSubfolder;
|
|
4260
|
+
const targetFileName = basePart.replace(/\.(js|ts|mjs|cjs)$/, "");
|
|
4261
|
+
const isTargetPort = /^I[A-Z]/.test(targetFileName);
|
|
4262
|
+
const isTargetRepo = isTargetPort && targetFileName.includes("Repo");
|
|
4263
|
+
return {
|
|
4264
|
+
isExternal: false,
|
|
4265
|
+
isDiFramework: false,
|
|
4266
|
+
targetFeature,
|
|
4267
|
+
targetLayer,
|
|
4268
|
+
targetSubfolder,
|
|
4269
|
+
targetFileName,
|
|
4270
|
+
isTargetPort,
|
|
4271
|
+
isTargetRepo
|
|
4272
|
+
};
|
|
4273
|
+
}
|
|
4274
|
+
const sharedMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
|
|
4275
|
+
if (sharedMatch) {
|
|
4276
|
+
const targetSharedModule = sharedMatch[1];
|
|
4277
|
+
const sharedRest = sharedMatch[2] || "";
|
|
4278
|
+
const isSharedRoot = !sharedRest.includes("/");
|
|
4279
|
+
const isSharedPorts = sharedRest.startsWith("ports/") || sharedRest === "ports";
|
|
4280
|
+
const isSharedInfra = sharedRest.startsWith("infrastructure/") || sharedRest === "infrastructure";
|
|
4281
|
+
const isSharedDomain = sharedRest.startsWith("domain/") || sharedRest === "domain";
|
|
4282
|
+
return {
|
|
4283
|
+
isExternal: false,
|
|
4284
|
+
isDiFramework: false,
|
|
4285
|
+
targetSharedModule,
|
|
4286
|
+
isSharedRoot,
|
|
4287
|
+
isSharedPorts,
|
|
4288
|
+
isSharedInfra,
|
|
4289
|
+
isSharedDomain
|
|
4290
|
+
};
|
|
4291
|
+
}
|
|
4292
|
+
return { isExternal: true, isDiFramework: false };
|
|
4293
|
+
}
|
|
4294
|
+
function extractImportStatements(content) {
|
|
4295
|
+
const statements = [];
|
|
4296
|
+
const lines = content.split(/\r?\n/);
|
|
4297
|
+
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;
|
|
4298
|
+
let match;
|
|
4299
|
+
while ((match = regex.exec(content)) !== null) {
|
|
4300
|
+
const specifier = match[1];
|
|
4301
|
+
const charIndex = match.index;
|
|
4302
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
4303
|
+
const rawCode = lines[lineNum - 1]?.trim() || match[0].trim();
|
|
4304
|
+
statements.push({
|
|
4305
|
+
specifier,
|
|
4306
|
+
line: lineNum,
|
|
4307
|
+
rawCode
|
|
4308
|
+
});
|
|
4309
|
+
}
|
|
4310
|
+
return statements;
|
|
4311
|
+
}
|
|
4312
|
+
var FeatureDependencyGraph = class {
|
|
4313
|
+
edges = /* @__PURE__ */ new Map();
|
|
4314
|
+
addEdge(evidence) {
|
|
4315
|
+
const { fromFeature, toFeature } = evidence;
|
|
4316
|
+
if (fromFeature === toFeature) {
|
|
4317
|
+
return { createsCycle: false };
|
|
4318
|
+
}
|
|
4319
|
+
if (!this.edges.has(fromFeature)) {
|
|
4320
|
+
this.edges.set(fromFeature, /* @__PURE__ */ new Map());
|
|
4321
|
+
}
|
|
4322
|
+
this.edges.get(fromFeature).set(toFeature, evidence);
|
|
4323
|
+
const reverse = this.edges.get(toFeature)?.get(fromFeature);
|
|
4324
|
+
if (reverse) {
|
|
4325
|
+
return { createsCycle: true, directPartner: reverse, cycleChain: [fromFeature, toFeature, fromFeature] };
|
|
4326
|
+
}
|
|
4327
|
+
const cycle = this.findCycleFrom(fromFeature);
|
|
4328
|
+
if (cycle && cycle.length > 0) {
|
|
4329
|
+
return { createsCycle: true, cycleChain: cycle };
|
|
4330
|
+
}
|
|
4331
|
+
return { createsCycle: false };
|
|
4332
|
+
}
|
|
4333
|
+
getEdges() {
|
|
4334
|
+
const list = [];
|
|
4335
|
+
for (const [from, toMap] of this.edges.entries()) {
|
|
4336
|
+
for (const [to, ev] of toMap.entries()) {
|
|
4337
|
+
list.push({ from, to, evidence: ev });
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
return list;
|
|
4341
|
+
}
|
|
4342
|
+
findDirectCycles() {
|
|
4343
|
+
const cycles = [];
|
|
4344
|
+
const checkedPairs = /* @__PURE__ */ new Set();
|
|
4345
|
+
for (const [from, toMap] of this.edges.entries()) {
|
|
4346
|
+
for (const [to, evAtoB] of toMap.entries()) {
|
|
4347
|
+
const pairKey = [from, to].sort().join("<->");
|
|
4348
|
+
if (checkedPairs.has(pairKey)) continue;
|
|
4349
|
+
const evBtoA = this.edges.get(to)?.get(from);
|
|
4350
|
+
if (evBtoA) {
|
|
4351
|
+
checkedPairs.add(pairKey);
|
|
4352
|
+
cycles.push({
|
|
4353
|
+
featureA: from,
|
|
4354
|
+
featureB: to,
|
|
4355
|
+
cycle: [from, to, from],
|
|
4356
|
+
evidenceAtoB: evAtoB,
|
|
4357
|
+
evidenceBtoA: evBtoA
|
|
4358
|
+
});
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
return cycles;
|
|
4363
|
+
}
|
|
4364
|
+
findCycleFrom(start) {
|
|
4365
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4366
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
4367
|
+
const parent = /* @__PURE__ */ new Map();
|
|
4368
|
+
const dfs = (curr) => {
|
|
4369
|
+
visited.add(curr);
|
|
4370
|
+
inStack.add(curr);
|
|
4371
|
+
const neighbors = this.edges.get(curr);
|
|
4372
|
+
if (neighbors) {
|
|
4373
|
+
for (const next of neighbors.keys()) {
|
|
4374
|
+
if (!visited.has(next)) {
|
|
4375
|
+
parent.set(next, curr);
|
|
4376
|
+
const cycle = dfs(next);
|
|
4377
|
+
if (cycle) return cycle;
|
|
4378
|
+
} else if (inStack.has(next)) {
|
|
4379
|
+
const cycle = [next];
|
|
4380
|
+
let p16 = curr;
|
|
4381
|
+
while (p16 !== next) {
|
|
4382
|
+
cycle.push(p16);
|
|
4383
|
+
p16 = parent.get(p16) || next;
|
|
4384
|
+
}
|
|
4385
|
+
cycle.push(next);
|
|
4386
|
+
cycle.reverse();
|
|
4387
|
+
return cycle;
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
inStack.delete(curr);
|
|
4392
|
+
return null;
|
|
4393
|
+
};
|
|
4394
|
+
return dfs(start);
|
|
4395
|
+
}
|
|
4396
|
+
};
|
|
4397
|
+
function discoverAllFeatures(projectRoot) {
|
|
4398
|
+
const featuresBase = resolveFeaturesDir(projectRoot);
|
|
4399
|
+
if (!fs9.existsSync(featuresBase)) return [];
|
|
4400
|
+
const entries = fs9.readdirSync(featuresBase, { withFileTypes: true });
|
|
4401
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "__tests__" && e.name !== "tests").map((e) => ({
|
|
4402
|
+
name: e.name,
|
|
4403
|
+
dir: path2.join(featuresBase, e.name)
|
|
4404
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
4405
|
+
}
|
|
4406
|
+
function lintCodebase(options = {}) {
|
|
4407
|
+
const startTime = Date.now();
|
|
4408
|
+
const projectRoot = options.projectRoot || findProjectRoot(options.targetDir || process.cwd());
|
|
4409
|
+
const failFast = !options.complete;
|
|
4410
|
+
const features = discoverAllFeatures(projectRoot);
|
|
4411
|
+
const graph = new FeatureDependencyGraph();
|
|
4412
|
+
const allViolations = [];
|
|
4413
|
+
const results = [];
|
|
4414
|
+
const detectedCycles = [];
|
|
4415
|
+
const addViolation = (v) => {
|
|
4416
|
+
allViolations.push(v);
|
|
4417
|
+
const featureResult = results.find((r) => r.featureName === v.feature);
|
|
4418
|
+
if (featureResult) {
|
|
4419
|
+
featureResult.violations.push(v);
|
|
4420
|
+
featureResult.passed = false;
|
|
4421
|
+
}
|
|
4422
|
+
if (failFast) {
|
|
4423
|
+
const errorMsg = `Dependency rule violation detected in ${v.relativePath}:${v.line}:
|
|
4424
|
+
[${v.category}] ${v.advice}
|
|
4425
|
+
Code: ${v.code}`;
|
|
4426
|
+
throw new DependencyRuleError(errorMsg, [v], true);
|
|
4427
|
+
}
|
|
4428
|
+
};
|
|
4429
|
+
for (const feat of features) {
|
|
4430
|
+
results.push({
|
|
4431
|
+
featureName: feat.name,
|
|
4432
|
+
featureDir: feat.dir,
|
|
4433
|
+
scannedFilesCount: 0,
|
|
4434
|
+
passed: true,
|
|
4435
|
+
violations: [],
|
|
4436
|
+
layersChecked: ["domain", "useCases", "services", "infrastructure", "presentation"]
|
|
4437
|
+
});
|
|
4438
|
+
}
|
|
4439
|
+
for (const feat of features) {
|
|
4440
|
+
const featResult = results.find((r) => r.featureName === feat.name);
|
|
4441
|
+
const allFiles = scanDirFiles(feat.dir, projectRoot);
|
|
4442
|
+
featResult.scannedFilesCount = allFiles.length;
|
|
4443
|
+
for (const file of allFiles) {
|
|
4444
|
+
const relPath = path2.relative(projectRoot, file).replace(/\\/g, "/");
|
|
4445
|
+
const { layer } = classifyFile(file, projectRoot);
|
|
4446
|
+
if (!layer) continue;
|
|
4447
|
+
const content = fs9.readFileSync(file, "utf-8");
|
|
4448
|
+
const statements = extractImportStatements(content);
|
|
4449
|
+
if (layer === "domain") {
|
|
4450
|
+
const lines = content.split(/\r?\n/);
|
|
4451
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4452
|
+
const lineText = lines[i].trim();
|
|
4453
|
+
if (lineText.includes("@MakeInjectable") || lineText.includes("public static deps =")) {
|
|
4454
|
+
addViolation({
|
|
4455
|
+
feature: feat.name,
|
|
4456
|
+
layer,
|
|
4457
|
+
file,
|
|
4458
|
+
relativePath: relPath,
|
|
4459
|
+
line: i + 1,
|
|
4460
|
+
category: "Domain Dependence Violation",
|
|
4461
|
+
code: lineText,
|
|
4462
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
4463
|
+
severity: "error",
|
|
4464
|
+
advice: "Domain layer must have zero DI dependence. Remove @MakeInjectable decorators and static deps declarations.",
|
|
4465
|
+
fixExample: {
|
|
4466
|
+
bad: lineText,
|
|
4467
|
+
good: "// Domain interfaces and value objects must remain plain TypeScript classes/interfaces."
|
|
4468
|
+
}
|
|
4469
|
+
});
|
|
4470
|
+
}
|
|
4471
|
+
}
|
|
4472
|
+
}
|
|
4473
|
+
for (const stmt of statements) {
|
|
4474
|
+
const target = classifyTarget(stmt.specifier, file, projectRoot);
|
|
4475
|
+
if (target.targetFeature && target.targetFeature !== feat.name) {
|
|
4476
|
+
const evidence = {
|
|
4477
|
+
fromFeature: feat.name,
|
|
4478
|
+
toFeature: target.targetFeature,
|
|
4479
|
+
file,
|
|
4480
|
+
relativePath: relPath,
|
|
4481
|
+
line: stmt.line,
|
|
4482
|
+
code: stmt.rawCode
|
|
4483
|
+
};
|
|
4484
|
+
const cycleResult = graph.addEdge(evidence);
|
|
4485
|
+
if (cycleResult.createsCycle) {
|
|
4486
|
+
const partner = cycleResult.directPartner;
|
|
4487
|
+
detectedCycles.push({
|
|
4488
|
+
featureA: feat.name,
|
|
4489
|
+
featureB: target.targetFeature,
|
|
4490
|
+
cycle: cycleResult.cycleChain,
|
|
4491
|
+
evidenceAtoB: evidence,
|
|
4492
|
+
evidenceBtoA: partner || evidence
|
|
4493
|
+
});
|
|
4494
|
+
addViolation({
|
|
4495
|
+
feature: feat.name,
|
|
4496
|
+
layer,
|
|
4497
|
+
file,
|
|
4498
|
+
relativePath: relPath,
|
|
4499
|
+
line: stmt.line,
|
|
4500
|
+
category: "Circular Feature Dependency Violation",
|
|
4501
|
+
code: stmt.rawCode,
|
|
4502
|
+
ruleId: "C1_CIRCULAR_FEATURE_DEPENDENCY",
|
|
4503
|
+
severity: "error",
|
|
4504
|
+
advice: `Circular feature dependency detected between '${feat.name}' and '${target.targetFeature}'. Dependency flow must be strictly one-way: for any two features, only one can know about the other exists. If '${feat.name}' needs to impose a side effect on '${target.targetFeature}' (or vice versa), it must do so via an event, never via a direct dependency.`,
|
|
4505
|
+
fixExample: {
|
|
4506
|
+
bad: stmt.rawCode,
|
|
4507
|
+
good: `// Emit a Domain/Application Event (e.g. publish '${feat.name}Changed') instead of depending on '${target.targetFeature}'.`
|
|
4508
|
+
}
|
|
4509
|
+
});
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
if (layer === "domain") {
|
|
4513
|
+
if (target.targetLayer && ["useCases", "services", "infrastructure", "presentation"].includes(target.targetLayer)) {
|
|
4514
|
+
addViolation({
|
|
4515
|
+
feature: feat.name,
|
|
4516
|
+
layer,
|
|
4517
|
+
file,
|
|
4518
|
+
relativePath: relPath,
|
|
4519
|
+
line: stmt.line,
|
|
4520
|
+
category: "Domain Purity Violation",
|
|
4521
|
+
code: stmt.rawCode,
|
|
4522
|
+
ruleId: "D1_OUTER_IMPORT",
|
|
4523
|
+
severity: "error",
|
|
4524
|
+
advice: `Domain layer must not import from outer layers (${target.targetLayer}). Keep the domain core pure.`,
|
|
4525
|
+
fixExample: {
|
|
4526
|
+
bad: stmt.rawCode,
|
|
4527
|
+
good: "// Define interfaces, types, or value objects directly within the domain layer."
|
|
4528
|
+
}
|
|
4529
|
+
});
|
|
4530
|
+
}
|
|
4531
|
+
if (target.isDiFramework) {
|
|
4532
|
+
addViolation({
|
|
4533
|
+
feature: feat.name,
|
|
4534
|
+
layer,
|
|
4535
|
+
file,
|
|
4536
|
+
relativePath: relPath,
|
|
4537
|
+
line: stmt.line,
|
|
4538
|
+
category: "Domain Dependence Violation",
|
|
4539
|
+
code: stmt.rawCode,
|
|
4540
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
4541
|
+
severity: "error",
|
|
4542
|
+
advice: "Domain layer must have zero DI dependence. Domain interfaces and value objects are plain classes/types.",
|
|
4543
|
+
fixExample: {
|
|
4544
|
+
bad: stmt.rawCode,
|
|
4545
|
+
good: "// Remove @solid-stack/di import. Domain entities are pure TypeScript."
|
|
4546
|
+
}
|
|
4547
|
+
});
|
|
4548
|
+
}
|
|
4549
|
+
if (target.targetSharedModule && !target.isSharedDomain) {
|
|
4550
|
+
addViolation({
|
|
4551
|
+
feature: feat.name,
|
|
4552
|
+
layer,
|
|
4553
|
+
file,
|
|
4554
|
+
relativePath: relPath,
|
|
4555
|
+
line: stmt.line,
|
|
4556
|
+
category: "Domain Purity Violation",
|
|
4557
|
+
code: stmt.rawCode,
|
|
4558
|
+
ruleId: "D3_SHARED_NON_DOMAIN_IMPORT",
|
|
4559
|
+
severity: "error",
|
|
4560
|
+
advice: "Domain layer can only import pure value objects from shared/<module>/domain/*. Ports, infrastructure, and root classes must not be imported.",
|
|
4561
|
+
fixExample: {
|
|
4562
|
+
bad: stmt.rawCode,
|
|
4563
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/domain/...";`
|
|
4564
|
+
}
|
|
4565
|
+
});
|
|
4566
|
+
}
|
|
4567
|
+
if (target.isExternal && !target.isDiFramework) {
|
|
4568
|
+
addViolation({
|
|
4569
|
+
feature: feat.name,
|
|
4570
|
+
layer,
|
|
4571
|
+
file,
|
|
4572
|
+
relativePath: relPath,
|
|
4573
|
+
line: stmt.line,
|
|
4574
|
+
category: "Domain Purity Violation",
|
|
4575
|
+
code: stmt.rawCode,
|
|
4576
|
+
ruleId: "D4_EXTERNAL_LIBRARY",
|
|
4577
|
+
severity: "error",
|
|
4578
|
+
advice: `Domain layer must be pure and must not depend on external runtime libraries ('${stmt.specifier}').`,
|
|
4579
|
+
fixExample: {
|
|
4580
|
+
bad: stmt.rawCode,
|
|
4581
|
+
good: "// Model pure business interfaces without external libraries."
|
|
4582
|
+
}
|
|
4583
|
+
});
|
|
4584
|
+
}
|
|
4585
|
+
} else if (layer === "useCases") {
|
|
4586
|
+
if (target.targetLayer === "useCases" && target.targetFeature === feat.name) {
|
|
4587
|
+
addViolation({
|
|
4588
|
+
feature: feat.name,
|
|
4589
|
+
layer,
|
|
4590
|
+
file,
|
|
4591
|
+
relativePath: relPath,
|
|
4592
|
+
line: stmt.line,
|
|
4593
|
+
category: "Use Case Intra-Feature Violation",
|
|
4594
|
+
code: stmt.rawCode,
|
|
4595
|
+
ruleId: "U1_INTRA_USE_CASE",
|
|
4596
|
+
severity: "error",
|
|
4597
|
+
advice: `Use cases cannot depend on other use cases of the same feature ('${feat.name}'). Extract shared business logic to a domain repository or domain service.`,
|
|
4598
|
+
fixExample: {
|
|
4599
|
+
bad: stmt.rawCode,
|
|
4600
|
+
good: "// Inject a domain interface or domain service instead of another use case."
|
|
4601
|
+
}
|
|
4602
|
+
});
|
|
4603
|
+
}
|
|
4604
|
+
if (target.targetLayer === "useCases" && target.targetFeature && target.targetFeature !== feat.name) {
|
|
4605
|
+
addViolation({
|
|
4606
|
+
feature: feat.name,
|
|
4607
|
+
layer,
|
|
4608
|
+
file,
|
|
4609
|
+
relativePath: relPath,
|
|
4610
|
+
line: stmt.line,
|
|
4611
|
+
category: "Use Case Cross-Feature Use Case Violation",
|
|
4612
|
+
code: stmt.rawCode,
|
|
4613
|
+
ruleId: "U2_CROSS_FEATURE_USE_CASE",
|
|
4614
|
+
severity: "error",
|
|
4615
|
+
advice: `Use cases cannot depend directly on use cases of foreign feature '${target.targetFeature}'. Treat features as separate microservices: define an interface in '${feat.name}/domain/' specifying what is needed, and implement it in '${feat.name}/infrastructure/'.`,
|
|
4616
|
+
fixExample: {
|
|
4617
|
+
bad: stmt.rawCode,
|
|
4618
|
+
good: `// Define an interface in ${feat.name}/domain/ and implement it in ${feat.name}/infrastructure/ using '${target.targetFileName}'.`
|
|
4619
|
+
}
|
|
4620
|
+
});
|
|
4621
|
+
}
|
|
4622
|
+
if (target.targetLayer === "infrastructure") {
|
|
4623
|
+
addViolation({
|
|
4624
|
+
feature: feat.name,
|
|
4625
|
+
layer,
|
|
4626
|
+
file,
|
|
4627
|
+
relativePath: relPath,
|
|
4628
|
+
line: stmt.line,
|
|
4629
|
+
category: "Use Case Layer Violation",
|
|
4630
|
+
code: stmt.rawCode,
|
|
4631
|
+
ruleId: "U3_DIRECT_INFRASTRUCTURE",
|
|
4632
|
+
severity: "error",
|
|
4633
|
+
advice: "Use cases cannot import concrete infrastructure directly. Depend on Domain Ports of the same feature via DI.",
|
|
4634
|
+
fixExample: {
|
|
4635
|
+
bad: stmt.rawCode,
|
|
4636
|
+
good: "// Depend on the domain interface port declared in static deps."
|
|
4637
|
+
}
|
|
4638
|
+
});
|
|
4639
|
+
}
|
|
4640
|
+
if (target.targetLayer === "domain" && target.targetFeature && target.targetFeature !== feat.name && target.isTargetPort) {
|
|
4641
|
+
addViolation({
|
|
4642
|
+
feature: feat.name,
|
|
4643
|
+
layer,
|
|
4644
|
+
file,
|
|
4645
|
+
relativePath: relPath,
|
|
4646
|
+
line: stmt.line,
|
|
4647
|
+
category: "Use Case Cross-Feature Port Violation",
|
|
4648
|
+
code: stmt.rawCode,
|
|
4649
|
+
ruleId: "U4_CROSS_FEATURE_PORT",
|
|
4650
|
+
severity: "error",
|
|
4651
|
+
advice: `Use cases cannot depend on domain interfaces/ports of foreign feature '${target.targetFeature}'. Define an interface in '${feat.name}/domain/' (consumer-driven port) and implement it in '${feat.name}/infrastructure/'.`,
|
|
4652
|
+
fixExample: {
|
|
4653
|
+
bad: stmt.rawCode,
|
|
4654
|
+
good: `// Define a local interface in ${feat.name}/domain/ and implement it in ${feat.name}/infrastructure/.`
|
|
4655
|
+
}
|
|
4656
|
+
});
|
|
4657
|
+
}
|
|
4658
|
+
if (target.targetLayer === "services" && target.targetFeature && target.targetFeature !== feat.name) {
|
|
4659
|
+
addViolation({
|
|
4660
|
+
feature: feat.name,
|
|
4661
|
+
layer,
|
|
4662
|
+
file,
|
|
4663
|
+
relativePath: relPath,
|
|
4664
|
+
line: stmt.line,
|
|
4665
|
+
category: "Use Case Layer Violation",
|
|
4666
|
+
code: stmt.rawCode,
|
|
4667
|
+
ruleId: "U5_CROSS_FEATURE_SERVICE",
|
|
4668
|
+
severity: "error",
|
|
4669
|
+
advice: `Use cases cannot depend on services of foreign feature '${target.targetFeature}'.`,
|
|
4670
|
+
fixExample: {
|
|
4671
|
+
bad: stmt.rawCode,
|
|
4672
|
+
good: `// Define an interface in ${feat.name}/domain/ and implement it in ${feat.name}/infrastructure/.`
|
|
4673
|
+
}
|
|
4674
|
+
});
|
|
4675
|
+
}
|
|
4676
|
+
if (target.targetSharedModule && (target.isSharedPorts || target.isSharedInfra)) {
|
|
4677
|
+
addViolation({
|
|
4678
|
+
feature: feat.name,
|
|
4679
|
+
layer,
|
|
4680
|
+
file,
|
|
4681
|
+
relativePath: relPath,
|
|
4682
|
+
line: stmt.line,
|
|
4683
|
+
category: "Shared Module Root Class Violation",
|
|
4684
|
+
code: stmt.rawCode,
|
|
4685
|
+
ruleId: "U6_SHARED_NON_ROOT",
|
|
4686
|
+
severity: "error",
|
|
4687
|
+
advice: `Use cases can only depend on root classes of shared modules. Never import from shared ports or infrastructure directly.`,
|
|
4688
|
+
fixExample: {
|
|
4689
|
+
bad: stmt.rawCode,
|
|
4690
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
4691
|
+
}
|
|
4692
|
+
});
|
|
4693
|
+
}
|
|
4694
|
+
} else if (layer === "services") {
|
|
4695
|
+
if (target.targetLayer === "useCases") {
|
|
4696
|
+
addViolation({
|
|
4697
|
+
feature: feat.name,
|
|
4698
|
+
layer,
|
|
4699
|
+
file,
|
|
4700
|
+
relativePath: relPath,
|
|
4701
|
+
line: stmt.line,
|
|
4702
|
+
category: "Service Intra-Feature Violation",
|
|
4703
|
+
code: stmt.rawCode,
|
|
4704
|
+
ruleId: "S1_USE_CASE_DEPENDENCY",
|
|
4705
|
+
severity: "error",
|
|
4706
|
+
advice: "Services cannot depend on use cases of any feature.",
|
|
4707
|
+
fixExample: {
|
|
4708
|
+
bad: stmt.rawCode,
|
|
4709
|
+
good: "// Inject domain repository ports directly instead of use cases."
|
|
4710
|
+
}
|
|
4711
|
+
});
|
|
4712
|
+
}
|
|
4713
|
+
if (target.targetLayer === "domain" && target.targetFeature && target.targetFeature !== feat.name && target.isTargetPort) {
|
|
4714
|
+
addViolation({
|
|
4715
|
+
feature: feat.name,
|
|
4716
|
+
layer,
|
|
4717
|
+
file,
|
|
4718
|
+
relativePath: relPath,
|
|
4719
|
+
line: stmt.line,
|
|
4720
|
+
category: "Service Cross-Feature Port Violation",
|
|
4721
|
+
code: stmt.rawCode,
|
|
4722
|
+
ruleId: "S2_CROSS_FEATURE_PORT",
|
|
4723
|
+
severity: "error",
|
|
4724
|
+
advice: `Services cannot depend on domain ports of foreign feature '${target.targetFeature}'.`,
|
|
4725
|
+
fixExample: {
|
|
4726
|
+
bad: stmt.rawCode,
|
|
4727
|
+
good: `// Define an interface in ${feat.name}/domain/ and implement it in ${feat.name}/infrastructure/.`
|
|
4728
|
+
}
|
|
4729
|
+
});
|
|
4730
|
+
}
|
|
4731
|
+
if (target.targetLayer === "infrastructure") {
|
|
4732
|
+
addViolation({
|
|
4733
|
+
feature: feat.name,
|
|
4734
|
+
layer,
|
|
4735
|
+
file,
|
|
4736
|
+
relativePath: relPath,
|
|
4737
|
+
line: stmt.line,
|
|
4738
|
+
category: "Service Layer Violation",
|
|
4739
|
+
code: stmt.rawCode,
|
|
4740
|
+
ruleId: "S3_DIRECT_INFRASTRUCTURE",
|
|
4741
|
+
severity: "error",
|
|
4742
|
+
advice: "Services cannot import concrete infrastructure directly. Depend on Domain Ports via DI.",
|
|
4743
|
+
fixExample: {
|
|
4744
|
+
bad: stmt.rawCode,
|
|
4745
|
+
good: "// Depend on the domain interface port declared in static deps."
|
|
4746
|
+
}
|
|
4747
|
+
});
|
|
4748
|
+
}
|
|
4749
|
+
if (target.targetSharedModule && (target.isSharedPorts || target.isSharedInfra)) {
|
|
4750
|
+
addViolation({
|
|
4751
|
+
feature: feat.name,
|
|
4752
|
+
layer,
|
|
4753
|
+
file,
|
|
4754
|
+
relativePath: relPath,
|
|
4755
|
+
line: stmt.line,
|
|
4756
|
+
category: "Shared Module Root Class Violation",
|
|
4757
|
+
code: stmt.rawCode,
|
|
4758
|
+
ruleId: "S4_SHARED_NON_ROOT",
|
|
4759
|
+
severity: "error",
|
|
4760
|
+
advice: "Services can only depend on root classes of shared modules. Never import from shared ports or infrastructure directly.",
|
|
4761
|
+
fixExample: {
|
|
4762
|
+
bad: stmt.rawCode,
|
|
4763
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
4764
|
+
}
|
|
4765
|
+
});
|
|
4766
|
+
}
|
|
4767
|
+
} else if (layer === "infrastructure") {
|
|
4768
|
+
if (target.targetLayer === "useCases" && target.targetFeature === feat.name) {
|
|
4769
|
+
addViolation({
|
|
4770
|
+
feature: feat.name,
|
|
4771
|
+
layer,
|
|
4772
|
+
file,
|
|
4773
|
+
relativePath: relPath,
|
|
4774
|
+
line: stmt.line,
|
|
4775
|
+
category: "Adapter Layer Violation",
|
|
4776
|
+
code: stmt.rawCode,
|
|
4777
|
+
ruleId: "I1_INTRA_USE_CASE",
|
|
4778
|
+
severity: "error",
|
|
4779
|
+
advice: `Infrastructure adapters cannot depend on use cases of the same feature ('${feat.name}'). Adapters implement domain ports.`,
|
|
4780
|
+
fixExample: {
|
|
4781
|
+
bad: stmt.rawCode,
|
|
4782
|
+
good: "// Adapters implement domain ports and should not invoke local application use cases."
|
|
4783
|
+
}
|
|
4784
|
+
});
|
|
4785
|
+
}
|
|
4786
|
+
if (target.targetSharedModule && (target.isSharedPorts || target.isSharedInfra)) {
|
|
4787
|
+
addViolation({
|
|
4788
|
+
feature: feat.name,
|
|
4789
|
+
layer,
|
|
4790
|
+
file,
|
|
4791
|
+
relativePath: relPath,
|
|
4792
|
+
line: stmt.line,
|
|
4793
|
+
category: "Shared Module Root Class Violation",
|
|
4794
|
+
code: stmt.rawCode,
|
|
4795
|
+
ruleId: "I2_SHARED_NON_ROOT",
|
|
4796
|
+
severity: "error",
|
|
4797
|
+
advice: "Infrastructure adapters can only depend on root classes of shared modules. Never import from shared ports or infrastructure directly.",
|
|
4798
|
+
fixExample: {
|
|
4799
|
+
bad: stmt.rawCode,
|
|
4800
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
4801
|
+
}
|
|
4802
|
+
});
|
|
4803
|
+
}
|
|
4804
|
+
} else if (layer === "presentation") {
|
|
4805
|
+
if (target.targetLayer === "infrastructure") {
|
|
4806
|
+
addViolation({
|
|
4807
|
+
feature: feat.name,
|
|
4808
|
+
layer,
|
|
4809
|
+
file,
|
|
4810
|
+
relativePath: relPath,
|
|
4811
|
+
line: stmt.line,
|
|
4812
|
+
category: "Presentation Layer Violation",
|
|
4813
|
+
code: stmt.rawCode,
|
|
4814
|
+
ruleId: "P1_DIRECT_INFRASTRUCTURE",
|
|
4815
|
+
severity: "error",
|
|
4816
|
+
advice: "Presentation handlers must not import infrastructure directly. Inject Use Cases via DI.",
|
|
4817
|
+
fixExample: {
|
|
4818
|
+
bad: stmt.rawCode,
|
|
4819
|
+
good: "// Inject the corresponding Use Case via static deps and invoke its execute() method."
|
|
4820
|
+
}
|
|
4821
|
+
});
|
|
4822
|
+
}
|
|
4823
|
+
if (target.targetLayer === "domain" && (target.isTargetRepo || target.isTargetPort)) {
|
|
4824
|
+
addViolation({
|
|
4825
|
+
feature: feat.name,
|
|
4826
|
+
layer,
|
|
4827
|
+
file,
|
|
4828
|
+
relativePath: relPath,
|
|
4829
|
+
line: stmt.line,
|
|
4830
|
+
category: "Presentation Layer Violation",
|
|
4831
|
+
code: stmt.rawCode,
|
|
4832
|
+
ruleId: "P2_DIRECT_REPOSITORY",
|
|
4833
|
+
severity: "error",
|
|
4834
|
+
advice: "Presentation handlers must not depend on domain repositories or ports directly. Inject Use Cases via DI.",
|
|
4835
|
+
fixExample: {
|
|
4836
|
+
bad: stmt.rawCode,
|
|
4837
|
+
good: "// Presentation layer must only orchestrate Use Cases."
|
|
4838
|
+
}
|
|
4839
|
+
});
|
|
4840
|
+
}
|
|
4841
|
+
if (target.targetSharedModule && (target.isSharedPorts || target.isSharedInfra)) {
|
|
4842
|
+
addViolation({
|
|
4843
|
+
feature: feat.name,
|
|
4844
|
+
layer,
|
|
4845
|
+
file,
|
|
4846
|
+
relativePath: relPath,
|
|
4847
|
+
line: stmt.line,
|
|
4848
|
+
category: "Shared Module Root Class Violation",
|
|
4849
|
+
code: stmt.rawCode,
|
|
4850
|
+
ruleId: "P3_SHARED_NON_ROOT",
|
|
4851
|
+
severity: "error",
|
|
4852
|
+
advice: "Presentation handlers can only depend on root classes of shared modules. Never import from shared ports or infrastructure directly.",
|
|
4853
|
+
fixExample: {
|
|
4854
|
+
bad: stmt.rawCode,
|
|
4855
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
4856
|
+
}
|
|
4857
|
+
});
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
}
|
|
4861
|
+
}
|
|
4862
|
+
}
|
|
4863
|
+
const directCycles = graph.findDirectCycles();
|
|
4864
|
+
for (const c of directCycles) {
|
|
4865
|
+
const existing = detectedCycles.find(
|
|
4866
|
+
(dc) => dc.featureA === c.featureA && dc.featureB === c.featureB || dc.featureA === c.featureB && dc.featureB === c.featureA
|
|
4867
|
+
);
|
|
4868
|
+
if (!existing) {
|
|
4869
|
+
detectedCycles.push(c);
|
|
4870
|
+
addViolation({
|
|
4871
|
+
feature: c.featureA,
|
|
4872
|
+
file: c.evidenceAtoB.file,
|
|
4873
|
+
relativePath: c.evidenceAtoB.relativePath,
|
|
4874
|
+
line: c.evidenceAtoB.line,
|
|
4875
|
+
category: "Circular Feature Dependency Violation",
|
|
4876
|
+
code: c.evidenceAtoB.code,
|
|
4877
|
+
ruleId: "C1_CIRCULAR_FEATURE_DEPENDENCY",
|
|
4878
|
+
severity: "error",
|
|
4879
|
+
advice: `Circular feature dependency detected between '${c.featureA}' and '${c.featureB}'. Dependency flow must be strictly one-way: for any two features, only one can know about the other exists. If the unaware feature needs to impose a side effect on the other, it must do so via an event, never via a direct dependency.`
|
|
4880
|
+
});
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4883
|
+
const durationMs = Date.now() - startTime;
|
|
4884
|
+
const cleanFeatures = results.filter((r) => r.passed).length;
|
|
4885
|
+
const violatedFeatures = results.filter((r) => !r.passed).length;
|
|
4886
|
+
const summary = {
|
|
4887
|
+
totalFeatures: results.length,
|
|
4888
|
+
cleanFeatures,
|
|
4889
|
+
violatedFeatures,
|
|
4890
|
+
totalViolations: allViolations.length,
|
|
4891
|
+
durationMs,
|
|
4892
|
+
results,
|
|
4893
|
+
allViolations,
|
|
4894
|
+
circularDependencies: detectedCycles
|
|
4895
|
+
};
|
|
4896
|
+
if (allViolations.length > 0 && options.throwOnError !== false) {
|
|
4897
|
+
const errorHeader = `Dependency rule audit failed with ${allViolations.length} violation(s) across ${violatedFeatures} feature(s):`;
|
|
4898
|
+
const compiledDetails = allViolations.map(
|
|
4899
|
+
(v, i) => `${i + 1}. [${v.category}] ${v.relativePath}:${v.line}
|
|
4900
|
+
Rule: ${v.ruleId}
|
|
4901
|
+
Code: ${v.code}
|
|
4902
|
+
Advice: ${v.advice}`
|
|
4903
|
+
).join("\n\n");
|
|
4904
|
+
throw new DependencyRuleError(`${errorHeader}
|
|
4905
|
+
|
|
4906
|
+
${compiledDetails}
|
|
4907
|
+
`, allViolations, false);
|
|
4908
|
+
}
|
|
4909
|
+
return summary;
|
|
4910
|
+
}
|
|
4911
|
+
|
|
4912
|
+
// src/commands/lint.ts
|
|
4913
|
+
function formatViolation(v) {
|
|
4914
|
+
const lines = [];
|
|
4915
|
+
lines.push(pc14.underline(pc14.cyan(v.relativePath)) + pc14.dim(":") + pc14.yellow(String(v.line)));
|
|
4916
|
+
const gutter = `${String(v.line).padStart(4, " ")} | `;
|
|
4917
|
+
lines.push(pc14.dim(gutter) + v.code);
|
|
4918
|
+
const categoryShort = v.category.replace(" Violation", "");
|
|
4919
|
+
lines.push(
|
|
4920
|
+
" " + pc14.red(pc14.bold("\u2716")) + " " + pc14.red(`[${categoryShort}]`) + " " + v.advice
|
|
4921
|
+
);
|
|
4922
|
+
if (v.fixExample?.good) {
|
|
4923
|
+
lines.push(" " + pc14.dim("\u2192") + " " + pc14.dim(v.fixExample.good));
|
|
4924
|
+
}
|
|
4925
|
+
return lines.join("\n");
|
|
4926
|
+
}
|
|
4927
|
+
async function handleLint(flags = {}) {
|
|
4928
|
+
const complete = Boolean(flags.complete);
|
|
4929
|
+
const quiet = Boolean(flags.quiet);
|
|
4930
|
+
const options = {
|
|
4931
|
+
complete,
|
|
4932
|
+
quiet,
|
|
4933
|
+
targetDir: flags.targetDir || flags.target,
|
|
4934
|
+
projectRoot: flags.projectRoot,
|
|
4935
|
+
fixHint: flags.fixHint !== false,
|
|
4936
|
+
throwOnError: false
|
|
4937
|
+
};
|
|
4938
|
+
try {
|
|
4939
|
+
const summary = lintCodebase(options);
|
|
4940
|
+
if (summary.totalViolations > 0) {
|
|
4941
|
+
if (!quiet) {
|
|
4942
|
+
console.log("");
|
|
4943
|
+
for (const v of summary.allViolations) {
|
|
4944
|
+
console.log(formatViolation(v));
|
|
4945
|
+
console.log("");
|
|
4946
|
+
}
|
|
4947
|
+
console.log(
|
|
4948
|
+
pc14.red(
|
|
4949
|
+
`\u2716 Dependency rules check failed with ${summary.totalViolations} error${summary.totalViolations > 1 ? "s" : ""} across ${summary.violatedFeatures} feature${summary.violatedFeatures > 1 ? "s" : ""} (${summary.totalFeatures} features scanned).`
|
|
4950
|
+
)
|
|
4951
|
+
);
|
|
4952
|
+
console.log("");
|
|
4953
|
+
}
|
|
4954
|
+
const errorHeader = `Dependency rule audit failed with ${summary.totalViolations} violation(s) across ${summary.violatedFeatures} feature(s):`;
|
|
4955
|
+
const compiledDetails = summary.allViolations.map(
|
|
4956
|
+
(v, i) => `${i + 1}. [${v.category}] ${v.relativePath}:${v.line}
|
|
4957
|
+
Code: ${v.code}
|
|
4958
|
+
Advice: ${v.advice}`
|
|
4959
|
+
).join("\n\n");
|
|
4960
|
+
throw new DependencyRuleError(`${errorHeader}
|
|
4961
|
+
|
|
4962
|
+
${compiledDetails}
|
|
4963
|
+
`, summary.allViolations, false);
|
|
4964
|
+
}
|
|
4965
|
+
if (!quiet) {
|
|
4966
|
+
console.log(
|
|
4967
|
+
pc14.green(
|
|
4968
|
+
`\u2714 Dependency rules verified across ${summary.totalFeatures} feature${summary.totalFeatures === 1 ? "" : "s"}.`
|
|
4969
|
+
)
|
|
4970
|
+
);
|
|
4971
|
+
}
|
|
4972
|
+
return summary;
|
|
4973
|
+
} catch (err) {
|
|
4974
|
+
if (err instanceof DependencyRuleError && err.isFailFast) {
|
|
4975
|
+
if (!quiet) {
|
|
4976
|
+
console.log("");
|
|
4977
|
+
const v = err.violations[0];
|
|
4978
|
+
if (v) {
|
|
4979
|
+
console.log(formatViolation(v));
|
|
4980
|
+
console.log("");
|
|
4981
|
+
}
|
|
4982
|
+
console.log(pc14.red(`\u2716 Dependency rules check failed (1 error).`));
|
|
4983
|
+
console.log("");
|
|
4984
|
+
}
|
|
4985
|
+
}
|
|
4986
|
+
throw err;
|
|
4987
|
+
}
|
|
4988
|
+
}
|
|
4163
4989
|
|
|
4164
4990
|
// src/cli.ts
|
|
4165
4991
|
function createCli() {
|
|
@@ -4233,14 +5059,21 @@ function createCli() {
|
|
|
4233
5059
|
value: "import",
|
|
4234
5060
|
label: "Import from registry",
|
|
4235
5061
|
hint: "Download and link pre-built shared modules or features from the registry"
|
|
5062
|
+
},
|
|
5063
|
+
{
|
|
5064
|
+
value: "lint",
|
|
5065
|
+
label: "Lint dependency rules",
|
|
5066
|
+
hint: "Scan codebase and verify Clean Architecture dependency rules"
|
|
4236
5067
|
}
|
|
4237
5068
|
]
|
|
4238
5069
|
})
|
|
4239
5070
|
);
|
|
4240
5071
|
if (choice === "create") {
|
|
4241
5072
|
await handleCreate(void 0, void 0, flags);
|
|
4242
|
-
} else {
|
|
5073
|
+
} else if (choice === "import") {
|
|
4243
5074
|
await handleImport(void 0, void 0, flags);
|
|
5075
|
+
} else if (choice === "lint") {
|
|
5076
|
+
await handleLint(flags);
|
|
4244
5077
|
}
|
|
4245
5078
|
} catch (err) {
|
|
4246
5079
|
handleCliError(err);
|
|
@@ -4266,6 +5099,28 @@ function createCli() {
|
|
|
4266
5099
|
handleCliError(err);
|
|
4267
5100
|
}
|
|
4268
5101
|
});
|
|
5102
|
+
cli.command(
|
|
5103
|
+
"lint [target]",
|
|
5104
|
+
"Scan codebase and verify Clean Architecture dependency rules"
|
|
5105
|
+
).option(
|
|
5106
|
+
"-c, --complete",
|
|
5107
|
+
"Scan entire codebase without failing fast, compiling all errors at the end"
|
|
5108
|
+
).option("-q, --quiet", "Suppress non-error output").example("mason lint").example("mason lint --complete").example("mason lint ./src").action(async (target, flags = {}) => {
|
|
5109
|
+
try {
|
|
5110
|
+
await handleLint({
|
|
5111
|
+
...flags,
|
|
5112
|
+
targetDir: target || flags.target || flags.dest
|
|
5113
|
+
});
|
|
5114
|
+
} catch (err) {
|
|
5115
|
+
if (err instanceof DependencyRuleError) {
|
|
5116
|
+
if (process.env.VITEST) {
|
|
5117
|
+
throw err;
|
|
5118
|
+
}
|
|
5119
|
+
process.exit(1);
|
|
5120
|
+
}
|
|
5121
|
+
handleCliError(err);
|
|
5122
|
+
}
|
|
5123
|
+
});
|
|
4269
5124
|
const createSubtypes = [
|
|
4270
5125
|
"feature",
|
|
4271
5126
|
"shared",
|
|
@@ -4298,6 +5153,9 @@ function handleCliError(err) {
|
|
|
4298
5153
|
}
|
|
4299
5154
|
const message = err instanceof Error ? err.message : String(err);
|
|
4300
5155
|
p14.log.error(pc14.red(`Error: ${message}`));
|
|
5156
|
+
if (process.env.VITEST) {
|
|
5157
|
+
throw err;
|
|
5158
|
+
}
|
|
4301
5159
|
process.exit(1);
|
|
4302
5160
|
}
|
|
4303
5161
|
async function runCli(argv = process.argv) {
|