technical-debt-radar 1.6.5 → 1.7.1

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 +461 -32
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12888,6 +12888,7 @@ var require_boundary_checker = __commonJS({
12888
12888
  "use strict";
12889
12889
  Object.defineProperty(exports2, "__esModule", { value: true });
12890
12890
  exports2.checkBoundaries = checkBoundaries;
12891
+ exports2._resetPathAliasCache = _resetPathAliasCache;
12891
12892
  var shared_1 = require_dist();
12892
12893
  var minimatch_1 = require_commonjs3();
12893
12894
  var ts_morph_1 = require("ts-morph");
@@ -13191,10 +13192,15 @@ var require_boundary_checker = __commonJS({
13191
13192
  specifiers.push(reqSpec);
13192
13193
  }
13193
13194
  for (const { specifier, line, typeOnly } of specifiers) {
13194
- if (!specifier.startsWith(".") && !specifier.startsWith("/"))
13195
+ let resolvedTarget;
13196
+ if (specifier.startsWith(".") || specifier.startsWith("/")) {
13197
+ const sourceDir = filePath.replace(/\/[^/]+$/, "");
13198
+ resolvedTarget = resolveRelativePath(sourceDir, specifier);
13199
+ } else {
13200
+ resolvedTarget = resolvePathAliasForBoundary(specifier, input);
13201
+ }
13202
+ if (!resolvedTarget)
13195
13203
  continue;
13196
- const sourceDir = filePath.replace(/\/[^/]+$/, "");
13197
- const resolvedTarget = resolveRelativePath(sourceDir, specifier);
13198
13204
  const targetModule = findModuleForFile(resolvedTarget, policy);
13199
13205
  if (!targetModule || targetModule === sourceModule)
13200
13206
  continue;
@@ -13379,6 +13385,82 @@ var require_boundary_checker = __commonJS({
13379
13385
  const normalized = normalizeForMatching(file);
13380
13386
  return exceptions.some((exc) => exc.rule === ruleId && ((0, minimatch_1.minimatch)(file, exc.file) || (0, minimatch_1.minimatch)(normalized, exc.file)));
13381
13387
  }
13388
+ var _cachedAliases;
13389
+ function parsePathAliasesFromProject(input) {
13390
+ if (_cachedAliases)
13391
+ return _cachedAliases;
13392
+ let tsconfigContent;
13393
+ const tsconfigFile = input.changedFiles.find((f) => f.status !== "deleted" && /tsconfig(?:\.build|\.app)?\.json$/.test(f.path));
13394
+ if (tsconfigFile) {
13395
+ tsconfigContent = tsconfigFile.content;
13396
+ }
13397
+ const root = input.projectRoot;
13398
+ if (!tsconfigContent && root) {
13399
+ try {
13400
+ const fs9 = require("fs");
13401
+ const path9 = require("path");
13402
+ for (const name of ["tsconfig.json", "tsconfig.app.json", "tsconfig.build.json"]) {
13403
+ const tsconfigPath = path9.join(root, name);
13404
+ if (fs9.existsSync(tsconfigPath)) {
13405
+ tsconfigContent = fs9.readFileSync(tsconfigPath, "utf-8");
13406
+ break;
13407
+ }
13408
+ }
13409
+ } catch {
13410
+ }
13411
+ }
13412
+ if (!tsconfigContent) {
13413
+ _cachedAliases = [];
13414
+ return _cachedAliases;
13415
+ }
13416
+ try {
13417
+ const stripped = tsconfigContent.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,\s*([}\]])/g, "$1");
13418
+ const tsconfig = JSON.parse(stripped);
13419
+ const compilerOptions = tsconfig.compilerOptions ?? {};
13420
+ const rawBaseUrl = (compilerOptions.baseUrl ?? ".").replace(/\/+$/, "");
13421
+ const baseUrl = rawBaseUrl === "" ? "." : rawBaseUrl;
13422
+ const paths = compilerOptions.paths ?? {};
13423
+ const aliases = [];
13424
+ for (const [pattern, targets] of Object.entries(paths)) {
13425
+ const prefix = pattern.replace(/\/?\*$/, "");
13426
+ const resolvedTargets = targets.map((t) => {
13427
+ const target = t.replace(/\/?\*$/, "");
13428
+ if (baseUrl !== "." && !target.startsWith("/")) {
13429
+ return (baseUrl + "/" + target).replace(/\/+/g, "/");
13430
+ }
13431
+ return target;
13432
+ });
13433
+ aliases.push({ prefix, targets: resolvedTargets });
13434
+ }
13435
+ _cachedAliases = aliases;
13436
+ return aliases;
13437
+ } catch {
13438
+ _cachedAliases = [];
13439
+ return _cachedAliases;
13440
+ }
13441
+ }
13442
+ function resolvePathAliasForBoundary(specifier, input) {
13443
+ const aliases = parsePathAliasesFromProject(input);
13444
+ for (const alias of aliases) {
13445
+ if (alias.prefix === "" || specifier === alias.prefix || specifier.startsWith(alias.prefix + "/")) {
13446
+ const remainder = alias.prefix === "" ? specifier : specifier.slice(alias.prefix.length).replace(/^\//, "");
13447
+ for (const target of alias.targets) {
13448
+ const candidate = remainder ? target ? target + "/" + remainder : remainder : target;
13449
+ if (candidate.startsWith("src/") || candidate.startsWith("lib/") || candidate.startsWith("app/")) {
13450
+ return candidate;
13451
+ }
13452
+ }
13453
+ }
13454
+ }
13455
+ if (specifier.startsWith("@/"))
13456
+ return "src/" + specifier.slice(2);
13457
+ if (specifier.startsWith("~/"))
13458
+ return "src/" + specifier.slice(2);
13459
+ return void 0;
13460
+ }
13461
+ function _resetPathAliasCache() {
13462
+ _cachedAliases = void 0;
13463
+ }
13382
13464
  function normalizeForMatching(filePath) {
13383
13465
  let normalized = filePath.replace(/\\/g, "/");
13384
13466
  if (normalized.startsWith("/")) {
@@ -16250,6 +16332,7 @@ var require_reliability_detector = __commonJS({
16250
16332
  exports2.detectReliabilityIssues = detectReliabilityIssues;
16251
16333
  var shared_1 = require_dist();
16252
16334
  var ts_morph_1 = require("ts-morph");
16335
+ var minimatch_1 = require_commonjs3();
16253
16336
  async function detectReliabilityIssues(input, policy) {
16254
16337
  const violations = [];
16255
16338
  const project = new ts_morph_1.Project({ useInMemoryFileSystem: true });
@@ -16280,6 +16363,10 @@ var require_reliability_detector = __commonJS({
16280
16363
  detectUnboundedParallelCalls(sourceFile, file.path, fns, policy, violations);
16281
16364
  project.removeSourceFile(sourceFile);
16282
16365
  }
16366
+ if (/mongoose/i.test(policy.stack.orm)) {
16367
+ const schemaViolations = detectQueryNonexistentField(input);
16368
+ violations.push(...schemaViolations);
16369
+ }
16283
16370
  return applyExceptions(deduplicateOverlapping(violations), policy);
16284
16371
  }
16285
16372
  function deduplicateOverlapping(violations) {
@@ -17200,9 +17287,44 @@ var require_reliability_detector = __commonJS({
17200
17287
  });
17201
17288
  });
17202
17289
  }
17290
+ function stripToSrcRoot(filePath) {
17291
+ const markers = ["src/", "apps/", "lib/", "packages/"];
17292
+ for (const marker of markers) {
17293
+ const idx = filePath.indexOf(marker);
17294
+ if (idx >= 0)
17295
+ return filePath.slice(idx);
17296
+ }
17297
+ return filePath;
17298
+ }
17299
+ function resolveRelativeImport(sourceDir, specifier) {
17300
+ const parts = (sourceDir + "/" + specifier).split("/");
17301
+ const resolved = [];
17302
+ for (const part of parts) {
17303
+ if (part === "." || part === "")
17304
+ continue;
17305
+ if (part === "..")
17306
+ resolved.pop();
17307
+ else
17308
+ resolved.push(part);
17309
+ }
17310
+ return resolved.join("/");
17311
+ }
17312
+ function findModuleName(filePath, policy) {
17313
+ const normalized = filePath.replace(/\\/g, "/");
17314
+ const stripped = normalized.startsWith("/") ? normalized.slice(1) : normalized;
17315
+ for (const mod of policy.modules) {
17316
+ if ((0, minimatch_1.minimatch)(stripped, mod.pathPattern) || (0, minimatch_1.minimatch)(normalized, mod.pathPattern)) {
17317
+ return mod.name;
17318
+ }
17319
+ }
17320
+ const match = stripped.match(/^src\/([^/]+)/);
17321
+ return match ? match[1] : void 0;
17322
+ }
17203
17323
  function detectProviderOutsideHomeModule(sourceFile, filePath, allFiles, policy, violations) {
17204
17324
  if (!filePath.endsWith(".module.ts") && !filePath.endsWith(".module.js"))
17205
17325
  return;
17326
+ if (!/nestjs/i.test(policy.stack.framework))
17327
+ return;
17206
17328
  sourceFile.forEachDescendant((node) => {
17207
17329
  if (!ts_morph_1.Node.isDecorator(node) || node.getName() !== "Module")
17208
17330
  return;
@@ -17233,24 +17355,40 @@ var require_reliability_detector = __commonJS({
17233
17355
  if (!matchingImport)
17234
17356
  continue;
17235
17357
  const specifier = importDecl.getModuleSpecifierValue();
17358
+ let resolvedPath;
17236
17359
  if (specifier.startsWith(".") || specifier.startsWith("/")) {
17237
- const resolvedPath = specifier.replace(/^\.\//, moduleFolder + "/").replace(/^\.\.\//, moduleFolder + "/../");
17238
- if (!resolvedPath.startsWith(moduleFolder) && !specifier.startsWith("./")) {
17239
- violations.push({
17240
- category: "architecture",
17241
- type: shared_1.ARCHITECTURE_RULES.PROVIDER_OUTSIDE_HOME,
17242
- ruleId: shared_1.ARCHITECTURE_RULES.PROVIDER_OUTSIDE_HOME,
17243
- severity: "warning",
17244
- source: "deterministic",
17245
- confidence: "high",
17246
- file: filePath,
17247
- line: element.getStartLineNumber(),
17248
- message: `Provider '${providerName}' is imported from outside this module's folder \u2014 consider moving it to its home module or a shared module`,
17249
- suggestion: `Move ${providerName} to this module's folder or register it in the module where it's defined`,
17250
- debtPoints: 3,
17251
- gateAction: "warn"
17252
- });
17253
- }
17360
+ resolvedPath = resolveRelativeImport(moduleFolder, specifier);
17361
+ } else if (specifier.startsWith("@/")) {
17362
+ resolvedPath = "src/" + specifier.slice(2);
17363
+ } else if (specifier.startsWith("~/")) {
17364
+ resolvedPath = "src/" + specifier.slice(2);
17365
+ }
17366
+ if (!resolvedPath)
17367
+ continue;
17368
+ const normalizedModule = stripToSrcRoot(moduleFolder.replace(/\\/g, "/"));
17369
+ const normalizedResolved = stripToSrcRoot(resolvedPath.replace(/\\/g, "/"));
17370
+ const isFromOutside = !normalizedResolved.startsWith(normalizedModule + "/") && normalizedResolved !== normalizedModule;
17371
+ const sharedFolders = ["shared", "common", "utils", "config", "guards", "interceptors", "decorators", "pipes", "filters", "strategies"];
17372
+ const isShared = sharedFolders.some((f) => normalizedResolved.includes("/" + f + "/"));
17373
+ if (isFromOutside && !isShared) {
17374
+ const sourceModuleName = findModuleName(normalizedResolved, policy);
17375
+ const targetModuleName = findModuleName(filePath, policy);
17376
+ const moduleInfo = sourceModuleName ? ` (${sourceModuleName} module)` : "";
17377
+ const targetInfo = targetModuleName ? ` (${targetModuleName} module)` : "";
17378
+ violations.push({
17379
+ category: "architecture",
17380
+ type: shared_1.ARCHITECTURE_RULES.PROVIDER_OUTSIDE_HOME,
17381
+ ruleId: shared_1.ARCHITECTURE_RULES.PROVIDER_OUTSIDE_HOME,
17382
+ severity: "warning",
17383
+ source: "deterministic",
17384
+ confidence: "high",
17385
+ file: filePath,
17386
+ line: element.getStartLineNumber(),
17387
+ message: `Provider '${providerName}'${moduleInfo} is registered outside its home module${targetInfo} \u2014 move it to its home module's providers and export it`,
17388
+ suggestion: `Move ${providerName} to its home module's providers array and export it, then import that module here`,
17389
+ debtPoints: 3,
17390
+ gateAction: "warn"
17391
+ });
17254
17392
  }
17255
17393
  }
17256
17394
  }
@@ -17407,6 +17545,163 @@ var require_reliability_detector = __commonJS({
17407
17545
  function escapeRegex(str) {
17408
17546
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17409
17547
  }
17548
+ var MONGOOSE_QUERY_METHODS = /* @__PURE__ */ new Set([
17549
+ "find",
17550
+ "findOne",
17551
+ "findById",
17552
+ "findOneAndUpdate",
17553
+ "findOneAndDelete",
17554
+ "findOneAndReplace",
17555
+ "findOneAndRemove",
17556
+ "findByIdAndUpdate",
17557
+ "findByIdAndDelete",
17558
+ "updateOne",
17559
+ "updateMany",
17560
+ "deleteOne",
17561
+ "deleteMany",
17562
+ "countDocuments",
17563
+ "exists",
17564
+ "distinct"
17565
+ ]);
17566
+ var BUILTIN_FIELDS = /* @__PURE__ */ new Set(["_id", "__v", "id", "createdAt", "updatedAt"]);
17567
+ function detectQueryNonexistentField(input) {
17568
+ const violations = [];
17569
+ const project = new ts_morph_1.Project({ useInMemoryFileSystem: true });
17570
+ const schemaFieldMap = /* @__PURE__ */ new Map();
17571
+ for (const file of input.changedFiles) {
17572
+ if (file.status === "deleted")
17573
+ continue;
17574
+ if (!/\.schema\.(ts|js)$/.test(file.path) && !/schemas?\//.test(file.path))
17575
+ continue;
17576
+ if (!/\.(ts|tsx|js|jsx)$/.test(file.path))
17577
+ continue;
17578
+ const sf = project.createSourceFile(file.path, file.content);
17579
+ for (const cls of sf.getClasses()) {
17580
+ const schemaDecorator = cls.getDecorators().find((d) => d.getName() === "Schema");
17581
+ if (!schemaDecorator)
17582
+ continue;
17583
+ const schemaName = cls.getName();
17584
+ if (!schemaName)
17585
+ continue;
17586
+ const fields = new Set(BUILTIN_FIELDS);
17587
+ for (const prop of cls.getProperties()) {
17588
+ if (prop.getDecorators().some((d) => d.getName() === "Prop")) {
17589
+ fields.add(prop.getName());
17590
+ }
17591
+ }
17592
+ const schemaArgs = schemaDecorator.getCallExpression?.()?.getArguments();
17593
+ if (schemaArgs && schemaArgs.length > 0) {
17594
+ const argText = schemaArgs[0].getText();
17595
+ if (/timestamps\s*:\s*true/.test(argText)) {
17596
+ fields.add("createdAt");
17597
+ fields.add("updatedAt");
17598
+ }
17599
+ }
17600
+ schemaFieldMap.set(schemaName, fields);
17601
+ }
17602
+ project.removeSourceFile(sf);
17603
+ }
17604
+ if (schemaFieldMap.size === 0)
17605
+ return violations;
17606
+ for (const file of input.changedFiles) {
17607
+ if (file.status === "deleted")
17608
+ continue;
17609
+ if (!/\.(ts|tsx|js|jsx)$/.test(file.path))
17610
+ continue;
17611
+ if (/\.schema\.(ts|js)$/.test(file.path))
17612
+ continue;
17613
+ const sf = project.createSourceFile(file.path, file.content);
17614
+ const modelToSchema = /* @__PURE__ */ new Map();
17615
+ for (const cls of sf.getClasses()) {
17616
+ const ctors = cls.getConstructors();
17617
+ for (const ctor of ctors) {
17618
+ for (const param of ctor.getParameters()) {
17619
+ const injectModel = param.getDecorators().find((d) => d.getName() === "InjectModel");
17620
+ if (!injectModel)
17621
+ continue;
17622
+ const callExpr = injectModel.getCallExpression?.();
17623
+ if (!callExpr)
17624
+ continue;
17625
+ const args = callExpr.getArguments();
17626
+ if (args.length === 0)
17627
+ continue;
17628
+ let schemaName = args[0].getText();
17629
+ schemaName = schemaName.replace(/\.name$/, "").replace(/['"]/g, "");
17630
+ const paramName = param.getName();
17631
+ modelToSchema.set(paramName, schemaName);
17632
+ }
17633
+ }
17634
+ }
17635
+ if (modelToSchema.size === 0) {
17636
+ project.removeSourceFile(sf);
17637
+ continue;
17638
+ }
17639
+ sf.forEachDescendant((node) => {
17640
+ if (!ts_morph_1.Node.isCallExpression(node))
17641
+ return;
17642
+ const expr = node.getExpression();
17643
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
17644
+ return;
17645
+ const methodName = expr.getName();
17646
+ if (!MONGOOSE_QUERY_METHODS.has(methodName))
17647
+ return;
17648
+ const obj = expr.getExpression();
17649
+ if (!ts_morph_1.Node.isPropertyAccessExpression(obj))
17650
+ return;
17651
+ const propName = obj.getName();
17652
+ const thisExpr = obj.getExpression();
17653
+ if (thisExpr.getKind() !== ts_morph_1.SyntaxKind.ThisKeyword)
17654
+ return;
17655
+ const schemaName = modelToSchema.get(propName);
17656
+ if (!schemaName)
17657
+ return;
17658
+ const schemaFields = schemaFieldMap.get(schemaName);
17659
+ if (!schemaFields)
17660
+ return;
17661
+ const args = node.getArguments();
17662
+ if (args.length === 0)
17663
+ return;
17664
+ const filterArg = args[0];
17665
+ if (!ts_morph_1.Node.isObjectLiteralExpression(filterArg))
17666
+ return;
17667
+ for (const prop of filterArg.getProperties()) {
17668
+ if (!ts_morph_1.Node.isPropertyAssignment(prop))
17669
+ continue;
17670
+ const nameNode = prop.getNameNode();
17671
+ let fieldName;
17672
+ if (ts_morph_1.Node.isStringLiteral(nameNode)) {
17673
+ fieldName = nameNode.getLiteralValue();
17674
+ } else if (ts_morph_1.Node.isIdentifier(nameNode)) {
17675
+ fieldName = nameNode.getText();
17676
+ } else if (ts_morph_1.Node.isComputedPropertyName(nameNode)) {
17677
+ continue;
17678
+ } else {
17679
+ fieldName = nameNode.getText();
17680
+ }
17681
+ const rootField = fieldName.split(".")[0];
17682
+ if (!schemaFields.has(rootField)) {
17683
+ const availableFields = [...schemaFields].filter((f) => !BUILTIN_FIELDS.has(f) || f === "_id").sort().join(", ");
17684
+ violations.push({
17685
+ category: "reliability",
17686
+ type: "query-references-nonexistent-field",
17687
+ ruleId: "query-references-nonexistent-field",
17688
+ severity: "critical",
17689
+ source: "deterministic",
17690
+ confidence: "high",
17691
+ file: file.path,
17692
+ line: prop.getStartLineNumber(),
17693
+ message: `Query filter references field '${fieldName}' which does not exist in ${schemaName} schema \u2014 available fields: ${availableFields}`,
17694
+ suggestion: `Check the ${schemaName} schema definition for the correct field name`,
17695
+ debtPoints: 8,
17696
+ gateAction: "block"
17697
+ });
17698
+ }
17699
+ }
17700
+ });
17701
+ project.removeSourceFile(sf);
17702
+ }
17703
+ return violations;
17704
+ }
17410
17705
  function applyExceptions(violations, policy) {
17411
17706
  return violations.filter((v) => !policy.exceptions.some((ex) => ex.isActive && ex.rule === v.ruleId && v.file.includes(ex.file)));
17412
17707
  }
@@ -18474,6 +18769,8 @@ var require_dead_code_detector = __commonJS({
18474
18769
  debtPoints: 1,
18475
18770
  gateAction: "warn"
18476
18771
  }));
18772
+ const unusedMethodViolations = detectUnusedPublicMethods(sourceFiles, input);
18773
+ violations.push(...unusedMethodViolations);
18477
18774
  const totalExports = Array.from(allExports.values()).reduce((sum, exps) => sum + exps.length, 0);
18478
18775
  return {
18479
18776
  unusedExports,
@@ -18829,6 +19126,121 @@ var require_dead_code_detector = __commonJS({
18829
19126
  }
18830
19127
  return result.join("/");
18831
19128
  }
19129
+ var NESTJS_METHOD_DECORATORS = /* @__PURE__ */ new Set([
19130
+ "Get",
19131
+ "Post",
19132
+ "Put",
19133
+ "Patch",
19134
+ "Delete",
19135
+ "Options",
19136
+ "Head",
19137
+ "All",
19138
+ "Cron",
19139
+ "OnEvent",
19140
+ "Process",
19141
+ "OnQueueActive",
19142
+ "OnQueueCompleted",
19143
+ "OnQueueFailed",
19144
+ "SubscribeMessage",
19145
+ "OnGatewayInit",
19146
+ "OnGatewayConnection",
19147
+ "OnGatewayDisconnect",
19148
+ "EventPattern",
19149
+ "MessagePattern",
19150
+ "GrpcMethod"
19151
+ ]);
19152
+ var LIFECYCLE_HOOKS = /* @__PURE__ */ new Set([
19153
+ "onModuleInit",
19154
+ "onModuleDestroy",
19155
+ "onApplicationBootstrap",
19156
+ "onApplicationShutdown",
19157
+ "beforeApplicationShutdown",
19158
+ "afterInit",
19159
+ "handleConnection",
19160
+ "handleDisconnect",
19161
+ "validate",
19162
+ // Passport strategy method — called by framework
19163
+ "canActivate",
19164
+ // Guard method
19165
+ "intercept",
19166
+ // Interceptor method
19167
+ "transform",
19168
+ // Pipe method
19169
+ "catch",
19170
+ // Exception filter method
19171
+ "use"
19172
+ // Middleware method
19173
+ ]);
19174
+ function detectUnusedPublicMethods(sourceFiles, input) {
19175
+ const violations = [];
19176
+ const isNestJS = /nestjs/i.test(input.policy.stack.framework);
19177
+ const allFileTexts = /* @__PURE__ */ new Map();
19178
+ for (const [filePath, sf] of sourceFiles) {
19179
+ allFileTexts.set(filePath, sf.getFullText());
19180
+ }
19181
+ for (const [filePath, sf] of sourceFiles) {
19182
+ if (/\.(spec|test)\./i.test(filePath))
19183
+ continue;
19184
+ if (/\.module\./i.test(filePath))
19185
+ continue;
19186
+ for (const cls of sf.getClasses()) {
19187
+ const hasInjectable = cls.getDecorators().some((d) => d.getName() === "Injectable");
19188
+ if (!hasInjectable)
19189
+ continue;
19190
+ const baseClass = cls.getExtends()?.getText() ?? "";
19191
+ if (/PassportStrategy|Strategy/.test(baseClass))
19192
+ continue;
19193
+ const className = cls.getName();
19194
+ if (!className)
19195
+ continue;
19196
+ for (const method of cls.getMethods()) {
19197
+ const methodName = method.getName();
19198
+ if (!methodName || methodName.startsWith("_"))
19199
+ continue;
19200
+ const scope = method.getScope();
19201
+ if (scope === "private" || scope === "protected")
19202
+ continue;
19203
+ if (LIFECYCLE_HOOKS.has(methodName))
19204
+ continue;
19205
+ if (isNestJS && method.getDecorators().some((d) => NESTJS_METHOD_DECORATORS.has(d.getName())))
19206
+ continue;
19207
+ const callPattern = new RegExp(`\\.${methodName}\\s*\\(`);
19208
+ let hasExternalCall = false;
19209
+ for (const [otherFilePath, otherText] of allFileTexts) {
19210
+ if (otherFilePath === filePath)
19211
+ continue;
19212
+ if (callPattern.test(otherText)) {
19213
+ hasExternalCall = true;
19214
+ break;
19215
+ }
19216
+ }
19217
+ if (!hasExternalCall) {
19218
+ const ownText = allFileTexts.get(filePath) ?? "";
19219
+ const matches = ownText.match(new RegExp(`\\.${methodName}\\s*\\(`, "g"));
19220
+ const internalCallCount = matches?.length ?? 0;
19221
+ if (internalCallCount === 0) {
19222
+ violations.push({
19223
+ category: "maintainability",
19224
+ type: "unused-public-method",
19225
+ ruleId: "unused-public-method",
19226
+ severity: "warning",
19227
+ source: "deterministic",
19228
+ confidence: "medium",
19229
+ file: filePath,
19230
+ line: method.getStartLineNumber(),
19231
+ function: methodName,
19232
+ message: `Public method '${methodName}' in ${className} has no call sites \u2014 consider removing it or marking it private`,
19233
+ suggestion: `Remove the unused method or mark it as private if it's only used internally`,
19234
+ debtPoints: 1,
19235
+ gateAction: "warn"
19236
+ });
19237
+ }
19238
+ }
19239
+ }
19240
+ }
19241
+ }
19242
+ return violations;
19243
+ }
18832
19244
  function emptyResult() {
18833
19245
  return {
18834
19246
  unusedExports: [],
@@ -19943,19 +20355,35 @@ var RadarApiClient = class _RadarApiClient {
19943
20355
  return null;
19944
20356
  }
19945
20357
  async fetch(path9, init) {
19946
- const res = await fetch(`${this.apiUrl}${path9}`, {
19947
- ...init,
19948
- headers: {
19949
- "Authorization": `Bearer ${this.token}`,
19950
- "Content-Type": "application/json",
19951
- ...init?.headers
19952
- }
19953
- });
20358
+ const url = `${this.apiUrl}${path9}`;
20359
+ let res;
20360
+ try {
20361
+ res = await fetch(url, {
20362
+ ...init,
20363
+ headers: {
20364
+ "Authorization": `Bearer ${this.token}`,
20365
+ "Content-Type": "application/json",
20366
+ ...init?.headers
20367
+ }
20368
+ });
20369
+ } catch (err) {
20370
+ const cause = err?.cause?.message || err?.cause?.code || "";
20371
+ throw new Error(`Network error connecting to ${url}: ${err.message}${cause ? ` (${cause})` : ""}`);
20372
+ }
19954
20373
  if (!res.ok) {
19955
20374
  const body = await res.text().catch(() => "");
19956
- throw new Error(`API error ${res.status}: ${body}`);
20375
+ throw new Error(`API error ${res.status} on ${path9}: ${body}`);
20376
+ }
20377
+ const contentType = res.headers.get("content-type") ?? "";
20378
+ const text = await res.text();
20379
+ if (!text || text.trim().length === 0) {
20380
+ return void 0;
20381
+ }
20382
+ try {
20383
+ return JSON.parse(text);
20384
+ } catch {
20385
+ return void 0;
19957
20386
  }
19958
- return res.json();
19959
20387
  }
19960
20388
  async verifyToken() {
19961
20389
  return this.fetch("/cli/auth/verify");
@@ -20262,8 +20690,9 @@ async function scanCommand(targetPath, options) {
20262
20690
  }).catch(() => {
20263
20691
  });
20264
20692
  console.log(import_chalk.default.green(" \u2713 Results synced to dashboard: https://www.technicaldebtradar.com/dashboard"));
20265
- } catch {
20266
- console.log(import_chalk.default.yellow(" \u26A0\uFE0F Could not sync results to dashboard. Check your connection."));
20693
+ } catch (err) {
20694
+ const detail = err?.message ? `: ${err.message}` : "";
20695
+ console.log(import_chalk.default.yellow(` \u26A0\uFE0F Could not sync results to dashboard${detail}`));
20267
20696
  }
20268
20697
  }
20269
20698
  if (mode === "warn") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "technical-debt-radar",
3
- "version": "1.6.5",
3
+ "version": "1.7.1",
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",