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.
Files changed (2) hide show
  1. package/dist/index.js +216 -8
  2. 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: "Unlimited scans, warn mode",
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: -1 },
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: -1 },
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: -1 },
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: -1 },
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 matches = ownText.match(new RegExp(`\\.${methodName}\\s*\\(`, "g"));
19341
- const internalCallCount = matches?.length ?? 0;
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),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "technical-debt-radar",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "description": "Stop Node.js production crashes before merge. 47 detection patterns across 5 categories.",
5
5
  "bin": {
6
6
  "radar": "dist/index.js",