technical-debt-radar 1.11.0 → 1.12.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 +92 -18
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -21949,6 +21949,7 @@ async function validateCommand(options) {
21949
21949
  var fs5 = __toESM(require("fs/promises"));
21950
21950
  var fsSync2 = __toESM(require("fs"));
21951
21951
  var path5 = __toESM(require("path"));
21952
+ var import_child_process2 = require("child_process");
21952
21953
  var import_chalk4 = __toESM(require("chalk"));
21953
21954
  var import_inquirer2 = __toESM(require("inquirer"));
21954
21955
  var import_analyzers3 = __toESM(require_dist3());
@@ -22003,6 +22004,38 @@ async function runScan(targetPath, options) {
22003
22004
  policy
22004
22005
  };
22005
22006
  }
22007
+ async function quickScanFile(targetPath, filePath, options) {
22008
+ try {
22009
+ const result = await runScan(targetPath, { ...options, file: filePath });
22010
+ return result.violations;
22011
+ } catch {
22012
+ return [];
22013
+ }
22014
+ }
22015
+ function resolveViolationPath(vFile, projectRoot) {
22016
+ if (path5.isAbsolute(vFile)) return vFile;
22017
+ return path5.resolve(projectRoot, vFile);
22018
+ }
22019
+ function detectTestCommand(projectRoot) {
22020
+ try {
22021
+ const pkg = JSON.parse(fsSync2.readFileSync(path5.join(projectRoot, "package.json"), "utf-8"));
22022
+ const scripts = pkg.scripts ?? {};
22023
+ if (scripts.test && scripts.test !== 'echo "Error: no test specified" && exit 1') return "npm test";
22024
+ if (scripts["test:unit"]) return "npm run test:unit";
22025
+ if (scripts.jest) return "npm run jest";
22026
+ return null;
22027
+ } catch {
22028
+ return null;
22029
+ }
22030
+ }
22031
+ function runTests(projectRoot, testCmd) {
22032
+ try {
22033
+ (0, import_child_process2.execSync)(testCmd, { cwd: projectRoot, stdio: "pipe", timeout: 12e4 });
22034
+ return true;
22035
+ } catch {
22036
+ return false;
22037
+ }
22038
+ }
22006
22039
  function groupBy(items, key) {
22007
22040
  const result = {};
22008
22041
  for (const item of items) {
@@ -22034,6 +22067,16 @@ async function fixCommand(targetPath, options) {
22034
22067
  console.error(import_chalk4.default.red("\u274C Invalid or expired token. Run: radar login"));
22035
22068
  process.exit(1);
22036
22069
  }
22070
+ const projectRoot = path5.resolve(targetPath);
22071
+ let testCmd = null;
22072
+ if (options.safe) {
22073
+ testCmd = detectTestCommand(projectRoot);
22074
+ if (!testCmd) {
22075
+ console.log(import_chalk4.default.yellow("\u26A0\uFE0F No test command found in package.json. --safe will skip test verification."));
22076
+ } else {
22077
+ console.log(import_chalk4.default.dim(` Test command: ${testCmd}`));
22078
+ }
22079
+ }
22037
22080
  console.log(import_chalk4.default.blue("Scanning for violations..."));
22038
22081
  const scanResult = await runScan(targetPath, {
22039
22082
  config: options.config,
@@ -22054,10 +22097,11 @@ async function fixCommand(targetPath, options) {
22054
22097
  }
22055
22098
  console.log(import_chalk4.default.yellow(`Found ${violations.length} violations. Generating AI fixes...
22056
22099
  `));
22057
- const byFile = groupBy(violations, (v) => v.file);
22100
+ const byFile = groupBy(violations, (v) => resolveViolationPath(v.file, projectRoot));
22058
22101
  let totalFixed = 0;
22059
22102
  let totalSkipped = 0;
22060
22103
  let totalFailed = 0;
22104
+ let totalReverted = 0;
22061
22105
  let totalCreditsUsed = 0;
22062
22106
  let creditsRemaining = -1;
22063
22107
  let fixCount = 0;
@@ -22067,15 +22111,16 @@ async function fixCommand(targetPath, options) {
22067
22111
  const orm = scanResult.config?.stack?.orm || "Prisma";
22068
22112
  for (const [filePath, fileViolations] of Object.entries(byFile)) {
22069
22113
  if (quit) break;
22070
- const relativePath = path5.relative(path5.resolve(targetPath), filePath);
22114
+ const absPath = path5.resolve(filePath);
22115
+ const relativePath = path5.relative(projectRoot, absPath);
22071
22116
  console.log(import_chalk4.default.bold(`
22072
22117
  ${relativePath} (${fileViolations.length} violations)
22073
22118
  `));
22074
22119
  let fileContent;
22075
22120
  try {
22076
- fileContent = await fs5.readFile(filePath, "utf-8");
22121
+ fileContent = await fs5.readFile(absPath, "utf-8");
22077
22122
  } catch {
22078
- console.log(import_chalk4.default.red(` Could not read file: ${filePath}`));
22123
+ console.log(import_chalk4.default.red(` Could not read file: ${absPath}`));
22079
22124
  totalFailed += fileViolations.length;
22080
22125
  continue;
22081
22126
  }
@@ -22164,9 +22209,9 @@ ${relativePath} (${fileViolations.length} violations)
22164
22209
  if (options.dryRun) {
22165
22210
  console.log(import_chalk4.default.dim(" [dry-run] Would apply this fix"));
22166
22211
  totalSkipped += lineViolations.length;
22167
- } else if (applyAll || options.auto && fix.confidence === "high") {
22212
+ } else if (applyAll || options.auto) {
22168
22213
  shouldApply = true;
22169
- console.log(import_chalk4.default.green(" Auto-applying..."));
22214
+ if (!options.auto) console.log(import_chalk4.default.green(" Auto-applying (all)..."));
22170
22215
  } else {
22171
22216
  const { action } = await import_inquirer2.default.prompt([
22172
22217
  {
@@ -22196,24 +22241,52 @@ ${relativePath} (${fileViolations.length} violations)
22196
22241
  }
22197
22242
  }
22198
22243
  if (shouldApply) {
22199
- applyFix(filePath, fix);
22200
- totalFixed += lineViolations.length;
22201
- console.log(import_chalk4.default.green(` Fixed! (${lineViolations.length} violation${lineViolations.length > 1 ? "s" : ""})`));
22202
- fileContent = await fs5.readFile(filePath, "utf-8");
22244
+ const originalContent = options.safe ? fileContent : null;
22245
+ applyFix(absPath, fix);
22246
+ console.log(import_chalk4.default.green(` Applied fix (${lineViolations.length} violation${lineViolations.length > 1 ? "s" : ""})`));
22247
+ const remaining = await quickScanFile(targetPath, absPath, {
22248
+ config: options.config,
22249
+ rules: options.rules
22250
+ });
22251
+ const stillPresent = remaining.some(
22252
+ (rv) => lineViolations.some(
22253
+ (lv) => rv.ruleId === lv.ruleId && rv.file === lv.file && Math.abs(rv.line - lv.line) <= 3
22254
+ )
22255
+ );
22256
+ if (stillPresent) {
22257
+ console.log(import_chalk4.default.yellow(" \u26A0\uFE0F Fix applied but violation persists \u2014 manual review needed"));
22258
+ } else {
22259
+ console.log(import_chalk4.default.green(" \u2705 Violation resolved"));
22260
+ }
22261
+ if (options.safe && testCmd) {
22262
+ console.log(import_chalk4.default.dim(` Running tests: ${testCmd}...`));
22263
+ const testsPassed = runTests(projectRoot, testCmd);
22264
+ if (testsPassed) {
22265
+ console.log(import_chalk4.default.green(" \u2705 Tests passed"));
22266
+ totalFixed += lineViolations.length;
22267
+ } else {
22268
+ console.log(import_chalk4.default.red(" \u274C Tests failed \u2014 reverting fix"));
22269
+ fsSync2.writeFileSync(absPath, originalContent, "utf-8");
22270
+ totalReverted += lineViolations.length;
22271
+ }
22272
+ } else {
22273
+ totalFixed += lineViolations.length;
22274
+ }
22275
+ fileContent = await fs5.readFile(absPath, "utf-8");
22203
22276
  lines = fileContent.split("\n");
22204
22277
  }
22205
22278
  } catch (error) {
22206
22279
  const message = error instanceof Error ? error.message : String(error);
22207
22280
  if (message.includes("403") && message.includes("credits exhausted")) {
22208
- console.log(import_chalk4.default.red(` \u274C AI credits exhausted. Upgrade for more: radar upgrade`));
22281
+ console.log(import_chalk4.default.red(" \u274C AI credits exhausted. Upgrade for more: radar upgrade"));
22209
22282
  quit = true;
22210
22283
  break;
22211
22284
  } 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`));
22285
+ console.log(import_chalk4.default.red(" \u274C radar fix requires Solo plan or higher. Upgrade: radar upgrade"));
22213
22286
  quit = true;
22214
22287
  break;
22215
22288
  } else if (message.includes("401")) {
22216
- console.log(import_chalk4.default.red(` \u274C Authentication failed. Run: radar login`));
22289
+ console.log(import_chalk4.default.red(" \u274C Authentication failed. Run: radar login"));
22217
22290
  quit = true;
22218
22291
  break;
22219
22292
  } else {
@@ -22227,16 +22300,17 @@ ${relativePath} (${fileViolations.length} violations)
22227
22300
  console.log(import_chalk4.default.bold("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
22228
22301
  console.log(import_chalk4.default.bold(" FIX SUMMARY"));
22229
22302
  console.log(import_chalk4.default.bold("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
22230
- console.log(import_chalk4.default.green(` Fixed: ${totalFixed}`));
22231
- console.log(import_chalk4.default.yellow(` Skipped: ${totalSkipped}`));
22232
- if (totalFailed > 0) console.log(import_chalk4.default.red(` Failed: ${totalFailed}`));
22303
+ console.log(import_chalk4.default.green(` Fixed: ${totalFixed}`));
22304
+ console.log(import_chalk4.default.yellow(` Skipped: ${totalSkipped}`));
22305
+ if (totalReverted > 0) console.log(import_chalk4.default.red(` Reverted: ${totalReverted} (tests failed)`));
22306
+ if (totalFailed > 0) console.log(import_chalk4.default.red(` Failed: ${totalFailed}`));
22233
22307
  console.log(import_chalk4.default.cyan(` AI credits used: ${totalCreditsUsed}`));
22234
22308
  if (creditsRemaining >= 0) {
22235
22309
  console.log(import_chalk4.default.cyan(` Credits remaining: ${creditsRemaining}`));
22236
22310
  }
22237
22311
  console.log("");
22238
22312
  if (totalFixed > 0 && !options.dryRun) {
22239
- console.log(import_chalk4.default.blue("Re-scanning to verify fixes...\n"));
22313
+ console.log(import_chalk4.default.blue("Re-scanning to verify all fixes...\n"));
22240
22314
  const newResult = await runScan(targetPath, {
22241
22315
  config: options.config,
22242
22316
  rules: options.rules,
@@ -22627,7 +22701,7 @@ program.command("check <file>").description("Check a single file against policy"
22627
22701
  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) => {
22628
22702
  await initCommand(options);
22629
22703
  });
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) => {
22704
+ 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 fixes automatically without asking", false).option("--safe", "Run tests after each fix, revert on failure", 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) => {
22631
22705
  await fixCommand(targetPath, {
22632
22706
  ...options,
22633
22707
  maxFixes: parseInt(options.maxFixes, 10)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "technical-debt-radar",
3
- "version": "1.11.0",
3
+ "version": "1.12.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",