technical-debt-radar 1.8.0 → 1.10.0
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/index.js +216 -8
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -231,11 +231,11 @@ var require_plans = __commonJS({
|
|
|
231
231
|
[PlanId.FREE]: {
|
|
232
232
|
id: PlanId.FREE,
|
|
233
233
|
name: "Free",
|
|
234
|
-
description: "
|
|
234
|
+
description: "5 scans/month, warn mode",
|
|
235
235
|
priceMonthly: 0,
|
|
236
236
|
priceAnnual: 0,
|
|
237
237
|
priceMonthlyIfAnnual: 0,
|
|
238
|
-
limits: { maxRepos: 2, maxMembers: 1, aiCreditsPerMonth: 0, maxOrgs: 1, scansPerMonth:
|
|
238
|
+
limits: { maxRepos: 2, maxMembers: 1, aiCreditsPerMonth: 0, maxOrgs: 1, scansPerMonth: 5 },
|
|
239
239
|
features: {
|
|
240
240
|
...ALL_CLI,
|
|
241
241
|
cliFixAI: false,
|
|
@@ -270,7 +270,7 @@ var require_plans = __commonJS({
|
|
|
270
270
|
priceMonthly: 15,
|
|
271
271
|
priceAnnual: 144,
|
|
272
272
|
priceMonthlyIfAnnual: 12,
|
|
273
|
-
limits: { maxRepos: 5, maxMembers: 1, aiCreditsPerMonth: 50, maxOrgs: 1, scansPerMonth:
|
|
273
|
+
limits: { maxRepos: 5, maxMembers: 1, aiCreditsPerMonth: 50, maxOrgs: 1, scansPerMonth: 200 },
|
|
274
274
|
features: {
|
|
275
275
|
...ALL_CLI,
|
|
276
276
|
cliFixAI: true,
|
|
@@ -305,7 +305,7 @@ var require_plans = __commonJS({
|
|
|
305
305
|
priceMonthly: 49,
|
|
306
306
|
priceAnnual: 468,
|
|
307
307
|
priceMonthlyIfAnnual: 39,
|
|
308
|
-
limits: { maxRepos: -1, maxMembers: -1, aiCreditsPerMonth: 500, maxOrgs: 3, scansPerMonth:
|
|
308
|
+
limits: { maxRepos: -1, maxMembers: -1, aiCreditsPerMonth: 500, maxOrgs: 3, scansPerMonth: 1e3 },
|
|
309
309
|
features: {
|
|
310
310
|
...ALL_CLI,
|
|
311
311
|
cliFixAI: true,
|
|
@@ -340,7 +340,7 @@ var require_plans = __commonJS({
|
|
|
340
340
|
priceMonthly: 99,
|
|
341
341
|
priceAnnual: 948,
|
|
342
342
|
priceMonthlyIfAnnual: 79,
|
|
343
|
-
limits: { maxRepos: -1, maxMembers: -1, aiCreditsPerMonth: 1800, maxOrgs: 10, scansPerMonth:
|
|
343
|
+
limits: { maxRepos: -1, maxMembers: -1, aiCreditsPerMonth: 1800, maxOrgs: 10, scansPerMonth: 5e3 },
|
|
344
344
|
features: {
|
|
345
345
|
...ALL_FEATURES_TRUE,
|
|
346
346
|
sso: false,
|
|
@@ -399,6 +399,145 @@ var require_plans = __commonJS({
|
|
|
399
399
|
}
|
|
400
400
|
});
|
|
401
401
|
|
|
402
|
+
// ../../packages/shared/dist/graph-builder.js
|
|
403
|
+
var require_graph_builder = __commonJS({
|
|
404
|
+
"../../packages/shared/dist/graph-builder.js"(exports2) {
|
|
405
|
+
"use strict";
|
|
406
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
407
|
+
exports2.buildImportGraphData = buildImportGraphData2;
|
|
408
|
+
var LAYER_COLORS = {
|
|
409
|
+
api: "#4CAF50",
|
|
410
|
+
application: "#2196F3",
|
|
411
|
+
domain: "#FF9800",
|
|
412
|
+
infrastructure: "#F44336",
|
|
413
|
+
shared: "#9C27B0",
|
|
414
|
+
common: "#9C27B0",
|
|
415
|
+
presentation: "#4CAF50",
|
|
416
|
+
business: "#2196F3",
|
|
417
|
+
"data-access": "#F44336",
|
|
418
|
+
models: "#FF9800",
|
|
419
|
+
controllers: "#4CAF50",
|
|
420
|
+
views: "#9E9E9E",
|
|
421
|
+
ports: "#2196F3",
|
|
422
|
+
adapters: "#F44336",
|
|
423
|
+
handlers: "#4CAF50",
|
|
424
|
+
commands: "#2196F3",
|
|
425
|
+
queries: "#FF9800",
|
|
426
|
+
events: "#9C27B0",
|
|
427
|
+
config: "#607D8B"
|
|
428
|
+
};
|
|
429
|
+
function buildImportGraphData2(edges, violations, files, modules) {
|
|
430
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
431
|
+
const violationsByFile = /* @__PURE__ */ new Map();
|
|
432
|
+
for (const v of violations) {
|
|
433
|
+
violationsByFile.set(v.file, (violationsByFile.get(v.file) ?? 0) + 1);
|
|
434
|
+
}
|
|
435
|
+
const fileLoc = /* @__PURE__ */ new Map();
|
|
436
|
+
for (const f of files) {
|
|
437
|
+
if (f.linesOfCode) {
|
|
438
|
+
fileLoc.set(f.path, f.linesOfCode);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const ensureNode = (filePath, layer, module3) => {
|
|
442
|
+
if (nodeMap.has(filePath))
|
|
443
|
+
return;
|
|
444
|
+
const parts = filePath.split("/");
|
|
445
|
+
const fileName = parts[parts.length - 1] ?? filePath;
|
|
446
|
+
const label = fileName.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
447
|
+
const ext = fileName.match(/\.(service|controller|module|entity|repository|guard|pipe|middleware|dto|resolver|gateway)/);
|
|
448
|
+
nodeMap.set(filePath, {
|
|
449
|
+
id: filePath,
|
|
450
|
+
label,
|
|
451
|
+
module: module3 ?? void 0,
|
|
452
|
+
layer: layer ?? void 0,
|
|
453
|
+
type: ext ? ext[1] : "file",
|
|
454
|
+
violations: violationsByFile.get(filePath) ?? 0,
|
|
455
|
+
complexity: 0,
|
|
456
|
+
linesOfCode: fileLoc.get(filePath) ?? 0,
|
|
457
|
+
isHotspot: (violationsByFile.get(filePath) ?? 0) >= 3
|
|
458
|
+
});
|
|
459
|
+
};
|
|
460
|
+
for (const edge of edges) {
|
|
461
|
+
ensureNode(edge.source, edge.sourceLayer, edge.sourceModule);
|
|
462
|
+
ensureNode(edge.target, edge.targetLayer, edge.targetModule);
|
|
463
|
+
}
|
|
464
|
+
const circularFiles = /* @__PURE__ */ new Set();
|
|
465
|
+
for (const v of violations) {
|
|
466
|
+
if (v.type === "circular_dependency") {
|
|
467
|
+
circularFiles.add(v.file);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const graphEdges = edges.map((e) => {
|
|
471
|
+
const isCircular = circularFiles.has(e.source) && circularFiles.has(e.target);
|
|
472
|
+
const matchingViolation = violations.find((v) => v.category === "architecture" && v.file === e.source && v.message.includes(e.target.split("/").pop() ?? ""));
|
|
473
|
+
const isViolation = !!matchingViolation || isCircular;
|
|
474
|
+
let type = "import";
|
|
475
|
+
let violationType = null;
|
|
476
|
+
let message = null;
|
|
477
|
+
if (isCircular) {
|
|
478
|
+
type = "circular";
|
|
479
|
+
violationType = "circular-dependency";
|
|
480
|
+
message = `Circular dependency involving ${e.source} and ${e.target}`;
|
|
481
|
+
} else if (matchingViolation) {
|
|
482
|
+
type = "violation";
|
|
483
|
+
violationType = matchingViolation.type;
|
|
484
|
+
message = matchingViolation.message;
|
|
485
|
+
}
|
|
486
|
+
return { source: e.source, target: e.target, type, isViolation, violationType, message };
|
|
487
|
+
});
|
|
488
|
+
const moduleMap = /* @__PURE__ */ new Map();
|
|
489
|
+
for (const node of nodeMap.values()) {
|
|
490
|
+
if (node.module) {
|
|
491
|
+
const existing = moduleMap.get(node.module);
|
|
492
|
+
if (existing) {
|
|
493
|
+
existing.nodeCount++;
|
|
494
|
+
existing.violations += node.violations;
|
|
495
|
+
} else {
|
|
496
|
+
const policyMod = modules?.find((m) => m.name === node.module);
|
|
497
|
+
moduleMap.set(node.module, {
|
|
498
|
+
nodeCount: 1,
|
|
499
|
+
violations: node.violations,
|
|
500
|
+
path: policyMod?.pathPattern ?? ""
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const graphModules = Array.from(moduleMap.entries()).map(([name, data]) => ({
|
|
506
|
+
name,
|
|
507
|
+
path: data.path,
|
|
508
|
+
nodeCount: data.nodeCount,
|
|
509
|
+
violations: data.violations
|
|
510
|
+
}));
|
|
511
|
+
const layerSet = /* @__PURE__ */ new Set();
|
|
512
|
+
for (const node of nodeMap.values()) {
|
|
513
|
+
if (node.layer)
|
|
514
|
+
layerSet.add(node.layer);
|
|
515
|
+
}
|
|
516
|
+
const layers = Array.from(layerSet).map((name) => ({
|
|
517
|
+
name,
|
|
518
|
+
color: LAYER_COLORS[name] ?? "#9E9E9E"
|
|
519
|
+
}));
|
|
520
|
+
const nodes = Array.from(nodeMap.values());
|
|
521
|
+
const violationEdgeCount = graphEdges.filter((e) => e.isViolation && e.type !== "circular").length;
|
|
522
|
+
const circularEdgeCount = graphEdges.filter((e) => e.type === "circular").length;
|
|
523
|
+
return {
|
|
524
|
+
nodes,
|
|
525
|
+
edges: graphEdges,
|
|
526
|
+
modules: graphModules,
|
|
527
|
+
layers,
|
|
528
|
+
stats: {
|
|
529
|
+
totalNodes: nodes.length,
|
|
530
|
+
totalEdges: graphEdges.length,
|
|
531
|
+
violationEdges: violationEdgeCount,
|
|
532
|
+
circularEdges: circularEdgeCount,
|
|
533
|
+
modules: graphModules.length,
|
|
534
|
+
layers: layers.length
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
402
541
|
// ../../packages/shared/dist/index.js
|
|
403
542
|
var require_dist = __commonJS({
|
|
404
543
|
"../../packages/shared/dist/index.js"(exports2) {
|
|
@@ -420,9 +559,14 @@ var require_dist = __commonJS({
|
|
|
420
559
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
|
|
421
560
|
};
|
|
422
561
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
562
|
+
exports2.buildImportGraphData = void 0;
|
|
423
563
|
__exportStar(require_types(), exports2);
|
|
424
564
|
__exportStar(require_constants(), exports2);
|
|
425
565
|
__exportStar(require_plans(), exports2);
|
|
566
|
+
var graph_builder_1 = require_graph_builder();
|
|
567
|
+
Object.defineProperty(exports2, "buildImportGraphData", { enumerable: true, get: function() {
|
|
568
|
+
return graph_builder_1.buildImportGraphData;
|
|
569
|
+
} });
|
|
426
570
|
}
|
|
427
571
|
});
|
|
428
572
|
|
|
@@ -13176,6 +13320,9 @@ var require_boundary_checker = __commonJS({
|
|
|
13176
13320
|
const filePath = file.path;
|
|
13177
13321
|
const sourceModule = findModuleForFile(filePath, policy);
|
|
13178
13322
|
if (!sourceModule) {
|
|
13323
|
+
if (isNestJSFramework(policy)) {
|
|
13324
|
+
checkSharedFileImports(sourceFile, filePath, policy, input, violations);
|
|
13325
|
+
}
|
|
13179
13326
|
project.removeSourceFile(sourceFile);
|
|
13180
13327
|
continue;
|
|
13181
13328
|
}
|
|
@@ -13463,6 +13610,9 @@ var require_boundary_checker = __commonJS({
|
|
|
13463
13610
|
return "src/" + specifier.slice(2);
|
|
13464
13611
|
if (specifier.startsWith("~/"))
|
|
13465
13612
|
return "src/" + specifier.slice(2);
|
|
13613
|
+
if (specifier.startsWith("src/") || specifier.startsWith("lib/") || specifier.startsWith("app/")) {
|
|
13614
|
+
return specifier;
|
|
13615
|
+
}
|
|
13466
13616
|
return void 0;
|
|
13467
13617
|
}
|
|
13468
13618
|
function _resetPathAliasCache() {
|
|
@@ -13579,6 +13729,53 @@ var require_boundary_checker = __commonJS({
|
|
|
13579
13729
|
function _resetModuleParsingCache() {
|
|
13580
13730
|
_cachedModuleParsing = void 0;
|
|
13581
13731
|
}
|
|
13732
|
+
function checkSharedFileImports(sourceFile, filePath, policy, input, violations) {
|
|
13733
|
+
const basename2 = filePath.replace(/^.*\//, "");
|
|
13734
|
+
if (/^(main|cli|bootstrap|server)\.(ts|js)$/.test(basename2))
|
|
13735
|
+
return;
|
|
13736
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
13737
|
+
const isSharedTypeFile = /\/(interfaces|types|shared|common|contracts)\//i.test(normalizedPath);
|
|
13738
|
+
if (!isSharedTypeFile)
|
|
13739
|
+
return;
|
|
13740
|
+
for (const decl of sourceFile.getImportDeclarations()) {
|
|
13741
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
13742
|
+
const line = decl.getStartLineNumber();
|
|
13743
|
+
let resolvedTarget;
|
|
13744
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
13745
|
+
const sourceDir = filePath.replace(/\/[^/]+$/, "");
|
|
13746
|
+
resolvedTarget = resolveRelativePath(sourceDir, specifier);
|
|
13747
|
+
} else {
|
|
13748
|
+
resolvedTarget = resolvePathAliasForBoundary(specifier, input);
|
|
13749
|
+
}
|
|
13750
|
+
if (!resolvedTarget)
|
|
13751
|
+
continue;
|
|
13752
|
+
const targetModule = findModuleForFile(resolvedTarget, policy);
|
|
13753
|
+
if (!targetModule)
|
|
13754
|
+
continue;
|
|
13755
|
+
if (/\.module\.(ts|js)$/.test(resolvedTarget))
|
|
13756
|
+
continue;
|
|
13757
|
+
const targetBasename = resolvedTarget.replace(/^.*\//, "");
|
|
13758
|
+
if (/\.(interface|dto|enum|types?)\.(ts|js)$/.test(targetBasename))
|
|
13759
|
+
continue;
|
|
13760
|
+
if (decl.isTypeOnly())
|
|
13761
|
+
continue;
|
|
13762
|
+
violations.push({
|
|
13763
|
+
category: "architecture",
|
|
13764
|
+
type: "module-boundary-violation",
|
|
13765
|
+
ruleId: shared_1.ARCHITECTURE_RULES.MODULE_BOUNDARY,
|
|
13766
|
+
severity: "critical",
|
|
13767
|
+
source: "deterministic",
|
|
13768
|
+
confidence: "high",
|
|
13769
|
+
file: filePath,
|
|
13770
|
+
line,
|
|
13771
|
+
module: "shared",
|
|
13772
|
+
message: `Shared file imports from "${targetModule}" module \u2014 shared files should not depend on feature modules (${filePath})`,
|
|
13773
|
+
explanation: `Shared/interfaces files should contain plain types, not dependencies on feature module internals.`,
|
|
13774
|
+
debtPoints: 5,
|
|
13775
|
+
gateAction: "block"
|
|
13776
|
+
});
|
|
13777
|
+
}
|
|
13778
|
+
}
|
|
13582
13779
|
function normalizeForMatching(filePath) {
|
|
13583
13780
|
let normalized = filePath.replace(/\\/g, "/");
|
|
13584
13781
|
if (normalized.startsWith("/")) {
|
|
@@ -18864,7 +19061,7 @@ var require_dead_code_detector = __commonJS({
|
|
|
18864
19061
|
const sf = sourceFiles.get(filePath);
|
|
18865
19062
|
if (sf && exp.type === "class" && isNestJSDIRegistered(sf, exp.name))
|
|
18866
19063
|
continue;
|
|
18867
|
-
if (sf && isReferencedInOwnFile(sf, exp.name))
|
|
19064
|
+
if (sf && !isTypeExport(exp.type) && isReferencedInOwnFile(sf, exp.name))
|
|
18868
19065
|
continue;
|
|
18869
19066
|
unusedExports.push({
|
|
18870
19067
|
export: exp,
|
|
@@ -19221,6 +19418,8 @@ var require_dead_code_detector = __commonJS({
|
|
|
19221
19418
|
for (const ext of extensions) {
|
|
19222
19419
|
if (fp.endsWith(suffix + ext))
|
|
19223
19420
|
return normalizeFilePath(fp);
|
|
19421
|
+
if (fp.endsWith(suffix + "/index" + ext))
|
|
19422
|
+
return normalizeFilePath(fp);
|
|
19224
19423
|
}
|
|
19225
19424
|
}
|
|
19226
19425
|
return void 0;
|
|
@@ -19337,8 +19536,9 @@ var require_dead_code_detector = __commonJS({
|
|
|
19337
19536
|
}
|
|
19338
19537
|
if (!hasExternalCall) {
|
|
19339
19538
|
const ownText = allFileTexts.get(filePath) ?? "";
|
|
19340
|
-
const
|
|
19341
|
-
const
|
|
19539
|
+
const selfCallPattern = new RegExp(`this\\.${methodName}\\s*\\(`, "g");
|
|
19540
|
+
const selfCallMatches = ownText.match(selfCallPattern);
|
|
19541
|
+
const internalCallCount = selfCallMatches?.length ?? 0;
|
|
19342
19542
|
if (internalCallCount === 0) {
|
|
19343
19543
|
violations.push({
|
|
19344
19544
|
category: "maintainability",
|
|
@@ -20205,6 +20405,7 @@ var os2 = __toESM(require("os"));
|
|
|
20205
20405
|
var import_chalk = __toESM(require("chalk"));
|
|
20206
20406
|
var import_policy_engine = __toESM(require_dist2());
|
|
20207
20407
|
var import_analyzers = __toESM(require_dist3());
|
|
20408
|
+
var import_shared = __toESM(require_dist());
|
|
20208
20409
|
var import_analyzers2 = __toESM(require_dist3());
|
|
20209
20410
|
|
|
20210
20411
|
// src/ai/scan-summary-generator.ts
|
|
@@ -20763,6 +20964,12 @@ async function scanCommand(targetPath, options) {
|
|
|
20763
20964
|
const blocking = result.violations.filter((v) => v.gateAction === "block");
|
|
20764
20965
|
const warns = result.violations.filter((v) => v.gateAction === "warn");
|
|
20765
20966
|
const byCat = countByCategory(result.violations);
|
|
20967
|
+
const graphData = result.importGraph && result.importGraph.length > 0 ? (0, import_shared.buildImportGraphData)(
|
|
20968
|
+
result.importGraph,
|
|
20969
|
+
result.violations,
|
|
20970
|
+
changedFiles.map((f) => ({ path: f.path, linesOfCode: f.content?.split("\n").length })),
|
|
20971
|
+
policy.modules
|
|
20972
|
+
) : void 0;
|
|
20766
20973
|
const reportData = {
|
|
20767
20974
|
repoName: path2.basename(path2.resolve(targetPath)),
|
|
20768
20975
|
violations: result.violations.map((v) => ({
|
|
@@ -20786,6 +20993,7 @@ async function scanCommand(targetPath, options) {
|
|
|
20786
20993
|
duration: Date.now() - scanStart,
|
|
20787
20994
|
usedAi: shouldRunAi,
|
|
20788
20995
|
aiCreditsUsed: 0,
|
|
20996
|
+
importGraphData: graphData,
|
|
20789
20997
|
// PR metadata from CI environment (set by GitHub Action or GitLab CI)
|
|
20790
20998
|
...process.env.RADAR_PR_NUMBER ? {
|
|
20791
20999
|
prNumber: parseInt(process.env.RADAR_PR_NUMBER, 10),
|