technical-debt-radar 1.11.0 → 1.13.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 +237 -25
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -161,6 +161,7 @@ var require_credits = __commonJS({
161
161
  (function(AiOperation2) {
162
162
  AiOperation2["FIX"] = "fix";
163
163
  AiOperation2["FIX_GROUPED"] = "fix-grouped";
164
+ AiOperation2["FIX_MULTI"] = "fix-multi";
164
165
  AiOperation2["SCAN_SUMMARY"] = "scan-summary";
165
166
  AiOperation2["PR_COMMENT"] = "pr-comment";
166
167
  AiOperation2["CROSS_FILE"] = "cross-file";
@@ -168,6 +169,7 @@ var require_credits = __commonJS({
168
169
  exports2.CREDIT_COSTS = {
169
170
  [AiOperation.FIX]: 1,
170
171
  [AiOperation.FIX_GROUPED]: 1,
172
+ [AiOperation.FIX_MULTI]: 3,
171
173
  [AiOperation.SCAN_SUMMARY]: 1,
172
174
  [AiOperation.PR_COMMENT]: 3,
173
175
  [AiOperation.CROSS_FILE]: 5
@@ -20737,6 +20739,12 @@ var RadarApiClient = class _RadarApiClient {
20737
20739
  body: JSON.stringify(data)
20738
20740
  });
20739
20741
  }
20742
+ async requestMultiFix(data) {
20743
+ return this.fetch("/cli/ai/fix-multi", {
20744
+ method: "POST",
20745
+ body: JSON.stringify(data)
20746
+ });
20747
+ }
20740
20748
  };
20741
20749
 
20742
20750
  // src/commands/scan.ts
@@ -21949,6 +21957,7 @@ async function validateCommand(options) {
21949
21957
  var fs5 = __toESM(require("fs/promises"));
21950
21958
  var fsSync2 = __toESM(require("fs"));
21951
21959
  var path5 = __toESM(require("path"));
21960
+ var import_child_process2 = require("child_process");
21952
21961
  var import_chalk4 = __toESM(require("chalk"));
21953
21962
  var import_inquirer2 = __toESM(require("inquirer"));
21954
21963
  var import_analyzers3 = __toESM(require_dist3());
@@ -22003,6 +22012,38 @@ async function runScan(targetPath, options) {
22003
22012
  policy
22004
22013
  };
22005
22014
  }
22015
+ async function quickScanFile(targetPath, filePath, options) {
22016
+ try {
22017
+ const result = await runScan(targetPath, { ...options, file: filePath });
22018
+ return result.violations;
22019
+ } catch {
22020
+ return [];
22021
+ }
22022
+ }
22023
+ function resolveViolationPath(vFile, projectRoot) {
22024
+ if (path5.isAbsolute(vFile)) return vFile;
22025
+ return path5.resolve(projectRoot, vFile);
22026
+ }
22027
+ function detectTestCommand(projectRoot) {
22028
+ try {
22029
+ const pkg = JSON.parse(fsSync2.readFileSync(path5.join(projectRoot, "package.json"), "utf-8"));
22030
+ const scripts = pkg.scripts ?? {};
22031
+ if (scripts.test && scripts.test !== 'echo "Error: no test specified" && exit 1') return "npm test";
22032
+ if (scripts["test:unit"]) return "npm run test:unit";
22033
+ if (scripts.jest) return "npm run jest";
22034
+ return null;
22035
+ } catch {
22036
+ return null;
22037
+ }
22038
+ }
22039
+ function runTests(projectRoot, testCmd) {
22040
+ try {
22041
+ (0, import_child_process2.execSync)(testCmd, { cwd: projectRoot, stdio: "pipe", timeout: 12e4 });
22042
+ return true;
22043
+ } catch {
22044
+ return false;
22045
+ }
22046
+ }
22006
22047
  function groupBy(items, key) {
22007
22048
  const result = {};
22008
22049
  for (const item of items) {
@@ -22012,6 +22053,102 @@ function groupBy(items, key) {
22012
22053
  }
22013
22054
  return result;
22014
22055
  }
22056
+ async function handleMultiFileFix(client, violation, violationFile, projectRoot, framework, architecture, filesToChange, options) {
22057
+ console.log(import_chalk4.default.magenta.bold("\n MULTI-FILE FIX"));
22058
+ console.log(import_chalk4.default.dim(" Gathering file contents..."));
22059
+ const files = {};
22060
+ const allPaths = [violationFile, ...filesToChange.map((f) => resolveViolationPath(f, projectRoot))];
22061
+ for (const fp of allPaths) {
22062
+ try {
22063
+ files[fp] = await fs5.readFile(fp, "utf-8");
22064
+ } catch {
22065
+ console.log(import_chalk4.default.yellow(` Could not read: ${fp} \u2014 skipping`));
22066
+ }
22067
+ }
22068
+ if (Object.keys(files).length < 2) {
22069
+ console.log(import_chalk4.default.yellow(" Not enough files found for multi-file fix."));
22070
+ return;
22071
+ }
22072
+ console.log(import_chalk4.default.dim(` Sending ${Object.keys(files).length} files to Radar for analysis...`));
22073
+ const result = await client.requestMultiFix({
22074
+ violation: {
22075
+ file: violation.file,
22076
+ line: violation.line,
22077
+ ruleId: violation.ruleId,
22078
+ message: violation.message,
22079
+ severity: violation.severity,
22080
+ category: violation.category
22081
+ },
22082
+ files,
22083
+ framework,
22084
+ architecture
22085
+ });
22086
+ console.log("");
22087
+ if (result.explanation) {
22088
+ console.log(import_chalk4.default.white.bold(" WHY:"));
22089
+ console.log(import_chalk4.default.white(` ${result.explanation}`));
22090
+ }
22091
+ if (result.risk) {
22092
+ console.log(import_chalk4.default.yellow(` RISK: ${result.risk}`));
22093
+ }
22094
+ if (result.pattern) {
22095
+ console.log(import_chalk4.default.cyan(` PATTERN: ${result.pattern}`));
22096
+ }
22097
+ console.log("");
22098
+ for (let i = 0; i < result.changes.length; i++) {
22099
+ const change = result.changes[i];
22100
+ const rel = path5.relative(projectRoot, resolveViolationPath(change.filePath, projectRoot));
22101
+ console.log(import_chalk4.default.bold(` FILE ${i + 1}: ${rel}`));
22102
+ console.log(import_chalk4.default.dim(` ${change.description}`));
22103
+ const beforeLines = change.before.split("\n");
22104
+ const afterLines = change.after.split("\n");
22105
+ let diffShown = 0;
22106
+ for (let j = 0; j < Math.max(beforeLines.length, afterLines.length) && diffShown < 10; j++) {
22107
+ if (beforeLines[j] !== afterLines[j]) {
22108
+ if (beforeLines[j]) console.log(import_chalk4.default.red(` - ${beforeLines[j]}`));
22109
+ if (afterLines[j]) console.log(import_chalk4.default.green(` + ${afterLines[j]}`));
22110
+ diffShown++;
22111
+ }
22112
+ }
22113
+ if (diffShown >= 10) console.log(import_chalk4.default.dim(" ... (more changes)"));
22114
+ console.log("");
22115
+ }
22116
+ console.log(import_chalk4.default.cyan(` ${result.creditsUsed} AI credits used (${result.creditsRemaining} remaining)`));
22117
+ if (options.dryRun) {
22118
+ console.log(import_chalk4.default.dim(" [dry-run] Would apply all changes"));
22119
+ return;
22120
+ }
22121
+ const originals = {};
22122
+ for (const change of result.changes) {
22123
+ const fp = resolveViolationPath(change.filePath, projectRoot);
22124
+ try {
22125
+ originals[fp] = await fs5.readFile(fp, "utf-8");
22126
+ } catch {
22127
+ originals[fp] = "";
22128
+ }
22129
+ }
22130
+ for (const change of result.changes) {
22131
+ const fp = resolveViolationPath(change.filePath, projectRoot);
22132
+ fsSync2.writeFileSync(fp, change.after, "utf-8");
22133
+ }
22134
+ console.log(import_chalk4.default.green(` Applied changes to ${result.changes.length} files`));
22135
+ if (options.safe) {
22136
+ const testCmd = detectTestCommand(projectRoot);
22137
+ if (testCmd) {
22138
+ console.log(import_chalk4.default.dim(` Running tests: ${testCmd}...`));
22139
+ const passed = runTests(projectRoot, testCmd);
22140
+ if (passed) {
22141
+ console.log(import_chalk4.default.green(" \u2705 Tests passed"));
22142
+ } else {
22143
+ console.log(import_chalk4.default.red(" \u274C Tests failed \u2014 reverting all changes"));
22144
+ for (const [fp, content] of Object.entries(originals)) {
22145
+ fsSync2.writeFileSync(fp, content, "utf-8");
22146
+ }
22147
+ return;
22148
+ }
22149
+ }
22150
+ }
22151
+ }
22015
22152
  async function fixCommand(targetPath, options) {
22016
22153
  const client = RadarApiClient.fromConfigOrEnv();
22017
22154
  if (!client) {
@@ -22034,6 +22171,16 @@ async function fixCommand(targetPath, options) {
22034
22171
  console.error(import_chalk4.default.red("\u274C Invalid or expired token. Run: radar login"));
22035
22172
  process.exit(1);
22036
22173
  }
22174
+ const projectRoot = path5.resolve(targetPath);
22175
+ let testCmd = null;
22176
+ if (options.safe) {
22177
+ testCmd = detectTestCommand(projectRoot);
22178
+ if (!testCmd) {
22179
+ console.log(import_chalk4.default.yellow("\u26A0\uFE0F No test command found in package.json. --safe will skip test verification."));
22180
+ } else {
22181
+ console.log(import_chalk4.default.dim(` Test command: ${testCmd}`));
22182
+ }
22183
+ }
22037
22184
  console.log(import_chalk4.default.blue("Scanning for violations..."));
22038
22185
  const scanResult = await runScan(targetPath, {
22039
22186
  config: options.config,
@@ -22054,10 +22201,11 @@ async function fixCommand(targetPath, options) {
22054
22201
  }
22055
22202
  console.log(import_chalk4.default.yellow(`Found ${violations.length} violations. Generating AI fixes...
22056
22203
  `));
22057
- const byFile = groupBy(violations, (v) => v.file);
22204
+ const byFile = groupBy(violations, (v) => resolveViolationPath(v.file, projectRoot));
22058
22205
  let totalFixed = 0;
22059
22206
  let totalSkipped = 0;
22060
22207
  let totalFailed = 0;
22208
+ let totalReverted = 0;
22061
22209
  let totalCreditsUsed = 0;
22062
22210
  let creditsRemaining = -1;
22063
22211
  let fixCount = 0;
@@ -22067,15 +22215,16 @@ async function fixCommand(targetPath, options) {
22067
22215
  const orm = scanResult.config?.stack?.orm || "Prisma";
22068
22216
  for (const [filePath, fileViolations] of Object.entries(byFile)) {
22069
22217
  if (quit) break;
22070
- const relativePath = path5.relative(path5.resolve(targetPath), filePath);
22218
+ const absPath = path5.resolve(filePath);
22219
+ const relativePath = path5.relative(projectRoot, absPath);
22071
22220
  console.log(import_chalk4.default.bold(`
22072
22221
  ${relativePath} (${fileViolations.length} violations)
22073
22222
  `));
22074
22223
  let fileContent;
22075
22224
  try {
22076
- fileContent = await fs5.readFile(filePath, "utf-8");
22225
+ fileContent = await fs5.readFile(absPath, "utf-8");
22077
22226
  } catch {
22078
- console.log(import_chalk4.default.red(` Could not read file: ${filePath}`));
22227
+ console.log(import_chalk4.default.red(` Could not read file: ${absPath}`));
22079
22228
  totalFailed += fileViolations.length;
22080
22229
  continue;
22081
22230
  }
@@ -22150,13 +22299,29 @@ ${relativePath} (${fileViolations.length} violations)
22150
22299
  totalCreditsUsed += fix.creditsUsed;
22151
22300
  creditsRemaining = fix.creditsRemaining;
22152
22301
  fixCount++;
22302
+ if (fix.explanation) {
22303
+ console.log(import_chalk4.default.white.bold(" WHY THIS IS A VIOLATION:"));
22304
+ console.log(import_chalk4.default.white(` ${fix.explanation}`));
22305
+ }
22306
+ if (fix.risk) {
22307
+ console.log(import_chalk4.default.yellow(` RISK: ${fix.risk}`));
22308
+ }
22309
+ if (fix.pattern) {
22310
+ console.log(import_chalk4.default.cyan(` PATTERN: ${fix.pattern}`));
22311
+ }
22312
+ if (fix.multiFile && fix.filesToChange && fix.filesToChange.length > 0) {
22313
+ console.log(import_chalk4.default.magenta(` MULTI-FILE: Also needs changes in ${fix.filesToChange.length} other file(s):`));
22314
+ for (const f of fix.filesToChange) {
22315
+ console.log(import_chalk4.default.magenta(` \u2192 ${f}`));
22316
+ }
22317
+ }
22318
+ console.log("");
22153
22319
  console.log(import_chalk4.default.red(" BEFORE:"));
22154
22320
  fix.originalCode.split("\n").forEach((l) => console.log(import_chalk4.default.red(` - ${l}`)));
22155
22321
  console.log("");
22156
22322
  console.log(import_chalk4.default.green(" AFTER:"));
22157
22323
  fix.fixedCode.split("\n").forEach((l) => console.log(import_chalk4.default.green(` + ${l}`)));
22158
22324
  console.log("");
22159
- console.log(import_chalk4.default.dim(` ${fix.explanation}`));
22160
22325
  console.log(import_chalk4.default.dim(` Confidence: ${fix.confidence}`));
22161
22326
  console.log(import_chalk4.default.cyan(` ${fix.creditsUsed} AI credit used (${fix.creditsRemaining} remaining)`));
22162
22327
  console.log("");
@@ -22164,21 +22329,23 @@ ${relativePath} (${fileViolations.length} violations)
22164
22329
  if (options.dryRun) {
22165
22330
  console.log(import_chalk4.default.dim(" [dry-run] Would apply this fix"));
22166
22331
  totalSkipped += lineViolations.length;
22167
- } else if (applyAll || options.auto && fix.confidence === "high") {
22332
+ } else if (applyAll || options.auto) {
22168
22333
  shouldApply = true;
22169
- console.log(import_chalk4.default.green(" Auto-applying..."));
22334
+ if (!options.auto) console.log(import_chalk4.default.green(" Auto-applying (all)..."));
22170
22335
  } else {
22336
+ const choices = [
22337
+ { name: "Yes - apply this fix", value: "yes" },
22338
+ ...fix.multiFile && fix.filesToChange?.length ? [{ name: `Multi-file fix (${fix.filesToChange.length + 1} files, 3 credits)`, value: "multi" }] : [],
22339
+ { name: "No - skip", value: "no" },
22340
+ { name: "All - apply all remaining fixes", value: "all" },
22341
+ { name: "Quit - stop fixing", value: "quit" }
22342
+ ];
22171
22343
  const { action } = await import_inquirer2.default.prompt([
22172
22344
  {
22173
22345
  type: "list",
22174
22346
  name: "action",
22175
22347
  message: `Apply this fix? (${lineViolations.length} violation${lineViolations.length > 1 ? "s" : ""})`,
22176
- choices: [
22177
- { name: "Yes - apply this fix", value: "yes" },
22178
- { name: "No - skip", value: "no" },
22179
- { name: "All - apply all remaining fixes", value: "all" },
22180
- { name: "Quit - stop fixing", value: "quit" }
22181
- ]
22348
+ choices
22182
22349
  }
22183
22350
  ]);
22184
22351
  if (action === "quit") {
@@ -22194,26 +22361,70 @@ ${relativePath} (${fileViolations.length} violations)
22194
22361
  totalSkipped += lineViolations.length;
22195
22362
  continue;
22196
22363
  }
22364
+ if (action === "multi") {
22365
+ await handleMultiFileFix(
22366
+ client,
22367
+ lineViolations[0],
22368
+ absPath,
22369
+ projectRoot,
22370
+ framework,
22371
+ scanResult.config?.architecture,
22372
+ fix.filesToChange ?? [],
22373
+ options
22374
+ );
22375
+ totalFixed += lineViolations.length;
22376
+ fileContent = await fs5.readFile(absPath, "utf-8");
22377
+ lines = fileContent.split("\n");
22378
+ continue;
22379
+ }
22197
22380
  }
22198
22381
  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");
22382
+ const originalContent = options.safe ? fileContent : null;
22383
+ applyFix(absPath, fix);
22384
+ console.log(import_chalk4.default.green(` Applied fix (${lineViolations.length} violation${lineViolations.length > 1 ? "s" : ""})`));
22385
+ const remaining = await quickScanFile(targetPath, absPath, {
22386
+ config: options.config,
22387
+ rules: options.rules
22388
+ });
22389
+ const stillPresent = remaining.some(
22390
+ (rv) => lineViolations.some(
22391
+ (lv) => rv.ruleId === lv.ruleId && rv.file === lv.file && Math.abs(rv.line - lv.line) <= 3
22392
+ )
22393
+ );
22394
+ if (stillPresent) {
22395
+ console.log(import_chalk4.default.yellow(" \u26A0\uFE0F Fix applied but violation persists \u2014 manual review needed"));
22396
+ } else {
22397
+ console.log(import_chalk4.default.green(" \u2705 Violation resolved"));
22398
+ }
22399
+ if (options.safe && testCmd) {
22400
+ console.log(import_chalk4.default.dim(` Running tests: ${testCmd}...`));
22401
+ const testsPassed = runTests(projectRoot, testCmd);
22402
+ if (testsPassed) {
22403
+ console.log(import_chalk4.default.green(" \u2705 Tests passed"));
22404
+ totalFixed += lineViolations.length;
22405
+ } else {
22406
+ console.log(import_chalk4.default.red(" \u274C Tests failed \u2014 reverting fix"));
22407
+ fsSync2.writeFileSync(absPath, originalContent, "utf-8");
22408
+ totalReverted += lineViolations.length;
22409
+ }
22410
+ } else {
22411
+ totalFixed += lineViolations.length;
22412
+ }
22413
+ fileContent = await fs5.readFile(absPath, "utf-8");
22203
22414
  lines = fileContent.split("\n");
22204
22415
  }
22205
22416
  } catch (error) {
22206
22417
  const message = error instanceof Error ? error.message : String(error);
22207
22418
  if (message.includes("403") && message.includes("credits exhausted")) {
22208
- console.log(import_chalk4.default.red(` \u274C AI credits exhausted. Upgrade for more: radar upgrade`));
22419
+ console.log(import_chalk4.default.red(" \u274C AI credits exhausted. Upgrade for more: radar upgrade"));
22209
22420
  quit = true;
22210
22421
  break;
22211
22422
  } 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`));
22423
+ console.log(import_chalk4.default.red(" \u274C radar fix requires Solo plan or higher. Upgrade: radar upgrade"));
22213
22424
  quit = true;
22214
22425
  break;
22215
22426
  } else if (message.includes("401")) {
22216
- console.log(import_chalk4.default.red(` \u274C Authentication failed. Run: radar login`));
22427
+ console.log(import_chalk4.default.red(" \u274C Authentication failed. Run: radar login"));
22217
22428
  quit = true;
22218
22429
  break;
22219
22430
  } else {
@@ -22227,16 +22438,17 @@ ${relativePath} (${fileViolations.length} violations)
22227
22438
  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
22439
  console.log(import_chalk4.default.bold(" FIX SUMMARY"));
22229
22440
  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}`));
22441
+ console.log(import_chalk4.default.green(` Fixed: ${totalFixed}`));
22442
+ console.log(import_chalk4.default.yellow(` Skipped: ${totalSkipped}`));
22443
+ if (totalReverted > 0) console.log(import_chalk4.default.red(` Reverted: ${totalReverted} (tests failed)`));
22444
+ if (totalFailed > 0) console.log(import_chalk4.default.red(` Failed: ${totalFailed}`));
22233
22445
  console.log(import_chalk4.default.cyan(` AI credits used: ${totalCreditsUsed}`));
22234
22446
  if (creditsRemaining >= 0) {
22235
22447
  console.log(import_chalk4.default.cyan(` Credits remaining: ${creditsRemaining}`));
22236
22448
  }
22237
22449
  console.log("");
22238
22450
  if (totalFixed > 0 && !options.dryRun) {
22239
- console.log(import_chalk4.default.blue("Re-scanning to verify fixes...\n"));
22451
+ console.log(import_chalk4.default.blue("Re-scanning to verify all fixes...\n"));
22240
22452
  const newResult = await runScan(targetPath, {
22241
22453
  config: options.config,
22242
22454
  rules: options.rules,
@@ -22627,7 +22839,7 @@ program.command("check <file>").description("Check a single file against policy"
22627
22839
  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
22840
  await initCommand(options);
22629
22841
  });
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) => {
22842
+ 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
22843
  await fixCommand(targetPath, {
22632
22844
  ...options,
22633
22845
  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.13.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",