technical-debt-radar 1.9.0 → 1.11.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 +246 -284
  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
 
@@ -18723,7 +18867,7 @@ var require_cross_file_ai = __commonJS({
18723
18867
  Object.defineProperty(exports2, "__esModule", { value: true });
18724
18868
  exports2.analyzeCrossFileWithAI = analyzeCrossFileWithAI2;
18725
18869
  var sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
18726
- var MODEL4 = "claude-sonnet-4-20250514";
18870
+ var MODEL3 = "claude-sonnet-4-20250514";
18727
18871
  var MAX_TOKENS = 3e3;
18728
18872
  async function analyzeCrossFileWithAI2(suspects, framework, orm, apiKey) {
18729
18873
  if (suspects.length === 0)
@@ -18778,7 +18922,7 @@ ${suspectsText}
18778
18922
  For each, determine if it's a real risk. Return JSON array.`;
18779
18923
  try {
18780
18924
  const response = await client.messages.create({
18781
- model: MODEL4,
18925
+ model: MODEL3,
18782
18926
  max_tokens: MAX_TOKENS,
18783
18927
  system: systemPrompt,
18784
18928
  messages: [{ role: "user", content: userPrompt }]
@@ -20261,6 +20405,7 @@ var os2 = __toESM(require("os"));
20261
20405
  var import_chalk = __toESM(require("chalk"));
20262
20406
  var import_policy_engine = __toESM(require_dist2());
20263
20407
  var import_analyzers = __toESM(require_dist3());
20408
+ var import_shared = __toESM(require_dist());
20264
20409
  var import_analyzers2 = __toESM(require_dist3());
20265
20410
 
20266
20411
  // src/ai/scan-summary-generator.ts
@@ -20580,6 +20725,18 @@ var RadarApiClient = class _RadarApiClient {
20580
20725
  body: JSON.stringify(data)
20581
20726
  });
20582
20727
  }
20728
+ async requestFix(data) {
20729
+ return this.fetch("/cli/ai/fix", {
20730
+ method: "POST",
20731
+ body: JSON.stringify(data)
20732
+ });
20733
+ }
20734
+ async requestFixGrouped(data) {
20735
+ return this.fetch("/cli/ai/fix-grouped", {
20736
+ method: "POST",
20737
+ body: JSON.stringify(data)
20738
+ });
20739
+ }
20583
20740
  };
20584
20741
 
20585
20742
  // src/commands/scan.ts
@@ -20819,6 +20976,12 @@ async function scanCommand(targetPath, options) {
20819
20976
  const blocking = result.violations.filter((v) => v.gateAction === "block");
20820
20977
  const warns = result.violations.filter((v) => v.gateAction === "warn");
20821
20978
  const byCat = countByCategory(result.violations);
20979
+ const graphData = result.importGraph && result.importGraph.length > 0 ? (0, import_shared.buildImportGraphData)(
20980
+ result.importGraph,
20981
+ result.violations,
20982
+ changedFiles.map((f) => ({ path: f.path, linesOfCode: f.content?.split("\n").length })),
20983
+ policy.modules
20984
+ ) : void 0;
20822
20985
  const reportData = {
20823
20986
  repoName: path2.basename(path2.resolve(targetPath)),
20824
20987
  violations: result.violations.map((v) => ({
@@ -20842,6 +21005,7 @@ async function scanCommand(targetPath, options) {
20842
21005
  duration: Date.now() - scanStart,
20843
21006
  usedAi: shouldRunAi,
20844
21007
  aiCreditsUsed: 0,
21008
+ importGraphData: graphData,
20845
21009
  // PR metadata from CI environment (set by GitHub Action or GitLab CI)
20846
21010
  ...process.env.RADAR_PR_NUMBER ? {
20847
21011
  prNumber: parseInt(process.env.RADAR_PR_NUMBER, 10),
@@ -21788,200 +21952,6 @@ var path5 = __toESM(require("path"));
21788
21952
  var import_chalk4 = __toESM(require("chalk"));
21789
21953
  var import_inquirer2 = __toESM(require("inquirer"));
21790
21954
  var import_analyzers3 = __toESM(require_dist3());
21791
-
21792
- // src/ai/fix-generator.ts
21793
- var import_sdk3 = __toESM(require("@anthropic-ai/sdk"));
21794
- var MODEL3 = "claude-sonnet-4-20250514";
21795
- var MAX_FIX_TOKENS = 2e3;
21796
- async function generateFix(request, apiKey) {
21797
- const client = new import_sdk3.default({ apiKey });
21798
- const ruleHint = getRuleSpecificHints(request.violation.ruleId, request.framework);
21799
- const systemPrompt = `You are a senior Node.js/TypeScript developer fixing code violations found by a static analysis tool called Technical Debt Radar.
21800
-
21801
- RULES:
21802
- - Return ONLY the fixed code snippet, not the entire file
21803
- - Keep the fix minimal \u2014 change only what's needed to resolve the violation
21804
- - Preserve existing code style, indentation, and conventions
21805
- - Add imports if needed (show them separately)
21806
- - Use the project's framework patterns (${request.framework}, ${request.orm})
21807
- ${ruleHint ? `
21808
- SPECIFIC GUIDANCE FOR THIS RULE:
21809
- ${ruleHint}` : ""}
21810
-
21811
- RESPONSE FORMAT (JSON only, no markdown):
21812
- {
21813
- "originalCode": "the exact lines that need to change",
21814
- "fixedCode": "the replacement code",
21815
- "explanation": "one sentence explaining the fix",
21816
- "startLine": <first line number to replace>,
21817
- "endLine": <last line number to replace>,
21818
- "newImports": ["import { X } from 'y'"] or [],
21819
- "confidence": "high" | "medium" | "low"
21820
- }`;
21821
- const userPrompt = `Fix this violation:
21822
-
21823
- RULE: ${request.violation.ruleId}
21824
- MESSAGE: ${request.violation.message}
21825
- FILE: ${request.violation.file}
21826
- LINE: ${request.violation.line}
21827
- SEVERITY: ${request.violation.severity}
21828
-
21829
- SURROUNDING CODE (lines ${Math.max(1, request.violation.line - 15)}-${request.violation.line + 15}):
21830
- \`\`\`typescript
21831
- ${request.surroundingCode}
21832
- \`\`\`
21833
-
21834
- FULL FILE (for context):
21835
- \`\`\`typescript
21836
- ${request.fileContent}
21837
- \`\`\`
21838
-
21839
- Generate the minimal fix. Return JSON only.`;
21840
- const response = await client.messages.create({
21841
- model: MODEL3,
21842
- max_tokens: MAX_FIX_TOKENS,
21843
- system: systemPrompt,
21844
- messages: [{ role: "user", content: userPrompt }]
21845
- });
21846
- const textBlock = response.content.find((b) => b.type === "text");
21847
- if (!textBlock || textBlock.type !== "text") {
21848
- throw new Error("No text content in AI response");
21849
- }
21850
- const cleaned = textBlock.text.replace(/^```json\s*/m, "").replace(/^```\s*/m, "").replace(/```\s*$/m, "").trim();
21851
- const parsed = JSON.parse(cleaned);
21852
- return {
21853
- violation: request.violation,
21854
- originalCode: String(parsed.originalCode || ""),
21855
- fixedCode: String(parsed.fixedCode || ""),
21856
- explanation: String(parsed.explanation || ""),
21857
- startLine: Number(parsed.startLine),
21858
- endLine: Number(parsed.endLine),
21859
- newImports: Array.isArray(parsed.newImports) ? parsed.newImports.map(String) : [],
21860
- confidence: validateConfidence(parsed.confidence)
21861
- };
21862
- }
21863
- async function generateGroupedFix(request, apiKey) {
21864
- const client = new import_sdk3.default({ apiKey });
21865
- const ruleHints = request.violations.map((v) => getRuleSpecificHints(v.ruleId, request.framework)).filter(Boolean);
21866
- const hintsBlock = ruleHints.length > 0 ? `
21867
- SPECIFIC GUIDANCE:
21868
- ${ruleHints.join("\n")}` : "";
21869
- const systemPrompt = `You are a senior Node.js/TypeScript developer fixing code violations found by a static analysis tool called Technical Debt Radar.
21870
-
21871
- RULES:
21872
- - Return ONLY the fixed code snippet, not the entire file
21873
- - Keep the fix minimal \u2014 change only what's needed to resolve ALL violations listed
21874
- - Preserve existing code style, indentation, and conventions
21875
- - Add imports if needed (show them separately)
21876
- - Use the project's framework patterns (${request.framework}, ${request.orm})
21877
- - You MUST address EVERY violation listed \u2014 do not skip any
21878
- ${hintsBlock}
21879
-
21880
- RESPONSE FORMAT (JSON only, no markdown):
21881
- {
21882
- "originalCode": "the exact lines that need to change",
21883
- "fixedCode": "the replacement code",
21884
- "explanation": "one sentence explaining the fix",
21885
- "startLine": <first line number to replace>,
21886
- "endLine": <last line number to replace>,
21887
- "newImports": ["import { X } from 'y'"] or [],
21888
- "confidence": "high" | "medium" | "low"
21889
- }`;
21890
- const violationList = request.violations.map((v, i) => ` ${i + 1}. [${v.ruleId}] ${v.message} (line ${v.line}, severity: ${v.severity})`).join("\n");
21891
- const refLine = request.violations[0].line;
21892
- const userPrompt = `Fix these ${request.violations.length} violations in the same code section:
21893
-
21894
- ${violationList}
21895
-
21896
- FILE: ${request.violations[0].file}
21897
-
21898
- SURROUNDING CODE (lines ${Math.max(1, refLine - 15)}-${refLine + 15}):
21899
- \`\`\`typescript
21900
- ${request.surroundingCode}
21901
- \`\`\`
21902
-
21903
- FULL FILE (for context):
21904
- \`\`\`typescript
21905
- ${request.fileContent}
21906
- \`\`\`
21907
-
21908
- Generate ONE fix that addresses ALL ${request.violations.length} violations. Return JSON only.`;
21909
- const response = await client.messages.create({
21910
- model: MODEL3,
21911
- max_tokens: MAX_FIX_TOKENS,
21912
- system: systemPrompt,
21913
- messages: [{ role: "user", content: userPrompt }]
21914
- });
21915
- const textBlock = response.content.find((b) => b.type === "text");
21916
- if (!textBlock || textBlock.type !== "text") {
21917
- throw new Error("No text content in AI response");
21918
- }
21919
- const cleaned = textBlock.text.replace(/^```json\s*/m, "").replace(/^```\s*/m, "").replace(/```\s*$/m, "").trim();
21920
- const parsed = JSON.parse(cleaned);
21921
- return {
21922
- violation: request.violations[0],
21923
- originalCode: String(parsed.originalCode || ""),
21924
- fixedCode: String(parsed.fixedCode || ""),
21925
- explanation: String(parsed.explanation || ""),
21926
- startLine: Number(parsed.startLine),
21927
- endLine: Number(parsed.endLine),
21928
- newImports: Array.isArray(parsed.newImports) ? parsed.newImports.map(String) : [],
21929
- confidence: validateConfidence(parsed.confidence)
21930
- };
21931
- }
21932
- function validateConfidence(value) {
21933
- if (value === "high" || value === "medium" || value === "low") {
21934
- return value;
21935
- }
21936
- return "medium";
21937
- }
21938
- function getRuleSpecificHints(ruleId, framework) {
21939
- const hints = {
21940
- "missing-null-guard": `Add a null/undefined check after the query. In ${framework === "NestJS" ? "NestJS, throw new NotFoundException()" : 'Express, return res.status(404).json({ error: "Not found" })'}. Check BEFORE any property access.`,
21941
- "sync-fs-in-handler": "Replace fs.readFileSync/writeFileSync with await fs.promises.readFile/writeFile. Make the function async if needed.",
21942
- "sync-crypto": "Replace crypto.pbkdf2Sync with the async crypto.pbkdf2 wrapped in a Promise, or use argon2/bcrypt async alternatives.",
21943
- "sync-compression": "Replace zlib.deflateSync/gzipSync with the async stream or callback versions.",
21944
- "redos-vulnerable-regex": "Rewrite the regex to avoid nested quantifiers. Remove patterns like (a+)+, (a|b)*, or use a regex linting library.",
21945
- "n-plus-one-query": "Replace the loop + individual query pattern with a batch query. Use include/join/populate for relations, or WHERE IN for ID lists.",
21946
- "unbounded-find-many": "Add pagination: { take: 20, skip: offset } for Prisma, { limit: 20, offset } for Sequelize, .limit(20) for Mongoose/Drizzle/Knex.",
21947
- "find-many-no-where": "Add a where/filter clause to prevent full table scans.",
21948
- "raw-sql-no-limit": "Add LIMIT clause to the SQL query.",
21949
- "unhandled-promise-rejection": "Add await before the async call, or explicitly handle the promise with .catch().",
21950
- "external-call-no-timeout": "Add { timeout: 10000 } (10s) to the HTTP client config. For axios: axios.post(url, data, { timeout: 10000 }). For fetch: use AbortController.",
21951
- "empty-catch-block": "Add error logging inside the catch block: console.error(error) or logger.error(error).",
21952
- "retry-without-backoff": "Replace fixed delay with exponential backoff: delay * Math.pow(2, attempt).",
21953
- "missing-error-logging": "Add a logging statement in the catch block.",
21954
- "missing-try-catch": "Wrap the multiple await calls in a try/catch block with proper error handling.",
21955
- "transaction-no-timeout": "Add a timeout option to the transaction: { timeout: 5000 }.",
21956
- "unfiltered-count-large-table": "Add a where/filter clause to the count query."
21957
- };
21958
- return hints[ruleId] || "";
21959
- }
21960
-
21961
- // src/commands/fix.ts
21962
- function buildFixInstruction(violation) {
21963
- const lines = [];
21964
- lines.push(`In ${violation.file} line ${violation.line}:`);
21965
- if (violation.suggestion) {
21966
- lines.push(` ${violation.suggestion}`);
21967
- } else {
21968
- lines.push(` ${violation.message}`);
21969
- }
21970
- return lines.join("\n");
21971
- }
21972
- function formatFixPrompt(violations) {
21973
- if (violations.length === 0) {
21974
- return "No issues found \u2014 nothing to fix.";
21975
- }
21976
- const lines = [];
21977
- lines.push("Fix these issues in my codebase:");
21978
- lines.push("");
21979
- violations.forEach((v, i) => {
21980
- lines.push(`${i + 1}. ${buildFixInstruction(v)}`);
21981
- lines.push("");
21982
- });
21983
- return lines.join("\n").trimEnd();
21984
- }
21985
21955
  function applyFix(filePath, fix) {
21986
21956
  const content = fsSync2.readFileSync(filePath, "utf-8");
21987
21957
  const lines = content.split("\n");
@@ -22033,33 +22003,6 @@ async function runScan(targetPath, options) {
22033
22003
  policy
22034
22004
  };
22035
22005
  }
22036
- async function runTextOnlyFix(targetPath, options) {
22037
- const { compiled: policy } = await loadPolicy(options.config, options.rules);
22038
- const files = await collectTsFiles(targetPath);
22039
- if (files.length === 0) {
22040
- console.log(import_chalk4.default.yellow("No .ts/.tsx files found."));
22041
- process.exit(0);
22042
- }
22043
- const changedFiles = await Promise.all(
22044
- files.map(async (filePath) => ({
22045
- path: filePath,
22046
- content: await fs5.readFile(filePath, "utf-8"),
22047
- status: "added"
22048
- }))
22049
- );
22050
- const input = { changedFiles, policy, headSha: "local", projectRoot: path5.resolve(targetPath) };
22051
- const result = await (0, import_analyzers3.runFullAnalysis)(input);
22052
- let violations = result.violations.filter((v) => v.severity === "critical" || v.severity === "warning").sort((a, b) => {
22053
- if (a.severity === "critical" && b.severity !== "critical") return -1;
22054
- if (a.severity !== "critical" && b.severity === "critical") return 1;
22055
- return b.debtPoints - a.debtPoints;
22056
- });
22057
- if (options.severity === "critical") {
22058
- violations = violations.filter((v) => v.severity === "critical");
22059
- }
22060
- const output = formatFixPrompt(violations);
22061
- console.log(output);
22062
- }
22063
22006
  function groupBy(items, key) {
22064
22007
  const result = {};
22065
22008
  for (const item of items) {
@@ -22072,22 +22015,24 @@ function groupBy(items, key) {
22072
22015
  async function fixCommand(targetPath, options) {
22073
22016
  const client = RadarApiClient.fromConfigOrEnv();
22074
22017
  if (!client) {
22075
- console.error(import_chalk4.default.red("Authentication required. Run: radar login"));
22018
+ console.error(import_chalk4.default.red("\u274C Authentication required. Run: radar login"));
22076
22019
  process.exit(1);
22077
22020
  }
22078
- const apiKey = options.apiKey || process.env.ANTHROPIC_API_KEY;
22079
- if (!apiKey) {
22080
- console.log(import_chalk4.default.yellow("No API key found."));
22081
- console.log(" Set ANTHROPIC_API_KEY environment variable");
22082
- console.log(" Or use: radar fix --api-key sk-ant-...");
22083
- console.log("");
22084
- console.log(import_chalk4.default.dim("Tip: Set ANTHROPIC_API_KEY for AI-powered auto-fixes"));
22085
- console.log(import_chalk4.default.dim(" export ANTHROPIC_API_KEY=sk-ant-..."));
22086
- console.log(import_chalk4.default.dim(" Then run: radar fix ."));
22087
- console.log("");
22088
- console.log(import_chalk4.default.dim("Without AI, showing fix instructions only:"));
22089
- console.log("");
22090
- return runTextOnlyFix(targetPath, options);
22021
+ let verified;
22022
+ try {
22023
+ verified = await client.verifyToken();
22024
+ } catch (err) {
22025
+ const msg = err instanceof Error ? err.message : String(err);
22026
+ if (msg.includes("Network error")) {
22027
+ console.error(import_chalk4.default.red("\u26A0\uFE0F Could not reach Radar API. Check your connection."));
22028
+ } else {
22029
+ console.error(import_chalk4.default.red("\u274C Authentication failed. Run: radar login"));
22030
+ }
22031
+ process.exit(1);
22032
+ }
22033
+ if (!verified?.valid) {
22034
+ console.error(import_chalk4.default.red("\u274C Invalid or expired token. Run: radar login"));
22035
+ process.exit(1);
22091
22036
  }
22092
22037
  console.log(import_chalk4.default.blue("Scanning for violations..."));
22093
22038
  const scanResult = await runScan(targetPath, {
@@ -22113,7 +22058,9 @@ async function fixCommand(targetPath, options) {
22113
22058
  let totalFixed = 0;
22114
22059
  let totalSkipped = 0;
22115
22060
  let totalFailed = 0;
22116
- let totalCost = 0;
22061
+ let totalCreditsUsed = 0;
22062
+ let creditsRemaining = -1;
22063
+ let fixCount = 0;
22117
22064
  let applyAll = false;
22118
22065
  let quit = false;
22119
22066
  const framework = scanResult.config?.stack?.framework || "NestJS";
@@ -22145,9 +22092,9 @@ ${relativePath} (${fileViolations.length} violations)
22145
22092
  const lineGroups = [...byLine.entries()].sort((a, b) => b[0] - a[0]);
22146
22093
  for (const [lineNum, lineViolations] of lineGroups) {
22147
22094
  if (quit) break;
22148
- if (totalCost >= options.maxCost) {
22095
+ if (options.maxFixes > 0 && fixCount >= options.maxFixes) {
22149
22096
  console.log(import_chalk4.default.yellow(`
22150
- Cost ceiling reached ($${totalCost.toFixed(2)}). Stopping AI fixes.`));
22097
+ Fix limit reached (${options.maxFixes}). Stopping.`));
22151
22098
  quit = true;
22152
22099
  break;
22153
22100
  }
@@ -22166,46 +22113,43 @@ ${relativePath} (${fileViolations.length} violations)
22166
22113
  }
22167
22114
  console.log("");
22168
22115
  try {
22116
+ console.log(import_chalk4.default.dim(" Requesting AI fix from Radar..."));
22169
22117
  let fix;
22170
22118
  if (lineViolations.length > 1) {
22171
- fix = await generateGroupedFix(
22172
- {
22173
- violations: lineViolations.map((v) => ({
22174
- file: v.file,
22175
- line: v.line,
22176
- ruleId: v.ruleId,
22177
- message: v.message,
22178
- severity: v.severity,
22179
- category: v.category
22180
- })),
22181
- fileContent,
22182
- surroundingCode,
22183
- framework,
22184
- orm
22185
- },
22186
- apiKey
22187
- );
22119
+ fix = await client.requestFixGrouped({
22120
+ violations: lineViolations.map((v) => ({
22121
+ file: v.file,
22122
+ line: v.line,
22123
+ ruleId: v.ruleId,
22124
+ message: v.message,
22125
+ severity: v.severity,
22126
+ category: v.category
22127
+ })),
22128
+ fileContent,
22129
+ surroundingCode,
22130
+ framework,
22131
+ orm
22132
+ });
22188
22133
  } else {
22189
22134
  const violation = lineViolations[0];
22190
- fix = await generateFix(
22191
- {
22192
- violation: {
22193
- file: violation.file,
22194
- line: violation.line,
22195
- ruleId: violation.ruleId,
22196
- message: violation.message,
22197
- severity: violation.severity,
22198
- category: violation.category
22199
- },
22200
- fileContent,
22201
- surroundingCode,
22202
- framework,
22203
- orm
22135
+ fix = await client.requestFix({
22136
+ violation: {
22137
+ file: violation.file,
22138
+ line: violation.line,
22139
+ ruleId: violation.ruleId,
22140
+ message: violation.message,
22141
+ severity: violation.severity,
22142
+ category: violation.category
22204
22143
  },
22205
- apiKey
22206
- );
22144
+ fileContent,
22145
+ surroundingCode,
22146
+ framework,
22147
+ orm
22148
+ });
22207
22149
  }
22208
- totalCost += 3e-3 * lineViolations.length;
22150
+ totalCreditsUsed += fix.creditsUsed;
22151
+ creditsRemaining = fix.creditsRemaining;
22152
+ fixCount++;
22209
22153
  console.log(import_chalk4.default.red(" BEFORE:"));
22210
22154
  fix.originalCode.split("\n").forEach((l) => console.log(import_chalk4.default.red(` - ${l}`)));
22211
22155
  console.log("");
@@ -22214,6 +22158,7 @@ ${relativePath} (${fileViolations.length} violations)
22214
22158
  console.log("");
22215
22159
  console.log(import_chalk4.default.dim(` ${fix.explanation}`));
22216
22160
  console.log(import_chalk4.default.dim(` Confidence: ${fix.confidence}`));
22161
+ console.log(import_chalk4.default.cyan(` ${fix.creditsUsed} AI credit used (${fix.creditsRemaining} remaining)`));
22217
22162
  console.log("");
22218
22163
  let shouldApply = false;
22219
22164
  if (options.dryRun) {
@@ -22259,8 +22204,22 @@ ${relativePath} (${fileViolations.length} violations)
22259
22204
  }
22260
22205
  } catch (error) {
22261
22206
  const message = error instanceof Error ? error.message : String(error);
22262
- console.log(import_chalk4.default.red(` AI fix failed: ${message}`));
22263
- totalFailed += lineViolations.length;
22207
+ if (message.includes("403") && message.includes("credits exhausted")) {
22208
+ console.log(import_chalk4.default.red(` \u274C AI credits exhausted. Upgrade for more: radar upgrade`));
22209
+ quit = true;
22210
+ break;
22211
+ } else if (message.includes("403") && message.includes("Solo")) {
22212
+ console.log(import_chalk4.default.red(` \u274C radar fix requires Solo plan or higher. Upgrade: radar upgrade`));
22213
+ quit = true;
22214
+ break;
22215
+ } else if (message.includes("401")) {
22216
+ console.log(import_chalk4.default.red(` \u274C Authentication failed. Run: radar login`));
22217
+ quit = true;
22218
+ break;
22219
+ } else {
22220
+ console.log(import_chalk4.default.red(` AI fix failed: ${message}`));
22221
+ totalFailed += lineViolations.length;
22222
+ }
22264
22223
  }
22265
22224
  }
22266
22225
  }
@@ -22271,7 +22230,10 @@ ${relativePath} (${fileViolations.length} violations)
22271
22230
  console.log(import_chalk4.default.green(` Fixed: ${totalFixed}`));
22272
22231
  console.log(import_chalk4.default.yellow(` Skipped: ${totalSkipped}`));
22273
22232
  if (totalFailed > 0) console.log(import_chalk4.default.red(` Failed: ${totalFailed}`));
22274
- console.log(import_chalk4.default.dim(` AI cost: $${totalCost.toFixed(3)}`));
22233
+ console.log(import_chalk4.default.cyan(` AI credits used: ${totalCreditsUsed}`));
22234
+ if (creditsRemaining >= 0) {
22235
+ console.log(import_chalk4.default.cyan(` Credits remaining: ${creditsRemaining}`));
22236
+ }
22275
22237
  console.log("");
22276
22238
  if (totalFixed > 0 && !options.dryRun) {
22277
22239
  console.log(import_chalk4.default.blue("Re-scanning to verify fixes...\n"));
@@ -22665,10 +22627,10 @@ program.command("check <file>").description("Check a single file against policy"
22665
22627
  program.command("init").description("Generate radar.yml + rules.yml from project structure").option("--path <dir>", "Target directory", ".").option("--architecture <pattern>", "Force architecture: ddd, hexagonal, clean, layered, mvc, event-driven").option("--regenerate-rules", "Regenerate rules.yml from existing radar.yml", false).option("--no-ai", "Skip AI refinement (deterministic only)").action(async (options) => {
22666
22628
  await initCommand(options);
22667
22629
  });
22668
- program.command("fix <path>").description("AI-powered fix: generates code diffs, applies with confirmation").option("-c, --config <path>", "Path to radar.yml", "./radar.yml").option("-r, --rules <path>", "Path to rules.yml (default: ./rules.yml)").option("--severity <level>", "Only include: critical, all", "all").option("--dry-run", "Show fixes without applying", false).option("--auto", "Apply all high-confidence fixes without asking", false).option("--file <file>", "Fix single file only").option("--max-cost <amount>", "Max AI cost in dollars", "1.00").option("--api-key <key>", "Anthropic API key (or ANTHROPIC_API_KEY env var)").action(async (targetPath, options) => {
22630
+ program.command("fix <path>").description("AI-powered fix: generates code diffs, applies with confirmation").option("-c, --config <path>", "Path to radar.yml", "./radar.yml").option("-r, --rules <path>", "Path to rules.yml (default: ./rules.yml)").option("--severity <level>", "Only include: critical, all", "all").option("--dry-run", "Show fixes without applying", false).option("--auto", "Apply all high-confidence fixes without asking", false).option("--file <file>", "Fix single file only").option("--max-fixes <count>", "Max number of AI fixes to generate (0 = unlimited)", "0").action(async (targetPath, options) => {
22669
22631
  await fixCommand(targetPath, {
22670
22632
  ...options,
22671
- maxCost: parseFloat(options.maxCost)
22633
+ maxFixes: parseInt(options.maxFixes, 10)
22672
22634
  });
22673
22635
  });
22674
22636
  program.command("validate").description("Validate radar.yml + rules.yml syntax and cross-references").option("-c, --config <path>", "Path to radar.yml", "./radar.yml").option("-r, --rules <path>", "Path to rules.yml (default: ./rules.yml)").action(async (options) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "technical-debt-radar",
3
- "version": "1.9.0",
3
+ "version": "1.11.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",