aislop 0.10.1 → 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.1";
37
+ const APP_VERSION = "0.10.2";
38
38
 
39
39
  //#endregion
40
40
  //#region src/telemetry/env.ts
@@ -244,9 +244,14 @@ const track = (input) => {
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.
@@ -836,218 +838,6 @@ const loadConfig = (directory) => {
836
838
  }
837
839
  };
838
840
 
839
- //#endregion
840
- //#region src/utils/source-masker.ts
841
- const JS_EXTS$2 = new Set([
842
- ".ts",
843
- ".tsx",
844
- ".js",
845
- ".jsx",
846
- ".mjs",
847
- ".cjs"
848
- ]);
849
- const PY_EXTS = new Set([".py"]);
850
- const RB_EXTS = new Set([".rb"]);
851
- const PHP_EXTS = new Set([".php"]);
852
- const familyForExt = (ext) => {
853
- if (JS_EXTS$2.has(ext)) return "js";
854
- if (PY_EXTS.has(ext)) return "py";
855
- if (RB_EXTS.has(ext)) return "rb";
856
- if (PHP_EXTS.has(ext)) return "php";
857
- return "none";
858
- };
859
- const maskStringsAndComments = (content, ext) => {
860
- const family = familyForExt(ext);
861
- if (family === "none") return content;
862
- if (family === "js") return maskJs(content, true);
863
- return maskSimple(content, family, true);
864
- };
865
- const maskComments = (content, ext) => {
866
- const family = familyForExt(ext);
867
- if (family === "none") return content;
868
- if (family === "js") return maskJs(content, false);
869
- return maskSimple(content, family, false);
870
- };
871
- const handleQuotesAndComments = (content, i, tplStack, mask, maskStrings) => {
872
- const len = content.length;
873
- const c = content[i];
874
- const next = content[i + 1];
875
- if (c === "\"" || c === "'") {
876
- const strStart = i;
877
- const end = consumeQuotedString(content, i, c);
878
- if (maskStrings) mask(strStart + 1, end - 1);
879
- return {
880
- handled: true,
881
- nextI: end
882
- };
883
- }
884
- if (c === "`") {
885
- const scan = consumeTemplateString(content, i + 1);
886
- if (maskStrings) mask(i + 1, scan.maskEnd);
887
- if (scan.openedInterp) tplStack.push(0);
888
- return {
889
- handled: true,
890
- nextI: scan.resumeAt
891
- };
892
- }
893
- if (c === "/" && next === "/") {
894
- const strStart = i;
895
- let k = i;
896
- while (k < len && content[k] !== "\n") k++;
897
- mask(strStart, k);
898
- return {
899
- handled: true,
900
- nextI: k
901
- };
902
- }
903
- if (c === "/" && next === "*") {
904
- const strStart = i;
905
- let k = i + 2;
906
- while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
907
- if (k < len - 1) k += 2;
908
- mask(strStart, k);
909
- return {
910
- handled: true,
911
- nextI: k
912
- };
913
- }
914
- return {
915
- handled: false,
916
- nextI: i
917
- };
918
- };
919
- const maskJs = (content, maskStrings) => {
920
- const out = content.split("");
921
- const len = content.length;
922
- const tplStack = [];
923
- let i = 0;
924
- const mask = (start, end) => {
925
- for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
926
- };
927
- while (i < len) {
928
- const c = content[i];
929
- if (tplStack.length > 0) {
930
- if (c === "{") {
931
- tplStack[tplStack.length - 1]++;
932
- i++;
933
- continue;
934
- }
935
- if (c === "}") {
936
- if (tplStack[tplStack.length - 1] === 0) {
937
- tplStack.pop();
938
- const scan = consumeTemplateString(content, i + 1);
939
- if (maskStrings) mask(i + 1, scan.maskEnd);
940
- if (scan.openedInterp) tplStack.push(0);
941
- i = scan.resumeAt;
942
- continue;
943
- }
944
- tplStack[tplStack.length - 1]--;
945
- i++;
946
- continue;
947
- }
948
- }
949
- const handled = handleQuotesAndComments(content, i, tplStack, mask, maskStrings);
950
- if (handled.handled) {
951
- i = handled.nextI;
952
- continue;
953
- }
954
- i++;
955
- }
956
- return out.join("");
957
- };
958
- const consumeQuotedString = (content, start, quote) => {
959
- const len = content.length;
960
- let i = start + 1;
961
- while (i < len) {
962
- const c = content[i];
963
- if (c === "\\" && i + 1 < len) {
964
- i += 2;
965
- continue;
966
- }
967
- if (c === quote) return i + 1;
968
- if (c === "\n") return i;
969
- i++;
970
- }
971
- return i;
972
- };
973
- const consumeTemplateString = (content, start) => {
974
- const len = content.length;
975
- let i = start;
976
- while (i < len) {
977
- const c = content[i];
978
- if (c === "\\" && i + 1 < len) {
979
- i += 2;
980
- continue;
981
- }
982
- if (c === "`") return {
983
- maskEnd: i,
984
- resumeAt: i + 1,
985
- openedInterp: false
986
- };
987
- if (c === "$" && content[i + 1] === "{") return {
988
- maskEnd: i,
989
- resumeAt: i + 2,
990
- openedInterp: true
991
- };
992
- i++;
993
- }
994
- return {
995
- maskEnd: i,
996
- resumeAt: i,
997
- openedInterp: false
998
- };
999
- };
1000
- const maskSimple = (content, family, maskStrings) => {
1001
- const out = content.split("");
1002
- const len = content.length;
1003
- let i = 0;
1004
- const mask = (start, end) => {
1005
- for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
1006
- };
1007
- while (i < len) {
1008
- const c = content[i];
1009
- const next = content[i + 1];
1010
- if (family === "py" && (c === "\"" || c === "'")) {
1011
- if (content[i + 1] === c && content[i + 2] === c) {
1012
- const triple = c + c + c;
1013
- const end = content.indexOf(triple, i + 3);
1014
- const stop = end === -1 ? len : end + 3;
1015
- if (maskStrings) mask(i + 3, stop - 3);
1016
- i = stop;
1017
- continue;
1018
- }
1019
- }
1020
- if (c === "\"" || c === "'") {
1021
- const strStart = i;
1022
- i = consumeQuotedString(content, i, c);
1023
- if (maskStrings) mask(strStart + 1, i - 1);
1024
- continue;
1025
- }
1026
- if ((family === "py" || family === "rb" || family === "php") && c === "#") {
1027
- const strStart = i;
1028
- while (i < len && content[i] !== "\n") i++;
1029
- mask(strStart, i);
1030
- continue;
1031
- }
1032
- if (family === "php" && c === "/" && next === "/") {
1033
- const strStart = i;
1034
- while (i < len && content[i] !== "\n") i++;
1035
- mask(strStart, i);
1036
- continue;
1037
- }
1038
- if (family === "php" && c === "/" && next === "*") {
1039
- const strStart = i;
1040
- i += 2;
1041
- while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
1042
- if (i < len - 1) i += 2;
1043
- mask(strStart, i);
1044
- continue;
1045
- }
1046
- i++;
1047
- }
1048
- return out.join("");
1049
- };
1050
-
1051
841
  //#endregion
1052
842
  //#region src/utils/source-files.ts
1053
843
  const MAX_BUFFER$1 = 50 * 1024 * 1024;
@@ -1224,6 +1014,15 @@ const listProjectFiles = (rootDirectory) => {
1224
1014
  if (findResult.error || findResult.status !== 0) return [];
1225
1015
  return findResult.stdout.split("\n").filter((file) => file.length > 0).map((file) => file.replace(/^\.\//, ""));
1226
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
+ };
1227
1026
  const normalizeExcludePatterns = (patterns) => {
1228
1027
  return patterns.flatMap((pattern) => {
1229
1028
  const p = pattern.trim();
@@ -1297,8 +1096,8 @@ const getSourceFilesWithExtras = (context, extraExtensions) => {
1297
1096
  };
1298
1097
 
1299
1098
  //#endregion
1300
- //#region src/engines/ai-slop/abstractions.ts
1301
- const JS_EXTS$1 = new Set([
1099
+ //#region src/utils/source-masker.ts
1100
+ const JS_EXTS$2 = new Set([
1302
1101
  ".ts",
1303
1102
  ".tsx",
1304
1103
  ".js",
@@ -1306,51 +1105,261 @@ const JS_EXTS$1 = new Set([
1306
1105
  ".mjs",
1307
1106
  ".cjs"
1308
1107
  ]);
1309
- const THIN_WRAPPER_PATTERNS = [
1310
- {
1311
- pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
1312
- extensions: JS_EXTS$1
1313
- },
1314
- {
1315
- pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
1316
- extensions: JS_EXTS$1
1317
- },
1318
- {
1319
- pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
1320
- extensions: new Set([".py"])
1321
- }
1322
- ];
1323
- const AI_NAMING_PATTERNS = [/(?:helper|util|handler|process|do|handle|execute|perform)_?\d+/i, /(?:data|temp|result|value|item|obj|arr|str|num|val)\d+/];
1324
- const FRAMEWORK_METHOD_NAMES = /^(?:setUp|tearDown|setUpClass|tearDownClass|setUpModule|tearDownModule)$/;
1325
- const DUNDER_PATTERN = /^__\w+__$/;
1326
- const stripParam = (p) => p.trim().split(/[:=]/)[0].trim().replace(/^[*&]+/, "");
1327
- const paramNames = (paramsText) => new Set(paramsText.split(",").map(stripParam).filter((p) => p && p !== "self" && p !== "cls"));
1328
- const isIdentityForward = (matchText) => {
1329
- const paramsMatch = matchText.match(/\(([^)]*)\)/);
1330
- const innerMatch = matchText.match(/(?:return\s+\w+|=>\s*\w+)\s*\(([^)]*)\)/);
1331
- if (!paramsMatch || !innerMatch) return false;
1332
- const params = paramNames(paramsMatch[1]);
1333
- const args = innerMatch[1].split(",").map((a) => a.trim()).filter((a) => a.length > 0);
1334
- if (args.length === 0) return false;
1335
- return args.every((a) => /^[A-Za-z_$][\w$]*$/.test(a) && params.has(a));
1336
- };
1337
- const isUseContextWrapper = (matchText) => /\buse\w+/.test(matchText) && /useContext\s*\(/.test(matchText);
1338
- const detectThinWrappers = (content, relativePath, ext) => {
1339
- const diagnostics = [];
1340
- const lines = content.split("\n");
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
+ ]);
1320
+ const THIN_WRAPPER_PATTERNS = [
1321
+ {
1322
+ pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
1323
+ extensions: JS_EXTS$1
1324
+ },
1325
+ {
1326
+ pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
1327
+ extensions: JS_EXTS$1
1328
+ },
1329
+ {
1330
+ pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
1331
+ extensions: new Set([".py"])
1332
+ }
1333
+ ];
1334
+ const AI_NAMING_PATTERNS = [/(?:helper|util|handler|process|do|handle|execute|perform)_?\d+/i, /(?:data|temp|result|value|item|obj|arr|str|num|val)\d+/];
1335
+ const FRAMEWORK_METHOD_NAMES = /^(?:setUp|tearDown|setUpClass|tearDownClass|setUpModule|tearDownModule)$/;
1336
+ const DUNDER_PATTERN = /^__\w+__$/;
1337
+ const stripParam = (p) => p.trim().split(/[:=]/)[0].trim().replace(/^[*&]+/, "");
1338
+ const paramNames = (paramsText) => new Set(paramsText.split(",").map(stripParam).filter((p) => p && p !== "self" && p !== "cls"));
1339
+ const isIdentityForward = (matchText) => {
1340
+ const paramsMatch = matchText.match(/\(([^)]*)\)/);
1341
+ const innerMatch = matchText.match(/(?:return\s+\w+|=>\s*\w+)\s*\(([^)]*)\)/);
1342
+ if (!paramsMatch || !innerMatch) return false;
1343
+ const params = paramNames(paramsMatch[1]);
1344
+ const args = innerMatch[1].split(",").map((a) => a.trim()).filter((a) => a.length > 0);
1345
+ if (args.length === 0) return false;
1346
+ return args.every((a) => /^[A-Za-z_$][\w$]*$/.test(a) && params.has(a));
1347
+ };
1348
+ const isUseContextWrapper = (matchText) => /\buse\w+/.test(matchText) && /useContext\s*\(/.test(matchText);
1349
+ const detectThinWrappers = (content, relativePath, ext) => {
1350
+ const diagnostics = [];
1351
+ const lines = content.split("\n");
1341
1352
  for (const { pattern, extensions } of THIN_WRAPPER_PATTERNS) {
1342
1353
  if (!extensions.has(ext)) continue;
1343
1354
  const regex = new RegExp(pattern.source, pattern.flags);
1344
- let match;
1345
- while ((match = regex.exec(content)) !== null) {
1355
+ for (const match of content.matchAll(regex)) {
1346
1356
  const funcName = match[1];
1347
1357
  const matchText = match[0];
1348
1358
  const lineNumber = content.slice(0, match.index).split("\n").length;
1349
1359
  if (DUNDER_PATTERN.test(funcName)) continue;
1350
1360
  if (FRAMEWORK_METHOD_NAMES.test(funcName)) continue;
1351
1361
  if (lineNumber >= 2) {
1352
- const prevLine = lines[lineNumber - 2]?.trim();
1353
- if (prevLine && prevLine.startsWith("@")) continue;
1362
+ if ((lines[lineNumber - 2]?.trim())?.startsWith("@")) continue;
1354
1363
  }
1355
1364
  if (!isIdentityForward(matchText)) continue;
1356
1365
  if (isUseContextWrapper(matchText)) continue;
@@ -1727,7 +1736,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
1727
1736
  const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
1728
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));
1729
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));
1730
- 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));
1731
1740
  }
1732
1741
  return diagnostics;
1733
1742
  };
@@ -2113,9 +2122,8 @@ const detectSwallowedExceptions = async (context) => {
2113
2122
  const relativePath = path.relative(context.rootDirectory, filePath);
2114
2123
  for (const { pattern, languages, message } of SWALLOWED_EXCEPTION_PATTERNS) {
2115
2124
  if (!languages.includes(ext)) continue;
2116
- let match;
2117
2125
  const regex = new RegExp(pattern.source, pattern.flags + (pattern.flags.includes("g") ? "" : "g"));
2118
- while ((match = regex.exec(content)) !== null) {
2126
+ for (const match of content.matchAll(regex)) {
2119
2127
  if (isIntentionalIgnore(match[0], ext)) continue;
2120
2128
  const line = content.slice(0, match.index).split("\n").length;
2121
2129
  diagnostics.push({
@@ -2210,170 +2218,10 @@ const detectGoPatterns = async (context) => {
2210
2218
  };
2211
2219
 
2212
2220
  //#endregion
2213
- //#region src/engines/ai-slop/hardcoded-config.ts
2214
- const SOURCE_EXTENSIONS = new Set([
2215
- ".ts",
2216
- ".tsx",
2217
- ".js",
2218
- ".jsx",
2219
- ".mjs",
2220
- ".cjs",
2221
- ".py",
2222
- ".go",
2223
- ".rs",
2224
- ".rb",
2225
- ".java",
2226
- ".php"
2227
- ]);
2228
- const URL_LITERAL_RE = /(["'`])(https?:\/\/[^"'`\s<>]+)\1/g;
2229
- const ID_LITERAL_RE = /(["'])([A-Za-z][A-Za-z0-9_-]{15,})\1/g;
2230
- const ENV_REFERENCE_RE = /\b(?:process\.env|import\.meta\.env|Deno\.env|os\.environ|getenv|env\()\b/i;
2231
- const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|license|readme|source|svgUrl|pageUrl|href|link|install)\b/i;
2232
- const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
2233
- 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;
2234
- 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;
2235
- const MIGRATION_PATH_RE$1 = /(?:^|[\\/])(?:migrations?|db[\\/]migrate)[\\/]/i;
2236
- const PLACEHOLDER_HOSTS = new Set([
2237
- "example.com",
2238
- "example.org",
2239
- "example.net"
2240
- ]);
2241
- const LOOPBACK_HOSTS = new Set([
2242
- "localhost",
2243
- "127.0.0.1",
2244
- "0.0.0.0",
2245
- "::1"
2246
- ]);
2247
- const VENDOR_API_DOMAINS = [
2248
- "github.com",
2249
- "githubusercontent.com",
2250
- "googleapis.com",
2251
- "accounts.google.com",
2252
- "stripe.com",
2253
- "openai.com",
2254
- "anthropic.com",
2255
- "slack.com",
2256
- "twilio.com",
2257
- "sendgrid.com",
2258
- "mailgun.net",
2259
- "cloudflare.com",
2260
- "discord.com",
2261
- "telegram.org",
2262
- "login.microsoftonline.com",
2263
- "graph.microsoft.com",
2264
- "twitter.com",
2265
- "x.com",
2266
- "twimg.com",
2267
- "t.co",
2268
- "api.telegram.org"
2269
- ];
2270
- const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
2271
- const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
2272
- const HARDCODED_URL_FINDING = {
2273
- rule: "ai-slop/hardcoded-url",
2274
- message: "Hardcoded environment URL in production code",
2275
- help: "Move deployment-specific URLs to environment variables or a typed config module. Keep only stable documentation/public links inline."
2276
- };
2277
- const HARDCODED_ID_FINDING = {
2278
- rule: "ai-slop/hardcoded-id",
2279
- message: "Hardcoded provider/project ID in production code",
2280
- 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."
2281
- };
2282
- const makeFinding = (filePath, line, spec) => ({
2283
- filePath,
2284
- engine: "ai-slop",
2285
- rule: spec.rule,
2286
- severity: "warning",
2287
- message: spec.message,
2288
- help: spec.help,
2289
- line,
2290
- column: 0,
2291
- category: "AI Slop",
2292
- fixable: false
2293
- });
2294
- const isCommentOnlyLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*");
2295
- const commentStartsBefore = (line, index, ext) => {
2296
- const prefix = line.slice(0, index);
2297
- if (ext === ".py" || ext === ".rb") return prefix.includes("#");
2298
- if (ext === ".php") return prefix.includes("//") || prefix.includes("#");
2299
- return prefix.includes("//") || prefix.includes("/*");
2300
- };
2301
- const safeUrlHost = (urlText) => {
2302
- try {
2303
- return new URL(urlText).hostname.toLowerCase();
2304
- } catch {
2305
- return null;
2306
- }
2307
- };
2308
- const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
2309
- const shouldFlagUrlLiteral = (line, urlText) => {
2310
- if (isEnvBackedLine(line)) return false;
2311
- const host = safeUrlHost(urlText);
2312
- if (!host) return false;
2313
- if (PLACEHOLDER_HOSTS.has(host)) return false;
2314
- if (LOOPBACK_HOSTS.has(host)) return false;
2315
- if (isVendorApiHost(host)) return false;
2316
- if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
2317
- return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
2318
- };
2319
- const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
2320
- const hasUsefulIdShape = (value) => {
2321
- if (PLACEHOLDER_ID_RE.test(value)) return false;
2322
- if (ENV_VAR_NAME_RE.test(value)) return false;
2323
- if (/^https?:\/\//i.test(value)) return false;
2324
- if (/^[A-Za-z]+$/.test(value)) return false;
2325
- return /[0-9]/.test(value);
2326
- };
2327
- const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
2328
- const diagnostics = [];
2329
- if (isCommentOnlyLine(line.trim())) return diagnostics;
2330
- URL_LITERAL_RE.lastIndex = 0;
2331
- let urlMatch;
2332
- while ((urlMatch = URL_LITERAL_RE.exec(line)) !== null) {
2333
- const urlText = urlMatch[2];
2334
- if (commentStartsBefore(line, urlMatch.index, ext)) continue;
2335
- if (!shouldFlagUrlLiteral(line, urlText)) continue;
2336
- diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_URL_FINDING));
2337
- }
2338
- if (!ID_CONTEXT_RE.test(line) || isEnvBackedLine(line) || DOC_URL_CONTEXT_RE.test(line)) return diagnostics;
2339
- ID_LITERAL_RE.lastIndex = 0;
2340
- let idMatch;
2341
- while ((idMatch = ID_LITERAL_RE.exec(line)) !== null) {
2342
- const value = idMatch[2];
2343
- if (commentStartsBefore(line, idMatch.index, ext)) continue;
2344
- if (!hasUsefulIdShape(value)) continue;
2345
- diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_ID_FINDING));
2346
- }
2347
- return diagnostics;
2348
- };
2349
- const scanFileForConfigLiterals = (content, relativePath, ext) => {
2350
- if (!SOURCE_EXTENSIONS.has(ext)) return [];
2351
- if (isNonProductionPath(relativePath)) return [];
2352
- if (MIGRATION_PATH_RE$1.test(relativePath)) return [];
2353
- return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
2354
- };
2355
- const detectHardcodedConfigLiterals = async (context) => {
2356
- const diagnostics = [];
2357
- for (const filePath of getSourceFiles(context)) {
2358
- if (isAutoGenerated(filePath)) continue;
2359
- let content;
2360
- try {
2361
- content = fs.readFileSync(filePath, "utf-8");
2362
- } catch {
2363
- continue;
2364
- }
2365
- const relativePath = path.relative(context.rootDirectory, filePath);
2366
- const ext = path.extname(filePath);
2367
- diagnostics.push(...scanFileForConfigLiterals(maskComments(content, ext), relativePath, ext));
2368
- }
2369
- return diagnostics;
2370
- };
2371
-
2372
- //#endregion
2373
- //#region src/engines/ai-slop/js-import-aliases.ts
2374
- const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
2375
- const JS_RESOLUTION_EXTENSIONS = [
2376
- "",
2221
+ //#region src/engines/ai-slop/js-import-aliases.ts
2222
+ const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
2223
+ const JS_RESOLUTION_EXTENSIONS = [
2224
+ "",
2377
2225
  ".ts",
2378
2226
  ".tsx",
2379
2227
  ".js",
@@ -2466,15 +2314,18 @@ const readWorkspaceGlobs = (rootDir, rootPkg) => {
2466
2314
  }
2467
2315
  return globs;
2468
2316
  };
2317
+ const readWorkspaceEntries = (dir) => {
2318
+ try {
2319
+ return fs.readdirSync(dir, { withFileTypes: true });
2320
+ } catch {
2321
+ return [];
2322
+ }
2323
+ };
2469
2324
  const expandWorkspaceDirs = (rootDir, globs) => {
2470
2325
  const dirs = [];
2471
2326
  for (const glob of globs) if (glob.endsWith("/*")) {
2472
2327
  const parent = path.join(rootDir, glob.slice(0, -2));
2473
- try {
2474
- for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
2475
- } catch {
2476
- continue;
2477
- }
2328
+ for (const entry of readWorkspaceEntries(parent)) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
2478
2329
  } else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
2479
2330
  return dirs;
2480
2331
  };
@@ -3061,21 +2912,194 @@ const checkPyImport = (spec, manifest) => {
3061
2912
  if ((PYTHON_IMPORT_TO_PIP[root] ?? PYTHON_IMPORT_TO_PIP[normalized])?.some((dist) => manifest.pyDeps.has(normalizePyName(dist)))) return null;
3062
2913
  return root;
3063
2914
  };
3064
- const detectHallucinatedImports = async (context) => {
3065
- const rootPkg = readJson(path.join(context.rootDirectory, "package.json"));
3066
- const workspaceDirs = collectWorkspaceDirs(context.rootDirectory, rootPkg);
3067
- const manifest = loadManifest(context.rootDirectory);
3068
- if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
3069
- const tsAliasMatchers = manifest.hasJsManifest ? collectTsPathAliases(context.rootDirectory, workspaceDirs) : [];
2915
+ const detectHallucinatedImports = async (context) => {
2916
+ const rootPkg = readJson(path.join(context.rootDirectory, "package.json"));
2917
+ const workspaceDirs = collectWorkspaceDirs(context.rootDirectory, rootPkg);
2918
+ const manifest = loadManifest(context.rootDirectory);
2919
+ if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
2920
+ const tsAliasMatchers = manifest.hasJsManifest ? collectTsPathAliases(context.rootDirectory, workspaceDirs) : [];
2921
+ const diagnostics = [];
2922
+ const files = getSourceFiles(context);
2923
+ for (const filePath of files) {
2924
+ const ext = path.extname(filePath);
2925
+ const isJs = JS_EXTENSIONS$2.has(ext);
2926
+ const isPy = PY_EXTENSIONS$2.has(ext);
2927
+ if (!isJs && !isPy) continue;
2928
+ if (isJs && !manifest.hasJsManifest) continue;
2929
+ if (isPy && !manifest.hasPyManifest) continue;
2930
+ if (isAutoGenerated(filePath)) continue;
2931
+ let content;
2932
+ try {
2933
+ content = fs.readFileSync(filePath, "utf-8");
2934
+ } catch {
2935
+ continue;
2936
+ }
2937
+ const relPath = path.relative(context.rootDirectory, filePath);
2938
+ if (isNonProductionPath(relPath)) continue;
2939
+ const imports = isJs ? extractJsImports(content) : extractPyImports(content);
2940
+ for (const { spec, line } of imports) {
2941
+ const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
2942
+ if (!hallucinated) continue;
2943
+ const manifestLabel = isJs ? "package.json" : "requirements.txt / pyproject.toml / Pipfile";
2944
+ diagnostics.push({
2945
+ filePath: relPath,
2946
+ engine: "ai-slop",
2947
+ rule: "ai-slop/hallucinated-import",
2948
+ severity: "error",
2949
+ message: `Imports "${hallucinated}" but it's not declared in ${manifestLabel}${isPy ? " and isn't Python stdlib" : ""}`,
2950
+ help: "Most often this is an LLM hallucinating a plausible-sounding package name. Either add the package to your manifest, or correct the import.",
2951
+ line,
2952
+ column: 1,
2953
+ category: "AI Slop",
2954
+ fixable: false
2955
+ });
2956
+ }
2957
+ }
2958
+ return diagnostics;
2959
+ };
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) => {
3070
3101
  const diagnostics = [];
3071
- const files = getSourceFiles(context);
3072
- for (const filePath of files) {
3073
- const ext = path.extname(filePath);
3074
- const isJs = JS_EXTENSIONS$2.has(ext);
3075
- const isPy = PY_EXTENSIONS$2.has(ext);
3076
- if (!isJs && !isPy) continue;
3077
- if (isJs && !manifest.hasJsManifest) continue;
3078
- if (isPy && !manifest.hasPyManifest) continue;
3102
+ for (const filePath of getSourceFiles(context)) {
3079
3103
  if (isAutoGenerated(filePath)) continue;
3080
3104
  let content;
3081
3105
  try {
@@ -3083,30 +3107,92 @@ const detectHallucinatedImports = async (context) => {
3083
3107
  } catch {
3084
3108
  continue;
3085
3109
  }
3086
- const relPath = path.relative(context.rootDirectory, filePath);
3087
- if (isNonProductionPath(relPath)) continue;
3088
- const imports = isJs ? extractJsImports(content) : extractPyImports(content);
3089
- for (const { spec, line } of imports) {
3090
- const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
3091
- if (!hallucinated) continue;
3092
- const manifestLabel = isJs ? "package.json" : "requirements.txt / pyproject.toml / Pipfile";
3093
- diagnostics.push({
3094
- filePath: relPath,
3095
- engine: "ai-slop",
3096
- rule: "ai-slop/hallucinated-import",
3097
- severity: "error",
3098
- message: `Imports "${hallucinated}" but it's not declared in ${manifestLabel}${isPy ? " and isn't Python stdlib" : ""}`,
3099
- help: "Most often this is an LLM hallucinating a plausible-sounding package name. Either add the package to your manifest, or correct the import.",
3100
- line,
3101
- column: 1,
3102
- category: "AI Slop",
3103
- fixable: false
3104
- });
3105
- }
3110
+ const relativePath = path.relative(context.rootDirectory, filePath);
3111
+ const ext = path.extname(filePath);
3112
+ diagnostics.push(...scanFileForConfigLiterals(maskComments(content, ext), relativePath, ext));
3106
3113
  }
3107
3114
  return diagnostics;
3108
3115
  };
3109
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
+
3110
3196
  //#endregion
3111
3197
  //#region src/engines/ai-slop/comment-blocks.ts
3112
3198
  const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
@@ -3130,6 +3216,7 @@ const getCommentSyntax = (ext) => {
3130
3216
  };
3131
3217
  const getMatchedLinePrefix = (line, syntax) => {
3132
3218
  const trimmed = line.trimStart();
3219
+ if (isAislopDirectiveLine(trimmed)) return null;
3133
3220
  for (const prefix of syntax.linePrefixes) {
3134
3221
  if (!trimmed.startsWith(prefix)) continue;
3135
3222
  if (prefix === "#" && trimmed.startsWith("#!")) return null;
@@ -3929,9 +4016,7 @@ const isLogOnlyBody = (body) => {
3929
4016
  };
3930
4017
  const detectJsSilentRecovery = (content, relPath) => {
3931
4018
  const out = [];
3932
- CATCH_HEAD_RE.lastIndex = 0;
3933
- let match;
3934
- while ((match = CATCH_HEAD_RE.exec(content)) !== null) {
4019
+ for (const match of content.matchAll(CATCH_HEAD_RE)) {
3935
4020
  const body = extractCatchBody(content, match.index + match[0].length - 1);
3936
4021
  if (body === null) continue;
3937
4022
  if (!isLogOnlyBody(body)) continue;
@@ -4108,18 +4193,22 @@ const extractPyImportedSymbols = (lines) => {
4108
4193
  }
4109
4194
  continue;
4110
4195
  }
4111
- const importMatch = trimmed.match(/^import\s+([\w.]+)(?:\s+as\s+(\w+))?/);
4196
+ const importMatch = trimmed.match(/^import\s+(.+)/);
4112
4197
  if (importMatch) {
4113
4198
  importLines.add(i);
4114
- const alias = importMatch[2];
4115
- if (alias && alias === importMatch[1]) continue;
4116
- const simpleName = (alias ?? importMatch[1]).split(".")[0];
4117
- if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
4118
- name: simpleName,
4119
- line: i + 1,
4120
- isDefault: false,
4121
- isNamespace: true
4122
- });
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
+ }
4123
4212
  }
4124
4213
  }
4125
4214
  return {
@@ -4129,8 +4218,7 @@ const extractPyImportedSymbols = (lines) => {
4129
4218
  };
4130
4219
  const isSymbolUsed = (name, content, importLines, lines) => {
4131
4220
  const pattern = new RegExp(`\\b${name}\\b`, "g");
4132
- let match;
4133
- while ((match = pattern.exec(content)) !== null) {
4221
+ for (const match of content.matchAll(pattern)) {
4134
4222
  const lineIndex = content.slice(0, match.index).split("\n").length - 1;
4135
4223
  if (!importLines.has(lineIndex)) return true;
4136
4224
  }
@@ -4231,6 +4319,18 @@ const aiSlopEngine = {
4231
4319
 
4232
4320
  //#endregion
4233
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
+ ]);
4234
4334
  const minimatch = (filePath, pattern) => {
4235
4335
  let regex = "";
4236
4336
  let i = 0;
@@ -4255,7 +4355,7 @@ const minimatch = (filePath, pattern) => {
4255
4355
  regex += pattern.slice(i, closeIndex + 1);
4256
4356
  i = closeIndex + 1;
4257
4357
  }
4258
- } else if (".+^${}()|\\".includes(ch)) {
4358
+ } else if (REGEX_SPECIAL_CHARS.has(ch)) {
4259
4359
  regex += `\\${ch}`;
4260
4360
  i++;
4261
4361
  } else {
@@ -4275,27 +4375,15 @@ const extractImports = (content, ext) => {
4275
4375
  ".mjs",
4276
4376
  ".cjs"
4277
4377
  ].includes(ext)) {
4278
- const esPattern = /(?:import|from)\s+["']([^"']+)["']/g;
4279
- let match;
4280
- while ((match = esPattern.exec(content)) !== null) imports.push(match[1]);
4281
- const reqPattern = /require\s*\(\s*["']([^"']+)["']\s*\)/g;
4282
- while ((match = reqPattern.exec(content)) !== null) imports.push(match[1]);
4283
- }
4284
- if (ext === ".py") {
4285
- const pyPattern = /(?:from|import)\s+([\w.]+)/g;
4286
- let match;
4287
- 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]);
4288
4380
  }
4381
+ if (ext === ".py") for (const match of content.matchAll(/(?:from|import)\s+([\w.]+)/g)) imports.push(match[1]);
4289
4382
  if (ext === ".go") {
4290
- const goSingleImport = /^\s*import\s+"([^"]+)"/gm;
4291
- let match;
4292
- while ((match = goSingleImport.exec(content)) !== null) imports.push(match[1]);
4293
- const goMultiImport = /import\s*\(([^)]*)\)/gs;
4294
- 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)) {
4295
4385
  const block = match[1];
4296
- const pkgPattern = /"([^"]+)"/g;
4297
- let pkgMatch;
4298
- while ((pkgMatch = pkgPattern.exec(block)) !== null) imports.push(pkgMatch[1]);
4386
+ for (const pkgMatch of block.matchAll(/"([^"]+)"/g)) imports.push(pkgMatch[1]);
4299
4387
  }
4300
4388
  }
4301
4389
  return imports;
@@ -4422,10 +4510,10 @@ const architectureEngine = {
4422
4510
  //#endregion
4423
4511
  //#region src/engines/code-quality/function-boundaries.ts
4424
4512
  const PYTHON_CONTROL_FLOW_RE = /^\s*(?:if|for|while|with|try|except|else|elif|finally|def|class)\b/;
4425
- const ARROW_BLOCK_RE = /* @__PURE__ */ new RegExp("=>\\s*\\{");
4426
- const ARROW_END_RE = /* @__PURE__ */ new RegExp("=>\\s*$");
4427
- const BRACE_START_RE = /* @__PURE__ */ new RegExp("^\\s*\\{");
4428
- 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/;
4429
4517
  const isControlFlowBrace = (lineText, braceIndex) => {
4430
4518
  const before = lineText.substring(0, braceIndex).trimEnd();
4431
4519
  if (before.endsWith(")")) return true;
@@ -4611,14 +4699,14 @@ const countTemplateLines = (bodyLines) => {
4611
4699
  let templateLineCount = 0;
4612
4700
  for (const line of bodyLines) {
4613
4701
  const startedInside = insideTemplate;
4614
- let escape = false;
4702
+ let escaped = false;
4615
4703
  for (const ch of line) {
4616
- if (escape) {
4617
- escape = false;
4704
+ if (escaped) {
4705
+ escaped = false;
4618
4706
  continue;
4619
4707
  }
4620
4708
  if (ch === "\\") {
4621
- escape = true;
4709
+ escaped = true;
4622
4710
  continue;
4623
4711
  }
4624
4712
  if (ch === "`") insideTemplate = !insideTemplate;
@@ -5662,9 +5750,7 @@ const runRuffFormat = async (context) => {
5662
5750
  };
5663
5751
  const parseRuffFormatOutput = (output, rootDir) => {
5664
5752
  const diagnostics = [];
5665
- const filePattern = /^--- (.+)$/gm;
5666
- let match;
5667
- while ((match = filePattern.exec(output)) !== null) {
5753
+ for (const match of output.matchAll(/^--- (.+)$/gm)) {
5668
5754
  const filePath = getRuffDiagnosticPath(rootDir, match[1]);
5669
5755
  diagnostics.push({
5670
5756
  filePath,
@@ -5698,10 +5784,10 @@ const formatEngine = {
5698
5784
  const { languages, installedTools } = context;
5699
5785
  const promises = [];
5700
5786
  if (languages.includes("typescript") || languages.includes("javascript")) promises.push(runBiomeFormat(context));
5701
- if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffFormat(context));
5702
- if (languages.includes("go") && installedTools["gofmt"]) promises.push(runGofmt(context));
5703
- if (languages.includes("rust") && installedTools["rustfmt"]) promises.push(runGenericFormatter(context, "rust"));
5704
- 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"));
5705
5791
  if (languages.includes("php") && installedTools["php-cs-fixer"]) promises.push(runGenericFormatter(context, "php"));
5706
5792
  const results = await Promise.allSettled(promises);
5707
5793
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
@@ -6137,6 +6223,8 @@ const createOxlintConfig = (options) => {
6137
6223
  if (options.mode === "fix") {
6138
6224
  rules["no-unused-vars"] = "off";
6139
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";
6140
6228
  }
6141
6229
  const plugins = [
6142
6230
  "import",
@@ -6301,9 +6389,7 @@ const collectAmbientGlobals = (rootDir) => {
6301
6389
  if (!relativePath.endsWith(".d.ts")) continue;
6302
6390
  const content = readTextFile$1(path.join(rootDir, relativePath));
6303
6391
  if (!content) continue;
6304
- AMBIENT_GLOBAL_RE.lastIndex = 0;
6305
- let match;
6306
- 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]);
6307
6393
  }
6308
6394
  const deps = collectPackageNames(rootDir);
6309
6395
  if (deps.has("@types/bun") || deps.has("bun-types")) globals.add("Bun");
@@ -6654,10 +6740,10 @@ const lintEngine = {
6654
6740
  if (context.config.lint.typecheck) promises.push(import("./typecheck-wVSohmOX.js").then((mod) => mod.runTypecheck(context)));
6655
6741
  }
6656
6742
  if (context.frameworks.includes("expo")) promises.push(Promise.resolve().then(() => expo_doctor_exports).then((mod) => mod.runExpoDoctor(context)));
6657
- if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
6743
+ if (languages.includes("python") && installedTools.ruff) promises.push(runRuffLint(context));
6658
6744
  if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
6659
- if (languages.includes("rust") && installedTools["cargo"]) promises.push(runGenericLinter(context, "rust"));
6660
- 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"));
6661
6747
  const results = await Promise.allSettled(promises);
6662
6748
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
6663
6749
  return {
@@ -6687,7 +6773,7 @@ const runDependencyAudit = async (context) => {
6687
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));
6688
6774
  }
6689
6775
  if (context.languages.includes("python") && context.installedTools["pip-audit"]) promises.push(runPipAudit(context.rootDirectory, timeout));
6690
- 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));
6691
6777
  if (context.languages.includes("rust")) promises.push(runCargoAudit(context.rootDirectory, timeout));
6692
6778
  const results = await Promise.allSettled(promises);
6693
6779
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
@@ -6792,9 +6878,12 @@ const parseLegacyAdvisories = (advisories, source) => {
6792
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 ?? "");
6793
6879
  return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
6794
6880
  };
6881
+ const carriesAdvisory = (vulnerability) => Array.isArray(vulnerability.via) && vulnerability.via.some((entry) => entry !== null && typeof entry === "object");
6795
6882
  const parseModernVulnerabilities = (vulnerabilities, source) => {
6796
6883
  const bucket = /* @__PURE__ */ new Map();
6884
+ const hasRootCauses = Object.values(vulnerabilities).some(carriesAdvisory);
6797
6885
  for (const [packageName, vulnerability] of Object.entries(vulnerabilities)) {
6886
+ if (hasRootCauses && !carriesAdvisory(vulnerability)) continue;
6798
6887
  const severity = (vulnerability.severity ?? "moderate").toLowerCase();
6799
6888
  const fixAvailable = vulnerability.fixAvailable;
6800
6889
  const isDirect = vulnerability.isDirect === true;
@@ -7082,8 +7171,7 @@ const detectRiskyConstructs = async (context) => {
7082
7171
  if (!extensions.includes(ext)) continue;
7083
7172
  if (isMigrationOrSeeder && name === "sql-injection") continue;
7084
7173
  const regex = new RegExp(pattern.source, pattern.flags);
7085
- let match;
7086
- while ((match = regex.exec(masked)) !== null) {
7174
+ for (const match of masked.matchAll(regex)) {
7087
7175
  const line = content.slice(0, match.index).split("\n").length;
7088
7176
  if (name === "innerhtml") {
7089
7177
  const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
@@ -7215,8 +7303,7 @@ const scanSecrets = async (context) => {
7215
7303
  const relativePath = path.relative(context.rootDirectory, filePath);
7216
7304
  for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
7217
7305
  const regex = new RegExp(pattern.source, pattern.flags);
7218
- let match;
7219
- while ((match = regex.exec(content)) !== null) {
7306
+ for (const match of content.matchAll(regex)) {
7220
7307
  if (isPlaceholderValue(match[1] ?? match[0])) continue;
7221
7308
  if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
7222
7309
  const line = content.slice(0, match.index).split("\n").length;
@@ -7337,6 +7424,64 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
7337
7424
 
7338
7425
  //#endregion
7339
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
+ };
7340
7485
  const LANGUAGE_SIGNALS = {
7341
7486
  "tsconfig.json": "typescript",
7342
7487
  "go.mod": "go",
@@ -7456,11 +7601,12 @@ const checkInstalledTools = async () => {
7456
7601
  }));
7457
7602
  return results;
7458
7603
  };
7459
- const discoverProject = async (directory) => {
7604
+ const discoverProject = async (directory, excludePatterns = []) => {
7460
7605
  const resolvedDir = path.resolve(directory);
7461
7606
  const languages = detectLanguages(resolvedDir);
7462
7607
  const frameworks = detectFrameworks(resolvedDir);
7463
7608
  const sourceFileCount = countSourceFiles(resolvedDir);
7609
+ const coverage = analyzeCoverage(resolvedDir, excludePatterns);
7464
7610
  const installedTools = await checkInstalledTools();
7465
7611
  return {
7466
7612
  rootDirectory: resolvedDir,
@@ -7468,6 +7614,7 @@ const discoverProject = async (directory) => {
7468
7614
  languages,
7469
7615
  frameworks,
7470
7616
  sourceFileCount,
7617
+ coverage,
7471
7618
  installedTools
7472
7619
  };
7473
7620
  };
@@ -8174,7 +8321,7 @@ const uninstallRulesOnly = (opts, paths) => {
8174
8321
  else result.skipped.push(paths.rules);
8175
8322
  if (paths.host && paths.marker) {
8176
8323
  const host = readIfExists(paths.host);
8177
- if (host != null && host.includes(paths.marker)) {
8324
+ if (host?.includes(paths.marker)) {
8178
8325
  const stripped = host.split("\n").filter((l) => l.trim() !== paths.marker).join("\n").replace(/\n{3,}/g, "\n\n").trim();
8179
8326
  applyRemoval(result, opts, paths.host, stripped.length === 0 ? null : `${stripped}\n`);
8180
8327
  } else result.skipped.push(paths.host);
@@ -8590,7 +8737,7 @@ const uninstallGemini = (opts) => {
8590
8737
  if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
8591
8738
  else result.skipped.push(paths.aislopMd);
8592
8739
  const geminiMd = readIfExists(paths.geminiMd);
8593
- if (geminiMd != null && geminiMd.includes("@AISLOP.md")) {
8740
+ if (geminiMd?.includes("@AISLOP.md")) {
8594
8741
  const stripped = geminiMd.split("\n").filter((l) => l.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
8595
8742
  applyRemoval(result, opts, paths.geminiMd, stripped.length === 0 ? null : `${stripped}\n`);
8596
8743
  } else result.skipped.push(paths.geminiMd);
@@ -8887,6 +9034,7 @@ const theme = createTheme();
8887
9034
 
8888
9035
  //#endregion
8889
9036
  //#region src/commands/hook.ts
9037
+ const HOOK_FLUSH_TIMEOUT_MS = 1500;
8890
9038
  const AGENT_LABELS = {
8891
9039
  claude: {
8892
9040
  label: "Claude Code",
@@ -9015,6 +9163,7 @@ const hookRun = async (agent, flags) => {
9015
9163
  process.stderr.write(`hook: agent "${agent}" has no runtime adapter (rules-file-only)\n`);
9016
9164
  process.exit(0);
9017
9165
  }
9166
+ await flushTelemetry(HOOK_FLUSH_TIMEOUT_MS);
9018
9167
  process.exit(exitCode);
9019
9168
  };
9020
9169
  const hookBaseline = async () => {
@@ -9268,12 +9417,12 @@ const badgeCommand = async (options = {}) => {
9268
9417
  svgUrl,
9269
9418
  pageUrl
9270
9419
  });
9271
- if (options.json) process.stdout.write(JSON.stringify({
9420
+ if (options.json) process.stdout.write(`${JSON.stringify({
9272
9421
  owner,
9273
9422
  repo,
9274
9423
  svgUrl,
9275
9424
  pageUrl
9276
- }) + "\n");
9425
+ })}\n`);
9277
9426
  else process.stdout.write(output);
9278
9427
  return {
9279
9428
  owner,
@@ -9574,7 +9723,7 @@ const renderHeader = (input, _deps = {}) => {
9574
9723
 
9575
9724
  //#endregion
9576
9725
  //#region src/ui/width.ts
9577
- const ANSI_RE = new RegExp(String.raw`\x1B\[[0-9;]*m`, "g");
9726
+ const ANSI_RE = new RegExp(`\\[[0-9;]*m`, "g");
9578
9727
  const stripAnsi = (s) => s.replace(ANSI_RE, "");
9579
9728
  const stringWidth = (s) => {
9580
9729
  const bare = stripAnsi(s);
@@ -9718,6 +9867,8 @@ var LiveGrid = class {
9718
9867
  const RULE_LABELS = {
9719
9868
  formatting: "Code not formatted",
9720
9869
  "code-quality/duplicate-block": "Duplicate code block",
9870
+ "code-quality/repeated-chained-call": "Repeated chained call",
9871
+ "code-quality/unused-declaration": "Unused declaration",
9721
9872
  "complexity/file-too-large": "File too large",
9722
9873
  "complexity/function-too-long": "Function too long",
9723
9874
  "complexity/deep-nesting": "Deeply nested code",
@@ -9730,6 +9881,7 @@ const RULE_LABELS = {
9730
9881
  "knip/binaries": "Unused binary",
9731
9882
  "knip/exports": "Unused export",
9732
9883
  "knip/types": "Unused type",
9884
+ "knip/duplicates": "Duplicate export",
9733
9885
  "ai-slop/trivial-comment": "Trivial restating comment",
9734
9886
  "ai-slop/swallowed-exception": "Empty catch (swallowed error)",
9735
9887
  "ai-slop/silent-recovery": "Catch logs then continues",
@@ -9766,6 +9918,7 @@ const RULE_LABELS = {
9766
9918
  "ai-slop/hallucinated-import": "Import not in package.json",
9767
9919
  "security/hardcoded-secret": "Possible hardcoded secret",
9768
9920
  "security/vulnerable-dependency": "Vulnerable dependency",
9921
+ "security/dependency-audit-skipped": "Dependency audit skipped",
9769
9922
  "security/eval": "eval() usage",
9770
9923
  "security/innerhtml": "innerHTML assignment",
9771
9924
  "security/dangerously-set-innerhtml": "dangerouslySetInnerHTML (XSS risk)",
@@ -9923,6 +10076,36 @@ const readHistory = (directory) => {
9923
10076
  return records;
9924
10077
  };
9925
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
+
9926
10109
  //#endregion
9927
10110
  //#region src/commands/scan.ts
9928
10111
  const isMachineOutput = (options) => Boolean(options.json) || Boolean(options.sarif);
@@ -10029,7 +10212,7 @@ const scanCommand = async (directory, config, options) => {
10029
10212
  else log.error(msg);
10030
10213
  return { exitCode: 1 };
10031
10214
  }
10032
- const projectInfo = await discoverProject(resolvedDir);
10215
+ const projectInfo = await discoverProject(resolvedDir, [...config.exclude, ...readAislopIgnorePatterns(resolvedDir)]);
10033
10216
  return withCommandLifecycle({
10034
10217
  command: options.command ?? "scan",
10035
10218
  config: config.telemetry,
@@ -10042,15 +10225,16 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
10042
10225
  const showHeader = options.showHeader !== false;
10043
10226
  const machineOutput = isMachineOutput(options);
10044
10227
  const useLiveProgress = !machineOutput && shouldUseSpinner();
10228
+ const excludePatterns = [...config.exclude, ...readAislopIgnorePatterns(resolvedDir)];
10045
10229
  let files;
10046
10230
  if (options.staged) {
10047
- files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir), [], config.exclude);
10231
+ files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir), [], excludePatterns);
10048
10232
  if (!machineOutput) log.muted(`Scope: ${files.length} staged file(s)`);
10049
10233
  } else if (options.changes) {
10050
- files = filterProjectFiles(resolvedDir, getChangedFiles(resolvedDir), [], config.exclude);
10234
+ files = filterProjectFiles(resolvedDir, getChangedFiles(resolvedDir), [], excludePatterns);
10051
10235
  if (!machineOutput) log.muted(`Scope: ${files.length} changed file(s)`);
10052
10236
  } else {
10053
- files = filterProjectFiles(resolvedDir, listProjectFiles(resolvedDir), [], config.exclude);
10237
+ files = filterProjectFiles(resolvedDir, listProjectFiles(resolvedDir), [], excludePatterns);
10054
10238
  if (!machineOutput) log.muted(`Scope: ${files.length} file(s) after exclusions`);
10055
10239
  }
10056
10240
  const configDir = findConfigDir(resolvedDir);
@@ -10104,14 +10288,21 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
10104
10288
  if (!machineOutput && !progressRenderer) printEngineStatus(result);
10105
10289
  });
10106
10290
  progressRenderer?.stop();
10107
- const results = rawResults.map((result) => ({
10291
+ const { results, suppressedCount } = applySuppressions(rawResults.map((result) => ({
10108
10292
  ...result,
10109
10293
  diagnostics: applyRuleSeverities(result.diagnostics, config.rules)
10110
- }));
10294
+ })), resolvedDir);
10295
+ if (suppressedCount > 0 && !machineOutput) log.muted(`Suppressed ${suppressedCount} finding(s) via aislop-ignore directives`);
10111
10296
  const allDiagnostics = results.flatMap((r) => r.diagnostics);
10112
10297
  const elapsedMs = performance.now() - startTime;
10113
10298
  const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
10114
- 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
+ });
10115
10306
  const engineIssues = {};
10116
10307
  const engineTimings = {};
10117
10308
  for (const r of results) {
@@ -10120,7 +10311,8 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
10120
10311
  }
10121
10312
  const completion = {
10122
10313
  exitCode,
10123
- score: scoreResult.score,
10314
+ score: scoreable ? scoreResult.score : null,
10315
+ scoreable,
10124
10316
  findingCount: allDiagnostics.length,
10125
10317
  errorCount: allDiagnostics.filter((d) => d.severity === "error").length,
10126
10318
  warningCount: allDiagnostics.filter((d) => d.severity === "warning").length,
@@ -10134,11 +10326,18 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
10134
10326
  return completion;
10135
10327
  }
10136
10328
  if (options.json) {
10137
- const { buildJsonOutput } = await import("./json-OIzja7OM.js");
10138
- 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);
10139
10331
  console.log(JSON.stringify(jsonOut, null, 2));
10140
10332
  return completion;
10141
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
+ }
10142
10341
  if (!options.staged && !options.changes && options.command !== "ci" && !isCiEnv()) appendHistory({
10143
10342
  directory: resolvedDir,
10144
10343
  score: scoreResult.score,
@@ -10918,7 +11117,7 @@ const ERROR_MESSAGE_PATTERNS = [
10918
11117
  /**
10919
11118
  * Extracts the full text of a console statement spanning multiple lines.
10920
11119
  */
10921
- const getStatementText = (lines, startIndex, span) => {
11120
+ const getStatementText = (lines, span) => {
10922
11121
  const spanLines = [];
10923
11122
  for (const lineNo of span) spanLines.push(lines[lineNo - 1]);
10924
11123
  return spanLines.join("\n");
@@ -10930,6 +11129,21 @@ const getStatementText = (lines, startIndex, span) => {
10930
11129
  const shouldUpgradeToError = (statementText) => {
10931
11130
  return ERROR_MESSAGE_PATTERNS.some((pattern) => pattern.test(statementText));
10932
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
+ };
10933
11147
  const fixDeadPatterns = async (context) => {
10934
11148
  const fixable = [...await detectTrivialComments(context), ...await detectDeadPatterns(context)].filter((d) => d.fixable);
10935
11149
  if (fixable.length === 0) return;
@@ -10943,24 +11157,31 @@ const fixDeadPatterns = async (context) => {
10943
11157
  });
10944
11158
  byFile.set(absolute, entries);
10945
11159
  }
10946
- for (const [filePath, entries] of byFile) fixFileDeadPatterns(filePath, entries);
11160
+ for (const [filePath, entries] of byFile) fixFileDeadPatterns(filePath, entries, context.rootDirectory);
10947
11161
  };
10948
- const fixFileDeadPatterns = (filePath, entries) => {
11162
+ const fixFileDeadPatterns = (filePath, entries, rootDirectory) => {
10949
11163
  if (!fs.existsSync(filePath)) return;
10950
11164
  const lines = fs.readFileSync(filePath, "utf-8").split("\n");
10951
11165
  const linesToRemove = /* @__PURE__ */ new Set();
10952
11166
  const lineReplacements = /* @__PURE__ */ new Map();
11167
+ const skipConsole = isDiagnosticScriptPath(path.relative(rootDirectory, filePath));
11168
+ const consoleSpans = [];
10953
11169
  for (const entry of entries) {
10954
11170
  const index = entry.line - 1;
10955
11171
  if (index < 0 || index >= lines.length) continue;
10956
11172
  if (entry.rule === "ai-slop/console-leftover") {
11173
+ if (skipConsole) continue;
10957
11174
  const span = findStatementSpan(lines, index);
10958
- if (shouldUpgradeToError(getStatementText(lines, index, span))) {
10959
- const replaced = lines[index].replace(/console\.(?:log|debug|info|trace|dir|table)\s*\(/, "console.error(");
10960
- lineReplacements.set(entry.line, replaced);
10961
- } 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);
10962
11177
  } else linesToRemove.add(entry.line);
10963
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
+ }
10964
11185
  const result = applyEditsAndCollapse(lines, linesToRemove, lineReplacements);
10965
11186
  fs.writeFileSync(filePath, result);
10966
11187
  };
@@ -11210,7 +11431,7 @@ const fixUnusedImports = async (context) => {
11210
11431
  const importSpan = JS_EXTENSIONS$1.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
11211
11432
  if (allUnused) for (const idx of importSpan) linesToRemove.add(idx);
11212
11433
  else if (JS_EXTENSIONS$1.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
11213
- else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, syms, unusedNames);
11434
+ else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, unusedNames);
11214
11435
  }
11215
11436
  if (linesToRemove.size === 0 && unused.length === 0) continue;
11216
11437
  const sortedRemove = [...linesToRemove].sort((a, b) => b - a);
@@ -11274,9 +11495,12 @@ const rewriteJsImportSpan = (lines, span, syms, unusedNames) => {
11274
11495
  lines[span[0]] = newImport;
11275
11496
  for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
11276
11497
  };
11277
- const rewritePyImportLine = (lines, lineIdx, syms, unusedNames) => {
11498
+ const rewritePyImportLine = (lines, lineIdx, unusedNames) => {
11278
11499
  const fromMatch = lines[lineIdx].match(/^(\s*from\s+[\w.]+\s+import\s+)(.+)$/);
11279
- if (!fromMatch) return;
11500
+ if (!fromMatch) {
11501
+ rewritePlainPyImportLine(lines, lineIdx, unusedNames);
11502
+ return;
11503
+ }
11280
11504
  const prefix = fromMatch[1];
11281
11505
  const importPart = fromMatch[2].replace(/#.*$/, "").trim();
11282
11506
  const hasParen = importPart.startsWith("(");
@@ -11289,6 +11513,19 @@ const rewritePyImportLine = (lines, lineIdx, syms, unusedNames) => {
11289
11513
  const joined = keptSpecifiers.join(", ");
11290
11514
  lines[lineIdx] = hasParen ? `${prefix}(${joined})` : `${prefix}${joined}`;
11291
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
+ };
11292
11529
 
11293
11530
  //#endregion
11294
11531
  //#region src/engines/code-quality/unused-removal-ast.ts
@@ -11730,6 +11967,61 @@ const runExpoDoctor = async (context) => {
11730
11967
  return toDiagnostics(parseIssues(output));
11731
11968
  };
11732
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
+
11733
12025
  //#endregion
11734
12026
  //#region src/commands/fix-force.ts
11735
12027
  const INSTALL_TIMEOUT = 1800 * 1e3;
@@ -11867,7 +12159,9 @@ const tryNpmOverrides = async (rootDir, onProgress) => {
11867
12159
  if (!auditResult.stdout) return;
11868
12160
  const vulnerabilities = JSON.parse(auditResult.stdout).vulnerabilities;
11869
12161
  if (!vulnerabilities) return;
11870
- 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);
11871
12165
  if (Object.keys(overrides).length === 0) return;
11872
12166
  const pkgPath = path.join(rootDir, "package.json");
11873
12167
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
@@ -11903,6 +12197,76 @@ const collectPnpmOverrides = (advisories) => {
11903
12197
  }
11904
12198
  return overrides;
11905
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
+ };
11906
12270
  const isPnpmAuditRetired = (stdout, stderr) => {
11907
12271
  const haystack = `${stdout}\n${stderr}`.toLowerCase();
11908
12272
  return haystack.includes("410") || haystack.includes("gone") || haystack.includes("retired") || haystack.includes("endpoint") || haystack.includes("err_pnpm_audit") || haystack.includes("audit endpoint");
@@ -11926,7 +12290,9 @@ const tryPnpmOverrides = async (rootDir, onProgress) => {
11926
12290
  }
11927
12291
  const advisories = parsed.advisories;
11928
12292
  if (!advisories || Object.keys(advisories).length === 0) return true;
11929
- 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);
11930
12296
  if (Object.keys(overrides).length === 0) return true;
11931
12297
  const pkgPath = path.join(rootDir, "package.json");
11932
12298
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
@@ -11947,58 +12313,6 @@ const tryPnpmOverrides = async (rootDir, onProgress) => {
11947
12313
  });
11948
12314
  return true;
11949
12315
  };
11950
- const fixExpoDependencies = async (context, onProgress) => {
11951
- await removeDisallowedExpoPackages(context.rootDirectory, onProgress);
11952
- onProgress?.("Expo dependency alignment · running expo install --fix (can take a few minutes)");
11953
- if ((await runSubprocess("npx", [
11954
- "--yes",
11955
- "expo",
11956
- "install",
11957
- "--fix"
11958
- ], {
11959
- cwd: context.rootDirectory,
11960
- timeout: INSTALL_TIMEOUT
11961
- })).exitCode === 0) return;
11962
- onProgress?.("Expo dependency alignment · checking remaining issues");
11963
- const checkResult = await runSubprocess("npx", [
11964
- "--yes",
11965
- "expo",
11966
- "install",
11967
- "--check"
11968
- ], {
11969
- cwd: context.rootDirectory,
11970
- timeout: INSTALL_TIMEOUT
11971
- });
11972
- if (checkResult.exitCode !== 0) throw new Error(checkResult.stderr || checkResult.stdout || "expo dependency check failed");
11973
- };
11974
- /**
11975
- * Run expo-doctor to detect packages that should not be installed directly,
11976
- * then uninstall them. No hardcoded list — expo-doctor is the source of truth.
11977
- */
11978
- const removeDisallowedExpoPackages = async (rootDir, onProgress) => {
11979
- try {
11980
- onProgress?.("Expo dependency alignment · running expo-doctor");
11981
- const result = await runSubprocess("npx", [
11982
- "--yes",
11983
- "expo-doctor",
11984
- rootDir
11985
- ], {
11986
- cwd: rootDir,
11987
- timeout: INSTALL_TIMEOUT
11988
- });
11989
- const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
11990
- const packagePattern = /The package "([^"]+)" should not be installed directly/g;
11991
- const toRemove = [];
11992
- let match;
11993
- while ((match = packagePattern.exec(output)) !== null) toRemove.push(match[1]);
11994
- if (toRemove.length === 0) return;
11995
- onProgress?.(`Expo dependency alignment · uninstalling ${toRemove.length} package(s)`);
11996
- await runSubprocess("npm", ["uninstall", ...toRemove], {
11997
- cwd: rootDir,
11998
- timeout: INSTALL_TIMEOUT
11999
- });
12000
- } catch {}
12001
- };
12002
12316
 
12003
12317
  //#endregion
12004
12318
  //#region src/commands/fix-pipeline.ts
@@ -12007,6 +12321,10 @@ const runAiSlopSteps = async (deps) => {
12007
12321
  if (!deps.config.engines["ai-slop"]) return;
12008
12322
  await deps.runStep("Unused imports", () => detectUnusedImports(deps.context), () => fixUnusedImports(deps.context));
12009
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
+ }
12010
12328
  const detectFixableSlop = async () => {
12011
12329
  const [comments, dead, narrative] = await Promise.all([
12012
12330
  detectTrivialComments(deps.context),
@@ -12157,19 +12475,23 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
12157
12475
  });
12158
12476
  return result;
12159
12477
  };
12478
+ const safe = Boolean(options.safe);
12160
12479
  const pipelineDeps = {
12161
12480
  rail,
12162
12481
  context,
12163
12482
  config,
12164
12483
  resolvedDir,
12165
12484
  projectInfo,
12166
- force: Boolean(options.force),
12485
+ force: safe ? false : Boolean(options.force),
12486
+ safe,
12167
12487
  runStep
12168
12488
  };
12169
12489
  await runAiSlopSteps(pipelineDeps);
12170
- await runDeclarationStep(pipelineDeps);
12171
- await runLintSteps(pipelineDeps);
12172
- await runDependencyStep(pipelineDeps);
12490
+ if (!safe) {
12491
+ await runDeclarationStep(pipelineDeps);
12492
+ await runLintSteps(pipelineDeps);
12493
+ await runDependencyStep(pipelineDeps);
12494
+ }
12173
12495
  await runFormattingStep(pipelineDeps);
12174
12496
  await runForceSteps(pipelineDeps);
12175
12497
  const totalResolved = steps.reduce((sum, s) => sum + s.resolvedIssues, 0);
@@ -12517,6 +12839,7 @@ const AI_SLOP_FIXABLE = new Set([
12517
12839
  "ai-slop/duplicate-import"
12518
12840
  ]);
12519
12841
  const AI_SLOP_ERRORS = new Set(["ai-slop/hallucinated-import"]);
12842
+ const SECURITY_INFO = new Set(["security/dependency-audit-skipped"]);
12520
12843
  const BUILTIN_RULES = [
12521
12844
  {
12522
12845
  engine: "format",
@@ -12552,6 +12875,10 @@ const BUILTIN_RULES = [
12552
12875
  "knip/binaries",
12553
12876
  "knip/exports",
12554
12877
  "knip/types",
12878
+ "knip/duplicates",
12879
+ "code-quality/duplicate-block",
12880
+ "code-quality/repeated-chained-call",
12881
+ "code-quality/unused-declaration",
12555
12882
  "complexity/file-too-large",
12556
12883
  "complexity/function-too-long",
12557
12884
  "complexity/deep-nesting",
@@ -12604,8 +12931,10 @@ const BUILTIN_RULES = [
12604
12931
  "security/vulnerable-dependency",
12605
12932
  "security/eval",
12606
12933
  "security/innerhtml",
12934
+ "security/dangerously-set-innerhtml",
12607
12935
  "security/sql-injection",
12608
- "security/shell-injection"
12936
+ "security/shell-injection",
12937
+ "security/dependency-audit-skipped"
12609
12938
  ]
12610
12939
  }
12611
12940
  ];
@@ -12619,7 +12948,7 @@ const toRuleEntry = (engine, ruleId) => {
12619
12948
  if (engine === "security") return {
12620
12949
  id: ruleId,
12621
12950
  engine,
12622
- severity: "error",
12951
+ severity: SECURITY_INFO.has(ruleId) ? "info" : "error",
12623
12952
  fixable: false
12624
12953
  };
12625
12954
  if (engine === "ai-slop") return {
@@ -13063,13 +13392,14 @@ const FIX_AGENT_FLAGS = [
13063
13392
  const matchFixAgent = (flags) => {
13064
13393
  return FIX_AGENT_FLAGS.find((a) => flags[a.name])?.flag;
13065
13394
  };
13066
- 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");
13067
13396
  for (const a of FIX_AGENT_FLAGS) fixProgram.option(`--${a.flag}`, a.help);
13068
13397
  fixProgram.action(async (directory = ".", _flags, command) => {
13069
13398
  const flags = command.optsWithGlobals();
13070
13399
  await fixCommand(directory, loadConfig(directory), {
13071
13400
  verbose: Boolean(flags.verbose),
13072
13401
  force: Boolean(flags.force),
13402
+ safe: Boolean(flags.safe),
13073
13403
  prompt: Boolean(flags.prompt),
13074
13404
  agent: matchFixAgent(flags)
13075
13405
  });