aislop 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -13,6 +13,23 @@ import pc from "picocolors";
13
13
  import ora from "ora";
14
14
  import { emitKeypressEvents } from "node:readline";
15
15
 
16
+ //#region \0rolldown/runtime.js
17
+ var __defProp = Object.defineProperty;
18
+ var __exportAll = (all, no_symbols) => {
19
+ let target = {};
20
+ for (var name in all) {
21
+ __defProp(target, name, {
22
+ get: all[name],
23
+ enumerable: true
24
+ });
25
+ }
26
+ if (!no_symbols) {
27
+ __defProp(target, Symbol.toStringTag, { value: "Module" });
28
+ }
29
+ return target;
30
+ };
31
+
32
+ //#endregion
16
33
  //#region src/config/defaults.ts
17
34
  const DEFAULT_CONFIG = {
18
35
  version: 1,
@@ -36,18 +53,18 @@ const DEFAULT_CONFIG = {
36
53
  },
37
54
  scoring: {
38
55
  weights: {
39
- format: .5,
40
- lint: 1,
41
- "code-quality": 1.5,
42
- "ai-slop": 1,
56
+ format: .3,
57
+ lint: .6,
58
+ "code-quality": .8,
59
+ "ai-slop": 2.5,
43
60
  architecture: 1,
44
- security: 2
61
+ security: 1.5
45
62
  },
46
63
  thresholds: {
47
64
  good: 75,
48
65
  ok: 50
49
66
  },
50
- smoothing: 10
67
+ smoothing: 20
51
68
  },
52
69
  ci: {
53
70
  failBelow: 0,
@@ -77,15 +94,16 @@ security:
77
94
 
78
95
  scoring:
79
96
  weights:
80
- format: 0.5
81
- lint: 1.0
82
- code-quality: 1.5
83
- ai-slop: 1.0
97
+ format: 0.3
98
+ lint: 0.6
99
+ code-quality: 0.8
100
+ ai-slop: 2.5
84
101
  architecture: 1.0
85
- security: 2.0
102
+ security: 1.5
86
103
  thresholds:
87
104
  good: 75
88
105
  ok: 50
106
+ smoothing: 20
89
107
 
90
108
  ci:
91
109
  failBelow: 0
@@ -113,12 +131,12 @@ const DEFAULT_RULES_YAML = `# Architecture rules (BYO)
113
131
  //#endregion
114
132
  //#region src/config/schema.ts
115
133
  const DEFAULT_WEIGHTS = {
116
- format: .5,
117
- lint: 1,
118
- "code-quality": 1.5,
119
- "ai-slop": 1,
134
+ format: .3,
135
+ lint: .6,
136
+ "code-quality": .8,
137
+ "ai-slop": 2.5,
120
138
  architecture: 1,
121
- security: 2
139
+ security: 1.5
122
140
  };
123
141
  const EnginesSchema = z.object({
124
142
  format: z.boolean().default(true),
@@ -148,7 +166,7 @@ const ScoringSchema = z.object({
148
166
  good: 75,
149
167
  ok: 50
150
168
  })),
151
- smoothing: z.number().nonnegative().default(10)
169
+ smoothing: z.number().nonnegative().default(20)
152
170
  });
153
171
  const CiSchema = z.object({
154
172
  failBelow: z.number().default(0),
@@ -181,7 +199,7 @@ const AislopConfigSchema = z.object({
181
199
  good: 75,
182
200
  ok: 50
183
201
  },
184
- smoothing: 10
202
+ smoothing: 20
185
203
  })),
186
204
  ci: CiSchema.default(() => ({
187
205
  failBelow: 0,
@@ -1842,10 +1860,10 @@ const codeQualityEngine = {
1842
1860
 
1843
1861
  //#endregion
1844
1862
  //#region src/engines/format/biome.ts
1845
- const esmRequire$2 = createRequire(import.meta.url);
1863
+ const esmRequire$3 = createRequire(import.meta.url);
1846
1864
  const resolveLocalBiomeScript = () => {
1847
1865
  try {
1848
- const packageJsonPath = esmRequire$2.resolve("@biomejs/biome/package.json");
1866
+ const packageJsonPath = esmRequire$3.resolve("@biomejs/biome/package.json");
1849
1867
  return path.join(path.dirname(packageJsonPath), "bin", "biome");
1850
1868
  } catch {
1851
1869
  return null;
@@ -1919,12 +1937,14 @@ const parseBiomeJsonOutput = (output, rootDir) => {
1919
1937
  const rawPath = entry.location?.path;
1920
1938
  if (!rawPath) continue;
1921
1939
  const severity = entry.severity === "error" ? "error" : "warning";
1940
+ const rawMessage = entry.message ?? "";
1941
+ const message = !rawMessage || rawMessage.toLowerCase().includes("would have printed") ? "File is not formatted correctly" : rawMessage;
1922
1942
  diagnostics.push({
1923
1943
  filePath: path.isAbsolute(rawPath) ? path.relative(rootDir, rawPath) : rawPath,
1924
1944
  engine: "format",
1925
1945
  rule: "formatting",
1926
1946
  severity,
1927
- message: entry.message ?? "File is not formatted correctly",
1947
+ message,
1928
1948
  help: "Run `aislop fix` to auto-format",
1929
1949
  line: entry.location?.start?.line ?? 0,
1930
1950
  column: entry.location?.start?.column ?? 0,
@@ -2092,7 +2112,7 @@ const fixGofmt = async (rootDirectory) => {
2092
2112
  //#endregion
2093
2113
  //#region src/utils/tooling.ts
2094
2114
  const THIS_FILE = fileURLToPath(import.meta.url);
2095
- const esmRequire$1 = createRequire(import.meta.url);
2115
+ const esmRequire$2 = createRequire(import.meta.url);
2096
2116
  const resolvePackageRoot = (startFile) => {
2097
2117
  let current = path.dirname(startFile);
2098
2118
  while (true) {
@@ -2123,7 +2143,7 @@ const isToolAvailable = async (toolName) => {
2123
2143
  };
2124
2144
  const isNodePackageAvailable = (packageName) => {
2125
2145
  try {
2126
- esmRequire$1.resolve(`${packageName}/package.json`);
2146
+ esmRequire$2.resolve(`${packageName}/package.json`);
2127
2147
  return true;
2128
2148
  } catch {
2129
2149
  return false;
@@ -2406,10 +2426,10 @@ const createOxlintConfig = (options) => {
2406
2426
 
2407
2427
  //#endregion
2408
2428
  //#region src/engines/lint/oxlint.ts
2409
- const esmRequire = createRequire(import.meta.url);
2429
+ const esmRequire$1 = createRequire(import.meta.url);
2410
2430
  const resolveOxlintBinary = () => {
2411
2431
  try {
2412
- const oxlintMainPath = esmRequire.resolve("oxlint");
2432
+ const oxlintMainPath = esmRequire$1.resolve("oxlint");
2413
2433
  const oxlintDir = path.resolve(path.dirname(oxlintMainPath), "..");
2414
2434
  return path.join(oxlintDir, "bin", "oxlint");
2415
2435
  } catch {
@@ -2417,6 +2437,10 @@ const resolveOxlintBinary = () => {
2417
2437
  }
2418
2438
  };
2419
2439
  const parseRuleCode = (code) => {
2440
+ if (!code) return {
2441
+ plugin: "unknown",
2442
+ rule: "unknown"
2443
+ };
2420
2444
  const match = code.match(/^(.+)\((.+)\)$/);
2421
2445
  if (!match) return {
2422
2446
  plugin: "unknown",
@@ -2444,6 +2468,143 @@ const detectTestFramework = (rootDir) => {
2444
2468
  } catch {}
2445
2469
  return null;
2446
2470
  };
2471
+ const extractUnusedVarName = (message) => {
2472
+ const variableMatch = message.match(/Variable '([^']+)' is declared but never used/);
2473
+ if (variableMatch?.[1]) return {
2474
+ name: variableMatch[1],
2475
+ type: "variable"
2476
+ };
2477
+ const paramMatch = message.match(/Parameter '([^']+)' is declared but never used/);
2478
+ if (paramMatch?.[1]) return {
2479
+ name: paramMatch[1],
2480
+ type: "parameter"
2481
+ };
2482
+ const catchMatch = message.match(/Catch parameter '([^']+)' is caught but never used/);
2483
+ if (catchMatch?.[1]) return {
2484
+ name: catchMatch[1],
2485
+ type: "parameter"
2486
+ };
2487
+ return null;
2488
+ };
2489
+ const collectUnusedVarCandidates = (diagnostics) => diagnostics.filter((d) => d.rule === "eslint/no-unused-vars").map((d) => {
2490
+ const extracted = extractUnusedVarName(d.message);
2491
+ if (!extracted || extracted.name.startsWith("_")) return null;
2492
+ return {
2493
+ filePath: d.filePath,
2494
+ line: d.line,
2495
+ column: d.column,
2496
+ name: extracted.name,
2497
+ type: extracted.type
2498
+ };
2499
+ }).filter((candidate) => candidate !== null);
2500
+ const prefixIdentifierOnLine = (line, name, column, type) => {
2501
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2502
+ if (type === "parameter") {
2503
+ const destructureMatch = line.match(/\{[^}]*\}/);
2504
+ if (destructureMatch) {
2505
+ const { 0: content, index: start } = destructureMatch;
2506
+ const propPattern = new RegExp(`(?<!:\\s*)\\b${escaped}\\b(?!\\s*:)`);
2507
+ if (propPattern.test(content)) {
2508
+ const updated = content.replace(propPattern, `${name}: _${name}`);
2509
+ if (updated !== content) return line.slice(0, start) + updated + line.slice(start + content.length);
2510
+ }
2511
+ }
2512
+ const paramPattern = new RegExp(`\\b${escaped}\\b`);
2513
+ return paramPattern.test(line) ? line.replace(paramPattern, `_${name}`) : line;
2514
+ }
2515
+ const assignPattern = new RegExp(`(\\s*)(const|let|var)\\s+${escaped}\\s*=\\s*(.+)$`);
2516
+ const assignMatch = line.match(assignPattern);
2517
+ if (assignMatch) {
2518
+ const [, indent, , expression] = assignMatch;
2519
+ if (/await\s/.test(expression) || /\w+\s*\(/.test(expression)) return `${indent}${expression}`;
2520
+ return "";
2521
+ }
2522
+ const destructureMatch = line.match(/\{[^}]*\}/);
2523
+ if (destructureMatch) {
2524
+ const { 0: content, index: start } = destructureMatch;
2525
+ if (new RegExp(`\\b${escaped}\\b`).test(content)) {
2526
+ let updated = content.replace(new RegExp(`\\b${escaped}\\b\\s*,?`), "");
2527
+ updated = updated.replace(/,\s*\},/, "}").replace(/\{,\s*/, "{").replace(/\s*,\s*\}/, "}");
2528
+ if (updated !== content) return line.slice(0, start) + updated + line.slice(start + content.length);
2529
+ }
2530
+ }
2531
+ let bestStart = -1;
2532
+ let bestEnd = -1;
2533
+ let bestDistance = Number.POSITIVE_INFINITY;
2534
+ const target = Math.max(0, column - 1);
2535
+ for (const match of line.matchAll(new RegExp(`\\b${escaped}\\b`, "g"))) {
2536
+ if (match.index === void 0) continue;
2537
+ const start = match.index;
2538
+ const end = start + name.length;
2539
+ if (start > 0 && line[start - 1] === "_") continue;
2540
+ const distance = target >= start && target <= end ? 0 : Math.abs(start - target);
2541
+ if (distance < bestDistance) {
2542
+ bestDistance = distance;
2543
+ bestStart = start;
2544
+ bestEnd = end;
2545
+ }
2546
+ }
2547
+ if (bestStart < 0 || bestEnd < 0) return line;
2548
+ return `${line.slice(0, bestStart)}_${name}${line.slice(bestEnd)}`;
2549
+ };
2550
+ const applyUnusedVarPrefixFixes = (rootDirectory, candidates) => {
2551
+ const byFile = /* @__PURE__ */ new Map();
2552
+ for (const candidate of candidates) {
2553
+ const absolute = path.isAbsolute(candidate.filePath) ? candidate.filePath : path.join(rootDirectory, candidate.filePath);
2554
+ const entries = byFile.get(absolute) ?? [];
2555
+ entries.push(candidate);
2556
+ byFile.set(absolute, entries);
2557
+ }
2558
+ for (const [filePath, fileCandidates] of byFile.entries()) {
2559
+ if (!fs.existsSync(filePath)) continue;
2560
+ const lines = fs.readFileSync(filePath, "utf-8").split("\n");
2561
+ const ordered = [...fileCandidates].sort((a, b) => {
2562
+ if (a.line !== b.line) return a.line - b.line;
2563
+ return a.column - b.column;
2564
+ });
2565
+ let changed = false;
2566
+ for (const candidate of ordered) {
2567
+ const lineIndex = candidate.line - 1;
2568
+ if (lineIndex < 0 || lineIndex >= lines.length) continue;
2569
+ const current = lines[lineIndex];
2570
+ const updated = prefixIdentifierOnLine(current, candidate.name, candidate.column, candidate.type);
2571
+ if (updated !== current) {
2572
+ lines[lineIndex] = updated;
2573
+ changed = true;
2574
+ }
2575
+ }
2576
+ if (changed) fs.writeFileSync(filePath, lines.join("\n"));
2577
+ }
2578
+ };
2579
+ const removeDuplicateKeyLines = (rootDirectory, diagnostics) => {
2580
+ const byFile = /* @__PURE__ */ new Map();
2581
+ for (const d of diagnostics) {
2582
+ const keyMatch = d.message.match(/Duplicate key '([^']+)'/);
2583
+ if (!keyMatch) continue;
2584
+ const absolute = path.isAbsolute(d.filePath) ? d.filePath : path.join(rootDirectory, d.filePath);
2585
+ const entries = byFile.get(absolute) ?? [];
2586
+ entries.push({
2587
+ key: keyMatch[1],
2588
+ line: d.line
2589
+ });
2590
+ byFile.set(absolute, entries);
2591
+ }
2592
+ for (const [filePath, dupes] of byFile) {
2593
+ if (!fs.existsSync(filePath)) continue;
2594
+ const lines = fs.readFileSync(filePath, "utf-8").split("\n");
2595
+ const toRemove = /* @__PURE__ */ new Set();
2596
+ for (const { key } of dupes) {
2597
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2598
+ const keyPattern = new RegExp(`^\\s*['"]?${escaped}['"]?\\s*:|^\\s*${escaped}\\s*:`);
2599
+ const matches = [];
2600
+ for (let i = 0; i < lines.length; i++) if (keyPattern.test(lines[i])) matches.push(i);
2601
+ for (let j = 1; j < matches.length; j++) toRemove.add(matches[j]);
2602
+ }
2603
+ if (toRemove.size === 0) continue;
2604
+ const filtered = lines.filter((_, i) => !toRemove.has(i));
2605
+ fs.writeFileSync(filePath, filtered.join("\n"));
2606
+ }
2607
+ };
2447
2608
  const runOxlint = async (context) => {
2448
2609
  const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-${process.pid}.json`);
2449
2610
  const config = createOxlintConfig({
@@ -2506,6 +2667,7 @@ const fixOxlint = async (context) => {
2506
2667
  configPath,
2507
2668
  "--fix",
2508
2669
  "--fix-suggestions",
2670
+ "--fix-dangerously",
2509
2671
  "."
2510
2672
  ];
2511
2673
  const result = await runSubprocess(process.execPath, args, {
@@ -2513,10 +2675,18 @@ const fixOxlint = async (context) => {
2513
2675
  timeout: 12e4
2514
2676
  });
2515
2677
  if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `Oxlint exited with code ${result.exitCode}`);
2678
+ const remaining = await runOxlint(context);
2679
+ const candidates = collectUnusedVarCandidates(remaining);
2680
+ if (candidates.length > 0) applyUnusedVarPrefixFixes(context.rootDirectory, candidates);
2681
+ const duplicateKeys = remaining.filter((d) => d.message.startsWith("Duplicate key"));
2682
+ if (duplicateKeys.length > 0) removeDuplicateKeyLines(context.rootDirectory, duplicateKeys);
2516
2683
  } finally {
2517
2684
  if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
2518
2685
  }
2519
2686
  };
2687
+ const fixOxlintForce = async (context) => {
2688
+ return fixOxlint(context);
2689
+ };
2520
2690
 
2521
2691
  //#endregion
2522
2692
  //#region src/engines/lint/ruff.ts
@@ -2559,6 +2729,18 @@ const fixRuffLint = async (rootDirectory) => {
2559
2729
  });
2560
2730
  if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `ruff check --fix exited with code ${result.exitCode}`);
2561
2731
  };
2732
+ const fixRuffLintForce = async (rootDirectory) => {
2733
+ const result = await runSubprocess(resolveToolBinary("ruff"), [
2734
+ "check",
2735
+ "--fix",
2736
+ "--unsafe-fixes",
2737
+ rootDirectory
2738
+ ], {
2739
+ cwd: rootDirectory,
2740
+ timeout: 6e4
2741
+ });
2742
+ if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `ruff check --fix exited with code ${result.exitCode}`);
2743
+ };
2562
2744
 
2563
2745
  //#endregion
2564
2746
  //#region src/engines/lint/index.ts
@@ -2569,7 +2751,7 @@ const lintEngine = {
2569
2751
  const { languages, installedTools } = context;
2570
2752
  const promises = [];
2571
2753
  if (languages.includes("typescript") || languages.includes("javascript")) promises.push(runOxlint(context));
2572
- if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-vDz4kh9-.js").then((mod) => mod.runExpoDoctor(context)));
2754
+ if (context.frameworks.includes("expo")) promises.push(Promise.resolve().then(() => expo_doctor_exports).then((mod) => mod.runExpoDoctor(context)));
2573
2755
  if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
2574
2756
  if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
2575
2757
  if (languages.includes("rust") && installedTools["cargo"]) promises.push(runGenericLinter(context, "rust"));
@@ -3194,11 +3376,11 @@ const logger = {
3194
3376
  * Application version — injected at build time by tsdown from package.json.
3195
3377
  * The fallback should always match the "version" field in package.json.
3196
3378
  */
3197
- const APP_VERSION = "0.2.1";
3379
+ const APP_VERSION = "0.3.1";
3198
3380
 
3199
3381
  //#endregion
3200
3382
  //#region src/output/layout.ts
3201
- const formatElapsed$1 = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
3383
+ const formatElapsed$2 = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
3202
3384
  const printCommandHeader = (commandName) => {
3203
3385
  logger.log(`${highlighter.bold(`aislop ${commandName.toLowerCase()}`)} ${highlighter.dim(`v${APP_VERSION}`)}`);
3204
3386
  logger.break();
@@ -3213,7 +3395,7 @@ const printProjectMetadata = (project) => {
3213
3395
 
3214
3396
  //#endregion
3215
3397
  //#region src/output/scan-progress.ts
3216
- const SPINNER_FRAMES = [
3398
+ const SPINNER_FRAMES$1 = [
3217
3399
  "⠋",
3218
3400
  "⠙",
3219
3401
  "⠹",
@@ -3226,20 +3408,20 @@ const SPINNER_FRAMES = [
3226
3408
  "⠏"
3227
3409
  ];
3228
3410
  const shouldRenderLiveScanProgress = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
3229
- const formatElapsed = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
3411
+ const formatElapsed$1 = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
3230
3412
  const truncateText = (text, maxLength = 52) => text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}…`;
3231
3413
  const getIssueSummary = (result) => {
3232
3414
  const errors = result.diagnostics.filter((d) => d.severity === "error").length;
3233
3415
  const warnings = result.diagnostics.filter((d) => d.severity === "warning").length;
3234
- if (errors === 0 && warnings === 0) return `Done (0 issues, ${formatElapsed(result.elapsed)})`;
3416
+ if (errors === 0 && warnings === 0) return `Done (0 issues, ${formatElapsed$1(result.elapsed)})`;
3235
3417
  const parts = [];
3236
3418
  if (errors > 0) parts.push(`${errors} error${errors === 1 ? "" : "s"}`);
3237
3419
  if (warnings > 0) parts.push(`${warnings} warning${warnings === 1 ? "" : "s"}`);
3238
- return `Done (${parts.join(", ")}, ${formatElapsed(result.elapsed)})`;
3420
+ return `Done (${parts.join(", ")}, ${formatElapsed$1(result.elapsed)})`;
3239
3421
  };
3240
- const getStatusParts = (state, frameIndex) => {
3422
+ const getStatusParts$1 = (state, frameIndex) => {
3241
3423
  if (state.status === "running") return {
3242
- icon: highlighter.info(SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length]),
3424
+ icon: highlighter.info(SPINNER_FRAMES$1[frameIndex % SPINNER_FRAMES$1.length]),
3243
3425
  detail: highlighter.info("Running")
3244
3426
  };
3245
3427
  if (state.status === "done") {
@@ -3278,11 +3460,11 @@ const renderScanProgressBlock = (states, frameIndex) => {
3278
3460
  const headingStatus = completedCount === states.length ? highlighter.dim("complete") : runningCount > 0 ? highlighter.dim(`${runningCount} running`) : highlighter.dim("starting");
3279
3461
  return `${[` ${highlighter.bold(`Engines ${completedCount}/${states.length}`)} ${headingStatus}`, ...states.map((state) => {
3280
3462
  const label = getEngineLabel(state.engine).padEnd(labelWidth, " ");
3281
- const { icon, detail } = getStatusParts(state, frameIndex);
3463
+ const { icon, detail } = getStatusParts$1(state, frameIndex);
3282
3464
  return ` ${icon} ${label} ${detail}`;
3283
3465
  })].join("\n")}\n`;
3284
3466
  };
3285
- const clearRenderedLines$1 = (lineCount) => {
3467
+ const clearRenderedLines$2 = (lineCount) => {
3286
3468
  if (lineCount === 0) return;
3287
3469
  process.stderr.write(`\u001B[${lineCount}F`);
3288
3470
  for (let index = 0; index < lineCount; index += 1) {
@@ -3334,7 +3516,7 @@ var ScanProgressRenderer = class {
3334
3516
  }
3335
3517
  render() {
3336
3518
  if (!shouldRenderLiveScanProgress()) return;
3337
- if (this.previousLineCount > 0) clearRenderedLines$1(this.previousLineCount);
3519
+ if (this.previousLineCount > 0) clearRenderedLines$2(this.previousLineCount);
3338
3520
  const output = renderScanProgressBlock(this.states, this.frameIndex);
3339
3521
  process.stderr.write(output);
3340
3522
  this.previousLineCount = output.split("\n").length - 1;
@@ -3976,6 +4158,42 @@ const doctorCommand = async (directory) => {
3976
4158
  printDoctorConclusion(isAllGood());
3977
4159
  };
3978
4160
 
4161
+ //#endregion
4162
+ //#region src/engines/ai-slop/dead-patterns-fix.ts
4163
+ /**
4164
+ * Removes lines flagged as fixable by the trivial-comment and dead-pattern detectors.
4165
+ * Specifically handles:
4166
+ * - ai-slop/trivial-comment (trivial comments that restate the code)
4167
+ * - ai-slop/console-leftover (console.log/debug/info left in production)
4168
+ */
4169
+ const fixDeadPatterns = async (context) => {
4170
+ const fixable = [...await detectTrivialComments(context), ...await detectDeadPatterns(context)].filter((d) => d.fixable);
4171
+ if (fixable.length === 0) return;
4172
+ const byFile = /* @__PURE__ */ new Map();
4173
+ for (const d of fixable) {
4174
+ const absolute = path.isAbsolute(d.filePath) ? d.filePath : path.join(context.rootDirectory, d.filePath);
4175
+ const lines = byFile.get(absolute) ?? /* @__PURE__ */ new Set();
4176
+ lines.add(d.line);
4177
+ byFile.set(absolute, lines);
4178
+ }
4179
+ for (const [filePath, lineNumbers] of byFile) {
4180
+ if (!fs.existsSync(filePath)) continue;
4181
+ const lines = fs.readFileSync(filePath, "utf-8").split("\n");
4182
+ const filtered = [];
4183
+ for (let i = 0; i < lines.length; i++) {
4184
+ const lineNo = i + 1;
4185
+ if (lineNumbers.has(lineNo)) continue;
4186
+ filtered.push(lines[i]);
4187
+ }
4188
+ const collapsed = [];
4189
+ for (const line of filtered) {
4190
+ if (line.trim() === "" && collapsed.length > 0 && collapsed[collapsed.length - 1].trim() === "") continue;
4191
+ collapsed.push(line);
4192
+ }
4193
+ fs.writeFileSync(filePath, collapsed.join("\n"));
4194
+ }
4195
+ };
4196
+
3979
4197
  //#endregion
3980
4198
  //#region src/engines/ai-slop/unused-imports-fix.ts
3981
4199
  const fixUnusedImports = async (context) => {
@@ -3997,7 +4215,7 @@ const fixUnusedImports = async (context) => {
3997
4215
  for (const [lineNo, syms] of symbolsByLine) {
3998
4216
  const lineIdx = lineNo - 1;
3999
4217
  const allUnused = syms.every((s) => unusedNames.has(s.name));
4000
- const importSpan = getImportSpan(lineIdx, analysis.importLines);
4218
+ const importSpan = JS_EXTENSIONS.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
4001
4219
  if (allUnused) for (const idx of importSpan) linesToRemove.add(idx);
4002
4220
  else if (JS_EXTENSIONS.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
4003
4221
  else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, syms, unusedNames);
@@ -4010,11 +4228,14 @@ const fixUnusedImports = async (context) => {
4010
4228
  fs.writeFileSync(filePath, filtered.join("\n"));
4011
4229
  }
4012
4230
  };
4013
- const getImportSpan = (startIdx, importLines) => {
4231
+ const getJsImportSpan = (lines, startIdx) => {
4014
4232
  const span = [startIdx];
4233
+ let fullImport = lines[startIdx]?.trim() ?? "";
4234
+ if (!fullImport.startsWith("import ")) return span;
4015
4235
  let idx = startIdx + 1;
4016
- while (importLines.has(idx)) {
4236
+ while (!fullImport.includes("from") && idx < lines.length) {
4017
4237
  span.push(idx);
4238
+ fullImport += ` ${lines[idx].trim()}`;
4018
4239
  idx++;
4019
4240
  }
4020
4241
  return span;
@@ -4024,23 +4245,31 @@ const rewriteJsImportSpan = (lines, span, syms, unusedNames) => {
4024
4245
  const namedMatch = fullImport.match(/\{([^}]+)\}/s);
4025
4246
  if (!namedMatch) return;
4026
4247
  const unusedNamed = syms.filter((s) => !s.isDefault && !s.isNamespace && unusedNames.has(s.name));
4027
- if (unusedNamed.length === 0) return;
4248
+ const defaultUnused = syms.some((s) => s.isDefault && unusedNames.has(s.name));
4249
+ if (unusedNamed.length === 0 && !defaultUnused) return;
4028
4250
  const unusedNamedSet = new Set(unusedNamed.map((s) => s.name));
4029
4251
  const keptSpecifiers = namedMatch[1].split(",").map((s) => s.trim()).filter(Boolean).filter((spec) => {
4030
4252
  const parts = spec.split(/\s+as\s+/);
4031
4253
  const localName = parts.length > 1 ? parts[1].trim().replace(/^type\s+/, "") : parts[0].trim().replace(/^type\s+/, "");
4032
4254
  return !unusedNamedSet.has(localName);
4033
4255
  });
4256
+ const fromMatch = fullImport.match(/from\s+["']([^"']+)["'];?/);
4257
+ const fromClause = fromMatch ? `from "${fromMatch[1]}"` : "";
4034
4258
  if (keptSpecifiers.length === 0) {
4035
- if (syms.find((s) => s.isDefault && !unusedNames.has(s.name))) {
4036
- const rewritten = fullImport.replace(/,\s*\{[^}]*\}/s, "").replace(/\{[^}]*\}\s*,?\s*/s, "");
4037
- lines[span[0]] = rewritten.replace(/\n/g, " ").replace(/\s+/g, " ");
4259
+ const usedDefault = syms.find((s) => s.isDefault && !unusedNames.has(s.name));
4260
+ if (usedDefault) {
4261
+ const defaultMatch = fullImport.match(/^import\s+(\w+)/);
4262
+ const defaultName = defaultMatch ? defaultMatch[1] : usedDefault.name;
4263
+ lines[span[0]] = `import ${defaultName} ${fromClause};`;
4038
4264
  for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
4039
- }
4265
+ } else for (const idx of span) lines[idx] = REMOVE_MARKER;
4266
+ return;
4267
+ }
4268
+ if (defaultUnused) {
4269
+ lines[span[0]] = `import { ${keptSpecifiers.join(", ")} } ${fromClause};`;
4270
+ for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
4040
4271
  return;
4041
4272
  }
4042
- const fromMatch = fullImport.match(/\}\s*(from\s+.+)$/s);
4043
- const fromClause = fromMatch ? fromMatch[1].trim() : "";
4044
4273
  const importPrefix = fullImport.match(/^(import\s+(?:\w+\s*,\s*)?)/);
4045
4274
  const prefix = importPrefix ? importPrefix[1] : "import ";
4046
4275
  const wasMultiLine = span.length > 1;
@@ -4048,8 +4277,8 @@ const rewriteJsImportSpan = (lines, span, syms, unusedNames) => {
4048
4277
  if (wasMultiLine && keptSpecifiers.length > 2) {
4049
4278
  const indentMatch = lines[span[1]]?.match(/^(\s+)/);
4050
4279
  const indent = indentMatch ? indentMatch[1] : " ";
4051
- newImport = `${prefix}{\n${keptSpecifiers.map((s) => `${indent}${s},`).join("\n")}\n} ${fromClause}`;
4052
- } else newImport = `${prefix}{ ${keptSpecifiers.join(", ")} } ${fromClause}`;
4280
+ newImport = `${prefix}{\n${keptSpecifiers.map((s) => `${indent}${s},`).join("\n")}\n} ${fromClause};`;
4281
+ } else newImport = `${prefix}{ ${keptSpecifiers.join(", ")} } ${fromClause};`;
4053
4282
  lines[span[0]] = newImport;
4054
4283
  for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
4055
4284
  };
@@ -4070,7 +4299,289 @@ const rewritePyImportLine = (lines, lineIdx, syms, unusedNames) => {
4070
4299
  };
4071
4300
 
4072
4301
  //#endregion
4073
- //#region src/commands/fix.ts
4302
+ //#region src/engines/lint/expo-doctor.ts
4303
+ var expo_doctor_exports = /* @__PURE__ */ __exportAll({ runExpoDoctor: () => runExpoDoctor });
4304
+ const esmRequire = createRequire(import.meta.url);
4305
+ const ISSUE_PREFIX = "✖ ";
4306
+ const resolveExpoDoctorScript = () => {
4307
+ try {
4308
+ const packageJsonPath = esmRequire.resolve("expo-doctor/package.json");
4309
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
4310
+ const binRelativePath = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.["expo-doctor"];
4311
+ if (!binRelativePath) return null;
4312
+ return path.join(path.dirname(packageJsonPath), binRelativePath);
4313
+ } catch {
4314
+ return null;
4315
+ }
4316
+ };
4317
+ const toRuleSuffix = (title) => {
4318
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
4319
+ return slug.length > 0 ? slug : "issue";
4320
+ };
4321
+ const parseIssues = (output) => {
4322
+ const lines = output.split("\n").map((line) => line.trimEnd());
4323
+ const startIndex = lines.findIndex((line) => line.includes("Possible issues detected:"));
4324
+ if (startIndex < 0) return [];
4325
+ const issues = [];
4326
+ let current = null;
4327
+ let inAdvice = false;
4328
+ for (let i = startIndex + 1; i < lines.length; i += 1) {
4329
+ const line = lines[i].trim();
4330
+ if (/^\d+\s+checks failed/.test(line)) break;
4331
+ if (line.length === 0) continue;
4332
+ if (line.startsWith(ISSUE_PREFIX)) {
4333
+ if (current) issues.push(current);
4334
+ current = {
4335
+ title: line.slice(2).trim(),
4336
+ details: [],
4337
+ advice: []
4338
+ };
4339
+ inAdvice = false;
4340
+ continue;
4341
+ }
4342
+ if (!current) continue;
4343
+ if (line === "Advice:") {
4344
+ inAdvice = true;
4345
+ continue;
4346
+ }
4347
+ if (inAdvice) current.advice.push(line);
4348
+ else current.details.push(line);
4349
+ }
4350
+ if (current) issues.push(current);
4351
+ return issues;
4352
+ };
4353
+ const parseConfigError = (output) => {
4354
+ const line = output.split("\n").find((candidate) => candidate.trim().startsWith("ConfigError:"));
4355
+ return line ? line.trim() : null;
4356
+ };
4357
+ const toDiagnostics = (issues) => issues.map((issue) => {
4358
+ const helpParts = [issue.details.join(" ").trim(), issue.advice.join(" ").trim()].filter((part) => part.length > 0);
4359
+ return {
4360
+ filePath: "package.json",
4361
+ engine: "lint",
4362
+ rule: `expo-doctor/${toRuleSuffix(issue.title)}`,
4363
+ severity: "warning",
4364
+ message: `Expo Doctor: ${issue.title}`,
4365
+ help: helpParts.join(" "),
4366
+ line: 0,
4367
+ column: 0,
4368
+ category: "Expo",
4369
+ fixable: false
4370
+ };
4371
+ });
4372
+ const runExpoDoctor = async (context) => {
4373
+ const scriptPath = resolveExpoDoctorScript();
4374
+ let stdout = "";
4375
+ let stderr = "";
4376
+ try {
4377
+ if (scriptPath) {
4378
+ const result = await runSubprocess(process.execPath, [
4379
+ scriptPath,
4380
+ context.rootDirectory,
4381
+ "--verbose"
4382
+ ], {
4383
+ cwd: context.rootDirectory,
4384
+ timeout: 12e4
4385
+ });
4386
+ stdout = result.stdout;
4387
+ stderr = result.stderr;
4388
+ } else {
4389
+ const result = await runSubprocess("npx", [
4390
+ "--yes",
4391
+ "expo-doctor",
4392
+ context.rootDirectory,
4393
+ "--verbose"
4394
+ ], {
4395
+ cwd: context.rootDirectory,
4396
+ timeout: 12e4
4397
+ });
4398
+ stdout = result.stdout;
4399
+ stderr = result.stderr;
4400
+ }
4401
+ } catch {
4402
+ return [];
4403
+ }
4404
+ const output = [stdout, stderr].filter(Boolean).join("\n");
4405
+ if (!output) return [];
4406
+ const configError = parseConfigError(output);
4407
+ if (configError) return [{
4408
+ filePath: "package.json",
4409
+ engine: "lint",
4410
+ rule: "expo-doctor/config-error",
4411
+ severity: "warning",
4412
+ message: configError,
4413
+ help: "Install project dependencies, then re-run `aislop scan`.",
4414
+ line: 0,
4415
+ column: 0,
4416
+ category: "Expo",
4417
+ fixable: false
4418
+ }];
4419
+ return toDiagnostics(parseIssues(output));
4420
+ };
4421
+
4422
+ //#endregion
4423
+ //#region src/output/fix-progress.ts
4424
+ const SPINNER_FRAMES = [
4425
+ "⠋",
4426
+ "⠙",
4427
+ "⠹",
4428
+ "⠸",
4429
+ "⠼",
4430
+ "⠴",
4431
+ "⠦",
4432
+ "⠧",
4433
+ "⠇",
4434
+ "⠏"
4435
+ ];
4436
+ const shouldRenderLive = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
4437
+ const formatElapsed = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
4438
+ const getStepSummary = (result) => {
4439
+ if (result.failed) return `Failed (${result.afterIssues} remain)`;
4440
+ if (result.beforeIssues === 0) return `0 issues, ${formatElapsed(result.elapsedMs)}`;
4441
+ if (result.afterIssues === 0) return `${result.resolvedIssues} resolved, ${formatElapsed(result.elapsedMs)}`;
4442
+ if (result.resolvedIssues > 0) return `${result.resolvedIssues} resolved, ${result.afterIssues} remaining, ${formatElapsed(result.elapsedMs)}`;
4443
+ return `no changes, ${result.afterIssues} issue${result.afterIssues === 1 ? "" : "s"}, ${formatElapsed(result.elapsedMs)}`;
4444
+ };
4445
+ const getStatusParts = (state, frameIndex) => {
4446
+ if (state.status === "running") return {
4447
+ icon: highlighter.info(SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length]),
4448
+ detail: highlighter.info("Running")
4449
+ };
4450
+ if (state.status === "done" && state.result) return {
4451
+ icon: state.result.afterIssues > 0 ? highlighter.warn("!") : highlighter.success("✓"),
4452
+ detail: highlighter.dim(`Done (${getStepSummary(state.result)})`)
4453
+ };
4454
+ if (state.status === "failed" && state.result) return {
4455
+ icon: highlighter.error("✗"),
4456
+ detail: highlighter.dim(`Failed (${getStepSummary(state.result)})`)
4457
+ };
4458
+ return {
4459
+ icon: highlighter.dim("○"),
4460
+ detail: highlighter.dim("Waiting")
4461
+ };
4462
+ };
4463
+ const clearRenderedLines$1 = (lineCount) => {
4464
+ if (lineCount === 0) return;
4465
+ process.stderr.write(`\u001B[${lineCount}F`);
4466
+ for (let index = 0; index < lineCount; index += 1) {
4467
+ process.stderr.write("\x1B[2K");
4468
+ if (index < lineCount - 1) process.stderr.write("\x1B[1E");
4469
+ }
4470
+ if (lineCount > 1) process.stderr.write(`\u001B[${lineCount - 1}F`);
4471
+ };
4472
+ const renderFixProgressBlock = (states, frameIndex) => {
4473
+ if (states.length === 0) return ` ${highlighter.bold("Fixes 0/0")} ${highlighter.dim("nothing to run")}\n`;
4474
+ const completedCount = states.filter((s) => s.status === "done" || s.status === "failed").length;
4475
+ const runningCount = states.filter((s) => s.status === "running").length;
4476
+ const labelWidth = Math.max(...states.map((s) => s.name.length));
4477
+ const headingStatus = completedCount === states.length ? highlighter.dim("complete") : runningCount > 0 ? highlighter.dim(`${runningCount} running`) : highlighter.dim("starting");
4478
+ return `${[` ${highlighter.bold(`Fixes ${completedCount}/${states.length}`)} ${headingStatus}`, ...states.map((state) => {
4479
+ const label = state.name.padEnd(labelWidth, " ");
4480
+ const { icon, detail } = getStatusParts(state, frameIndex);
4481
+ return ` ${icon} ${label} ${detail}`;
4482
+ })].join("\n")}\n`;
4483
+ };
4484
+ var FixProgressRenderer = class {
4485
+ states;
4486
+ previousLineCount = 0;
4487
+ frameIndex = 0;
4488
+ timer;
4489
+ live;
4490
+ constructor(stepNames) {
4491
+ this.states = stepNames.map((name) => ({
4492
+ name,
4493
+ status: "pending"
4494
+ }));
4495
+ this.live = shouldRenderLive();
4496
+ }
4497
+ isLive() {
4498
+ return this.live;
4499
+ }
4500
+ start() {
4501
+ if (!this.live) return;
4502
+ this.render();
4503
+ this.timer = setInterval(() => {
4504
+ this.frameIndex += 1;
4505
+ this.render();
4506
+ }, 100);
4507
+ this.timer.unref();
4508
+ }
4509
+ markStarted(name) {
4510
+ const state = this.states.find((s) => s.name === name);
4511
+ if (!state) return;
4512
+ state.status = "running";
4513
+ this.render();
4514
+ }
4515
+ markComplete(name, result) {
4516
+ const state = this.states.find((s) => s.name === name);
4517
+ if (!state) return;
4518
+ state.status = result.failed ? "failed" : "done";
4519
+ state.result = result;
4520
+ this.render();
4521
+ }
4522
+ stop() {
4523
+ if (this.timer) {
4524
+ clearInterval(this.timer);
4525
+ this.timer = void 0;
4526
+ }
4527
+ if (!this.live) return;
4528
+ this.render();
4529
+ }
4530
+ render() {
4531
+ if (!this.live) return;
4532
+ if (this.previousLineCount > 0) clearRenderedLines$1(this.previousLineCount);
4533
+ const output = renderFixProgressBlock(this.states, this.frameIndex);
4534
+ process.stderr.write(output);
4535
+ this.previousLineCount = output.split("\n").length - 1;
4536
+ }
4537
+ };
4538
+
4539
+ //#endregion
4540
+ //#region src/commands/fix-force.ts
4541
+ const getJsAuditFixCommand = (rootDirectory) => {
4542
+ if (fs.existsSync(path.join(rootDirectory, "pnpm-lock.yaml"))) return {
4543
+ command: "pnpm",
4544
+ args: ["audit", "--fix"]
4545
+ };
4546
+ if (fs.existsSync(path.join(rootDirectory, "package-lock.json")) || fs.existsSync(path.join(rootDirectory, "package.json"))) return {
4547
+ command: "npm",
4548
+ args: ["audit", "fix"]
4549
+ };
4550
+ return null;
4551
+ };
4552
+ const fixDependencyAudit = async (context) => {
4553
+ const auditFix = getJsAuditFixCommand(context.rootDirectory);
4554
+ if (!auditFix) return;
4555
+ const result = await runSubprocess(auditFix.command, auditFix.args, {
4556
+ cwd: context.rootDirectory,
4557
+ timeout: 18e4
4558
+ });
4559
+ if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `${auditFix.command} audit fix failed`);
4560
+ };
4561
+ const fixExpoDependencies = async (context) => {
4562
+ if ((await runSubprocess("npx", [
4563
+ "--yes",
4564
+ "expo",
4565
+ "install",
4566
+ "--fix"
4567
+ ], {
4568
+ cwd: context.rootDirectory,
4569
+ timeout: 18e4
4570
+ })).exitCode === 0) return;
4571
+ const checkResult = await runSubprocess("npx", [
4572
+ "--yes",
4573
+ "expo",
4574
+ "install",
4575
+ "--check"
4576
+ ], {
4577
+ cwd: context.rootDirectory,
4578
+ timeout: 18e4
4579
+ });
4580
+ if (checkResult.exitCode !== 0) throw new Error(checkResult.stderr || checkResult.stdout || "expo dependency check failed");
4581
+ };
4582
+
4583
+ //#endregion
4584
+ //#region src/commands/fix-step.ts
4074
4585
  const uniqueFiles = (diagnostics) => [...new Set(diagnostics.map((d) => d.filePath))];
4075
4586
  const uniqueFileCount = (diagnostics) => uniqueFiles(diagnostics).length;
4076
4587
  const getFilePreviewLines = (title, files, verbose) => {
@@ -4094,7 +4605,8 @@ const getStepStatusLine = (result, name, elapsedLabel) => {
4094
4605
  if (result.resolvedIssues > 0) return highlighter.warn(` ! ${name}: done (${result.resolvedIssues} resolved, ${result.afterIssues} remaining, ${elapsedLabel})`);
4095
4606
  return highlighter.warn(` ! ${name}: done (no auto-fix changes, ${result.afterIssues} issue${result.afterIssues === 1 ? "" : "s"}, ${elapsedLabel})`);
4096
4607
  };
4097
- const runFixStep = async (name, detect, applyFix, options) => {
4608
+ const runFixStep = async (name, detect, applyFix, options, progress) => {
4609
+ progress.markStarted(name);
4098
4610
  const stepStart = performance.now();
4099
4611
  const before = await detect();
4100
4612
  let applyError = null;
@@ -4114,28 +4626,21 @@ const runFixStep = async (name, detect, applyFix, options) => {
4114
4626
  failed: applyError !== null && before.length === after.length,
4115
4627
  elapsedMs
4116
4628
  };
4117
- const lines = [getStepStatusLine(result, name, formatElapsed$1(result.elapsedMs))];
4118
- if (applyError) {
4119
- const reasonLines = getReasonLines(applyError instanceof Error ? applyError.message : String(applyError));
4120
- const reasonToPrint = options.verbose ? reasonLines.printable : reasonLines.firstLine;
4121
- for (const line of reasonToPrint.split("\n")) lines.push(highlighter.dim(` ${line}`));
4122
- if (!options.verbose && reasonLines.printable !== reasonToPrint) lines.push(highlighter.dim(" Re-run with -d for full tool output."));
4123
- }
4124
- lines.push(...getFilePreviewLines("Affected", uniqueFiles(before), options.verbose));
4125
- if (after.length > 0) lines.push(...getFilePreviewLines("Remaining", uniqueFiles(after), options.verbose));
4126
- process.stdout.write(`${lines.join("\n")}\n\n`);
4629
+ progress.markComplete(name, result);
4630
+ if (!progress.isLive()) {
4631
+ const lines = [getStepStatusLine(result, name, formatElapsed$2(result.elapsedMs))];
4632
+ if (applyError) {
4633
+ const reasonLines = getReasonLines(applyError instanceof Error ? applyError.message : String(applyError));
4634
+ const reasonToPrint = options.verbose ? reasonLines.printable : reasonLines.firstLine;
4635
+ for (const line of reasonToPrint.split("\n")) lines.push(highlighter.dim(` ${line}`));
4636
+ if (!options.verbose && reasonLines.printable !== reasonToPrint) lines.push(highlighter.dim(" Re-run with -d for full tool output."));
4637
+ }
4638
+ lines.push(...getFilePreviewLines("Affected", uniqueFiles(before), options.verbose));
4639
+ if (after.length > 0) lines.push(...getFilePreviewLines("Remaining", uniqueFiles(after), options.verbose));
4640
+ process.stdout.write(`${lines.join("\n")}\n\n`);
4641
+ }
4127
4642
  return result;
4128
4643
  };
4129
- const createEngineContext = (rootDirectory, projectInfo, config) => ({
4130
- rootDirectory,
4131
- languages: projectInfo.languages,
4132
- frameworks: projectInfo.frameworks,
4133
- installedTools: projectInfo.installedTools,
4134
- config: {
4135
- quality: config.quality,
4136
- security: config.security
4137
- }
4138
- });
4139
4644
  const summarizeFixRun = (steps) => {
4140
4645
  const totals = steps.reduce((acc, step) => {
4141
4646
  acc.beforeIssues += step.beforeIssues;
@@ -4155,6 +4660,19 @@ const summarizeFixRun = (steps) => {
4155
4660
  } else logger.log(` Fix summary: checked ${steps.length} step(s), resolved ${totals.resolvedIssues} issue(s), remaining ${totals.afterIssues}.`);
4156
4661
  if (totals.failedSteps === 0 && totals.beforeIssues > 0 && totals.resolvedIssues === 0) logger.dim(" No auto-fixable changes were applied. Current findings are likely manual-fix categories.");
4157
4662
  };
4663
+
4664
+ //#endregion
4665
+ //#region src/commands/fix.ts
4666
+ const createEngineContext = (rootDirectory, projectInfo, config) => ({
4667
+ rootDirectory,
4668
+ languages: projectInfo.languages,
4669
+ frameworks: projectInfo.frameworks,
4670
+ installedTools: projectInfo.installedTools,
4671
+ config: {
4672
+ quality: config.quality,
4673
+ security: config.security
4674
+ }
4675
+ });
4158
4676
  const fixCommand = async (directory, config, options = {
4159
4677
  verbose: false,
4160
4678
  showHeader: true
@@ -4166,38 +4684,98 @@ const fixCommand = async (directory, config, options = {
4166
4684
  printProjectMetadata(projectInfo);
4167
4685
  const context = createEngineContext(resolvedDir, projectInfo, config);
4168
4686
  const steps = [];
4169
- if (config.engines["ai-slop"]) steps.push(await runFixStep("Unused imports", () => detectUnusedImports(context), () => fixUnusedImports(context), options));
4687
+ const stepNames = [];
4688
+ if (config.engines["ai-slop"]) {
4689
+ stepNames.push("Unused imports");
4690
+ stepNames.push("Dead code & comments");
4691
+ }
4692
+ if (config.engines.lint) {
4693
+ if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) stepNames.push("JS/TS lint fixes");
4694
+ if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) stepNames.push("Python lint fixes");
4695
+ }
4696
+ if (config.engines["code-quality"]) {
4697
+ if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) stepNames.push("Unused dependencies");
4698
+ }
4699
+ if (config.engines.format) {
4700
+ if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) stepNames.push("JS/TS formatting");
4701
+ if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) stepNames.push("Python formatting");
4702
+ if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) stepNames.push("Go formatting");
4703
+ }
4704
+ if (options.force) {
4705
+ if (config.engines.security) stepNames.push("Dependency audit fixes");
4706
+ if (projectInfo.frameworks.includes("expo")) stepNames.push("Expo dependency alignment");
4707
+ }
4708
+ const progress = new FixProgressRenderer(stepNames);
4709
+ progress.start();
4710
+ if (config.engines["ai-slop"]) {
4711
+ steps.push(await runFixStep("Unused imports", () => detectUnusedImports(context), () => fixUnusedImports(context), options, progress));
4712
+ const detectFixableSlop = async () => {
4713
+ const [comments, dead] = await Promise.all([detectTrivialComments(context), detectDeadPatterns(context)]);
4714
+ return [...comments, ...dead].filter((d) => d.fixable);
4715
+ };
4716
+ steps.push(await runFixStep("Dead code & comments", detectFixableSlop, () => fixDeadPatterns(context), options, progress));
4717
+ }
4170
4718
  if (config.engines.lint) {
4171
- if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS lint fixes", () => runOxlint(context), () => fixOxlint(context), options));
4172
- if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python lint fixes", () => runRuffLint(context), () => fixRuffLint(resolvedDir), options));
4719
+ if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS lint fixes", () => runOxlint(context), () => options.force ? fixOxlintForce(context) : fixOxlint(context), options, progress));
4720
+ if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python lint fixes", () => runRuffLint(context), () => options.force ? fixRuffLintForce(resolvedDir) : fixRuffLint(resolvedDir), options, progress));
4173
4721
  else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python lint fixes.");
4174
4722
  }
4175
4723
  if (config.engines["code-quality"]) {
4176
- if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("Unused dependencies", () => runKnipDependencyCheck(resolvedDir), () => fixUnusedDependencies(resolvedDir), options));
4724
+ if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("Unused dependencies", () => runKnipDependencyCheck(resolvedDir), () => fixUnusedDependencies(resolvedDir), options, progress));
4177
4725
  }
4178
4726
  if (config.engines.format) {
4179
- if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS formatting", () => runBiomeFormat(context), () => fixBiomeFormat(context), options));
4180
- if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python formatting", () => runRuffFormat(context), () => fixRuffFormat(resolvedDir), options));
4727
+ if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS formatting", () => runBiomeFormat(context), () => fixBiomeFormat(context), options, progress));
4728
+ if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python formatting", () => runRuffFormat(context), () => fixRuffFormat(resolvedDir), options, progress));
4181
4729
  else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python formatting fixes.");
4182
- if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) steps.push(await runFixStep("Go formatting", () => runGofmt(context), () => fixGofmt(resolvedDir), options));
4730
+ if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) steps.push(await runFixStep("Go formatting", () => runGofmt(context), () => fixGofmt(resolvedDir), options, progress));
4183
4731
  else if (projectInfo.languages.includes("go")) logger.warn(" Go detected but gofmt is not installed; skipping Go formatting fixes.");
4184
4732
  }
4733
+ if (options.force) {
4734
+ if (config.engines.security) steps.push(await runFixStep("Dependency audit fixes", () => runDependencyAudit(context), () => fixDependencyAudit(context), options, progress));
4735
+ if (projectInfo.frameworks.includes("expo")) steps.push(await runFixStep("Expo dependency alignment", () => runExpoDoctor(context), () => fixExpoDependencies(context), options, progress));
4736
+ }
4737
+ progress.stop();
4738
+ const totalResolved = steps.reduce((sum, s) => sum + s.resolvedIssues, 0);
4185
4739
  if (steps.length === 0) logger.dim(" No applicable auto-fixers found for this project.");
4186
4740
  else {
4187
4741
  logger.break();
4188
4742
  summarizeFixRun(steps);
4189
4743
  }
4190
- if (!isTelemetryDisabled(config.telemetry?.enabled)) {
4191
- const totalResolved = steps.reduce((sum, s) => sum + s.resolvedIssues, 0);
4192
- trackEvent({
4193
- command: "fix",
4194
- languages: projectInfo.languages,
4195
- fixSteps: steps.length,
4196
- fixResolved: totalResolved
4197
- });
4198
- }
4744
+ if (!isTelemetryDisabled(config.telemetry?.enabled)) trackEvent({
4745
+ command: "fix",
4746
+ languages: projectInfo.languages,
4747
+ fixSteps: steps.length,
4748
+ fixResolved: totalResolved
4749
+ });
4199
4750
  logger.break();
4200
- logger.success(" ✓ Done. Run `aislop scan` to verify.");
4751
+ const configDir = findConfigDir(resolvedDir);
4752
+ const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
4753
+ const engineConfig = {
4754
+ quality: config.quality,
4755
+ security: config.security,
4756
+ architectureRulesPath: config.engines.architecture ? rulesPath : void 0
4757
+ };
4758
+ const allDiagnostics = (await runEngines({
4759
+ rootDirectory: resolvedDir,
4760
+ languages: projectInfo.languages,
4761
+ frameworks: projectInfo.frameworks,
4762
+ installedTools: projectInfo.installedTools,
4763
+ config: engineConfig
4764
+ }, config.engines, () => {}, () => {})).flatMap((r) => r.diagnostics);
4765
+ const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
4766
+ const errors = allDiagnostics.filter((d) => d.severity === "error").length;
4767
+ const warnings = allDiagnostics.filter((d) => d.severity === "warning").length;
4768
+ const fixable = allDiagnostics.filter((d) => d.fixable).length;
4769
+ const manual = errors + warnings - fixable;
4770
+ const scoreColor = scoreResult.score >= config.scoring.thresholds.good ? highlighter.success : scoreResult.score >= config.scoring.thresholds.ok ? highlighter.warn : highlighter.error;
4771
+ logger.log(highlighter.dim("------------------------------------------------------------"));
4772
+ logger.log(highlighter.bold("Result"));
4773
+ logger.log(` Score: ${scoreColor(`${scoreResult.score}/100`)} ${scoreColor(`(${scoreResult.label})`)}`);
4774
+ logger.log(` Resolved: ${highlighter.success(String(totalResolved))} issue${totalResolved === 1 ? "" : "s"}`);
4775
+ logger.log(` Remaining: ${errors + warnings > 0 ? highlighter.warn(String(errors + warnings)) : highlighter.success("0")} (${errors} error${errors === 1 ? "" : "s"}, ${warnings} warning${warnings === 1 ? "" : "s"})`);
4776
+ if (fixable > 0) logger.log(` Auto-fixable: ${highlighter.info(String(fixable))}`);
4777
+ if (manual > 0) logger.log(` Manual effort: ${highlighter.dim(String(manual))}`);
4778
+ logger.log(highlighter.dim("------------------------------------------------------------"));
4201
4779
  logger.break();
4202
4780
  };
4203
4781
 
@@ -4558,7 +5136,7 @@ const program = new Command().name("aislop").description("The unified code quali
4558
5136
  }).addHelpText("after", `
4559
5137
  ${highlighter.dim("Commands:")}
4560
5138
  aislop scan [dir] Full code quality scan
4561
- aislop fix [dir] Auto-fix formatting and lint issues
5139
+ aislop fix [dir] Auto-fix ai slop in codebase
4562
5140
  aislop init [dir] Initialize aislop config
4563
5141
  aislop doctor [dir] Check installed tools
4564
5142
  aislop ci [dir] CI-friendly JSON output
@@ -4570,7 +5148,8 @@ ${highlighter.dim("Examples:")}
4570
5148
  aislop scan -d Scan with file/line details
4571
5149
  aislop scan --changes Scan only changed files
4572
5150
  aislop scan --staged Scan only staged files (for hooks)
4573
- aislop fix Auto-fix issues
5151
+ aislop fix Auto-fix ai slop in codebase
5152
+ aislop fix --force Run aggressive fixes (includes audit and dependency alignment)
4574
5153
  aislop ci JSON output for CI pipelines
4575
5154
  `);
4576
5155
  program.command("scan [directory]").description("Run full code quality scan").option("--changes", "only scan changed files").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON").action(async (directory = ".", _flags, command) => {
@@ -4586,9 +5165,12 @@ program.command("scan [directory]").description("Run full code quality scan").op
4586
5165
  process.exit(exitCode);
4587
5166
  }
4588
5167
  });
4589
- program.command("fix [directory]").description("Auto-fix formatting and lint issues").option("-d, --verbose", "show detailed fix progress").action(async (directory = ".", _flags, command) => {
5168
+ program.command("fix [directory]").description("Auto-fix ai slop in codebase").option("-d, --verbose", "show detailed fix progress").option("-f, --force", "run aggressive fixes (audit and framework dependency alignment)").action(async (directory = ".", _flags, command) => {
4590
5169
  const flags = command.optsWithGlobals();
4591
- await fixCommand(directory, loadConfig(directory), { verbose: Boolean(flags.verbose) });
5170
+ await fixCommand(directory, loadConfig(directory), {
5171
+ verbose: Boolean(flags.verbose),
5172
+ force: Boolean(flags.force)
5173
+ });
4592
5174
  });
4593
5175
  program.command("init [directory]").description("Initialize aislop config in project").action(async (directory = ".") => {
4594
5176
  await initCommand(directory);
@@ -4613,4 +5195,4 @@ const main = async () => {
4613
5195
  main();
4614
5196
 
4615
5197
  //#endregion
4616
- export { ENGINE_INFO as n, runSubprocess as r, APP_VERSION as t };
5198
+ export { ENGINE_INFO as n, APP_VERSION as t };