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.
- package/dist/index.js +246 -284
- 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
|
|
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
22079
|
-
|
|
22080
|
-
|
|
22081
|
-
|
|
22082
|
-
|
|
22083
|
-
|
|
22084
|
-
|
|
22085
|
-
|
|
22086
|
-
|
|
22087
|
-
|
|
22088
|
-
|
|
22089
|
-
|
|
22090
|
-
|
|
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
|
|
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 (
|
|
22095
|
+
if (options.maxFixes > 0 && fixCount >= options.maxFixes) {
|
|
22149
22096
|
console.log(import_chalk4.default.yellow(`
|
|
22150
|
-
|
|
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
|
|
22172
|
-
{
|
|
22173
|
-
|
|
22174
|
-
|
|
22175
|
-
|
|
22176
|
-
|
|
22177
|
-
|
|
22178
|
-
|
|
22179
|
-
|
|
22180
|
-
|
|
22181
|
-
|
|
22182
|
-
|
|
22183
|
-
|
|
22184
|
-
|
|
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
|
|
22191
|
-
{
|
|
22192
|
-
|
|
22193
|
-
|
|
22194
|
-
|
|
22195
|
-
|
|
22196
|
-
|
|
22197
|
-
|
|
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
|
-
|
|
22206
|
-
|
|
22144
|
+
fileContent,
|
|
22145
|
+
surroundingCode,
|
|
22146
|
+
framework,
|
|
22147
|
+
orm
|
|
22148
|
+
});
|
|
22207
22149
|
}
|
|
22208
|
-
|
|
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
|
-
|
|
22263
|
-
|
|
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.
|
|
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-
|
|
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
|
-
|
|
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) => {
|