aislop 0.10.0 → 0.10.2

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
@@ -34,7 +34,7 @@ var __exportAll = (all, no_symbols) => {
34
34
 
35
35
  //#endregion
36
36
  //#region src/version.ts
37
- const APP_VERSION = "0.10.0";
37
+ const APP_VERSION = "0.10.2";
38
38
 
39
39
  //#endregion
40
40
  //#region src/telemetry/env.ts
@@ -183,7 +183,7 @@ const redactProperties = (props) => {
183
183
  const POSTHOG_HOST = process.env.AISLOP_POSTHOG_HOST ?? "https://eu.i.posthog.com";
184
184
  const POSTHOG_KEY = process.env.AISLOP_POSTHOG_KEY ?? "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
185
185
  const SCHEMA_VERSION = "v2";
186
- const REQUEST_TIMEOUT_MS = 3e3;
186
+ const REQUEST_TIMEOUT_MS$1 = 3e3;
187
187
  const isTelemetryDisabled = (config) => {
188
188
  const env = process.env;
189
189
  if (env.AISLOP_NO_TELEMETRY === "1" || env.DO_NOT_TRACK === "1") return true;
@@ -237,16 +237,21 @@ const track = (input) => {
237
237
  method: "POST",
238
238
  headers: { "Content-Type": "application/json" },
239
239
  body: JSON.stringify(payload),
240
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
240
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS$1)
241
241
  }).then(() => {}).catch(() => {}).finally(() => {
242
242
  pendingRequests.delete(request);
243
243
  });
244
244
  pendingRequests.add(request);
245
245
  return { installCreated };
246
246
  };
247
- const flushTelemetry = async () => {
247
+ const flushTelemetry = async (timeoutMs) => {
248
248
  if (pendingRequests.size === 0) return;
249
- await Promise.all(pendingRequests);
249
+ const all = Promise.all(pendingRequests);
250
+ if (timeoutMs == null) {
251
+ await all;
252
+ return;
253
+ }
254
+ await Promise.race([all, new Promise((resolve) => setTimeout(resolve, timeoutMs))]);
250
255
  };
251
256
 
252
257
  //#endregion
@@ -361,7 +366,7 @@ const withCommandLifecycle = async (start, run) => {
361
366
  startProps,
362
367
  exitCode: result.exitCode,
363
368
  durationMs,
364
- score: result.score,
369
+ score: result.score ?? void 0,
365
370
  findingCount: result.findingCount,
366
371
  errorCount: result.errorCount,
367
372
  warningCount: result.warningCount,
@@ -619,12 +624,9 @@ jobs:
619
624
  runs-on: ubuntu-latest
620
625
  steps:
621
626
  - uses: actions/checkout@v4
622
- - uses: actions/setup-node@v4
627
+ - uses: scanaislop/aislop@v${APP_VERSION}
623
628
  with:
624
- node-version: 20
625
- # Quality gate: exits 1 when score < ci.failBelow in .aislop/config.yml
626
- # or when any error-severity diagnostic is present.
627
- - run: npx aislop@latest ci .
629
+ version: ${APP_VERSION}
628
630
  `;
629
631
  const DEFAULT_RULES_YAML = `# Architecture rules (BYO)
630
632
  # Uncomment and customize to enforce your project's conventions.
@@ -1012,6 +1014,15 @@ const listProjectFiles = (rootDirectory) => {
1012
1014
  if (findResult.error || findResult.status !== 0) return [];
1013
1015
  return findResult.stdout.split("\n").filter((file) => file.length > 0).map((file) => file.replace(/^\.\//, ""));
1014
1016
  };
1017
+ const readAislopIgnorePatterns = (rootDirectory) => {
1018
+ const ignorePath = path.join(rootDirectory, ".aislopignore");
1019
+ if (!fs.existsSync(ignorePath)) return [];
1020
+ try {
1021
+ return fs.readFileSync(ignorePath, "utf-8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
1022
+ } catch {
1023
+ return [];
1024
+ }
1025
+ };
1015
1026
  const normalizeExcludePatterns = (patterns) => {
1016
1027
  return patterns.flatMap((pattern) => {
1017
1028
  const p = pattern.trim();
@@ -1085,7 +1096,7 @@ const getSourceFilesWithExtras = (context, extraExtensions) => {
1085
1096
  };
1086
1097
 
1087
1098
  //#endregion
1088
- //#region src/engines/ai-slop/abstractions.ts
1099
+ //#region src/utils/source-masker.ts
1089
1100
  const JS_EXTS$2 = new Set([
1090
1101
  ".ts",
1091
1102
  ".tsx",
@@ -1094,14 +1105,226 @@ const JS_EXTS$2 = new Set([
1094
1105
  ".mjs",
1095
1106
  ".cjs"
1096
1107
  ]);
1108
+ const PY_EXTS = new Set([".py"]);
1109
+ const RB_EXTS = new Set([".rb"]);
1110
+ const PHP_EXTS = new Set([".php"]);
1111
+ const familyForExt = (ext) => {
1112
+ if (JS_EXTS$2.has(ext)) return "js";
1113
+ if (PY_EXTS.has(ext)) return "py";
1114
+ if (RB_EXTS.has(ext)) return "rb";
1115
+ if (PHP_EXTS.has(ext)) return "php";
1116
+ return "none";
1117
+ };
1118
+ const maskStringsAndComments = (content, ext) => {
1119
+ const family = familyForExt(ext);
1120
+ if (family === "none") return content;
1121
+ if (family === "js") return maskJs(content, true);
1122
+ return maskSimple(content, family, true);
1123
+ };
1124
+ const maskComments = (content, ext) => {
1125
+ const family = familyForExt(ext);
1126
+ if (family === "none") return content;
1127
+ if (family === "js") return maskJs(content, false);
1128
+ return maskSimple(content, family, false);
1129
+ };
1130
+ const handleQuotesAndComments = (content, i, tplStack, mask, maskStrings) => {
1131
+ const len = content.length;
1132
+ const c = content[i];
1133
+ const next = content[i + 1];
1134
+ if (c === "\"" || c === "'") {
1135
+ const strStart = i;
1136
+ const end = consumeQuotedString(content, i, c);
1137
+ if (maskStrings) mask(strStart + 1, end - 1);
1138
+ return {
1139
+ handled: true,
1140
+ nextI: end
1141
+ };
1142
+ }
1143
+ if (c === "`") {
1144
+ const scan = consumeTemplateString(content, i + 1);
1145
+ if (maskStrings) mask(i + 1, scan.maskEnd);
1146
+ if (scan.openedInterp) tplStack.push(0);
1147
+ return {
1148
+ handled: true,
1149
+ nextI: scan.resumeAt
1150
+ };
1151
+ }
1152
+ if (c === "/" && next === "/") {
1153
+ const strStart = i;
1154
+ let k = i;
1155
+ while (k < len && content[k] !== "\n") k++;
1156
+ mask(strStart, k);
1157
+ return {
1158
+ handled: true,
1159
+ nextI: k
1160
+ };
1161
+ }
1162
+ if (c === "/" && next === "*") {
1163
+ const strStart = i;
1164
+ let k = i + 2;
1165
+ while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
1166
+ if (k < len - 1) k += 2;
1167
+ mask(strStart, k);
1168
+ return {
1169
+ handled: true,
1170
+ nextI: k
1171
+ };
1172
+ }
1173
+ return {
1174
+ handled: false,
1175
+ nextI: i
1176
+ };
1177
+ };
1178
+ const maskJs = (content, maskStrings) => {
1179
+ const out = content.split("");
1180
+ const len = content.length;
1181
+ const tplStack = [];
1182
+ let i = 0;
1183
+ const mask = (start, end) => {
1184
+ for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
1185
+ };
1186
+ while (i < len) {
1187
+ const c = content[i];
1188
+ if (tplStack.length > 0) {
1189
+ if (c === "{") {
1190
+ tplStack[tplStack.length - 1]++;
1191
+ i++;
1192
+ continue;
1193
+ }
1194
+ if (c === "}") {
1195
+ if (tplStack[tplStack.length - 1] === 0) {
1196
+ tplStack.pop();
1197
+ const scan = consumeTemplateString(content, i + 1);
1198
+ if (maskStrings) mask(i + 1, scan.maskEnd);
1199
+ if (scan.openedInterp) tplStack.push(0);
1200
+ i = scan.resumeAt;
1201
+ continue;
1202
+ }
1203
+ tplStack[tplStack.length - 1]--;
1204
+ i++;
1205
+ continue;
1206
+ }
1207
+ }
1208
+ const handled = handleQuotesAndComments(content, i, tplStack, mask, maskStrings);
1209
+ if (handled.handled) {
1210
+ i = handled.nextI;
1211
+ continue;
1212
+ }
1213
+ i++;
1214
+ }
1215
+ return out.join("");
1216
+ };
1217
+ const consumeQuotedString = (content, start, quote) => {
1218
+ const len = content.length;
1219
+ let i = start + 1;
1220
+ while (i < len) {
1221
+ const c = content[i];
1222
+ if (c === "\\" && i + 1 < len) {
1223
+ i += 2;
1224
+ continue;
1225
+ }
1226
+ if (c === quote) return i + 1;
1227
+ if (c === "\n") return i;
1228
+ i++;
1229
+ }
1230
+ return i;
1231
+ };
1232
+ const consumeTemplateString = (content, start) => {
1233
+ const len = content.length;
1234
+ let i = start;
1235
+ while (i < len) {
1236
+ const c = content[i];
1237
+ if (c === "\\" && i + 1 < len) {
1238
+ i += 2;
1239
+ continue;
1240
+ }
1241
+ if (c === "`") return {
1242
+ maskEnd: i,
1243
+ resumeAt: i + 1,
1244
+ openedInterp: false
1245
+ };
1246
+ if (c === "$" && content[i + 1] === "{") return {
1247
+ maskEnd: i,
1248
+ resumeAt: i + 2,
1249
+ openedInterp: true
1250
+ };
1251
+ i++;
1252
+ }
1253
+ return {
1254
+ maskEnd: i,
1255
+ resumeAt: i,
1256
+ openedInterp: false
1257
+ };
1258
+ };
1259
+ const maskSimple = (content, family, maskStrings) => {
1260
+ const out = content.split("");
1261
+ const len = content.length;
1262
+ let i = 0;
1263
+ const mask = (start, end) => {
1264
+ for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
1265
+ };
1266
+ while (i < len) {
1267
+ const c = content[i];
1268
+ const next = content[i + 1];
1269
+ if (family === "py" && (c === "\"" || c === "'")) {
1270
+ if (content[i + 1] === c && content[i + 2] === c) {
1271
+ const triple = c + c + c;
1272
+ const end = content.indexOf(triple, i + 3);
1273
+ const stop = end === -1 ? len : end + 3;
1274
+ if (maskStrings) mask(i + 3, stop - 3);
1275
+ i = stop;
1276
+ continue;
1277
+ }
1278
+ }
1279
+ if (c === "\"" || c === "'") {
1280
+ const strStart = i;
1281
+ i = consumeQuotedString(content, i, c);
1282
+ if (maskStrings) mask(strStart + 1, i - 1);
1283
+ continue;
1284
+ }
1285
+ if ((family === "py" || family === "rb" || family === "php") && c === "#") {
1286
+ const strStart = i;
1287
+ while (i < len && content[i] !== "\n") i++;
1288
+ mask(strStart, i);
1289
+ continue;
1290
+ }
1291
+ if (family === "php" && c === "/" && next === "/") {
1292
+ const strStart = i;
1293
+ while (i < len && content[i] !== "\n") i++;
1294
+ mask(strStart, i);
1295
+ continue;
1296
+ }
1297
+ if (family === "php" && c === "/" && next === "*") {
1298
+ const strStart = i;
1299
+ i += 2;
1300
+ while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
1301
+ if (i < len - 1) i += 2;
1302
+ mask(strStart, i);
1303
+ continue;
1304
+ }
1305
+ i++;
1306
+ }
1307
+ return out.join("");
1308
+ };
1309
+
1310
+ //#endregion
1311
+ //#region src/engines/ai-slop/abstractions.ts
1312
+ const JS_EXTS$1 = new Set([
1313
+ ".ts",
1314
+ ".tsx",
1315
+ ".js",
1316
+ ".jsx",
1317
+ ".mjs",
1318
+ ".cjs"
1319
+ ]);
1097
1320
  const THIN_WRAPPER_PATTERNS = [
1098
1321
  {
1099
1322
  pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
1100
- extensions: JS_EXTS$2
1323
+ extensions: JS_EXTS$1
1101
1324
  },
1102
1325
  {
1103
1326
  pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
1104
- extensions: JS_EXTS$2
1327
+ extensions: JS_EXTS$1
1105
1328
  },
1106
1329
  {
1107
1330
  pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
@@ -1129,16 +1352,14 @@ const detectThinWrappers = (content, relativePath, ext) => {
1129
1352
  for (const { pattern, extensions } of THIN_WRAPPER_PATTERNS) {
1130
1353
  if (!extensions.has(ext)) continue;
1131
1354
  const regex = new RegExp(pattern.source, pattern.flags);
1132
- let match;
1133
- while ((match = regex.exec(content)) !== null) {
1355
+ for (const match of content.matchAll(regex)) {
1134
1356
  const funcName = match[1];
1135
1357
  const matchText = match[0];
1136
1358
  const lineNumber = content.slice(0, match.index).split("\n").length;
1137
1359
  if (DUNDER_PATTERN.test(funcName)) continue;
1138
1360
  if (FRAMEWORK_METHOD_NAMES.test(funcName)) continue;
1139
1361
  if (lineNumber >= 2) {
1140
- const prevLine = lines[lineNumber - 2]?.trim();
1141
- if (prevLine && prevLine.startsWith("@")) continue;
1362
+ if ((lines[lineNumber - 2]?.trim())?.startsWith("@")) continue;
1142
1363
  }
1143
1364
  if (!isIdentityForward(matchText)) continue;
1144
1365
  if (isUseContextWrapper(matchText)) continue;
@@ -1194,8 +1415,9 @@ const detectOverAbstraction = async (context) => {
1194
1415
  }
1195
1416
  const relativePath = path.relative(context.rootDirectory, filePath);
1196
1417
  const ext = path.extname(filePath);
1197
- diagnostics.push(...detectThinWrappers(content, relativePath, ext));
1198
- diagnostics.push(...detectAiNaming(content, relativePath));
1418
+ const codeOnly = maskComments(content, ext);
1419
+ diagnostics.push(...detectThinWrappers(codeOnly, relativePath, ext));
1420
+ diagnostics.push(...detectAiNaming(codeOnly, relativePath));
1199
1421
  }
1200
1422
  return diagnostics;
1201
1423
  };
@@ -1514,7 +1736,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
1514
1736
  const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
1515
1737
  if (JS_EXTENSIONS$4.has(ext) && /^(?:return|throw)\b/.test(trimmed) && trimmed.endsWith(";") && nextLine && nextLine.length > 0 && !isGuardedSingleLineExit(lines, i) && !isBlockCloserAfterReturn(nextLine) && !nextLine.startsWith("//") && !nextLine.startsWith("/*") && !nextLine.startsWith("case ") && !nextLine.startsWith("default:") && !nextLine.startsWith("if ") && !nextLine.startsWith("if(") && !nextLine.startsWith("else")) diagnostics.push(slop(relativePath, i + 2, "ai-slop/unreachable-code", "warning", "Code after return/throw statement is unreachable", "Remove the unreachable code or restructure the control flow", false));
1516
1738
  if (/\bif\s*\(\s*(?:false|true|0|1)\s*\)/.test(trimmed) && !trimmed.startsWith("//") && !trimmed.startsWith("*") && !/["'`].*\bif\s*\(/.test(trimmed) && !/\/.*\bif\s*\(/.test(trimmed.replace(/\/\/.*$/, ""))) diagnostics.push(slop(relativePath, i + 1, "ai-slop/constant-condition", "warning", "Conditional with a constant value — likely debugging leftover", "Remove the constant condition or replace with proper logic", false));
1517
- if (JS_EXTENSIONS$4.has(ext) && /(?:function\s+\w+|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ")) diagnostics.push(slop(relativePath, i + 1, "ai-slop/empty-function", "info", "Empty function body — possible stub or unfinished implementation", "Implement the function body or add a comment explaining why it's empty", false));
1739
+ if (JS_EXTENSIONS$4.has(ext) && /(?:function\s+\w+\s*\([^)]*\)|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ")) diagnostics.push(slop(relativePath, i + 1, "ai-slop/empty-function", "info", "Empty function body — possible stub or unfinished implementation", "Implement the function body or add a comment explaining why it's empty", false));
1518
1740
  }
1519
1741
  return diagnostics;
1520
1742
  };
@@ -1551,9 +1773,10 @@ const detectDeadPatterns = async (context) => {
1551
1773
  }
1552
1774
  const ext = path.extname(filePath);
1553
1775
  const relativePath = path.relative(context.rootDirectory, filePath);
1554
- diagnostics.push(...detectConsoleLeftovers(content, relativePath, ext));
1776
+ const codeOnly = maskComments(content, ext);
1777
+ diagnostics.push(...detectConsoleLeftovers(codeOnly, relativePath, ext));
1555
1778
  diagnostics.push(...detectTodoStubs(content, relativePath));
1556
- diagnostics.push(...detectDeadCodePatterns(content, relativePath, ext));
1779
+ diagnostics.push(...detectDeadCodePatterns(codeOnly, relativePath, ext));
1557
1780
  diagnostics.push(...detectUnsafeTypePatterns(content, relativePath, ext));
1558
1781
  }
1559
1782
  return diagnostics;
@@ -1743,6 +1966,7 @@ const JS_EXTENSIONS$3 = new Set([
1743
1966
  const IMPORT_FROM_RE$1 = /^\s*import\s+([^;]*?)\s+from\s+["']([^"']+)["']/;
1744
1967
  const TYPE_ONLY_RE = /^\s*type\b/;
1745
1968
  const VALUE_BINDING_RE = /\{([^}]*)\}/;
1969
+ const NAMESPACE_RE = /\*\s+as\s+/;
1746
1970
  const isTypeOnly = (clause) => {
1747
1971
  if (TYPE_ONLY_RE.test(clause)) return true;
1748
1972
  const braces = VALUE_BINDING_RE.exec(clause);
@@ -1760,7 +1984,8 @@ const extractImportLines = (content) => {
1760
1984
  results.push({
1761
1985
  spec: match[2],
1762
1986
  line: i + 1,
1763
- typeOnly: isTypeOnly(match[1])
1987
+ typeOnly: isTypeOnly(match[1]),
1988
+ namespace: NAMESPACE_RE.test(match[1])
1764
1989
  });
1765
1990
  }
1766
1991
  return results;
@@ -1777,11 +2002,11 @@ const detectDuplicateImports = async (context) => {
1777
2002
  } catch {
1778
2003
  continue;
1779
2004
  }
1780
- const imports = extractImportLines(content);
2005
+ const imports = extractImportLines(maskComments(content, path.extname(filePath)));
1781
2006
  if (imports.length < 2) continue;
1782
2007
  const byBucket = /* @__PURE__ */ new Map();
1783
2008
  for (const imp of imports) {
1784
- const key = `${imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
2009
+ const key = `${imp.namespace ? "ns" : imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
1785
2010
  const list = byBucket.get(key) ?? [];
1786
2011
  list.push(imp);
1787
2012
  byBucket.set(key, list);
@@ -1897,9 +2122,8 @@ const detectSwallowedExceptions = async (context) => {
1897
2122
  const relativePath = path.relative(context.rootDirectory, filePath);
1898
2123
  for (const { pattern, languages, message } of SWALLOWED_EXCEPTION_PATTERNS) {
1899
2124
  if (!languages.includes(ext)) continue;
1900
- let match;
1901
2125
  const regex = new RegExp(pattern.source, pattern.flags + (pattern.flags.includes("g") ? "" : "g"));
1902
- while ((match = regex.exec(content)) !== null) {
2126
+ for (const match of content.matchAll(regex)) {
1903
2127
  if (isIntentionalIgnore(match[0], ext)) continue;
1904
2128
  const line = content.slice(0, match.index).split("\n").length;
1905
2129
  diagnostics.push({
@@ -1965,190 +2189,30 @@ const flagLibraryPanic = (lines, relPath, pkg, out) => {
1965
2189
  severity: "warning",
1966
2190
  message: `\`panic()\` in package \`${pkg}\` (non-main, non-test). Library code should return errors, not unwind the goroutine.`,
1967
2191
  help: "Convert to `return fmt.Errorf(...)` (or a wrapped error) and let the caller decide. Reserve `panic` for genuinely-impossible states (corrupt internal invariants), and mark those with a comment so future readers know it's intentional.",
1968
- line: i + 1,
1969
- column: 1,
1970
- category: "AI Slop",
1971
- fixable: false
1972
- });
1973
- }
1974
- };
1975
- const detectGoPatterns = async (context) => {
1976
- const diagnostics = [];
1977
- const files = getSourceFiles(context);
1978
- for (const filePath of files) {
1979
- if (!GO_EXTENSIONS.has(path.extname(filePath))) continue;
1980
- if (isAutoGenerated(filePath)) continue;
1981
- if (filePath.endsWith("_test.go")) continue;
1982
- let content;
1983
- try {
1984
- content = fs.readFileSync(filePath, "utf-8");
1985
- } catch {
1986
- continue;
1987
- }
1988
- const lines = content.split("\n");
1989
- const pkg = detectPackageName(lines);
1990
- if (!pkg) continue;
1991
- flagLibraryPanic(lines, path.relative(context.rootDirectory, filePath), pkg, diagnostics);
1992
- }
1993
- return diagnostics;
1994
- };
1995
-
1996
- //#endregion
1997
- //#region src/engines/ai-slop/hardcoded-config.ts
1998
- const SOURCE_EXTENSIONS = new Set([
1999
- ".ts",
2000
- ".tsx",
2001
- ".js",
2002
- ".jsx",
2003
- ".mjs",
2004
- ".cjs",
2005
- ".py",
2006
- ".go",
2007
- ".rs",
2008
- ".rb",
2009
- ".java",
2010
- ".php"
2011
- ]);
2012
- const URL_LITERAL_RE = /(["'`])(https?:\/\/[^"'`\s<>]+)\1/g;
2013
- const ID_LITERAL_RE = /(["'])([A-Za-z][A-Za-z0-9_-]{15,})\1/g;
2014
- const ENV_REFERENCE_RE = /\b(?:process\.env|import\.meta\.env|Deno\.env|os\.environ|getenv|env\()\b/i;
2015
- const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|license|readme|source|svgUrl|pageUrl|href|link|install)\b/i;
2016
- const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
2017
- const ENVIRONMENT_HOST_RE = /(?:^|[.-])(?:api|app|admin|auth|staging|stage|prod|dev|sandbox|webhook|internal)(?:[.-]|$)|^(?:localhost|127\.0\.0\.1|0\.0\.0\.0)$/i;
2018
- const ID_CONTEXT_RE = /(?:^|[^A-Za-z0-9])(?:api[_-]?key|client[_-]?id|project[_-]?id|org(?:anization)?[_-]?id|workspace[_-]?id|tenant[_-]?id|price[_-]?id|product[_-]?id|customer[_-]?id|subscription[_-]?id|account[_-]?id|app[_-]?id|key|token|secret)(?:$|[^A-Za-z0-9])/i;
2019
- const MIGRATION_PATH_RE$1 = /(?:^|[\\/])(?:migrations?|db[\\/]migrate)[\\/]/i;
2020
- const PLACEHOLDER_HOSTS = new Set([
2021
- "example.com",
2022
- "example.org",
2023
- "example.net"
2024
- ]);
2025
- const LOOPBACK_HOSTS = new Set([
2026
- "localhost",
2027
- "127.0.0.1",
2028
- "0.0.0.0",
2029
- "::1"
2030
- ]);
2031
- const VENDOR_API_DOMAINS = [
2032
- "github.com",
2033
- "githubusercontent.com",
2034
- "googleapis.com",
2035
- "accounts.google.com",
2036
- "stripe.com",
2037
- "openai.com",
2038
- "anthropic.com",
2039
- "slack.com",
2040
- "twilio.com",
2041
- "sendgrid.com",
2042
- "mailgun.net",
2043
- "cloudflare.com",
2044
- "discord.com",
2045
- "telegram.org",
2046
- "login.microsoftonline.com",
2047
- "graph.microsoft.com",
2048
- "twitter.com",
2049
- "x.com",
2050
- "twimg.com",
2051
- "t.co",
2052
- "api.telegram.org"
2053
- ];
2054
- const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
2055
- const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
2056
- const HARDCODED_URL_FINDING = {
2057
- rule: "ai-slop/hardcoded-url",
2058
- message: "Hardcoded environment URL in production code",
2059
- help: "Move deployment-specific URLs to environment variables or a typed config module. Keep only stable documentation/public links inline."
2060
- };
2061
- const HARDCODED_ID_FINDING = {
2062
- rule: "ai-slop/hardcoded-id",
2063
- message: "Hardcoded provider/project ID in production code",
2064
- help: "Move provider IDs, tenant IDs, price IDs, and similar deployment-specific identifiers to env/config so agents do not bake one environment into source."
2065
- };
2066
- const makeFinding = (filePath, line, spec) => ({
2067
- filePath,
2068
- engine: "ai-slop",
2069
- rule: spec.rule,
2070
- severity: "warning",
2071
- message: spec.message,
2072
- help: spec.help,
2073
- line,
2074
- column: 0,
2075
- category: "AI Slop",
2076
- fixable: false
2077
- });
2078
- const isCommentOnlyLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*");
2079
- const commentStartsBefore = (line, index, ext) => {
2080
- const prefix = line.slice(0, index);
2081
- if (ext === ".py" || ext === ".rb") return prefix.includes("#");
2082
- if (ext === ".php") return prefix.includes("//") || prefix.includes("#");
2083
- return prefix.includes("//") || prefix.includes("/*");
2084
- };
2085
- const safeUrlHost = (urlText) => {
2086
- try {
2087
- return new URL(urlText).hostname.toLowerCase();
2088
- } catch {
2089
- return null;
2090
- }
2091
- };
2092
- const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
2093
- const shouldFlagUrlLiteral = (line, urlText) => {
2094
- if (isEnvBackedLine(line)) return false;
2095
- const host = safeUrlHost(urlText);
2096
- if (!host) return false;
2097
- if (PLACEHOLDER_HOSTS.has(host)) return false;
2098
- if (LOOPBACK_HOSTS.has(host)) return false;
2099
- if (isVendorApiHost(host)) return false;
2100
- if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
2101
- return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
2102
- };
2103
- const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
2104
- const hasUsefulIdShape = (value) => {
2105
- if (PLACEHOLDER_ID_RE.test(value)) return false;
2106
- if (ENV_VAR_NAME_RE.test(value)) return false;
2107
- if (/^https?:\/\//i.test(value)) return false;
2108
- if (/^[A-Za-z]+$/.test(value)) return false;
2109
- return /[0-9]/.test(value);
2110
- };
2111
- const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
2112
- const diagnostics = [];
2113
- if (isCommentOnlyLine(line.trim())) return diagnostics;
2114
- URL_LITERAL_RE.lastIndex = 0;
2115
- let urlMatch;
2116
- while ((urlMatch = URL_LITERAL_RE.exec(line)) !== null) {
2117
- const urlText = urlMatch[2];
2118
- if (commentStartsBefore(line, urlMatch.index, ext)) continue;
2119
- if (!shouldFlagUrlLiteral(line, urlText)) continue;
2120
- diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_URL_FINDING));
2121
- }
2122
- if (!ID_CONTEXT_RE.test(line) || isEnvBackedLine(line) || DOC_URL_CONTEXT_RE.test(line)) return diagnostics;
2123
- ID_LITERAL_RE.lastIndex = 0;
2124
- let idMatch;
2125
- while ((idMatch = ID_LITERAL_RE.exec(line)) !== null) {
2126
- const value = idMatch[2];
2127
- if (commentStartsBefore(line, idMatch.index, ext)) continue;
2128
- if (!hasUsefulIdShape(value)) continue;
2129
- diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_ID_FINDING));
2130
- }
2131
- return diagnostics;
2132
- };
2133
- const scanFileForConfigLiterals = (content, relativePath, ext) => {
2134
- if (!SOURCE_EXTENSIONS.has(ext)) return [];
2135
- if (isNonProductionPath(relativePath)) return [];
2136
- if (MIGRATION_PATH_RE$1.test(relativePath)) return [];
2137
- return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
2192
+ line: i + 1,
2193
+ column: 1,
2194
+ category: "AI Slop",
2195
+ fixable: false
2196
+ });
2197
+ }
2138
2198
  };
2139
- const detectHardcodedConfigLiterals = async (context) => {
2199
+ const detectGoPatterns = async (context) => {
2140
2200
  const diagnostics = [];
2141
- for (const filePath of getSourceFiles(context)) {
2201
+ const files = getSourceFiles(context);
2202
+ for (const filePath of files) {
2203
+ if (!GO_EXTENSIONS.has(path.extname(filePath))) continue;
2142
2204
  if (isAutoGenerated(filePath)) continue;
2205
+ if (filePath.endsWith("_test.go")) continue;
2143
2206
  let content;
2144
2207
  try {
2145
2208
  content = fs.readFileSync(filePath, "utf-8");
2146
2209
  } catch {
2147
2210
  continue;
2148
2211
  }
2149
- const relativePath = path.relative(context.rootDirectory, filePath);
2150
- const ext = path.extname(filePath);
2151
- diagnostics.push(...scanFileForConfigLiterals(content, relativePath, ext));
2212
+ const lines = content.split("\n");
2213
+ const pkg = detectPackageName(lines);
2214
+ if (!pkg) continue;
2215
+ flagLibraryPanic(lines, path.relative(context.rootDirectory, filePath), pkg, diagnostics);
2152
2216
  }
2153
2217
  return diagnostics;
2154
2218
  };
@@ -2250,15 +2314,18 @@ const readWorkspaceGlobs = (rootDir, rootPkg) => {
2250
2314
  }
2251
2315
  return globs;
2252
2316
  };
2317
+ const readWorkspaceEntries = (dir) => {
2318
+ try {
2319
+ return fs.readdirSync(dir, { withFileTypes: true });
2320
+ } catch {
2321
+ return [];
2322
+ }
2323
+ };
2253
2324
  const expandWorkspaceDirs = (rootDir, globs) => {
2254
2325
  const dirs = [];
2255
2326
  for (const glob of globs) if (glob.endsWith("/*")) {
2256
2327
  const parent = path.join(rootDir, glob.slice(0, -2));
2257
- try {
2258
- for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
2259
- } catch {
2260
- continue;
2261
- }
2328
+ for (const entry of readWorkspaceEntries(parent)) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
2262
2329
  } else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
2263
2330
  return dirs;
2264
2331
  };
@@ -2891,6 +2958,241 @@ const detectHallucinatedImports = async (context) => {
2891
2958
  return diagnostics;
2892
2959
  };
2893
2960
 
2961
+ //#endregion
2962
+ //#region src/engines/ai-slop/hardcoded-config.ts
2963
+ const SOURCE_EXTENSIONS = new Set([
2964
+ ".ts",
2965
+ ".tsx",
2966
+ ".js",
2967
+ ".jsx",
2968
+ ".mjs",
2969
+ ".cjs",
2970
+ ".py",
2971
+ ".go",
2972
+ ".rs",
2973
+ ".rb",
2974
+ ".java",
2975
+ ".php"
2976
+ ]);
2977
+ const URL_LITERAL_RE = /(["'`])(https?:\/\/[^"'`\s<>]+)\1/g;
2978
+ const ID_LITERAL_RE = /(["'])([A-Za-z][A-Za-z0-9_-]{15,})\1/g;
2979
+ const ENV_REFERENCE_RE = /\b(?:process\.env|import\.meta\.env|Deno\.env|os\.environ|getenv|env\()\b/i;
2980
+ const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|license|readme|source|svgUrl|pageUrl|href|link|install)\b/i;
2981
+ const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
2982
+ const ENVIRONMENT_HOST_RE = /(?:^|[.-])(?:api|app|admin|auth|staging|stage|prod|dev|sandbox|webhook|internal)(?:[.-]|$)|^(?:localhost|127\.0\.0\.1|0\.0\.0\.0)$/i;
2983
+ const ID_CONTEXT_RE = /(?:^|[^A-Za-z0-9])(?:api[_-]?key|client[_-]?id|project[_-]?id|org(?:anization)?[_-]?id|workspace[_-]?id|tenant[_-]?id|price[_-]?id|product[_-]?id|customer[_-]?id|subscription[_-]?id|account[_-]?id|app[_-]?id|key|token|secret)(?:$|[^A-Za-z0-9])/i;
2984
+ const MIGRATION_PATH_RE$1 = /(?:^|[\\/])(?:migrations?|db[\\/]migrate)[\\/]/i;
2985
+ const PLACEHOLDER_HOSTS = new Set([
2986
+ "example.com",
2987
+ "example.org",
2988
+ "example.net"
2989
+ ]);
2990
+ const LOOPBACK_HOSTS = new Set([
2991
+ "localhost",
2992
+ "127.0.0.1",
2993
+ "0.0.0.0",
2994
+ "::1"
2995
+ ]);
2996
+ const VENDOR_API_DOMAINS = [
2997
+ "github.com",
2998
+ "githubusercontent.com",
2999
+ "googleapis.com",
3000
+ "accounts.google.com",
3001
+ "stripe.com",
3002
+ "openai.com",
3003
+ "anthropic.com",
3004
+ "slack.com",
3005
+ "twilio.com",
3006
+ "sendgrid.com",
3007
+ "mailgun.net",
3008
+ "cloudflare.com",
3009
+ "discord.com",
3010
+ "telegram.org",
3011
+ "login.microsoftonline.com",
3012
+ "graph.microsoft.com",
3013
+ "twitter.com",
3014
+ "x.com",
3015
+ "twimg.com",
3016
+ "t.co",
3017
+ "api.telegram.org"
3018
+ ];
3019
+ const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
3020
+ const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
3021
+ const HARDCODED_URL_FINDING = {
3022
+ rule: "ai-slop/hardcoded-url",
3023
+ message: "Hardcoded environment URL in production code",
3024
+ help: "Move deployment-specific URLs to environment variables or a typed config module. Keep only stable documentation/public links inline."
3025
+ };
3026
+ const HARDCODED_ID_FINDING = {
3027
+ rule: "ai-slop/hardcoded-id",
3028
+ message: "Hardcoded provider/project ID in production code",
3029
+ help: "Move provider IDs, tenant IDs, price IDs, and similar deployment-specific identifiers to env/config so agents do not bake one environment into source."
3030
+ };
3031
+ const makeFinding = (filePath, line, spec) => ({
3032
+ filePath,
3033
+ engine: "ai-slop",
3034
+ rule: spec.rule,
3035
+ severity: "warning",
3036
+ message: spec.message,
3037
+ help: spec.help,
3038
+ line,
3039
+ column: 0,
3040
+ category: "AI Slop",
3041
+ fixable: false
3042
+ });
3043
+ const isCommentOnlyLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*");
3044
+ const commentStartsBefore = (line, index, ext) => {
3045
+ const prefix = line.slice(0, index);
3046
+ if (ext === ".py" || ext === ".rb") return prefix.includes("#");
3047
+ if (ext === ".php") return prefix.includes("//") || prefix.includes("#");
3048
+ return prefix.includes("//") || prefix.includes("/*");
3049
+ };
3050
+ const safeUrlHost = (urlText) => {
3051
+ try {
3052
+ return new URL(urlText).hostname.toLowerCase();
3053
+ } catch {
3054
+ return null;
3055
+ }
3056
+ };
3057
+ const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
3058
+ const shouldFlagUrlLiteral = (line, urlText) => {
3059
+ if (isEnvBackedLine(line)) return false;
3060
+ const host = safeUrlHost(urlText);
3061
+ if (!host) return false;
3062
+ if (PLACEHOLDER_HOSTS.has(host)) return false;
3063
+ if (LOOPBACK_HOSTS.has(host)) return false;
3064
+ if (isVendorApiHost(host)) return false;
3065
+ if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
3066
+ return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
3067
+ };
3068
+ const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
3069
+ const hasUsefulIdShape = (value) => {
3070
+ if (PLACEHOLDER_ID_RE.test(value)) return false;
3071
+ if (ENV_VAR_NAME_RE.test(value)) return false;
3072
+ if (/^https?:\/\//i.test(value)) return false;
3073
+ if (/^[A-Za-z]+$/.test(value)) return false;
3074
+ return /[0-9]/.test(value);
3075
+ };
3076
+ const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
3077
+ const diagnostics = [];
3078
+ if (isCommentOnlyLine(line.trim())) return diagnostics;
3079
+ for (const urlMatch of line.matchAll(URL_LITERAL_RE)) {
3080
+ const urlText = urlMatch[2];
3081
+ if (commentStartsBefore(line, urlMatch.index, ext)) continue;
3082
+ if (!shouldFlagUrlLiteral(line, urlText)) continue;
3083
+ diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_URL_FINDING));
3084
+ }
3085
+ if (!ID_CONTEXT_RE.test(line) || isEnvBackedLine(line) || DOC_URL_CONTEXT_RE.test(line)) return diagnostics;
3086
+ for (const idMatch of line.matchAll(ID_LITERAL_RE)) {
3087
+ const value = idMatch[2];
3088
+ if (commentStartsBefore(line, idMatch.index, ext)) continue;
3089
+ if (!hasUsefulIdShape(value)) continue;
3090
+ diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_ID_FINDING));
3091
+ }
3092
+ return diagnostics;
3093
+ };
3094
+ const scanFileForConfigLiterals = (content, relativePath, ext) => {
3095
+ if (!SOURCE_EXTENSIONS.has(ext)) return [];
3096
+ if (isNonProductionPath(relativePath)) return [];
3097
+ if (MIGRATION_PATH_RE$1.test(relativePath)) return [];
3098
+ return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
3099
+ };
3100
+ const detectHardcodedConfigLiterals = async (context) => {
3101
+ const diagnostics = [];
3102
+ for (const filePath of getSourceFiles(context)) {
3103
+ if (isAutoGenerated(filePath)) continue;
3104
+ let content;
3105
+ try {
3106
+ content = fs.readFileSync(filePath, "utf-8");
3107
+ } catch {
3108
+ continue;
3109
+ }
3110
+ const relativePath = path.relative(context.rootDirectory, filePath);
3111
+ const ext = path.extname(filePath);
3112
+ diagnostics.push(...scanFileForConfigLiterals(maskComments(content, ext), relativePath, ext));
3113
+ }
3114
+ return diagnostics;
3115
+ };
3116
+
3117
+ //#endregion
3118
+ //#region src/utils/suppress.ts
3119
+ const DIRECTIVE_RE = /(?:\/\/|\/\*|#|<!--|\*)\s*aislop-ignore-(next-line|line|file)\b([^\n]*)/;
3120
+ const isAislopDirectiveLine = (line) => DIRECTIVE_RE.test(line);
3121
+ const parseDirective = (rest) => {
3122
+ const tokens = rest.split("--")[0].match(/[A-Za-z0-9@][\w@/.-]*/g) ?? [];
3123
+ if (tokens.length === 0) return {
3124
+ rules: /* @__PURE__ */ new Set(),
3125
+ all: true
3126
+ };
3127
+ return {
3128
+ rules: new Set(tokens),
3129
+ all: false
3130
+ };
3131
+ };
3132
+ const covers = (directive, rule) => directive.all || [...directive.rules].some((r) => r === rule || rule.endsWith(`/${r}`));
3133
+ const parseFileDirectives = (content) => {
3134
+ const lines = content.split(/\r?\n/);
3135
+ const file = [];
3136
+ const byLine = /* @__PURE__ */ new Map();
3137
+ const addLine = (target, directive) => {
3138
+ const list = byLine.get(target) ?? [];
3139
+ list.push(directive);
3140
+ byLine.set(target, list);
3141
+ };
3142
+ for (let i = 0; i < lines.length; i++) {
3143
+ const match = DIRECTIVE_RE.exec(lines[i]);
3144
+ if (!match) continue;
3145
+ const scope = match[1];
3146
+ const directive = parseDirective(match[2] ?? "");
3147
+ if (scope === "file") file.push(directive);
3148
+ else if (scope === "next-line") addLine(i + 2, directive);
3149
+ else addLine(i + 1, directive);
3150
+ }
3151
+ return {
3152
+ file,
3153
+ byLine
3154
+ };
3155
+ };
3156
+ const applySuppressions = (results, rootDirectory) => {
3157
+ const cache = /* @__PURE__ */ new Map();
3158
+ let suppressedCount = 0;
3159
+ const load = (filePath) => {
3160
+ const cached = cache.get(filePath);
3161
+ if (cached !== void 0) return cached;
3162
+ const absolute = path.isAbsolute(filePath) ? filePath : path.join(rootDirectory, filePath);
3163
+ let parsed = null;
3164
+ try {
3165
+ parsed = parseFileDirectives(fs.readFileSync(absolute, "utf-8"));
3166
+ } catch {
3167
+ parsed = null;
3168
+ }
3169
+ cache.set(filePath, parsed);
3170
+ return parsed;
3171
+ };
3172
+ const isSuppressed = (diagnostic) => {
3173
+ const directives = load(diagnostic.filePath);
3174
+ if (!directives) return false;
3175
+ if (directives.file.some((d) => covers(d, diagnostic.rule))) return true;
3176
+ return (directives.byLine.get(diagnostic.line) ?? []).some((d) => covers(d, diagnostic.rule));
3177
+ };
3178
+ return {
3179
+ results: results.map((result) => {
3180
+ const kept = result.diagnostics.filter((diagnostic) => {
3181
+ if (isSuppressed(diagnostic)) {
3182
+ suppressedCount += 1;
3183
+ return false;
3184
+ }
3185
+ return true;
3186
+ });
3187
+ return {
3188
+ ...result,
3189
+ diagnostics: kept
3190
+ };
3191
+ }),
3192
+ suppressedCount
3193
+ };
3194
+ };
3195
+
2894
3196
  //#endregion
2895
3197
  //#region src/engines/ai-slop/comment-blocks.ts
2896
3198
  const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
@@ -2914,6 +3216,7 @@ const getCommentSyntax = (ext) => {
2914
3216
  };
2915
3217
  const getMatchedLinePrefix = (line, syntax) => {
2916
3218
  const trimmed = line.trimStart();
3219
+ if (isAislopDirectiveLine(trimmed)) return null;
2917
3220
  for (const prefix of syntax.linePrefixes) {
2918
3221
  if (!trimmed.startsWith(prefix)) continue;
2919
3222
  if (prefix === "#" && trimmed.startsWith("#!")) return null;
@@ -3654,7 +3957,7 @@ const detectRustPatterns = async (context) => {
3654
3957
 
3655
3958
  //#endregion
3656
3959
  //#region src/engines/ai-slop/silent-recovery.ts
3657
- const JS_EXTS$1 = new Set([
3960
+ const JS_EXTS = new Set([
3658
3961
  ".ts",
3659
3962
  ".tsx",
3660
3963
  ".js",
@@ -3713,9 +4016,7 @@ const isLogOnlyBody = (body) => {
3713
4016
  };
3714
4017
  const detectJsSilentRecovery = (content, relPath) => {
3715
4018
  const out = [];
3716
- CATCH_HEAD_RE.lastIndex = 0;
3717
- let match;
3718
- while ((match = CATCH_HEAD_RE.exec(content)) !== null) {
4019
+ for (const match of content.matchAll(CATCH_HEAD_RE)) {
3719
4020
  const body = extractCatchBody(content, match.index + match[0].length - 1);
3720
4021
  if (body === null) continue;
3721
4022
  if (!isLogOnlyBody(body)) continue;
@@ -3783,7 +4084,7 @@ const detectSilentRecovery = async (context) => {
3783
4084
  for (const filePath of files) {
3784
4085
  if (isAutoGenerated(filePath)) continue;
3785
4086
  const ext = path.extname(filePath);
3786
- const isJs = JS_EXTS$1.has(ext);
4087
+ const isJs = JS_EXTS.has(ext);
3787
4088
  if (!isJs && !(ext === ".py")) continue;
3788
4089
  const relPath = path.relative(context.rootDirectory, filePath);
3789
4090
  if (isNonProductionPath(relPath)) continue;
@@ -3892,18 +4193,22 @@ const extractPyImportedSymbols = (lines) => {
3892
4193
  }
3893
4194
  continue;
3894
4195
  }
3895
- const importMatch = trimmed.match(/^import\s+([\w.]+)(?:\s+as\s+(\w+))?/);
4196
+ const importMatch = trimmed.match(/^import\s+(.+)/);
3896
4197
  if (importMatch) {
3897
4198
  importLines.add(i);
3898
- const alias = importMatch[2];
3899
- if (alias && alias === importMatch[1]) continue;
3900
- const simpleName = (alias ?? importMatch[1]).split(".")[0];
3901
- if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
3902
- name: simpleName,
3903
- line: i + 1,
3904
- isDefault: false,
3905
- isNamespace: true
3906
- });
4199
+ for (const clause of importMatch[1].replace(/#.*$/, "").split(",")) {
4200
+ const clauseMatch = clause.trim().match(/^([\w.]+)(?:\s+as\s+(\w+))?/);
4201
+ if (!clauseMatch) continue;
4202
+ const alias = clauseMatch[2];
4203
+ if (alias && alias === clauseMatch[1]) continue;
4204
+ const simpleName = (alias ?? clauseMatch[1]).split(".")[0];
4205
+ if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
4206
+ name: simpleName,
4207
+ line: i + 1,
4208
+ isDefault: false,
4209
+ isNamespace: true
4210
+ });
4211
+ }
3907
4212
  }
3908
4213
  }
3909
4214
  return {
@@ -3913,8 +4218,7 @@ const extractPyImportedSymbols = (lines) => {
3913
4218
  };
3914
4219
  const isSymbolUsed = (name, content, importLines, lines) => {
3915
4220
  const pattern = new RegExp(`\\b${name}\\b`, "g");
3916
- let match;
3917
- while ((match = pattern.exec(content)) !== null) {
4221
+ for (const match of content.matchAll(pattern)) {
3918
4222
  const lineIndex = content.slice(0, match.index).split("\n").length - 1;
3919
4223
  if (!importLines.has(lineIndex)) return true;
3920
4224
  }
@@ -4015,6 +4319,18 @@ const aiSlopEngine = {
4015
4319
 
4016
4320
  //#endregion
4017
4321
  //#region src/engines/architecture/matchers.ts
4322
+ const REGEX_SPECIAL_CHARS = new Set([
4323
+ ".",
4324
+ "+",
4325
+ "^",
4326
+ "$",
4327
+ "{",
4328
+ "}",
4329
+ "(",
4330
+ ")",
4331
+ "|",
4332
+ "\\"
4333
+ ]);
4018
4334
  const minimatch = (filePath, pattern) => {
4019
4335
  let regex = "";
4020
4336
  let i = 0;
@@ -4039,7 +4355,7 @@ const minimatch = (filePath, pattern) => {
4039
4355
  regex += pattern.slice(i, closeIndex + 1);
4040
4356
  i = closeIndex + 1;
4041
4357
  }
4042
- } else if (".+^${}()|\\".includes(ch)) {
4358
+ } else if (REGEX_SPECIAL_CHARS.has(ch)) {
4043
4359
  regex += `\\${ch}`;
4044
4360
  i++;
4045
4361
  } else {
@@ -4059,27 +4375,15 @@ const extractImports = (content, ext) => {
4059
4375
  ".mjs",
4060
4376
  ".cjs"
4061
4377
  ].includes(ext)) {
4062
- const esPattern = /(?:import|from)\s+["']([^"']+)["']/g;
4063
- let match;
4064
- while ((match = esPattern.exec(content)) !== null) imports.push(match[1]);
4065
- const reqPattern = /require\s*\(\s*["']([^"']+)["']\s*\)/g;
4066
- while ((match = reqPattern.exec(content)) !== null) imports.push(match[1]);
4067
- }
4068
- if (ext === ".py") {
4069
- const pyPattern = /(?:from|import)\s+([\w.]+)/g;
4070
- let match;
4071
- while ((match = pyPattern.exec(content)) !== null) imports.push(match[1]);
4378
+ for (const match of content.matchAll(/(?:import|from)\s+["']([^"']+)["']/g)) imports.push(match[1]);
4379
+ for (const match of content.matchAll(/require\s*\(\s*["']([^"']+)["']\s*\)/g)) imports.push(match[1]);
4072
4380
  }
4381
+ if (ext === ".py") for (const match of content.matchAll(/(?:from|import)\s+([\w.]+)/g)) imports.push(match[1]);
4073
4382
  if (ext === ".go") {
4074
- const goSingleImport = /^\s*import\s+"([^"]+)"/gm;
4075
- let match;
4076
- while ((match = goSingleImport.exec(content)) !== null) imports.push(match[1]);
4077
- const goMultiImport = /import\s*\(([^)]*)\)/gs;
4078
- while ((match = goMultiImport.exec(content)) !== null) {
4383
+ for (const match of content.matchAll(/^\s*import\s+"([^"]+)"/gm)) imports.push(match[1]);
4384
+ for (const match of content.matchAll(/import\s*\(([^)]*)\)/gs)) {
4079
4385
  const block = match[1];
4080
- const pkgPattern = /"([^"]+)"/g;
4081
- let pkgMatch;
4082
- while ((pkgMatch = pkgPattern.exec(block)) !== null) imports.push(pkgMatch[1]);
4386
+ for (const pkgMatch of block.matchAll(/"([^"]+)"/g)) imports.push(pkgMatch[1]);
4083
4387
  }
4084
4388
  }
4085
4389
  return imports;
@@ -4206,10 +4510,10 @@ const architectureEngine = {
4206
4510
  //#endregion
4207
4511
  //#region src/engines/code-quality/function-boundaries.ts
4208
4512
  const PYTHON_CONTROL_FLOW_RE = /^\s*(?:if|for|while|with|try|except|else|elif|finally|def|class)\b/;
4209
- const ARROW_BLOCK_RE = /* @__PURE__ */ new RegExp("=>\\s*\\{");
4210
- const ARROW_END_RE = /* @__PURE__ */ new RegExp("=>\\s*$");
4211
- const BRACE_START_RE = /* @__PURE__ */ new RegExp("^\\s*\\{");
4212
- const NEW_STATEMENT_RE = /* @__PURE__ */ new RegExp("^(?:export\\s+)?(?:const|let|var|function|class)\\s");
4513
+ const ARROW_BLOCK_RE = /=>\s*\{/;
4514
+ const ARROW_END_RE = /=>\s*$/;
4515
+ const BRACE_START_RE = /^\s*\{/;
4516
+ const NEW_STATEMENT_RE = /^(?:export\s+)?(?:const|let|var|function|class)\s/;
4213
4517
  const isControlFlowBrace = (lineText, braceIndex) => {
4214
4518
  const before = lineText.substring(0, braceIndex).trimEnd();
4215
4519
  if (before.endsWith(")")) return true;
@@ -4263,12 +4567,92 @@ const findBraceFunctionEnd = (lines, startIndex) => {
4263
4567
  maxNesting
4264
4568
  };
4265
4569
  };
4266
- const findPythonFunctionEnd = (lines, startIndex) => {
4267
- const baseIndent = lines[startIndex].match(/^(\s*)/)?.[1].length ?? 0;
4268
- let endLine = startIndex;
4570
+ const extractPythonSignature = (lines, startIndex) => {
4571
+ let depth = 0;
4572
+ let started = false;
4573
+ let params = "";
4574
+ for (let j = startIndex; j < lines.length; j++) {
4575
+ const l = lines[j];
4576
+ for (let ci = 0; ci < l.length; ci++) {
4577
+ const ch = l[ci];
4578
+ if (ch === "(") {
4579
+ depth++;
4580
+ if (depth === 1 && !started) {
4581
+ started = true;
4582
+ continue;
4583
+ }
4584
+ } else if (ch === ")") {
4585
+ depth--;
4586
+ if (depth === 0) return {
4587
+ params,
4588
+ sigEndIndex: j
4589
+ };
4590
+ }
4591
+ if (started) params += ch;
4592
+ }
4593
+ if (started) params += " ";
4594
+ }
4595
+ return {
4596
+ params,
4597
+ sigEndIndex: startIndex
4598
+ };
4599
+ };
4600
+ const countPythonParams = (signature) => {
4601
+ let depth = 0;
4602
+ const parts = [];
4603
+ let current = "";
4604
+ for (const ch of signature) {
4605
+ if (ch === "(" || ch === "[" || ch === "{") depth++;
4606
+ else if (ch === ")" || ch === "]" || ch === "}") depth--;
4607
+ if (ch === "," && depth === 0) {
4608
+ parts.push(current);
4609
+ current = "";
4610
+ continue;
4611
+ }
4612
+ current += ch;
4613
+ }
4614
+ parts.push(current);
4615
+ let count = 0;
4616
+ for (const raw of parts) {
4617
+ const p = raw.trim();
4618
+ if (p.length === 0 || p === "*" || p === "/") continue;
4619
+ if (p.startsWith("*")) continue;
4620
+ if (p.includes("=")) continue;
4621
+ const name = p.split(":")[0].trim();
4622
+ if (name === "self" || name === "cls") continue;
4623
+ count++;
4624
+ }
4625
+ return count;
4626
+ };
4627
+ const countPythonBodyCodeLines = (lines, sigEndIndex, endLine) => {
4628
+ let count = 0;
4629
+ let inDoc = false;
4630
+ let delim = "";
4631
+ for (let j = sigEndIndex + 1; j <= endLine && j < lines.length; j++) {
4632
+ const t = lines[j].trim();
4633
+ if (inDoc) {
4634
+ if (t.includes(delim)) inDoc = false;
4635
+ continue;
4636
+ }
4637
+ if (t === "" || t.startsWith("#")) continue;
4638
+ const opener = t.startsWith("\"\"\"") ? "\"\"\"" : t.startsWith("'''") ? "'''" : "";
4639
+ if (opener) {
4640
+ if (!t.slice(3).includes(opener)) {
4641
+ inDoc = true;
4642
+ delim = opener;
4643
+ }
4644
+ continue;
4645
+ }
4646
+ count++;
4647
+ }
4648
+ return count;
4649
+ };
4650
+ const findPythonFunctionEnd = (lines, defIndex, bodyStartIndex) => {
4651
+ const baseIndent = lines[defIndex].match(/^(\s*)/)?.[1].length ?? 0;
4652
+ let endLine = bodyStartIndex;
4269
4653
  let maxNesting = 0;
4270
4654
  const controlIndentStack = [];
4271
- for (let j = startIndex + 1; j < lines.length; j++) {
4655
+ for (let j = bodyStartIndex + 1; j < lines.length; j++) {
4272
4656
  const l = lines[j];
4273
4657
  if (l.trim() === "") {
4274
4658
  endLine = j;
@@ -4290,7 +4674,10 @@ const findPythonFunctionEnd = (lines, startIndex) => {
4290
4674
  };
4291
4675
  };
4292
4676
  const findFunctionEnd = (lines, startIndex, isPython) => {
4293
- if (isPython) return findPythonFunctionEnd(lines, startIndex);
4677
+ if (isPython) {
4678
+ const { sigEndIndex } = extractPythonSignature(lines, startIndex);
4679
+ return findPythonFunctionEnd(lines, startIndex, sigEndIndex);
4680
+ }
4294
4681
  return findBraceFunctionEnd(lines, startIndex);
4295
4682
  };
4296
4683
  const isBlockArrow = (lines, startIndex) => {
@@ -4312,14 +4699,14 @@ const countTemplateLines = (bodyLines) => {
4312
4699
  let templateLineCount = 0;
4313
4700
  for (const line of bodyLines) {
4314
4701
  const startedInside = insideTemplate;
4315
- let escape = false;
4702
+ let escaped = false;
4316
4703
  for (const ch of line) {
4317
- if (escape) {
4318
- escape = false;
4704
+ if (escaped) {
4705
+ escaped = false;
4319
4706
  continue;
4320
4707
  }
4321
4708
  if (ch === "\\") {
4322
- escape = true;
4709
+ escaped = true;
4323
4710
  continue;
4324
4711
  }
4325
4712
  if (ch === "`") insideTemplate = !insideTemplate;
@@ -4355,7 +4742,7 @@ const FUNCTION_PATTERNS = [
4355
4742
  ]
4356
4743
  },
4357
4744
  {
4358
- regex: /^\s*def\s+(\w+)\s*\(([^)]*)\)/,
4745
+ regex: /^\s*(?:async\s+)?def\s+(\w+)\s*\(/,
4359
4746
  langFilter: [".py"]
4360
4747
  },
4361
4748
  {
@@ -4412,14 +4799,23 @@ const analyzeFunctions = (content, ext) => {
4412
4799
  const isPython = fnMatch.patternIndex === 2;
4413
4800
  if (fnMatch.patternIndex === 1 && !isBlockArrow(lines, i)) continue;
4414
4801
  const { endLine, maxNesting } = findFunctionEnd(lines, i, isPython);
4415
- const bodyLines = lines.slice(i + 1, endLine);
4416
- const templateLines = isPython ? 0 : countTemplateLines(bodyLines);
4802
+ let templateLines;
4803
+ let paramCount;
4804
+ if (isPython) {
4805
+ const sig = extractPythonSignature(lines, i);
4806
+ const codeLines = countPythonBodyCodeLines(lines, sig.sigEndIndex, endLine);
4807
+ templateLines = endLine - i + 1 - codeLines;
4808
+ paramCount = countPythonParams(sig.params);
4809
+ } else {
4810
+ templateLines = countTemplateLines(lines.slice(i + 1, endLine));
4811
+ paramCount = countParams(fnMatch.params);
4812
+ }
4417
4813
  functions.push({
4418
4814
  name: fnMatch.name,
4419
4815
  startLine: i + 1,
4420
4816
  lineCount: endLine - i + 1,
4421
4817
  maxNesting,
4422
- paramCount: countParams(fnMatch.params),
4818
+ paramCount,
4423
4819
  templateLines
4424
4820
  });
4425
4821
  }
@@ -5354,9 +5750,7 @@ const runRuffFormat = async (context) => {
5354
5750
  };
5355
5751
  const parseRuffFormatOutput = (output, rootDir) => {
5356
5752
  const diagnostics = [];
5357
- const filePattern = /^--- (.+)$/gm;
5358
- let match;
5359
- while ((match = filePattern.exec(output)) !== null) {
5753
+ for (const match of output.matchAll(/^--- (.+)$/gm)) {
5360
5754
  const filePath = getRuffDiagnosticPath(rootDir, match[1]);
5361
5755
  diagnostics.push({
5362
5756
  filePath,
@@ -5390,10 +5784,10 @@ const formatEngine = {
5390
5784
  const { languages, installedTools } = context;
5391
5785
  const promises = [];
5392
5786
  if (languages.includes("typescript") || languages.includes("javascript")) promises.push(runBiomeFormat(context));
5393
- if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffFormat(context));
5394
- if (languages.includes("go") && installedTools["gofmt"]) promises.push(runGofmt(context));
5395
- if (languages.includes("rust") && installedTools["rustfmt"]) promises.push(runGenericFormatter(context, "rust"));
5396
- if (languages.includes("ruby") && installedTools["rubocop"]) promises.push(runGenericFormatter(context, "ruby"));
5787
+ if (languages.includes("python") && installedTools.ruff) promises.push(runRuffFormat(context));
5788
+ if (languages.includes("go") && installedTools.gofmt) promises.push(runGofmt(context));
5789
+ if (languages.includes("rust") && installedTools.rustfmt) promises.push(runGenericFormatter(context, "rust"));
5790
+ if (languages.includes("ruby") && installedTools.rubocop) promises.push(runGenericFormatter(context, "ruby"));
5397
5791
  if (languages.includes("php") && installedTools["php-cs-fixer"]) promises.push(runGenericFormatter(context, "php"));
5398
5792
  const results = await Promise.allSettled(promises);
5399
5793
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
@@ -5829,6 +6223,8 @@ const createOxlintConfig = (options) => {
5829
6223
  if (options.mode === "fix") {
5830
6224
  rules["no-unused-vars"] = "off";
5831
6225
  rules["react-hooks/exhaustive-deps"] = "off";
6226
+ rules["jsx-a11y/no-aria-hidden-on-focusable"] = "off";
6227
+ rules["unicorn/no-useless-fallback-in-spread"] = "off";
5832
6228
  }
5833
6229
  const plugins = [
5834
6230
  "import",
@@ -5993,9 +6389,7 @@ const collectAmbientGlobals = (rootDir) => {
5993
6389
  if (!relativePath.endsWith(".d.ts")) continue;
5994
6390
  const content = readTextFile$1(path.join(rootDir, relativePath));
5995
6391
  if (!content) continue;
5996
- AMBIENT_GLOBAL_RE.lastIndex = 0;
5997
- let match;
5998
- while ((match = AMBIENT_GLOBAL_RE.exec(content)) !== null) globals.add(match[1]);
6392
+ for (const match of content.matchAll(AMBIENT_GLOBAL_RE)) globals.add(match[1]);
5999
6393
  }
6000
6394
  const deps = collectPackageNames(rootDir);
6001
6395
  if (deps.has("@types/bun") || deps.has("bun-types")) globals.add("Bun");
@@ -6346,10 +6740,10 @@ const lintEngine = {
6346
6740
  if (context.config.lint.typecheck) promises.push(import("./typecheck-wVSohmOX.js").then((mod) => mod.runTypecheck(context)));
6347
6741
  }
6348
6742
  if (context.frameworks.includes("expo")) promises.push(Promise.resolve().then(() => expo_doctor_exports).then((mod) => mod.runExpoDoctor(context)));
6349
- if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
6743
+ if (languages.includes("python") && installedTools.ruff) promises.push(runRuffLint(context));
6350
6744
  if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
6351
- if (languages.includes("rust") && installedTools["cargo"]) promises.push(runGenericLinter(context, "rust"));
6352
- if (languages.includes("ruby") && installedTools["rubocop"]) promises.push(runGenericLinter(context, "ruby"));
6745
+ if (languages.includes("rust") && installedTools.cargo) promises.push(runGenericLinter(context, "rust"));
6746
+ if (languages.includes("ruby") && installedTools.rubocop) promises.push(runGenericLinter(context, "ruby"));
6353
6747
  const results = await Promise.allSettled(promises);
6354
6748
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
6355
6749
  return {
@@ -6379,7 +6773,7 @@ const runDependencyAudit = async (context) => {
6379
6773
  else if (fs.existsSync(path.join(context.rootDirectory, "package-lock.json")) || fs.existsSync(path.join(context.rootDirectory, "package.json"))) promises.push(runNpmAudit(context.rootDirectory, timeout));
6380
6774
  }
6381
6775
  if (context.languages.includes("python") && context.installedTools["pip-audit"]) promises.push(runPipAudit(context.rootDirectory, timeout));
6382
- if (context.languages.includes("go") && context.installedTools["govulncheck"]) promises.push(runGovulncheck(context.rootDirectory, timeout));
6776
+ if (context.languages.includes("go") && context.installedTools.govulncheck) promises.push(runGovulncheck(context.rootDirectory, timeout));
6383
6777
  if (context.languages.includes("rust")) promises.push(runCargoAudit(context.rootDirectory, timeout));
6384
6778
  const results = await Promise.allSettled(promises);
6385
6779
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
@@ -6484,9 +6878,12 @@ const parseLegacyAdvisories = (advisories, source) => {
6484
6878
  for (const [key, advisory] of Object.entries(advisories)) upsertVuln(bucket, advisory.module_name ?? advisory.name ?? advisory.package ?? key, (advisory.severity ?? "moderate").toLowerCase(), advisory.recommendation ?? advisory.title ?? "");
6485
6879
  return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
6486
6880
  };
6881
+ const carriesAdvisory = (vulnerability) => Array.isArray(vulnerability.via) && vulnerability.via.some((entry) => entry !== null && typeof entry === "object");
6487
6882
  const parseModernVulnerabilities = (vulnerabilities, source) => {
6488
6883
  const bucket = /* @__PURE__ */ new Map();
6884
+ const hasRootCauses = Object.values(vulnerabilities).some(carriesAdvisory);
6489
6885
  for (const [packageName, vulnerability] of Object.entries(vulnerabilities)) {
6886
+ if (hasRootCauses && !carriesAdvisory(vulnerability)) continue;
6490
6887
  const severity = (vulnerability.severity ?? "moderate").toLowerCase();
6491
6888
  const fixAvailable = vulnerability.fixAvailable;
6492
6889
  const isDirect = vulnerability.isDirect === true;
@@ -6568,271 +6965,65 @@ const runGovulncheck = async (rootDir, timeout) => {
6568
6965
  cwd: rootDir,
6569
6966
  timeout
6570
6967
  });
6571
- if (!result.stdout) return [];
6572
- return parseGovulncheckOutput(result.stdout);
6573
- } catch {
6574
- return [];
6575
- }
6576
- };
6577
- const toGovulnDiagnostic = (entry) => {
6578
- if (!entry.vulnerability) return null;
6579
- return {
6580
- filePath: "go.mod",
6581
- engine: "security",
6582
- rule: "security/vulnerable-dependency",
6583
- severity: "error",
6584
- message: `Go vulnerability: ${entry.vulnerability.id ?? "unknown"}`,
6585
- help: withFixHint(entry.vulnerability.details ?? ""),
6586
- line: 0,
6587
- column: 0,
6588
- category: "Security",
6589
- fixable: false
6590
- };
6591
- };
6592
- const parseGovulncheckOutput = (output) => {
6593
- const diagnostics = [];
6594
- for (const line of output.split("\n")) {
6595
- if (!line.startsWith("{")) continue;
6596
- let parsed = null;
6597
- try {
6598
- parsed = JSON.parse(line);
6599
- } catch {
6600
- parsed = null;
6601
- }
6602
- if (!parsed) continue;
6603
- const diagnostic = toGovulnDiagnostic(parsed);
6604
- if (diagnostic) diagnostics.push(diagnostic);
6605
- }
6606
- return diagnostics;
6607
- };
6608
- const runCargoAudit = async (rootDir, timeout) => {
6609
- try {
6610
- const result = await runSubprocess("cargo", ["audit", "--json"], {
6611
- cwd: rootDir,
6612
- timeout
6613
- });
6614
- if (!result.stdout) return [];
6615
- return (JSON.parse(result.stdout).vulnerabilities?.list ?? []).map((v) => ({
6616
- filePath: "Cargo.toml",
6617
- engine: "security",
6618
- rule: "security/vulnerable-dependency",
6619
- severity: "error",
6620
- message: `Rust vulnerability: ${v.advisory?.id ?? "unknown"}`,
6621
- help: withFixHint(v.advisory?.title ?? ""),
6622
- line: 0,
6623
- column: 0,
6624
- category: "Security",
6625
- fixable: false
6626
- }));
6627
- } catch {
6628
- return [];
6629
- }
6630
- };
6631
-
6632
- //#endregion
6633
- //#region src/utils/source-masker.ts
6634
- const JS_EXTS = new Set([
6635
- ".ts",
6636
- ".tsx",
6637
- ".js",
6638
- ".jsx",
6639
- ".mjs",
6640
- ".cjs"
6641
- ]);
6642
- const PY_EXTS = new Set([".py"]);
6643
- const RB_EXTS = new Set([".rb"]);
6644
- const PHP_EXTS = new Set([".php"]);
6645
- const familyForExt = (ext) => {
6646
- if (JS_EXTS.has(ext)) return "js";
6647
- if (PY_EXTS.has(ext)) return "py";
6648
- if (RB_EXTS.has(ext)) return "rb";
6649
- if (PHP_EXTS.has(ext)) return "php";
6650
- return "none";
6651
- };
6652
- const maskStringsAndComments = (content, ext) => {
6653
- const family = familyForExt(ext);
6654
- if (family === "none") return content;
6655
- if (family === "js") return maskJs(content);
6656
- return maskSimple(content, family);
6657
- };
6658
- const handleQuotesAndComments = (content, i, tplStack, mask) => {
6659
- const len = content.length;
6660
- const c = content[i];
6661
- const next = content[i + 1];
6662
- if (c === "\"" || c === "'") {
6663
- const strStart = i;
6664
- const end = consumeQuotedString(content, i, c);
6665
- mask(strStart + 1, end - 1);
6666
- return {
6667
- handled: true,
6668
- nextI: end
6669
- };
6670
- }
6671
- if (c === "`") {
6672
- const scan = consumeTemplateString(content, i + 1);
6673
- mask(i + 1, scan.maskEnd);
6674
- if (scan.openedInterp) tplStack.push(0);
6675
- return {
6676
- handled: true,
6677
- nextI: scan.resumeAt
6678
- };
6679
- }
6680
- if (c === "/" && next === "/") {
6681
- const strStart = i;
6682
- let k = i;
6683
- while (k < len && content[k] !== "\n") k++;
6684
- mask(strStart, k);
6685
- return {
6686
- handled: true,
6687
- nextI: k
6688
- };
6689
- }
6690
- if (c === "/" && next === "*") {
6691
- const strStart = i;
6692
- let k = i + 2;
6693
- while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
6694
- if (k < len - 1) k += 2;
6695
- mask(strStart, k);
6696
- return {
6697
- handled: true,
6698
- nextI: k
6699
- };
6700
- }
6701
- return {
6702
- handled: false,
6703
- nextI: i
6704
- };
6705
- };
6706
- const maskJs = (content) => {
6707
- const out = content.split("");
6708
- const len = content.length;
6709
- const tplStack = [];
6710
- let i = 0;
6711
- const mask = (start, end) => {
6712
- for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
6713
- };
6714
- while (i < len) {
6715
- const c = content[i];
6716
- if (tplStack.length > 0) {
6717
- if (c === "{") {
6718
- tplStack[tplStack.length - 1]++;
6719
- i++;
6720
- continue;
6721
- }
6722
- if (c === "}") {
6723
- if (tplStack[tplStack.length - 1] === 0) {
6724
- tplStack.pop();
6725
- const scan = consumeTemplateString(content, i + 1);
6726
- mask(i + 1, scan.maskEnd);
6727
- if (scan.openedInterp) tplStack.push(0);
6728
- i = scan.resumeAt;
6729
- continue;
6730
- }
6731
- tplStack[tplStack.length - 1]--;
6732
- i++;
6733
- continue;
6734
- }
6735
- }
6736
- const handled = handleQuotesAndComments(content, i, tplStack, mask);
6737
- if (handled.handled) {
6738
- i = handled.nextI;
6739
- continue;
6740
- }
6741
- i++;
6742
- }
6743
- return out.join("");
6744
- };
6745
- const consumeQuotedString = (content, start, quote) => {
6746
- const len = content.length;
6747
- let i = start + 1;
6748
- while (i < len) {
6749
- const c = content[i];
6750
- if (c === "\\" && i + 1 < len) {
6751
- i += 2;
6752
- continue;
6753
- }
6754
- if (c === quote) return i + 1;
6755
- if (c === "\n") return i;
6756
- i++;
6757
- }
6758
- return i;
6759
- };
6760
- const consumeTemplateString = (content, start) => {
6761
- const len = content.length;
6762
- let i = start;
6763
- while (i < len) {
6764
- const c = content[i];
6765
- if (c === "\\" && i + 1 < len) {
6766
- i += 2;
6767
- continue;
6768
- }
6769
- if (c === "`") return {
6770
- maskEnd: i,
6771
- resumeAt: i + 1,
6772
- openedInterp: false
6773
- };
6774
- if (c === "$" && content[i + 1] === "{") return {
6775
- maskEnd: i,
6776
- resumeAt: i + 2,
6777
- openedInterp: true
6778
- };
6779
- i++;
6780
- }
6781
- return {
6782
- maskEnd: i,
6783
- resumeAt: i,
6784
- openedInterp: false
6785
- };
6786
- };
6787
- const maskSimple = (content, family) => {
6788
- const out = content.split("");
6789
- const len = content.length;
6790
- let i = 0;
6791
- const mask = (start, end) => {
6792
- for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
6793
- };
6794
- while (i < len) {
6795
- const c = content[i];
6796
- const next = content[i + 1];
6797
- if (family === "py" && (c === "\"" || c === "'")) {
6798
- if (content[i + 1] === c && content[i + 2] === c) {
6799
- const triple = c + c + c;
6800
- const end = content.indexOf(triple, i + 3);
6801
- const stop = end === -1 ? len : end + 3;
6802
- mask(i + 3, stop - 3);
6803
- i = stop;
6804
- continue;
6805
- }
6806
- }
6807
- if (c === "\"" || c === "'") {
6808
- const strStart = i;
6809
- i = consumeQuotedString(content, i, c);
6810
- mask(strStart + 1, i - 1);
6811
- continue;
6812
- }
6813
- if ((family === "py" || family === "rb" || family === "php") && c === "#") {
6814
- const strStart = i;
6815
- while (i < len && content[i] !== "\n") i++;
6816
- mask(strStart, i);
6817
- continue;
6818
- }
6819
- if (family === "php" && c === "/" && next === "/") {
6820
- const strStart = i;
6821
- while (i < len && content[i] !== "\n") i++;
6822
- mask(strStart, i);
6823
- continue;
6824
- }
6825
- if (family === "php" && c === "/" && next === "*") {
6826
- const strStart = i;
6827
- i += 2;
6828
- while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
6829
- if (i < len - 1) i += 2;
6830
- mask(strStart, i);
6831
- continue;
6968
+ if (!result.stdout) return [];
6969
+ return parseGovulncheckOutput(result.stdout);
6970
+ } catch {
6971
+ return [];
6972
+ }
6973
+ };
6974
+ const toGovulnDiagnostic = (entry) => {
6975
+ if (!entry.vulnerability) return null;
6976
+ return {
6977
+ filePath: "go.mod",
6978
+ engine: "security",
6979
+ rule: "security/vulnerable-dependency",
6980
+ severity: "error",
6981
+ message: `Go vulnerability: ${entry.vulnerability.id ?? "unknown"}`,
6982
+ help: withFixHint(entry.vulnerability.details ?? ""),
6983
+ line: 0,
6984
+ column: 0,
6985
+ category: "Security",
6986
+ fixable: false
6987
+ };
6988
+ };
6989
+ const parseGovulncheckOutput = (output) => {
6990
+ const diagnostics = [];
6991
+ for (const line of output.split("\n")) {
6992
+ if (!line.startsWith("{")) continue;
6993
+ let parsed = null;
6994
+ try {
6995
+ parsed = JSON.parse(line);
6996
+ } catch {
6997
+ parsed = null;
6832
6998
  }
6833
- i++;
6999
+ if (!parsed) continue;
7000
+ const diagnostic = toGovulnDiagnostic(parsed);
7001
+ if (diagnostic) diagnostics.push(diagnostic);
7002
+ }
7003
+ return diagnostics;
7004
+ };
7005
+ const runCargoAudit = async (rootDir, timeout) => {
7006
+ try {
7007
+ const result = await runSubprocess("cargo", ["audit", "--json"], {
7008
+ cwd: rootDir,
7009
+ timeout
7010
+ });
7011
+ if (!result.stdout) return [];
7012
+ return (JSON.parse(result.stdout).vulnerabilities?.list ?? []).map((v) => ({
7013
+ filePath: "Cargo.toml",
7014
+ engine: "security",
7015
+ rule: "security/vulnerable-dependency",
7016
+ severity: "error",
7017
+ message: `Rust vulnerability: ${v.advisory?.id ?? "unknown"}`,
7018
+ help: withFixHint(v.advisory?.title ?? ""),
7019
+ line: 0,
7020
+ column: 0,
7021
+ category: "Security",
7022
+ fixable: false
7023
+ }));
7024
+ } catch {
7025
+ return [];
6834
7026
  }
6835
- return out.join("");
6836
7027
  };
6837
7028
 
6838
7029
  //#endregion
@@ -6980,8 +7171,7 @@ const detectRiskyConstructs = async (context) => {
6980
7171
  if (!extensions.includes(ext)) continue;
6981
7172
  if (isMigrationOrSeeder && name === "sql-injection") continue;
6982
7173
  const regex = new RegExp(pattern.source, pattern.flags);
6983
- let match;
6984
- while ((match = regex.exec(masked)) !== null) {
7174
+ for (const match of masked.matchAll(regex)) {
6985
7175
  const line = content.slice(0, match.index).split("\n").length;
6986
7176
  if (name === "innerhtml") {
6987
7177
  const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
@@ -7109,11 +7299,11 @@ const scanSecrets = async (context) => {
7109
7299
  } catch {
7110
7300
  continue;
7111
7301
  }
7302
+ content = maskComments(content, path.extname(filePath));
7112
7303
  const relativePath = path.relative(context.rootDirectory, filePath);
7113
7304
  for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
7114
7305
  const regex = new RegExp(pattern.source, pattern.flags);
7115
- let match;
7116
- while ((match = regex.exec(content)) !== null) {
7306
+ for (const match of content.matchAll(regex)) {
7117
7307
  if (isPlaceholderValue(match[1] ?? match[0])) continue;
7118
7308
  if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
7119
7309
  const line = content.slice(0, match.index).split("\n").length;
@@ -7234,6 +7424,64 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
7234
7424
 
7235
7425
  //#endregion
7236
7426
  //#region src/utils/discover.ts
7427
+ const UNSUPPORTED_CODE_EXTENSIONS = {
7428
+ ".c": "C/C++",
7429
+ ".h": "C/C++",
7430
+ ".cc": "C/C++",
7431
+ ".cpp": "C/C++",
7432
+ ".cxx": "C/C++",
7433
+ ".hpp": "C/C++",
7434
+ ".hh": "C/C++",
7435
+ ".hxx": "C/C++",
7436
+ ".cs": "C#",
7437
+ ".swift": "Swift",
7438
+ ".kt": "Kotlin",
7439
+ ".kts": "Kotlin",
7440
+ ".m": "Objective-C",
7441
+ ".mm": "Objective-C",
7442
+ ".scala": "Scala",
7443
+ ".dart": "Dart",
7444
+ ".ex": "Elixir",
7445
+ ".exs": "Elixir",
7446
+ ".erl": "Erlang",
7447
+ ".hs": "Haskell",
7448
+ ".clj": "Clojure",
7449
+ ".cljs": "Clojure",
7450
+ ".lua": "Lua",
7451
+ ".jl": "Julia",
7452
+ ".zig": "Zig",
7453
+ ".nim": "Nim",
7454
+ ".ml": "OCaml",
7455
+ ".fs": "F#",
7456
+ ".sol": "Solidity",
7457
+ ".groovy": "Groovy"
7458
+ };
7459
+ const analyzeCoverage = (rootDirectory, excludePatterns = []) => {
7460
+ const allFiles = listProjectFiles(rootDirectory);
7461
+ const supportedFiles = filterProjectFiles(rootDirectory, allFiles, [], excludePatterns).length;
7462
+ const counts = /* @__PURE__ */ new Map();
7463
+ let unsupportedFiles = 0;
7464
+ const candidates = filterProjectFiles(rootDirectory, allFiles, Object.keys(UNSUPPORTED_CODE_EXTENSIONS), excludePatterns);
7465
+ for (const file of candidates) {
7466
+ const lang = UNSUPPORTED_CODE_EXTENSIONS[path.extname(file).toLowerCase()];
7467
+ if (!lang) continue;
7468
+ unsupportedFiles += 1;
7469
+ counts.set(lang, (counts.get(lang) ?? 0) + 1);
7470
+ }
7471
+ let dominantUnsupported = null;
7472
+ let max = 0;
7473
+ for (const [lang, count] of counts) if (count > max) {
7474
+ max = count;
7475
+ dominantUnsupported = lang;
7476
+ }
7477
+ const negligible = supportedFiles === 0 || unsupportedFiles >= 10 && unsupportedFiles > supportedFiles * 3;
7478
+ return {
7479
+ supportedFiles,
7480
+ unsupportedFiles,
7481
+ dominantUnsupported,
7482
+ scoreable: !negligible
7483
+ };
7484
+ };
7237
7485
  const LANGUAGE_SIGNALS = {
7238
7486
  "tsconfig.json": "typescript",
7239
7487
  "go.mod": "go",
@@ -7353,11 +7601,12 @@ const checkInstalledTools = async () => {
7353
7601
  }));
7354
7602
  return results;
7355
7603
  };
7356
- const discoverProject = async (directory) => {
7604
+ const discoverProject = async (directory, excludePatterns = []) => {
7357
7605
  const resolvedDir = path.resolve(directory);
7358
7606
  const languages = detectLanguages(resolvedDir);
7359
7607
  const frameworks = detectFrameworks(resolvedDir);
7360
7608
  const sourceFileCount = countSourceFiles(resolvedDir);
7609
+ const coverage = analyzeCoverage(resolvedDir, excludePatterns);
7361
7610
  const installedTools = await checkInstalledTools();
7362
7611
  return {
7363
7612
  rootDirectory: resolvedDir,
@@ -7365,6 +7614,7 @@ const discoverProject = async (directory) => {
7365
7614
  languages,
7366
7615
  frameworks,
7367
7616
  sourceFileCount,
7617
+ coverage,
7368
7618
  installedTools
7369
7619
  };
7370
7620
  };
@@ -8071,7 +8321,7 @@ const uninstallRulesOnly = (opts, paths) => {
8071
8321
  else result.skipped.push(paths.rules);
8072
8322
  if (paths.host && paths.marker) {
8073
8323
  const host = readIfExists(paths.host);
8074
- if (host != null && host.includes(paths.marker)) {
8324
+ if (host?.includes(paths.marker)) {
8075
8325
  const stripped = host.split("\n").filter((l) => l.trim() !== paths.marker).join("\n").replace(/\n{3,}/g, "\n\n").trim();
8076
8326
  applyRemoval(result, opts, paths.host, stripped.length === 0 ? null : `${stripped}\n`);
8077
8327
  } else result.skipped.push(paths.host);
@@ -8487,7 +8737,7 @@ const uninstallGemini = (opts) => {
8487
8737
  if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
8488
8738
  else result.skipped.push(paths.aislopMd);
8489
8739
  const geminiMd = readIfExists(paths.geminiMd);
8490
- if (geminiMd != null && geminiMd.includes("@AISLOP.md")) {
8740
+ if (geminiMd?.includes("@AISLOP.md")) {
8491
8741
  const stripped = geminiMd.split("\n").filter((l) => l.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
8492
8742
  applyRemoval(result, opts, paths.geminiMd, stripped.length === 0 ? null : `${stripped}\n`);
8493
8743
  } else result.skipped.push(paths.geminiMd);
@@ -8784,6 +9034,7 @@ const theme = createTheme();
8784
9034
 
8785
9035
  //#endregion
8786
9036
  //#region src/commands/hook.ts
9037
+ const HOOK_FLUSH_TIMEOUT_MS = 1500;
8787
9038
  const AGENT_LABELS = {
8788
9039
  claude: {
8789
9040
  label: "Claude Code",
@@ -8912,6 +9163,7 @@ const hookRun = async (agent, flags) => {
8912
9163
  process.stderr.write(`hook: agent "${agent}" has no runtime adapter (rules-file-only)\n`);
8913
9164
  process.exit(0);
8914
9165
  }
9166
+ await flushTelemetry(HOOK_FLUSH_TIMEOUT_MS);
8915
9167
  process.exit(exitCode);
8916
9168
  };
8917
9169
  const hookBaseline = async () => {
@@ -9165,12 +9417,12 @@ const badgeCommand = async (options = {}) => {
9165
9417
  svgUrl,
9166
9418
  pageUrl
9167
9419
  });
9168
- if (options.json) process.stdout.write(JSON.stringify({
9420
+ if (options.json) process.stdout.write(`${JSON.stringify({
9169
9421
  owner,
9170
9422
  repo,
9171
9423
  svgUrl,
9172
9424
  pageUrl
9173
- }) + "\n");
9425
+ })}\n`);
9174
9426
  else process.stdout.write(output);
9175
9427
  return {
9176
9428
  owner,
@@ -9471,7 +9723,7 @@ const renderHeader = (input, _deps = {}) => {
9471
9723
 
9472
9724
  //#endregion
9473
9725
  //#region src/ui/width.ts
9474
- const ANSI_RE = new RegExp(String.raw`\x1B\[[0-9;]*m`, "g");
9726
+ const ANSI_RE = new RegExp(`\\[[0-9;]*m`, "g");
9475
9727
  const stripAnsi = (s) => s.replace(ANSI_RE, "");
9476
9728
  const stringWidth = (s) => {
9477
9729
  const bare = stripAnsi(s);
@@ -9615,6 +9867,8 @@ var LiveGrid = class {
9615
9867
  const RULE_LABELS = {
9616
9868
  formatting: "Code not formatted",
9617
9869
  "code-quality/duplicate-block": "Duplicate code block",
9870
+ "code-quality/repeated-chained-call": "Repeated chained call",
9871
+ "code-quality/unused-declaration": "Unused declaration",
9618
9872
  "complexity/file-too-large": "File too large",
9619
9873
  "complexity/function-too-long": "Function too long",
9620
9874
  "complexity/deep-nesting": "Deeply nested code",
@@ -9627,6 +9881,7 @@ const RULE_LABELS = {
9627
9881
  "knip/binaries": "Unused binary",
9628
9882
  "knip/exports": "Unused export",
9629
9883
  "knip/types": "Unused type",
9884
+ "knip/duplicates": "Duplicate export",
9630
9885
  "ai-slop/trivial-comment": "Trivial restating comment",
9631
9886
  "ai-slop/swallowed-exception": "Empty catch (swallowed error)",
9632
9887
  "ai-slop/silent-recovery": "Catch logs then continues",
@@ -9663,6 +9918,7 @@ const RULE_LABELS = {
9663
9918
  "ai-slop/hallucinated-import": "Import not in package.json",
9664
9919
  "security/hardcoded-secret": "Possible hardcoded secret",
9665
9920
  "security/vulnerable-dependency": "Vulnerable dependency",
9921
+ "security/dependency-audit-skipped": "Dependency audit skipped",
9666
9922
  "security/eval": "eval() usage",
9667
9923
  "security/innerhtml": "innerHTML assignment",
9668
9924
  "security/dangerously-set-innerhtml": "dangerouslySetInnerHTML (XSS risk)",
@@ -9820,6 +10076,36 @@ const readHistory = (directory) => {
9820
10076
  return records;
9821
10077
  };
9822
10078
 
10079
+ //#endregion
10080
+ //#region src/commands/scan-coverage.ts
10081
+ const coverageReason = (c) => {
10082
+ if (c.supportedFiles === 0 && c.dominantUnsupported) return `This repository is ${c.dominantUnsupported} (${c.unsupportedFiles} files), which aislop does not analyze. No score — it would not reflect this code.`;
10083
+ if (c.supportedFiles === 0) return "No files in a language aislop analyzes (TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP, Java). Nothing to score.";
10084
+ const lang = c.dominantUnsupported ?? "an unsupported language";
10085
+ const files = `${c.supportedFiles} supported file${c.supportedFiles === 1 ? "" : "s"}`;
10086
+ return `This repository is mostly ${lang} (${c.unsupportedFiles} files); aislop analyzed only ${files}. Score withheld — it would represent a sliver of the codebase.`;
10087
+ };
10088
+ const renderCoverageNotice = (projectInfo, includeHeader) => {
10089
+ const deps = {
10090
+ theme: createTheme(),
10091
+ symbols: createSymbols({ plain: false })
10092
+ };
10093
+ return `${includeHeader === false ? "" : renderHeader({
10094
+ version: APP_VERSION,
10095
+ command: "scan",
10096
+ context: [
10097
+ projectInfo.projectName,
10098
+ projectInfo.languages[0] ?? "unknown",
10099
+ `${projectInfo.sourceFileCount} files`
10100
+ ],
10101
+ brand: true
10102
+ }, deps)} ${coverageReason(projectInfo.coverage)}\n\n`;
10103
+ };
10104
+
10105
+ //#endregion
10106
+ //#region src/commands/scan-exit-code.ts
10107
+ const computeScanExitCode = (opts) => opts.hasErrors || opts.scoreable && opts.score < opts.failBelow ? 1 : 0;
10108
+
9823
10109
  //#endregion
9824
10110
  //#region src/commands/scan.ts
9825
10111
  const isMachineOutput = (options) => Boolean(options.json) || Boolean(options.sarif);
@@ -9926,7 +10212,7 @@ const scanCommand = async (directory, config, options) => {
9926
10212
  else log.error(msg);
9927
10213
  return { exitCode: 1 };
9928
10214
  }
9929
- const projectInfo = await discoverProject(resolvedDir);
10215
+ const projectInfo = await discoverProject(resolvedDir, [...config.exclude, ...readAislopIgnorePatterns(resolvedDir)]);
9930
10216
  return withCommandLifecycle({
9931
10217
  command: options.command ?? "scan",
9932
10218
  config: config.telemetry,
@@ -9939,15 +10225,16 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
9939
10225
  const showHeader = options.showHeader !== false;
9940
10226
  const machineOutput = isMachineOutput(options);
9941
10227
  const useLiveProgress = !machineOutput && shouldUseSpinner();
10228
+ const excludePatterns = [...config.exclude, ...readAislopIgnorePatterns(resolvedDir)];
9942
10229
  let files;
9943
10230
  if (options.staged) {
9944
- files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir), [], config.exclude);
10231
+ files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir), [], excludePatterns);
9945
10232
  if (!machineOutput) log.muted(`Scope: ${files.length} staged file(s)`);
9946
10233
  } else if (options.changes) {
9947
- files = filterProjectFiles(resolvedDir, getChangedFiles(resolvedDir), [], config.exclude);
10234
+ files = filterProjectFiles(resolvedDir, getChangedFiles(resolvedDir), [], excludePatterns);
9948
10235
  if (!machineOutput) log.muted(`Scope: ${files.length} changed file(s)`);
9949
10236
  } else {
9950
- files = filterProjectFiles(resolvedDir, listProjectFiles(resolvedDir), [], config.exclude);
10237
+ files = filterProjectFiles(resolvedDir, listProjectFiles(resolvedDir), [], excludePatterns);
9951
10238
  if (!machineOutput) log.muted(`Scope: ${files.length} file(s) after exclusions`);
9952
10239
  }
9953
10240
  const configDir = findConfigDir(resolvedDir);
@@ -10001,14 +10288,21 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
10001
10288
  if (!machineOutput && !progressRenderer) printEngineStatus(result);
10002
10289
  });
10003
10290
  progressRenderer?.stop();
10004
- const results = rawResults.map((result) => ({
10291
+ const { results, suppressedCount } = applySuppressions(rawResults.map((result) => ({
10005
10292
  ...result,
10006
10293
  diagnostics: applyRuleSeverities(result.diagnostics, config.rules)
10007
- }));
10294
+ })), resolvedDir);
10295
+ if (suppressedCount > 0 && !machineOutput) log.muted(`Suppressed ${suppressedCount} finding(s) via aislop-ignore directives`);
10008
10296
  const allDiagnostics = results.flatMap((r) => r.diagnostics);
10009
10297
  const elapsedMs = performance.now() - startTime;
10010
10298
  const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
10011
- const exitCode = allDiagnostics.some((d) => d.severity === "error") || scoreResult.score < config.ci.failBelow ? 1 : 0;
10299
+ const scoreable = projectInfo.coverage.scoreable;
10300
+ const exitCode = computeScanExitCode({
10301
+ hasErrors: allDiagnostics.some((d) => d.severity === "error"),
10302
+ scoreable,
10303
+ score: scoreResult.score,
10304
+ failBelow: config.ci.failBelow
10305
+ });
10012
10306
  const engineIssues = {};
10013
10307
  const engineTimings = {};
10014
10308
  for (const r of results) {
@@ -10017,7 +10311,8 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
10017
10311
  }
10018
10312
  const completion = {
10019
10313
  exitCode,
10020
- score: scoreResult.score,
10314
+ score: scoreable ? scoreResult.score : null,
10315
+ scoreable,
10021
10316
  findingCount: allDiagnostics.length,
10022
10317
  errorCount: allDiagnostics.filter((d) => d.severity === "error").length,
10023
10318
  warningCount: allDiagnostics.filter((d) => d.severity === "warning").length,
@@ -10031,11 +10326,18 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
10031
10326
  return completion;
10032
10327
  }
10033
10328
  if (options.json) {
10034
- const { buildJsonOutput } = await import("./json-OIzja7OM.js");
10035
- const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
10329
+ const { buildJsonOutput } = await import("./json-CXV4D0Ib.js");
10330
+ const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs, projectInfo.coverage);
10036
10331
  console.log(JSON.stringify(jsonOut, null, 2));
10037
10332
  return completion;
10038
10333
  }
10334
+ if (!scoreable) {
10335
+ if (!machineOutput) {
10336
+ process.stdout.write(renderCoverageNotice(projectInfo, showHeader));
10337
+ if (allDiagnostics.length > 0) process.stdout.write(renderDiagnostics(allDiagnostics, options.verbose ?? false));
10338
+ }
10339
+ return completion;
10340
+ }
10039
10341
  if (!options.staged && !options.changes && options.command !== "ci" && !isCiEnv()) appendHistory({
10040
10342
  directory: resolvedDir,
10041
10343
  score: scoreResult.score,
@@ -10815,7 +11117,7 @@ const ERROR_MESSAGE_PATTERNS = [
10815
11117
  /**
10816
11118
  * Extracts the full text of a console statement spanning multiple lines.
10817
11119
  */
10818
- const getStatementText = (lines, startIndex, span) => {
11120
+ const getStatementText = (lines, span) => {
10819
11121
  const spanLines = [];
10820
11122
  for (const lineNo of span) spanLines.push(lines[lineNo - 1]);
10821
11123
  return spanLines.join("\n");
@@ -10827,6 +11129,21 @@ const getStatementText = (lines, startIndex, span) => {
10827
11129
  const shouldUpgradeToError = (statementText) => {
10828
11130
  return ERROR_MESSAGE_PATTERNS.some((pattern) => pattern.test(statementText));
10829
11131
  };
11132
+ const DIAGNOSTIC_PATH_RE = /(?:^|\/)(?:tools|scripts|cli|bin)\/|(?:^|\/)test-[^/]*\.[tj]sx?$|[.-](?:test|spec)\.[tj]sx?$/i;
11133
+ const isDiagnosticScriptPath = (filePath) => DIAGNOSTIC_PATH_RE.test(filePath.replace(/\\/g, "/"));
11134
+ const firstNonBlank = (lines, from, step, skip) => {
11135
+ for (let i = from; i >= 0 && i < lines.length; i += step) {
11136
+ if (skip.has(i + 1)) continue;
11137
+ if (lines[i].trim() !== "") return lines[i].trim();
11138
+ }
11139
+ return "";
11140
+ };
11141
+ const wouldEmptyEnclosingBlock = (lines, span, removed) => {
11142
+ const sorted = [...span].sort((a, b) => a - b);
11143
+ const before = firstNonBlank(lines, sorted[0] - 2, -1, removed);
11144
+ const after = firstNonBlank(lines, sorted[sorted.length - 1], 1, removed);
11145
+ return before.endsWith("{") && after.startsWith("}");
11146
+ };
10830
11147
  const fixDeadPatterns = async (context) => {
10831
11148
  const fixable = [...await detectTrivialComments(context), ...await detectDeadPatterns(context)].filter((d) => d.fixable);
10832
11149
  if (fixable.length === 0) return;
@@ -10840,24 +11157,31 @@ const fixDeadPatterns = async (context) => {
10840
11157
  });
10841
11158
  byFile.set(absolute, entries);
10842
11159
  }
10843
- for (const [filePath, entries] of byFile) fixFileDeadPatterns(filePath, entries);
11160
+ for (const [filePath, entries] of byFile) fixFileDeadPatterns(filePath, entries, context.rootDirectory);
10844
11161
  };
10845
- const fixFileDeadPatterns = (filePath, entries) => {
11162
+ const fixFileDeadPatterns = (filePath, entries, rootDirectory) => {
10846
11163
  if (!fs.existsSync(filePath)) return;
10847
11164
  const lines = fs.readFileSync(filePath, "utf-8").split("\n");
10848
11165
  const linesToRemove = /* @__PURE__ */ new Set();
10849
11166
  const lineReplacements = /* @__PURE__ */ new Map();
11167
+ const skipConsole = isDiagnosticScriptPath(path.relative(rootDirectory, filePath));
11168
+ const consoleSpans = [];
10850
11169
  for (const entry of entries) {
10851
11170
  const index = entry.line - 1;
10852
11171
  if (index < 0 || index >= lines.length) continue;
10853
11172
  if (entry.rule === "ai-slop/console-leftover") {
11173
+ if (skipConsole) continue;
10854
11174
  const span = findStatementSpan(lines, index);
10855
- if (shouldUpgradeToError(getStatementText(lines, index, span))) {
10856
- const replaced = lines[index].replace(/console\.(?:log|debug|info|trace|dir|table)\s*\(/, "console.error(");
10857
- lineReplacements.set(entry.line, replaced);
10858
- } else for (const lineNo of span) linesToRemove.add(lineNo);
11175
+ if (shouldUpgradeToError(getStatementText(lines, span))) lineReplacements.set(entry.line, lines[index].replace(/console\.(?:log|debug|info|trace|dir|table)\s*\(/, "console.error("));
11176
+ else consoleSpans.push(span);
10859
11177
  } else linesToRemove.add(entry.line);
10860
11178
  }
11179
+ const candidateLines = /* @__PURE__ */ new Set();
11180
+ for (const span of consoleSpans) for (const lineNo of span) candidateLines.add(lineNo);
11181
+ for (const span of consoleSpans) {
11182
+ if (wouldEmptyEnclosingBlock(lines, span, candidateLines)) continue;
11183
+ for (const lineNo of span) linesToRemove.add(lineNo);
11184
+ }
10861
11185
  const result = applyEditsAndCollapse(lines, linesToRemove, lineReplacements);
10862
11186
  fs.writeFileSync(filePath, result);
10863
11187
  };
@@ -11107,7 +11431,7 @@ const fixUnusedImports = async (context) => {
11107
11431
  const importSpan = JS_EXTENSIONS$1.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
11108
11432
  if (allUnused) for (const idx of importSpan) linesToRemove.add(idx);
11109
11433
  else if (JS_EXTENSIONS$1.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
11110
- else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, syms, unusedNames);
11434
+ else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, unusedNames);
11111
11435
  }
11112
11436
  if (linesToRemove.size === 0 && unused.length === 0) continue;
11113
11437
  const sortedRemove = [...linesToRemove].sort((a, b) => b - a);
@@ -11171,9 +11495,12 @@ const rewriteJsImportSpan = (lines, span, syms, unusedNames) => {
11171
11495
  lines[span[0]] = newImport;
11172
11496
  for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
11173
11497
  };
11174
- const rewritePyImportLine = (lines, lineIdx, syms, unusedNames) => {
11498
+ const rewritePyImportLine = (lines, lineIdx, unusedNames) => {
11175
11499
  const fromMatch = lines[lineIdx].match(/^(\s*from\s+[\w.]+\s+import\s+)(.+)$/);
11176
- if (!fromMatch) return;
11500
+ if (!fromMatch) {
11501
+ rewritePlainPyImportLine(lines, lineIdx, unusedNames);
11502
+ return;
11503
+ }
11177
11504
  const prefix = fromMatch[1];
11178
11505
  const importPart = fromMatch[2].replace(/#.*$/, "").trim();
11179
11506
  const hasParen = importPart.startsWith("(");
@@ -11186,6 +11513,19 @@ const rewritePyImportLine = (lines, lineIdx, syms, unusedNames) => {
11186
11513
  const joined = keptSpecifiers.join(", ");
11187
11514
  lines[lineIdx] = hasParen ? `${prefix}(${joined})` : `${prefix}${joined}`;
11188
11515
  };
11516
+ const rewritePlainPyImportLine = (lines, lineIdx, unusedNames) => {
11517
+ const match = lines[lineIdx].match(/^(\s*import\s+)(.+)$/);
11518
+ if (!match) return;
11519
+ const prefix = match[1];
11520
+ const specifiers = match[2].replace(/#.*$/, "").split(",").map((s) => s.trim()).filter(Boolean);
11521
+ const kept = specifiers.filter((spec) => {
11522
+ const parts = spec.split(/\s+as\s+/);
11523
+ const localName = parts.length > 1 ? parts[1].trim() : parts[0].trim().split(".")[0];
11524
+ return !unusedNames.has(localName);
11525
+ });
11526
+ if (kept.length === 0 || kept.length === specifiers.length) return;
11527
+ lines[lineIdx] = `${prefix}${kept.join(", ")}`;
11528
+ };
11189
11529
 
11190
11530
  //#endregion
11191
11531
  //#region src/engines/code-quality/unused-removal-ast.ts
@@ -11627,6 +11967,61 @@ const runExpoDoctor = async (context) => {
11627
11967
  return toDiagnostics(parseIssues(output));
11628
11968
  };
11629
11969
 
11970
+ //#endregion
11971
+ //#region src/commands/fix-expo.ts
11972
+ const INSTALL_TIMEOUT$1 = 1800 * 1e3;
11973
+ const fixExpoDependencies = async (context, onProgress) => {
11974
+ await removeDisallowedExpoPackages(context.rootDirectory, onProgress);
11975
+ onProgress?.("Expo dependency alignment · running expo install --fix (can take a few minutes)");
11976
+ if ((await runSubprocess("npx", [
11977
+ "--yes",
11978
+ "expo",
11979
+ "install",
11980
+ "--fix"
11981
+ ], {
11982
+ cwd: context.rootDirectory,
11983
+ timeout: INSTALL_TIMEOUT$1
11984
+ })).exitCode === 0) return;
11985
+ onProgress?.("Expo dependency alignment · checking remaining issues");
11986
+ const checkResult = await runSubprocess("npx", [
11987
+ "--yes",
11988
+ "expo",
11989
+ "install",
11990
+ "--check"
11991
+ ], {
11992
+ cwd: context.rootDirectory,
11993
+ timeout: INSTALL_TIMEOUT$1
11994
+ });
11995
+ if (checkResult.exitCode !== 0) throw new Error(checkResult.stderr || checkResult.stdout || "expo dependency check failed");
11996
+ };
11997
+ /**
11998
+ * Run expo-doctor to detect packages that should not be installed directly,
11999
+ * then uninstall them. No hardcoded list — expo-doctor is the source of truth.
12000
+ */
12001
+ const removeDisallowedExpoPackages = async (rootDir, onProgress) => {
12002
+ try {
12003
+ onProgress?.("Expo dependency alignment · running expo-doctor");
12004
+ const result = await runSubprocess("npx", [
12005
+ "--yes",
12006
+ "expo-doctor",
12007
+ rootDir
12008
+ ], {
12009
+ cwd: rootDir,
12010
+ timeout: INSTALL_TIMEOUT$1
12011
+ });
12012
+ const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
12013
+ const packagePattern = /The package "([^"]+)" should not be installed directly/g;
12014
+ const toRemove = [];
12015
+ for (const match of output.matchAll(packagePattern)) toRemove.push(match[1]);
12016
+ if (toRemove.length === 0) return;
12017
+ onProgress?.(`Expo dependency alignment · uninstalling ${toRemove.length} package(s)`);
12018
+ await runSubprocess("npm", ["uninstall", ...toRemove], {
12019
+ cwd: rootDir,
12020
+ timeout: INSTALL_TIMEOUT$1
12021
+ });
12022
+ } catch {}
12023
+ };
12024
+
11630
12025
  //#endregion
11631
12026
  //#region src/commands/fix-force.ts
11632
12027
  const INSTALL_TIMEOUT = 1800 * 1e3;
@@ -11730,7 +12125,7 @@ const runNpmAuditFix = async (rootDir, onProgress) => {
11730
12125
  });
11731
12126
  if (installResult.exitCode !== 0) throw new Error(installResult.stderr || installResult.stdout || "npm install failed after audit fix");
11732
12127
  };
11733
- const fetchLatestVersion = async (rootDir, pkgName, pm) => {
12128
+ const fetchLatestVersion$1 = async (rootDir, pkgName, pm) => {
11734
12129
  try {
11735
12130
  const result = await runSubprocess(pm, [
11736
12131
  "view",
@@ -11750,7 +12145,7 @@ const collectOverrides = async (rootDir, vulnerabilities, pm) => {
11750
12145
  const overrides = {};
11751
12146
  for (const [pkgName, vuln] of Object.entries(vulnerabilities)) {
11752
12147
  if (vuln.fixAvailable !== false || !vuln.range) continue;
11753
- const latest = await fetchLatestVersion(rootDir, pkgName, pm);
12148
+ const latest = await fetchLatestVersion$1(rootDir, pkgName, pm);
11754
12149
  if (latest) overrides[pkgName] = latest;
11755
12150
  }
11756
12151
  return overrides;
@@ -11764,7 +12159,9 @@ const tryNpmOverrides = async (rootDir, onProgress) => {
11764
12159
  if (!auditResult.stdout) return;
11765
12160
  const vulnerabilities = JSON.parse(auditResult.stdout).vulnerabilities;
11766
12161
  if (!vulnerabilities) return;
11767
- const overrides = await collectOverrides(rootDir, vulnerabilities, "npm");
12162
+ const rawOverrides = await collectOverrides(rootDir, vulnerabilities, "npm");
12163
+ if (Object.keys(rawOverrides).length === 0) return;
12164
+ const overrides = guardAndReport(rootDir, rawOverrides, onProgress);
11768
12165
  if (Object.keys(overrides).length === 0) return;
11769
12166
  const pkgPath = path.join(rootDir, "package.json");
11770
12167
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
@@ -11800,6 +12197,76 @@ const collectPnpmOverrides = (advisories) => {
11800
12197
  }
11801
12198
  return overrides;
11802
12199
  };
12200
+ const overrideName = (key) => {
12201
+ const at = key.lastIndexOf("@");
12202
+ return at > 0 ? key.slice(0, at) : key;
12203
+ };
12204
+ const guardOverrides = (overrides, installed) => {
12205
+ const safe = {};
12206
+ const skipped = [];
12207
+ for (const [key, target] of Object.entries(overrides)) {
12208
+ const current = installed.get(overrideName(key));
12209
+ if (current && isDowngrade(current, target)) {
12210
+ skipped.push(`${overrideName(key)} ${current} → ${target}`);
12211
+ continue;
12212
+ }
12213
+ safe[key] = target;
12214
+ }
12215
+ return {
12216
+ safe,
12217
+ skipped
12218
+ };
12219
+ };
12220
+ const readRootNodeModulesVersion = (rootDir, name) => {
12221
+ try {
12222
+ const manifest = path.join(rootDir, "node_modules", name, "package.json");
12223
+ const version = JSON.parse(fs.readFileSync(manifest, "utf-8")).version;
12224
+ return typeof version === "string" ? version : null;
12225
+ } catch {
12226
+ return null;
12227
+ }
12228
+ };
12229
+ const PNPM_STORE_VERSION_RE = /^(\d+\.\d+\.\d+[^_(]*)/;
12230
+ const isHigherVersion = (candidate, current) => {
12231
+ if (!current) return true;
12232
+ const a = parseSemverMin(candidate);
12233
+ const b = parseSemverMin(current);
12234
+ if (!a || !b) return false;
12235
+ for (let i = 0; i < 3; i++) {
12236
+ if ((a[i] ?? 0) > (b[i] ?? 0)) return true;
12237
+ if ((a[i] ?? 0) < (b[i] ?? 0)) return false;
12238
+ }
12239
+ return false;
12240
+ };
12241
+ const readPnpmStoreVersion = (rootDir, name) => {
12242
+ let entries;
12243
+ try {
12244
+ entries = fs.readdirSync(path.join(rootDir, "node_modules", ".pnpm"));
12245
+ } catch {
12246
+ return null;
12247
+ }
12248
+ const prefix = `${name.replace(/\//g, "+")}@`;
12249
+ let best = null;
12250
+ for (const entry of entries) {
12251
+ if (!entry.startsWith(prefix)) continue;
12252
+ const match = PNPM_STORE_VERSION_RE.exec(entry.slice(prefix.length));
12253
+ if (match && isHigherVersion(match[1], best)) best = match[1];
12254
+ }
12255
+ return best;
12256
+ };
12257
+ const readInstalledVersions = (rootDir, names) => {
12258
+ const map = /* @__PURE__ */ new Map();
12259
+ for (const name of names) {
12260
+ const version = readRootNodeModulesVersion(rootDir, name) ?? readPnpmStoreVersion(rootDir, name);
12261
+ if (version) map.set(name, version);
12262
+ }
12263
+ return map;
12264
+ };
12265
+ const guardAndReport = (rootDir, rawOverrides, onProgress) => {
12266
+ const { safe, skipped } = guardOverrides(rawOverrides, readInstalledVersions(rootDir, Object.keys(rawOverrides).map(overrideName)));
12267
+ if (skipped.length > 0) onProgress?.(`Dependency audit fixes · skipped ${skipped.length} downgrade(s), verify intent: ${skipped.join(", ")}`);
12268
+ return safe;
12269
+ };
11803
12270
  const isPnpmAuditRetired = (stdout, stderr) => {
11804
12271
  const haystack = `${stdout}\n${stderr}`.toLowerCase();
11805
12272
  return haystack.includes("410") || haystack.includes("gone") || haystack.includes("retired") || haystack.includes("endpoint") || haystack.includes("err_pnpm_audit") || haystack.includes("audit endpoint");
@@ -11823,7 +12290,9 @@ const tryPnpmOverrides = async (rootDir, onProgress) => {
11823
12290
  }
11824
12291
  const advisories = parsed.advisories;
11825
12292
  if (!advisories || Object.keys(advisories).length === 0) return true;
11826
- const overrides = collectPnpmOverrides(advisories);
12293
+ const rawOverrides = collectPnpmOverrides(advisories);
12294
+ if (Object.keys(rawOverrides).length === 0) return true;
12295
+ const overrides = guardAndReport(rootDir, rawOverrides, onProgress);
11827
12296
  if (Object.keys(overrides).length === 0) return true;
11828
12297
  const pkgPath = path.join(rootDir, "package.json");
11829
12298
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
@@ -11844,58 +12313,6 @@ const tryPnpmOverrides = async (rootDir, onProgress) => {
11844
12313
  });
11845
12314
  return true;
11846
12315
  };
11847
- const fixExpoDependencies = async (context, onProgress) => {
11848
- await removeDisallowedExpoPackages(context.rootDirectory, onProgress);
11849
- onProgress?.("Expo dependency alignment · running expo install --fix (can take a few minutes)");
11850
- if ((await runSubprocess("npx", [
11851
- "--yes",
11852
- "expo",
11853
- "install",
11854
- "--fix"
11855
- ], {
11856
- cwd: context.rootDirectory,
11857
- timeout: INSTALL_TIMEOUT
11858
- })).exitCode === 0) return;
11859
- onProgress?.("Expo dependency alignment · checking remaining issues");
11860
- const checkResult = await runSubprocess("npx", [
11861
- "--yes",
11862
- "expo",
11863
- "install",
11864
- "--check"
11865
- ], {
11866
- cwd: context.rootDirectory,
11867
- timeout: INSTALL_TIMEOUT
11868
- });
11869
- if (checkResult.exitCode !== 0) throw new Error(checkResult.stderr || checkResult.stdout || "expo dependency check failed");
11870
- };
11871
- /**
11872
- * Run expo-doctor to detect packages that should not be installed directly,
11873
- * then uninstall them. No hardcoded list — expo-doctor is the source of truth.
11874
- */
11875
- const removeDisallowedExpoPackages = async (rootDir, onProgress) => {
11876
- try {
11877
- onProgress?.("Expo dependency alignment · running expo-doctor");
11878
- const result = await runSubprocess("npx", [
11879
- "--yes",
11880
- "expo-doctor",
11881
- rootDir
11882
- ], {
11883
- cwd: rootDir,
11884
- timeout: INSTALL_TIMEOUT
11885
- });
11886
- const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
11887
- const packagePattern = /The package "([^"]+)" should not be installed directly/g;
11888
- const toRemove = [];
11889
- let match;
11890
- while ((match = packagePattern.exec(output)) !== null) toRemove.push(match[1]);
11891
- if (toRemove.length === 0) return;
11892
- onProgress?.(`Expo dependency alignment · uninstalling ${toRemove.length} package(s)`);
11893
- await runSubprocess("npm", ["uninstall", ...toRemove], {
11894
- cwd: rootDir,
11895
- timeout: INSTALL_TIMEOUT
11896
- });
11897
- } catch {}
11898
- };
11899
12316
 
11900
12317
  //#endregion
11901
12318
  //#region src/commands/fix-pipeline.ts
@@ -11904,6 +12321,10 @@ const runAiSlopSteps = async (deps) => {
11904
12321
  if (!deps.config.engines["ai-slop"]) return;
11905
12322
  await deps.runStep("Unused imports", () => detectUnusedImports(deps.context), () => fixUnusedImports(deps.context));
11906
12323
  await deps.runStep("Duplicate imports", () => detectDuplicateImports(deps.context), () => fixDuplicateImports(deps.context));
12324
+ if (deps.safe) {
12325
+ await deps.runStep("Narrative comments", async () => (await detectNarrativeComments(deps.context)).filter((d) => d.fixable), () => fixNarrativeComments(deps.context));
12326
+ return;
12327
+ }
11907
12328
  const detectFixableSlop = async () => {
11908
12329
  const [comments, dead, narrative] = await Promise.all([
11909
12330
  detectTrivialComments(deps.context),
@@ -12054,19 +12475,23 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
12054
12475
  });
12055
12476
  return result;
12056
12477
  };
12478
+ const safe = Boolean(options.safe);
12057
12479
  const pipelineDeps = {
12058
12480
  rail,
12059
12481
  context,
12060
12482
  config,
12061
12483
  resolvedDir,
12062
12484
  projectInfo,
12063
- force: Boolean(options.force),
12485
+ force: safe ? false : Boolean(options.force),
12486
+ safe,
12064
12487
  runStep
12065
12488
  };
12066
12489
  await runAiSlopSteps(pipelineDeps);
12067
- await runDeclarationStep(pipelineDeps);
12068
- await runLintSteps(pipelineDeps);
12069
- await runDependencyStep(pipelineDeps);
12490
+ if (!safe) {
12491
+ await runDeclarationStep(pipelineDeps);
12492
+ await runLintSteps(pipelineDeps);
12493
+ await runDependencyStep(pipelineDeps);
12494
+ }
12070
12495
  await runFormattingStep(pipelineDeps);
12071
12496
  await runForceSteps(pipelineDeps);
12072
12497
  const totalResolved = steps.reduce((sum, s) => sum + s.resolvedIssues, 0);
@@ -12414,6 +12839,7 @@ const AI_SLOP_FIXABLE = new Set([
12414
12839
  "ai-slop/duplicate-import"
12415
12840
  ]);
12416
12841
  const AI_SLOP_ERRORS = new Set(["ai-slop/hallucinated-import"]);
12842
+ const SECURITY_INFO = new Set(["security/dependency-audit-skipped"]);
12417
12843
  const BUILTIN_RULES = [
12418
12844
  {
12419
12845
  engine: "format",
@@ -12449,6 +12875,10 @@ const BUILTIN_RULES = [
12449
12875
  "knip/binaries",
12450
12876
  "knip/exports",
12451
12877
  "knip/types",
12878
+ "knip/duplicates",
12879
+ "code-quality/duplicate-block",
12880
+ "code-quality/repeated-chained-call",
12881
+ "code-quality/unused-declaration",
12452
12882
  "complexity/file-too-large",
12453
12883
  "complexity/function-too-long",
12454
12884
  "complexity/deep-nesting",
@@ -12501,8 +12931,10 @@ const BUILTIN_RULES = [
12501
12931
  "security/vulnerable-dependency",
12502
12932
  "security/eval",
12503
12933
  "security/innerhtml",
12934
+ "security/dangerously-set-innerhtml",
12504
12935
  "security/sql-injection",
12505
- "security/shell-injection"
12936
+ "security/shell-injection",
12937
+ "security/dependency-audit-skipped"
12506
12938
  ]
12507
12939
  }
12508
12940
  ];
@@ -12516,7 +12948,7 @@ const toRuleEntry = (engine, ruleId) => {
12516
12948
  if (engine === "security") return {
12517
12949
  id: ruleId,
12518
12950
  engine,
12519
- severity: "error",
12951
+ severity: SECURITY_INFO.has(ruleId) ? "info" : "error",
12520
12952
  fixable: false
12521
12953
  };
12522
12954
  if (engine === "ai-slop") return {
@@ -12713,6 +13145,86 @@ const trendCommand = (directory, limit) => {
12713
13145
  }));
12714
13146
  };
12715
13147
 
13148
+ //#endregion
13149
+ //#region src/update-notifier.ts
13150
+ const REGISTRY_URL = "https://registry.npmjs.org/aislop/latest";
13151
+ const CHECK_INTERVAL_MS = 1440 * 60 * 1e3;
13152
+ const REQUEST_TIMEOUT_MS = 2e3;
13153
+ const CACHE_BASENAME = "update_check.json";
13154
+ const isUpdateNotifierDisabled = (env = process.env) => {
13155
+ if (env.AISLOP_NO_UPDATE_NOTIFIER === "1") return true;
13156
+ if (env.NO_UPDATE_NOTIFIER === "1") return true;
13157
+ if (env.DO_NOT_TRACK === "1") return true;
13158
+ return isCiEnv(env);
13159
+ };
13160
+ const resolveUpdateCachePath = (homedir = os.homedir(), env = process.env) => {
13161
+ if (process.platform === "linux" && env.XDG_STATE_HOME) return path.join(env.XDG_STATE_HOME, "aislop", CACHE_BASENAME);
13162
+ return path.join(homedir, ".aislop", CACHE_BASENAME);
13163
+ };
13164
+ const parseVersion = (raw) => {
13165
+ const m = raw.trim().replace(/^v/, "").split(/[-+]/, 1)[0].match(/^(\d+)\.(\d+)\.(\d+)$/);
13166
+ if (!m) return null;
13167
+ return {
13168
+ major: Number(m[1]),
13169
+ minor: Number(m[2]),
13170
+ patch: Number(m[3])
13171
+ };
13172
+ };
13173
+ const isOutdated = (current, latest) => {
13174
+ const c = parseVersion(current);
13175
+ const l = parseVersion(latest);
13176
+ if (!c || !l) return false;
13177
+ if (l.major !== c.major) return l.major > c.major;
13178
+ if (l.minor !== c.minor) return l.minor > c.minor;
13179
+ return l.patch > c.patch;
13180
+ };
13181
+ const formatUpdateNotice = (current, latest) => `\nUpdate available: ${current} -> ${latest}. Run npx aislop@latest to upgrade.\n`;
13182
+ const readCache = (cachePath) => {
13183
+ try {
13184
+ const parsed = JSON.parse(fs.readFileSync(cachePath, "utf-8"));
13185
+ if (typeof parsed?.latest === "string" && typeof parsed?.checkedAt === "number") return {
13186
+ latest: parsed.latest,
13187
+ checkedAt: parsed.checkedAt
13188
+ };
13189
+ return null;
13190
+ } catch {
13191
+ return null;
13192
+ }
13193
+ };
13194
+ const writeCache = (cachePath, cache) => {
13195
+ try {
13196
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
13197
+ fs.writeFileSync(cachePath, JSON.stringify(cache));
13198
+ return true;
13199
+ } catch {
13200
+ return false;
13201
+ }
13202
+ };
13203
+ const fetchLatestVersion = async () => {
13204
+ try {
13205
+ const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
13206
+ if (!res.ok) return null;
13207
+ const data = await res.json();
13208
+ return typeof data.version === "string" ? data.version : null;
13209
+ } catch {
13210
+ return null;
13211
+ }
13212
+ };
13213
+ const maybeNotifyUpdate = async (now = Date.now()) => {
13214
+ if (isUpdateNotifierDisabled()) return;
13215
+ if (!process.stderr.isTTY) return;
13216
+ const cachePath = resolveUpdateCachePath();
13217
+ const cache = readCache(cachePath);
13218
+ if (cache && isOutdated(APP_VERSION, cache.latest)) process.stderr.write(formatUpdateNotice(APP_VERSION, cache.latest));
13219
+ if (!cache || now - cache.checkedAt > CHECK_INTERVAL_MS) {
13220
+ const latest = await fetchLatestVersion();
13221
+ if (latest) writeCache(cachePath, {
13222
+ latest,
13223
+ checkedAt: now
13224
+ });
13225
+ }
13226
+ };
13227
+
12716
13228
  //#endregion
12717
13229
  //#region src/cli.ts
12718
13230
  process.on("SIGINT", () => process.exit(0));
@@ -12880,13 +13392,14 @@ const FIX_AGENT_FLAGS = [
12880
13392
  const matchFixAgent = (flags) => {
12881
13393
  return FIX_AGENT_FLAGS.find((a) => flags[a.name])?.flag;
12882
13394
  };
12883
- const fixProgram = 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)").option("-p, --prompt", "print a prompt for your coding agent to fix remaining issues");
13395
+ const fixProgram = 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)").option("--safe", "only apply reversible fixes (imports, comment removal, formatting); skip anything that deletes code or rewrites behaviour").option("-p, --prompt", "print a prompt for your coding agent to fix remaining issues");
12884
13396
  for (const a of FIX_AGENT_FLAGS) fixProgram.option(`--${a.flag}`, a.help);
12885
13397
  fixProgram.action(async (directory = ".", _flags, command) => {
12886
13398
  const flags = command.optsWithGlobals();
12887
13399
  await fixCommand(directory, loadConfig(directory), {
12888
13400
  verbose: Boolean(flags.verbose),
12889
13401
  force: Boolean(flags.force),
13402
+ safe: Boolean(flags.safe),
12890
13403
  prompt: Boolean(flags.prompt),
12891
13404
  agent: matchFixAgent(flags)
12892
13405
  });
@@ -12966,6 +13479,7 @@ const main = async () => {
12966
13479
  fireInstalledOnce();
12967
13480
  await program.parseAsync();
12968
13481
  await flushTelemetry();
13482
+ await maybeNotifyUpdate();
12969
13483
  };
12970
13484
  main();
12971
13485