aislop 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js CHANGED
@@ -59,7 +59,8 @@ const DEFAULT_CONFIG = {
59
59
  good: 75,
60
60
  ok: 50
61
61
  },
62
- smoothing: 20
62
+ smoothing: 20,
63
+ maxPerRule: 40
63
64
  },
64
65
  ci: {
65
66
  failBelow: 70,
@@ -150,7 +151,8 @@ const ScoringSchema = z$1.object({
150
151
  good: 75,
151
152
  ok: 50
152
153
  })),
153
- smoothing: z$1.number().nonnegative().default(20)
154
+ smoothing: z$1.number().nonnegative().default(20),
155
+ maxPerRule: z$1.number().positive().default(40)
154
156
  });
155
157
  const CiSchema = z$1.object({
156
158
  failBelow: z$1.number().default(70),
@@ -190,7 +192,8 @@ const AislopConfigSchema = z$1.object({
190
192
  good: 75,
191
193
  ok: 50
192
194
  },
193
- smoothing: 20
195
+ smoothing: 20,
196
+ maxPerRule: 40
194
197
  })),
195
198
  ci: CiSchema.default(() => ({
196
199
  failBelow: 70,
@@ -263,218 +266,6 @@ const loadConfig = (directory) => {
263
266
  }
264
267
  };
265
268
 
266
- //#endregion
267
- //#region src/utils/source-masker.ts
268
- const JS_EXTS$2 = new Set([
269
- ".ts",
270
- ".tsx",
271
- ".js",
272
- ".jsx",
273
- ".mjs",
274
- ".cjs"
275
- ]);
276
- const PY_EXTS = new Set([".py"]);
277
- const RB_EXTS = new Set([".rb"]);
278
- const PHP_EXTS = new Set([".php"]);
279
- const familyForExt = (ext) => {
280
- if (JS_EXTS$2.has(ext)) return "js";
281
- if (PY_EXTS.has(ext)) return "py";
282
- if (RB_EXTS.has(ext)) return "rb";
283
- if (PHP_EXTS.has(ext)) return "php";
284
- return "none";
285
- };
286
- const maskStringsAndComments = (content, ext) => {
287
- const family = familyForExt(ext);
288
- if (family === "none") return content;
289
- if (family === "js") return maskJs(content, true);
290
- return maskSimple(content, family, true);
291
- };
292
- const maskComments = (content, ext) => {
293
- const family = familyForExt(ext);
294
- if (family === "none") return content;
295
- if (family === "js") return maskJs(content, false);
296
- return maskSimple(content, family, false);
297
- };
298
- const handleQuotesAndComments = (content, i, tplStack, mask, maskStrings) => {
299
- const len = content.length;
300
- const c = content[i];
301
- const next = content[i + 1];
302
- if (c === "\"" || c === "'") {
303
- const strStart = i;
304
- const end = consumeQuotedString(content, i, c);
305
- if (maskStrings) mask(strStart + 1, end - 1);
306
- return {
307
- handled: true,
308
- nextI: end
309
- };
310
- }
311
- if (c === "`") {
312
- const scan = consumeTemplateString(content, i + 1);
313
- if (maskStrings) mask(i + 1, scan.maskEnd);
314
- if (scan.openedInterp) tplStack.push(0);
315
- return {
316
- handled: true,
317
- nextI: scan.resumeAt
318
- };
319
- }
320
- if (c === "/" && next === "/") {
321
- const strStart = i;
322
- let k = i;
323
- while (k < len && content[k] !== "\n") k++;
324
- mask(strStart, k);
325
- return {
326
- handled: true,
327
- nextI: k
328
- };
329
- }
330
- if (c === "/" && next === "*") {
331
- const strStart = i;
332
- let k = i + 2;
333
- while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
334
- if (k < len - 1) k += 2;
335
- mask(strStart, k);
336
- return {
337
- handled: true,
338
- nextI: k
339
- };
340
- }
341
- return {
342
- handled: false,
343
- nextI: i
344
- };
345
- };
346
- const maskJs = (content, maskStrings) => {
347
- const out = content.split("");
348
- const len = content.length;
349
- const tplStack = [];
350
- let i = 0;
351
- const mask = (start, end) => {
352
- for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
353
- };
354
- while (i < len) {
355
- const c = content[i];
356
- if (tplStack.length > 0) {
357
- if (c === "{") {
358
- tplStack[tplStack.length - 1]++;
359
- i++;
360
- continue;
361
- }
362
- if (c === "}") {
363
- if (tplStack[tplStack.length - 1] === 0) {
364
- tplStack.pop();
365
- const scan = consumeTemplateString(content, i + 1);
366
- if (maskStrings) mask(i + 1, scan.maskEnd);
367
- if (scan.openedInterp) tplStack.push(0);
368
- i = scan.resumeAt;
369
- continue;
370
- }
371
- tplStack[tplStack.length - 1]--;
372
- i++;
373
- continue;
374
- }
375
- }
376
- const handled = handleQuotesAndComments(content, i, tplStack, mask, maskStrings);
377
- if (handled.handled) {
378
- i = handled.nextI;
379
- continue;
380
- }
381
- i++;
382
- }
383
- return out.join("");
384
- };
385
- const consumeQuotedString = (content, start, quote) => {
386
- const len = content.length;
387
- let i = start + 1;
388
- while (i < len) {
389
- const c = content[i];
390
- if (c === "\\" && i + 1 < len) {
391
- i += 2;
392
- continue;
393
- }
394
- if (c === quote) return i + 1;
395
- if (c === "\n") return i;
396
- i++;
397
- }
398
- return i;
399
- };
400
- const consumeTemplateString = (content, start) => {
401
- const len = content.length;
402
- let i = start;
403
- while (i < len) {
404
- const c = content[i];
405
- if (c === "\\" && i + 1 < len) {
406
- i += 2;
407
- continue;
408
- }
409
- if (c === "`") return {
410
- maskEnd: i,
411
- resumeAt: i + 1,
412
- openedInterp: false
413
- };
414
- if (c === "$" && content[i + 1] === "{") return {
415
- maskEnd: i,
416
- resumeAt: i + 2,
417
- openedInterp: true
418
- };
419
- i++;
420
- }
421
- return {
422
- maskEnd: i,
423
- resumeAt: i,
424
- openedInterp: false
425
- };
426
- };
427
- const maskSimple = (content, family, maskStrings) => {
428
- const out = content.split("");
429
- const len = content.length;
430
- let i = 0;
431
- const mask = (start, end) => {
432
- for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
433
- };
434
- while (i < len) {
435
- const c = content[i];
436
- const next = content[i + 1];
437
- if (family === "py" && (c === "\"" || c === "'")) {
438
- if (content[i + 1] === c && content[i + 2] === c) {
439
- const triple = c + c + c;
440
- const end = content.indexOf(triple, i + 3);
441
- const stop = end === -1 ? len : end + 3;
442
- if (maskStrings) mask(i + 3, stop - 3);
443
- i = stop;
444
- continue;
445
- }
446
- }
447
- if (c === "\"" || c === "'") {
448
- const strStart = i;
449
- i = consumeQuotedString(content, i, c);
450
- if (maskStrings) mask(strStart + 1, i - 1);
451
- continue;
452
- }
453
- if ((family === "py" || family === "rb" || family === "php") && c === "#") {
454
- const strStart = i;
455
- while (i < len && content[i] !== "\n") i++;
456
- mask(strStart, i);
457
- continue;
458
- }
459
- if (family === "php" && c === "/" && next === "/") {
460
- const strStart = i;
461
- while (i < len && content[i] !== "\n") i++;
462
- mask(strStart, i);
463
- continue;
464
- }
465
- if (family === "php" && c === "/" && next === "*") {
466
- const strStart = i;
467
- i += 2;
468
- while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
469
- if (i < len - 1) i += 2;
470
- mask(strStart, i);
471
- continue;
472
- }
473
- i++;
474
- }
475
- return out.join("");
476
- };
477
-
478
269
  //#endregion
479
270
  //#region src/utils/source-files.ts
480
271
  const MAX_BUFFER = 50 * 1024 * 1024;
@@ -713,14 +504,226 @@ const isAutoGenerated = (filePath) => {
713
504
  } catch {}
714
505
  }
715
506
  };
716
- const getSourceFilesForRoot = (rootDirectory) => filterProjectFiles(rootDirectory, listProjectFiles(rootDirectory));
717
- const getSourceFiles = (context) => {
718
- if (context.files) return filterExplicitFiles(context.rootDirectory, context.files);
719
- return getSourceFilesForRoot(context.rootDirectory);
720
- };
721
- const getSourceFilesWithExtras = (context, extraExtensions) => {
722
- if (context.files) return filterExplicitFiles(context.rootDirectory, context.files, extraExtensions);
723
- return filterProjectFiles(context.rootDirectory, listProjectFiles(context.rootDirectory), extraExtensions);
507
+ const getSourceFilesForRoot = (rootDirectory) => filterProjectFiles(rootDirectory, listProjectFiles(rootDirectory));
508
+ const getSourceFiles = (context) => {
509
+ if (context.files) return filterExplicitFiles(context.rootDirectory, context.files);
510
+ return getSourceFilesForRoot(context.rootDirectory);
511
+ };
512
+ const getSourceFilesWithExtras = (context, extraExtensions) => {
513
+ if (context.files) return filterExplicitFiles(context.rootDirectory, context.files, extraExtensions);
514
+ return filterProjectFiles(context.rootDirectory, listProjectFiles(context.rootDirectory), extraExtensions);
515
+ };
516
+
517
+ //#endregion
518
+ //#region src/utils/source-masker.ts
519
+ const JS_EXTS$2 = new Set([
520
+ ".ts",
521
+ ".tsx",
522
+ ".js",
523
+ ".jsx",
524
+ ".mjs",
525
+ ".cjs"
526
+ ]);
527
+ const PY_EXTS = new Set([".py"]);
528
+ const RB_EXTS = new Set([".rb"]);
529
+ const PHP_EXTS = new Set([".php"]);
530
+ const familyForExt = (ext) => {
531
+ if (JS_EXTS$2.has(ext)) return "js";
532
+ if (PY_EXTS.has(ext)) return "py";
533
+ if (RB_EXTS.has(ext)) return "rb";
534
+ if (PHP_EXTS.has(ext)) return "php";
535
+ return "none";
536
+ };
537
+ const maskStringsAndComments = (content, ext) => {
538
+ const family = familyForExt(ext);
539
+ if (family === "none") return content;
540
+ if (family === "js") return maskJs(content, true);
541
+ return maskSimple(content, family, true);
542
+ };
543
+ const maskComments = (content, ext) => {
544
+ const family = familyForExt(ext);
545
+ if (family === "none") return content;
546
+ if (family === "js") return maskJs(content, false);
547
+ return maskSimple(content, family, false);
548
+ };
549
+ const handleQuotesAndComments = (content, i, tplStack, mask, maskStrings) => {
550
+ const len = content.length;
551
+ const c = content[i];
552
+ const next = content[i + 1];
553
+ if (c === "\"" || c === "'") {
554
+ const strStart = i;
555
+ const end = consumeQuotedString(content, i, c);
556
+ if (maskStrings) mask(strStart + 1, end - 1);
557
+ return {
558
+ handled: true,
559
+ nextI: end
560
+ };
561
+ }
562
+ if (c === "`") {
563
+ const scan = consumeTemplateString(content, i + 1);
564
+ if (maskStrings) mask(i + 1, scan.maskEnd);
565
+ if (scan.openedInterp) tplStack.push(0);
566
+ return {
567
+ handled: true,
568
+ nextI: scan.resumeAt
569
+ };
570
+ }
571
+ if (c === "/" && next === "/") {
572
+ const strStart = i;
573
+ let k = i;
574
+ while (k < len && content[k] !== "\n") k++;
575
+ mask(strStart, k);
576
+ return {
577
+ handled: true,
578
+ nextI: k
579
+ };
580
+ }
581
+ if (c === "/" && next === "*") {
582
+ const strStart = i;
583
+ let k = i + 2;
584
+ while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
585
+ if (k < len - 1) k += 2;
586
+ mask(strStart, k);
587
+ return {
588
+ handled: true,
589
+ nextI: k
590
+ };
591
+ }
592
+ return {
593
+ handled: false,
594
+ nextI: i
595
+ };
596
+ };
597
+ const maskJs = (content, maskStrings) => {
598
+ const out = content.split("");
599
+ const len = content.length;
600
+ const tplStack = [];
601
+ let i = 0;
602
+ const mask = (start, end) => {
603
+ for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
604
+ };
605
+ while (i < len) {
606
+ const c = content[i];
607
+ if (tplStack.length > 0) {
608
+ if (c === "{") {
609
+ tplStack[tplStack.length - 1]++;
610
+ i++;
611
+ continue;
612
+ }
613
+ if (c === "}") {
614
+ if (tplStack[tplStack.length - 1] === 0) {
615
+ tplStack.pop();
616
+ const scan = consumeTemplateString(content, i + 1);
617
+ if (maskStrings) mask(i + 1, scan.maskEnd);
618
+ if (scan.openedInterp) tplStack.push(0);
619
+ i = scan.resumeAt;
620
+ continue;
621
+ }
622
+ tplStack[tplStack.length - 1]--;
623
+ i++;
624
+ continue;
625
+ }
626
+ }
627
+ const handled = handleQuotesAndComments(content, i, tplStack, mask, maskStrings);
628
+ if (handled.handled) {
629
+ i = handled.nextI;
630
+ continue;
631
+ }
632
+ i++;
633
+ }
634
+ return out.join("");
635
+ };
636
+ const consumeQuotedString = (content, start, quote) => {
637
+ const len = content.length;
638
+ let i = start + 1;
639
+ while (i < len) {
640
+ const c = content[i];
641
+ if (c === "\\" && i + 1 < len) {
642
+ i += 2;
643
+ continue;
644
+ }
645
+ if (c === quote) return i + 1;
646
+ if (c === "\n") return i;
647
+ i++;
648
+ }
649
+ return i;
650
+ };
651
+ const consumeTemplateString = (content, start) => {
652
+ const len = content.length;
653
+ let i = start;
654
+ while (i < len) {
655
+ const c = content[i];
656
+ if (c === "\\" && i + 1 < len) {
657
+ i += 2;
658
+ continue;
659
+ }
660
+ if (c === "`") return {
661
+ maskEnd: i,
662
+ resumeAt: i + 1,
663
+ openedInterp: false
664
+ };
665
+ if (c === "$" && content[i + 1] === "{") return {
666
+ maskEnd: i,
667
+ resumeAt: i + 2,
668
+ openedInterp: true
669
+ };
670
+ i++;
671
+ }
672
+ return {
673
+ maskEnd: i,
674
+ resumeAt: i,
675
+ openedInterp: false
676
+ };
677
+ };
678
+ const maskSimple = (content, family, maskStrings) => {
679
+ const out = content.split("");
680
+ const len = content.length;
681
+ let i = 0;
682
+ const mask = (start, end) => {
683
+ for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
684
+ };
685
+ while (i < len) {
686
+ const c = content[i];
687
+ const next = content[i + 1];
688
+ if (family === "py" && (c === "\"" || c === "'")) {
689
+ if (content[i + 1] === c && content[i + 2] === c) {
690
+ const triple = c + c + c;
691
+ const end = content.indexOf(triple, i + 3);
692
+ const stop = end === -1 ? len : end + 3;
693
+ if (maskStrings) mask(i + 3, stop - 3);
694
+ i = stop;
695
+ continue;
696
+ }
697
+ }
698
+ if (c === "\"" || c === "'") {
699
+ const strStart = i;
700
+ i = consumeQuotedString(content, i, c);
701
+ if (maskStrings) mask(strStart + 1, i - 1);
702
+ continue;
703
+ }
704
+ if ((family === "py" || family === "rb" || family === "php") && c === "#") {
705
+ const strStart = i;
706
+ while (i < len && content[i] !== "\n") i++;
707
+ mask(strStart, i);
708
+ continue;
709
+ }
710
+ if (family === "php" && c === "/" && next === "/") {
711
+ const strStart = i;
712
+ while (i < len && content[i] !== "\n") i++;
713
+ mask(strStart, i);
714
+ continue;
715
+ }
716
+ if (family === "php" && c === "/" && next === "*") {
717
+ const strStart = i;
718
+ i += 2;
719
+ while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
720
+ if (i < len - 1) i += 2;
721
+ mask(strStart, i);
722
+ continue;
723
+ }
724
+ i++;
725
+ }
726
+ return out.join("");
724
727
  };
725
728
 
726
729
  //#endregion
@@ -768,16 +771,14 @@ const detectThinWrappers = (content, relativePath, ext) => {
768
771
  for (const { pattern, extensions } of THIN_WRAPPER_PATTERNS) {
769
772
  if (!extensions.has(ext)) continue;
770
773
  const regex = new RegExp(pattern.source, pattern.flags);
771
- let match;
772
- while ((match = regex.exec(content)) !== null) {
774
+ for (const match of content.matchAll(regex)) {
773
775
  const funcName = match[1];
774
776
  const matchText = match[0];
775
777
  const lineNumber = content.slice(0, match.index).split("\n").length;
776
778
  if (DUNDER_PATTERN.test(funcName)) continue;
777
779
  if (FRAMEWORK_METHOD_NAMES.test(funcName)) continue;
778
780
  if (lineNumber >= 2) {
779
- const prevLine = lines[lineNumber - 2]?.trim();
780
- if (prevLine && prevLine.startsWith("@")) continue;
781
+ if ((lines[lineNumber - 2]?.trim())?.startsWith("@")) continue;
781
782
  }
782
783
  if (!isIdentityForward(matchText)) continue;
783
784
  if (isUseContextWrapper(matchText)) continue;
@@ -961,8 +962,8 @@ const PHP_DECL_START = /^\s*(?:(?:public|private|protected|static|final|abstract
961
962
 
962
963
  //#endregion
963
964
  //#region src/engines/ai-slop/non-production-paths.ts
964
- const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|docs?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
965
- const BASENAME_PATTERN = /(?:^|\/)(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]*\.[mc]?[jt]sx?$|(?:^|\/)[^/]+[-_](?:benchmark|bench|demo|example)\.[mc]?[jt]sx?$/i;
965
+ const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|docs?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|prototypes?|experiments?|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
966
+ const BASENAME_PATTERN = /(?:^|\/)(?:(?:prototype|experiment)(?:[-_.][^/]*)?|(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]*)\.[mc]?[jt]sx?$|(?:^|\/)[^/]+[-_](?:benchmark|bench|demo|example|prototype|experiment)\.[mc]?[jt]sx?$/i;
966
967
  const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) || BASENAME_PATTERN.test(relativePath);
967
968
 
968
969
  //#endregion
@@ -1078,7 +1079,7 @@ const JS_EXTENSIONS$3 = new Set([
1078
1079
  ".mjs",
1079
1080
  ".cjs"
1080
1081
  ]);
1081
- const CONSOLE_LOG_PATTERN = /\bconsole\.(?:log|debug|info|trace|dir|table)\s*\(/;
1082
+ const CONSOLE_CALL_PATTERN = /\bconsole\.(log|debug|info|trace|dir|table)\s*\(/;
1082
1083
  const slop = (filePath, line, rule, severity, message, help, fixable) => ({
1083
1084
  filePath,
1084
1085
  engine: "ai-slop",
@@ -1092,20 +1093,35 @@ const slop = (filePath, line, rule, severity, message, help, fixable) => ({
1092
1093
  fixable
1093
1094
  });
1094
1095
  const LOGGER_FILE_PATTERN = /(?:^|\/)(?:logger|logging|log)\.[^/]+$/i;
1096
+ const CLI_ENTRYPOINT_PATTERN = /(?:^|\/)(?:cli|cli[-_.][^/]*|[^/]+[-_]cli)\.[mc]?[jt]sx?$/i;
1097
+ const ENTRYPOINT_GUARD_PATTERN = /\b(?:import\.meta\.main|require\.main\s*===\s*module)\b/;
1098
+ const OPERATIONAL_LOG_PATTERN = /\bconsole\.(?:log|info)\s*\(\s*(?:`|["'])\s*\[[^\]\n]{1,48}\]/;
1099
+ const DEBUG_SIGNAL_PATTERN = /\b(?:debug|dbg|trace|dump|inspect|todo|tmp|temp|remove\s+me|leftover|here|checkpoint)\b/i;
1100
+ const shouldFlagConsoleCall = (trimmed) => {
1101
+ const match = CONSOLE_CALL_PATTERN.exec(trimmed);
1102
+ if (!match) return false;
1103
+ const method = match[1];
1104
+ if (method === "trace" || method === "dir" || method === "table") return true;
1105
+ if (method === "debug") return DEBUG_SIGNAL_PATTERN.test(trimmed) || !OPERATIONAL_LOG_PATTERN.test(trimmed);
1106
+ if (method === "info" || method === "log") {
1107
+ if (/console\.log\(\s*JSON\.stringify\b/.test(trimmed)) return false;
1108
+ if (OPERATIONAL_LOG_PATTERN.test(trimmed)) return false;
1109
+ return true;
1110
+ }
1111
+ return false;
1112
+ };
1095
1113
  const detectConsoleLeftovers = (content, relativePath, ext) => {
1096
1114
  if (!JS_EXTENSIONS$3.has(ext)) return [];
1097
1115
  if (LOGGER_FILE_PATTERN.test(relativePath)) return [];
1098
- if (isNonProductionPath(relativePath)) return [];
1116
+ if (isNonProductionPath(relativePath) || CLI_ENTRYPOINT_PATTERN.test(relativePath)) return [];
1117
+ if (content.startsWith("#!")) return [];
1118
+ if (ENTRYPOINT_GUARD_PATTERN.test(content)) return [];
1099
1119
  const diagnostics = [];
1100
1120
  const lines = content.split("\n");
1101
1121
  for (let i = 0; i < lines.length; i++) {
1102
1122
  const trimmed = lines[i].trim();
1103
1123
  if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
1104
- if (CONSOLE_LOG_PATTERN.test(trimmed)) {
1105
- if (/console\.(?:error|warn)\s*\(/.test(trimmed)) continue;
1106
- if (/console\.log\(\s*JSON\.stringify\b/.test(trimmed)) continue;
1107
- diagnostics.push(slop(relativePath, i + 1, "ai-slop/console-leftover", "warning", "console.log/debug/info statement left in production code", "Remove debugging console statements or replace with a proper logger", true));
1108
- }
1124
+ if (shouldFlagConsoleCall(trimmed)) diagnostics.push(slop(relativePath, i + 1, "ai-slop/console-leftover", "warning", "console.log/debug/info statement left in production code", "Remove debugging console statements or replace with a proper logger", true));
1109
1125
  }
1110
1126
  return diagnostics;
1111
1127
  };
@@ -1133,6 +1149,7 @@ const isGuardedSingleLineExit = (lines, lineIndex) => {
1133
1149
  const control = contextLines.join(" ");
1134
1150
  return /(?:^|[}\s])(?:if|else\s+if|for|while)\s*\(/.test(control) && !/{\s*$/.test(control);
1135
1151
  };
1152
+ const isPropertyNoopAssignment = (trimmed) => /^(?:[\w$]+\.)+[\w$]+\s*=\s*(?:function\s*)?\([^)]*\)\s*(?:=>)?\s*\{\s*\}\s*;?$/.test(trimmed);
1136
1153
  const detectTodoStubs = (content, relativePath) => {
1137
1154
  const diagnostics = [];
1138
1155
  const lines = content.split("\n");
@@ -1154,7 +1171,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
1154
1171
  const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
1155
1172
  if (JS_EXTENSIONS$3.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));
1156
1173
  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));
1157
- if (JS_EXTENSIONS$3.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));
1174
+ if (JS_EXTENSIONS$3.has(ext) && /(?:function\s+\w+\s*\([^)]*\)|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ") && !isPropertyNoopAssignment(trimmed)) 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));
1158
1175
  }
1159
1176
  return diagnostics;
1160
1177
  };
@@ -1540,9 +1557,8 @@ const detectSwallowedExceptions = async (context) => {
1540
1557
  const relativePath = path.relative(context.rootDirectory, filePath);
1541
1558
  for (const { pattern, languages, message } of SWALLOWED_EXCEPTION_PATTERNS) {
1542
1559
  if (!languages.includes(ext)) continue;
1543
- let match;
1544
1560
  const regex = new RegExp(pattern.source, pattern.flags + (pattern.flags.includes("g") ? "" : "g"));
1545
- while ((match = regex.exec(content)) !== null) {
1561
+ for (const match of content.matchAll(regex)) {
1546
1562
  if (isIntentionalIgnore(match[0], ext)) continue;
1547
1563
  const line = content.slice(0, match.index).split("\n").length;
1548
1564
  diagnostics.push({
@@ -1558,240 +1574,80 @@ const detectSwallowedExceptions = async (context) => {
1558
1574
  fixable: false
1559
1575
  });
1560
1576
  }
1561
- }
1562
- }
1563
- return diagnostics;
1564
- };
1565
-
1566
- //#endregion
1567
- //#region src/engines/ai-slop/go-patterns.ts
1568
- const GO_EXTENSIONS = new Set([".go"]);
1569
- const PACKAGE_DECL_RE = /^\s*package\s+(\w+)/;
1570
- const PANIC_CALL_RE = /\bpanic\s*\(/;
1571
- const COMMENT_LINE_RE$1 = /^\s*\/\//;
1572
- const NIL_GUARD_RE = /^\s*if\s+[\w.]+(?:\(\))?\s*==\s*nil\s*\{?\s*$/;
1573
- const SHORT_STRING_PANIC_RE = /\bpanic\s*\(\s*"[^"]{1,40}"\s*\)/;
1574
- const detectPackageName = (lines) => {
1575
- for (const line of lines) {
1576
- const m = PACKAGE_DECL_RE.exec(line);
1577
- if (m) return m[1];
1578
- }
1579
- return null;
1580
- };
1581
- const PANIC_INTENT_LOOKBACK = 3;
1582
- const hasIntentComment$1 = (lines, panicLineIdx) => {
1583
- for (let j = panicLineIdx - 1; j >= Math.max(0, panicLineIdx - PANIC_INTENT_LOOKBACK); j--) if (COMMENT_LINE_RE$1.test(lines[j])) return true;
1584
- return false;
1585
- };
1586
- const isNilGuardPanic = (lines, panicLineIdx, line) => {
1587
- if (!SHORT_STRING_PANIC_RE.test(line)) return false;
1588
- for (let j = panicLineIdx - 1; j >= Math.max(0, panicLineIdx - 2); j--) {
1589
- const prev = lines[j];
1590
- if (prev.trim() === "") continue;
1591
- return NIL_GUARD_RE.test(prev);
1592
- }
1593
- return false;
1594
- };
1595
- const flagLibraryPanic = (lines, relPath, pkg, out) => {
1596
- if (pkg === "main") return;
1597
- for (let i = 0; i < lines.length; i++) {
1598
- const line = lines[i];
1599
- if (COMMENT_LINE_RE$1.test(line)) continue;
1600
- PANIC_CALL_RE.lastIndex = 0;
1601
- if (!PANIC_CALL_RE.test(line)) continue;
1602
- if (hasIntentComment$1(lines, i)) continue;
1603
- if (isNilGuardPanic(lines, i, line)) continue;
1604
- out.push({
1605
- filePath: relPath,
1606
- engine: "ai-slop",
1607
- rule: "ai-slop/go-library-panic",
1608
- severity: "warning",
1609
- message: `\`panic()\` in package \`${pkg}\` (non-main, non-test). Library code should return errors, not unwind the goroutine.`,
1610
- help: "Convert to `return fmt.Errorf(...)` (or a wrapped error) and let the caller decide. Reserve `panic` for genuinely-impossible states (corrupt internal invariants), and mark those with a comment so future readers know it's intentional.",
1611
- line: i + 1,
1612
- column: 1,
1613
- category: "AI Slop",
1614
- fixable: false
1615
- });
1616
- }
1617
- };
1618
- const detectGoPatterns = async (context) => {
1619
- const diagnostics = [];
1620
- const files = getSourceFiles(context);
1621
- for (const filePath of files) {
1622
- if (!GO_EXTENSIONS.has(path.extname(filePath))) continue;
1623
- if (isAutoGenerated(filePath)) continue;
1624
- if (filePath.endsWith("_test.go")) continue;
1625
- let content;
1626
- try {
1627
- content = fs.readFileSync(filePath, "utf-8");
1628
- } catch {
1629
- continue;
1630
- }
1631
- const lines = content.split("\n");
1632
- const pkg = detectPackageName(lines);
1633
- if (!pkg) continue;
1634
- flagLibraryPanic(lines, path.relative(context.rootDirectory, filePath), pkg, diagnostics);
1635
- }
1636
- return diagnostics;
1637
- };
1638
-
1639
- //#endregion
1640
- //#region src/engines/ai-slop/hardcoded-config.ts
1641
- const SOURCE_EXTENSIONS = new Set([
1642
- ".ts",
1643
- ".tsx",
1644
- ".js",
1645
- ".jsx",
1646
- ".mjs",
1647
- ".cjs",
1648
- ".py",
1649
- ".go",
1650
- ".rs",
1651
- ".rb",
1652
- ".java",
1653
- ".php"
1654
- ]);
1655
- const URL_LITERAL_RE = /(["'`])(https?:\/\/[^"'`\s<>]+)\1/g;
1656
- const ID_LITERAL_RE = /(["'])([A-Za-z][A-Za-z0-9_-]{15,})\1/g;
1657
- const ENV_REFERENCE_RE = /\b(?:process\.env|import\.meta\.env|Deno\.env|os\.environ|getenv|env\()\b/i;
1658
- const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|license|readme|source|svgUrl|pageUrl|href|link|install)\b/i;
1659
- const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
1660
- 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;
1661
- 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;
1662
- const MIGRATION_PATH_RE$1 = /(?:^|[\\/])(?:migrations?|db[\\/]migrate)[\\/]/i;
1663
- const PLACEHOLDER_HOSTS = new Set([
1664
- "example.com",
1665
- "example.org",
1666
- "example.net"
1667
- ]);
1668
- const LOOPBACK_HOSTS = new Set([
1669
- "localhost",
1670
- "127.0.0.1",
1671
- "0.0.0.0",
1672
- "::1"
1673
- ]);
1674
- const VENDOR_API_DOMAINS = [
1675
- "github.com",
1676
- "githubusercontent.com",
1677
- "googleapis.com",
1678
- "accounts.google.com",
1679
- "stripe.com",
1680
- "openai.com",
1681
- "anthropic.com",
1682
- "slack.com",
1683
- "twilio.com",
1684
- "sendgrid.com",
1685
- "mailgun.net",
1686
- "cloudflare.com",
1687
- "discord.com",
1688
- "telegram.org",
1689
- "login.microsoftonline.com",
1690
- "graph.microsoft.com",
1691
- "twitter.com",
1692
- "x.com",
1693
- "twimg.com",
1694
- "t.co",
1695
- "api.telegram.org"
1696
- ];
1697
- const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
1698
- const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
1699
- const HARDCODED_URL_FINDING = {
1700
- rule: "ai-slop/hardcoded-url",
1701
- message: "Hardcoded environment URL in production code",
1702
- help: "Move deployment-specific URLs to environment variables or a typed config module. Keep only stable documentation/public links inline."
1703
- };
1704
- const HARDCODED_ID_FINDING = {
1705
- rule: "ai-slop/hardcoded-id",
1706
- message: "Hardcoded provider/project ID in production code",
1707
- 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."
1708
- };
1709
- const makeFinding = (filePath, line, spec) => ({
1710
- filePath,
1711
- engine: "ai-slop",
1712
- rule: spec.rule,
1713
- severity: "warning",
1714
- message: spec.message,
1715
- help: spec.help,
1716
- line,
1717
- column: 0,
1718
- category: "AI Slop",
1719
- fixable: false
1720
- });
1721
- const isCommentOnlyLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*");
1722
- const commentStartsBefore = (line, index, ext) => {
1723
- const prefix = line.slice(0, index);
1724
- if (ext === ".py" || ext === ".rb") return prefix.includes("#");
1725
- if (ext === ".php") return prefix.includes("//") || prefix.includes("#");
1726
- return prefix.includes("//") || prefix.includes("/*");
1727
- };
1728
- const safeUrlHost = (urlText) => {
1729
- try {
1730
- return new URL(urlText).hostname.toLowerCase();
1731
- } catch {
1732
- return null;
1577
+ }
1733
1578
  }
1579
+ return diagnostics;
1734
1580
  };
1735
- const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
1736
- const shouldFlagUrlLiteral = (line, urlText) => {
1737
- if (isEnvBackedLine(line)) return false;
1738
- const host = safeUrlHost(urlText);
1739
- if (!host) return false;
1740
- if (PLACEHOLDER_HOSTS.has(host)) return false;
1741
- if (LOOPBACK_HOSTS.has(host)) return false;
1742
- if (isVendorApiHost(host)) return false;
1743
- if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
1744
- return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
1581
+
1582
+ //#endregion
1583
+ //#region src/engines/ai-slop/go-patterns.ts
1584
+ const GO_EXTENSIONS = new Set([".go"]);
1585
+ const PACKAGE_DECL_RE = /^\s*package\s+(\w+)/;
1586
+ const PANIC_CALL_RE = /\bpanic\s*\(/;
1587
+ const COMMENT_LINE_RE$1 = /^\s*\/\//;
1588
+ const NIL_GUARD_RE = /^\s*if\s+[\w.]+(?:\(\))?\s*==\s*nil\s*\{?\s*$/;
1589
+ const SHORT_STRING_PANIC_RE = /\bpanic\s*\(\s*"[^"]{1,40}"\s*\)/;
1590
+ const detectPackageName = (lines) => {
1591
+ for (const line of lines) {
1592
+ const m = PACKAGE_DECL_RE.exec(line);
1593
+ if (m) return m[1];
1594
+ }
1595
+ return null;
1745
1596
  };
1746
- const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
1747
- const hasUsefulIdShape = (value) => {
1748
- if (PLACEHOLDER_ID_RE.test(value)) return false;
1749
- if (ENV_VAR_NAME_RE.test(value)) return false;
1750
- if (/^https?:\/\//i.test(value)) return false;
1751
- if (/^[A-Za-z]+$/.test(value)) return false;
1752
- return /[0-9]/.test(value);
1597
+ const PANIC_INTENT_LOOKBACK = 3;
1598
+ const hasIntentComment$1 = (lines, panicLineIdx) => {
1599
+ for (let j = panicLineIdx - 1; j >= Math.max(0, panicLineIdx - PANIC_INTENT_LOOKBACK); j--) if (COMMENT_LINE_RE$1.test(lines[j])) return true;
1600
+ return false;
1753
1601
  };
1754
- const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
1755
- const diagnostics = [];
1756
- if (isCommentOnlyLine(line.trim())) return diagnostics;
1757
- URL_LITERAL_RE.lastIndex = 0;
1758
- let urlMatch;
1759
- while ((urlMatch = URL_LITERAL_RE.exec(line)) !== null) {
1760
- const urlText = urlMatch[2];
1761
- if (commentStartsBefore(line, urlMatch.index, ext)) continue;
1762
- if (!shouldFlagUrlLiteral(line, urlText)) continue;
1763
- diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_URL_FINDING));
1764
- }
1765
- if (!ID_CONTEXT_RE.test(line) || isEnvBackedLine(line) || DOC_URL_CONTEXT_RE.test(line)) return diagnostics;
1766
- ID_LITERAL_RE.lastIndex = 0;
1767
- let idMatch;
1768
- while ((idMatch = ID_LITERAL_RE.exec(line)) !== null) {
1769
- const value = idMatch[2];
1770
- if (commentStartsBefore(line, idMatch.index, ext)) continue;
1771
- if (!hasUsefulIdShape(value)) continue;
1772
- diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_ID_FINDING));
1602
+ const isNilGuardPanic = (lines, panicLineIdx, line) => {
1603
+ if (!SHORT_STRING_PANIC_RE.test(line)) return false;
1604
+ for (let j = panicLineIdx - 1; j >= Math.max(0, panicLineIdx - 2); j--) {
1605
+ const prev = lines[j];
1606
+ if (prev.trim() === "") continue;
1607
+ return NIL_GUARD_RE.test(prev);
1773
1608
  }
1774
- return diagnostics;
1609
+ return false;
1775
1610
  };
1776
- const scanFileForConfigLiterals = (content, relativePath, ext) => {
1777
- if (!SOURCE_EXTENSIONS.has(ext)) return [];
1778
- if (isNonProductionPath(relativePath)) return [];
1779
- if (MIGRATION_PATH_RE$1.test(relativePath)) return [];
1780
- return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
1611
+ const flagLibraryPanic = (lines, relPath, pkg, out) => {
1612
+ if (pkg === "main") return;
1613
+ for (let i = 0; i < lines.length; i++) {
1614
+ const line = lines[i];
1615
+ if (COMMENT_LINE_RE$1.test(line)) continue;
1616
+ PANIC_CALL_RE.lastIndex = 0;
1617
+ if (!PANIC_CALL_RE.test(line)) continue;
1618
+ if (hasIntentComment$1(lines, i)) continue;
1619
+ if (isNilGuardPanic(lines, i, line)) continue;
1620
+ out.push({
1621
+ filePath: relPath,
1622
+ engine: "ai-slop",
1623
+ rule: "ai-slop/go-library-panic",
1624
+ severity: "warning",
1625
+ message: `\`panic()\` in package \`${pkg}\` (non-main, non-test). Library code should return errors, not unwind the goroutine.`,
1626
+ help: "Convert to `return fmt.Errorf(...)` (or a wrapped error) and let the caller decide. Reserve `panic` for genuinely-impossible states (corrupt internal invariants), and mark those with a comment so future readers know it's intentional.",
1627
+ line: i + 1,
1628
+ column: 1,
1629
+ category: "AI Slop",
1630
+ fixable: false
1631
+ });
1632
+ }
1781
1633
  };
1782
- const detectHardcodedConfigLiterals = async (context) => {
1634
+ const detectGoPatterns = async (context) => {
1783
1635
  const diagnostics = [];
1784
- for (const filePath of getSourceFiles(context)) {
1636
+ const files = getSourceFiles(context);
1637
+ for (const filePath of files) {
1638
+ if (!GO_EXTENSIONS.has(path.extname(filePath))) continue;
1785
1639
  if (isAutoGenerated(filePath)) continue;
1640
+ if (filePath.endsWith("_test.go")) continue;
1786
1641
  let content;
1787
1642
  try {
1788
1643
  content = fs.readFileSync(filePath, "utf-8");
1789
1644
  } catch {
1790
1645
  continue;
1791
1646
  }
1792
- const relativePath = path.relative(context.rootDirectory, filePath);
1793
- const ext = path.extname(filePath);
1794
- diagnostics.push(...scanFileForConfigLiterals(maskComments(content, ext), relativePath, ext));
1647
+ const lines = content.split("\n");
1648
+ const pkg = detectPackageName(lines);
1649
+ if (!pkg) continue;
1650
+ flagLibraryPanic(lines, path.relative(context.rootDirectory, filePath), pkg, diagnostics);
1795
1651
  }
1796
1652
  return diagnostics;
1797
1653
  };
@@ -1813,7 +1669,7 @@ const JS_RESOLUTION_EXTENSIONS = [
1813
1669
  "/index.js",
1814
1670
  "/index.jsx"
1815
1671
  ];
1816
- const readJson$2 = (filePath) => {
1672
+ const readJson$3 = (filePath) => {
1817
1673
  try {
1818
1674
  return JSON.parse(fs.readFileSync(filePath, "utf-8"));
1819
1675
  } catch {
@@ -1828,7 +1684,7 @@ const buildAliasMatcher = (key) => {
1828
1684
  return (spec) => spec.length >= before.length + after.length && spec.startsWith(before) && spec.endsWith(after);
1829
1685
  };
1830
1686
  const collectAliasMatchersFromConfig = (configPath, matchers) => {
1831
- const opts = readJson$2(configPath)?.compilerOptions;
1687
+ const opts = readJson$3(configPath)?.compilerOptions;
1832
1688
  if (!opts || typeof opts !== "object") return;
1833
1689
  const configDir = path.dirname(configPath);
1834
1690
  const paths = opts.paths;
@@ -1851,7 +1707,7 @@ const collectTsPathAliases = (rootDir, workspaceDirs) => {
1851
1707
 
1852
1708
  //#endregion
1853
1709
  //#region src/engines/ai-slop/js-workspaces.ts
1854
- const readJson$1 = (filePath) => {
1710
+ const readJson$2 = (filePath) => {
1855
1711
  try {
1856
1712
  return JSON.parse(fs.readFileSync(filePath, "utf-8"));
1857
1713
  } catch {
@@ -1871,7 +1727,7 @@ const readWorkspaceGlobs = (rootDir, rootPkg) => {
1871
1727
  }
1872
1728
  }
1873
1729
  }
1874
- const lerna = readJson$1(path.join(rootDir, "lerna.json"));
1730
+ const lerna = readJson$2(path.join(rootDir, "lerna.json"));
1875
1731
  if (lerna && Array.isArray(lerna.packages)) {
1876
1732
  for (const g of lerna.packages) if (typeof g === "string") globs.push(g);
1877
1733
  }
@@ -1893,15 +1749,18 @@ const readWorkspaceGlobs = (rootDir, rootPkg) => {
1893
1749
  }
1894
1750
  return globs;
1895
1751
  };
1752
+ const readWorkspaceEntries = (dir) => {
1753
+ try {
1754
+ return fs.readdirSync(dir, { withFileTypes: true });
1755
+ } catch {
1756
+ return [];
1757
+ }
1758
+ };
1896
1759
  const expandWorkspaceDirs = (rootDir, globs) => {
1897
1760
  const dirs = [];
1898
1761
  for (const glob of globs) if (glob.endsWith("/*")) {
1899
1762
  const parent = path.join(rootDir, glob.slice(0, -2));
1900
- try {
1901
- for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
1902
- } catch {
1903
- continue;
1904
- }
1763
+ for (const entry of readWorkspaceEntries(parent)) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
1905
1764
  } else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
1906
1765
  return dirs;
1907
1766
  };
@@ -2269,7 +2128,7 @@ const JS_EXTENSIONS$1 = new Set([
2269
2128
  ".cjs"
2270
2129
  ]);
2271
2130
  const PY_EXTENSIONS$2 = new Set([".py"]);
2272
- const readJson = (filePath) => {
2131
+ const readJson$1 = (filePath) => {
2273
2132
  try {
2274
2133
  return JSON.parse(fs.readFileSync(filePath, "utf-8"));
2275
2134
  } catch {
@@ -2313,7 +2172,7 @@ const collectNestedManifests = (rootDir, jsDeps) => {
2313
2172
  const full = path.join(dir, entry.name);
2314
2173
  if (entry.isDirectory()) walk(full, depth + 1);
2315
2174
  else if (entry.name === "package.json" && depth > 0) {
2316
- const wsPkg = readJson(full);
2175
+ const wsPkg = readJson$1(full);
2317
2176
  if (!wsPkg) continue;
2318
2177
  if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
2319
2178
  addDepsFromPkg(wsPkg, jsDeps);
@@ -2325,13 +2184,13 @@ const collectNestedManifests = (rootDir, jsDeps) => {
2325
2184
  const collectJsDeps = (rootDir, jsDeps) => {
2326
2185
  const pkgPath = path.join(rootDir, "package.json");
2327
2186
  if (!fs.existsSync(pkgPath)) return false;
2328
- const pkg = readJson(pkgPath);
2187
+ const pkg = readJson$1(pkgPath);
2329
2188
  if (!pkg || typeof pkg !== "object") return false;
2330
2189
  addDepsFromPkg(pkg, jsDeps);
2331
2190
  if (typeof pkg.name === "string") jsDeps.add(pkg.name);
2332
2191
  const workspaceDirs = collectWorkspaceDirs(rootDir, pkg);
2333
2192
  for (const wsDir of workspaceDirs) {
2334
- const wsPkg = readJson(path.join(wsDir, "package.json"));
2193
+ const wsPkg = readJson$1(path.join(wsDir, "package.json"));
2335
2194
  if (!wsPkg) continue;
2336
2195
  if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
2337
2196
  addDepsFromPkg(wsPkg, jsDeps);
@@ -2360,7 +2219,11 @@ const VIRTUAL_MODULE_PREFIXES = [
2360
2219
  "astro:",
2361
2220
  "virtual:",
2362
2221
  "bun:",
2363
- "file:"
2222
+ "file:",
2223
+ "http:",
2224
+ "https:",
2225
+ "jsr:",
2226
+ "npm:"
2364
2227
  ];
2365
2228
  const isJsVirtualModule = (spec, manifest) => {
2366
2229
  if (VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p))) return true;
@@ -2461,48 +2324,230 @@ const extractPyImports = (content) => {
2461
2324
  });
2462
2325
  }
2463
2326
  }
2464
- return results;
2327
+ return results;
2328
+ };
2329
+ const checkJsImport = (rawSpec, manifest, tsAliasMatchers) => {
2330
+ const spec = stripImportQuery(rawSpec);
2331
+ if (spec.length === 0) return null;
2332
+ if (isJsRelativeOrAbsolute(spec)) return null;
2333
+ if (isJsBuiltin(spec)) return null;
2334
+ if (isJsVirtualModule(spec, manifest)) return null;
2335
+ if (tsAliasMatchers.some((m) => m(spec))) return null;
2336
+ const pkg = packageNameFromImport(spec);
2337
+ if (manifest.jsDeps.has(pkg)) return null;
2338
+ if (pkg.startsWith("@types/")) {
2339
+ const realPkg = pkg.slice(7);
2340
+ if (manifest.jsDeps.has(realPkg)) return null;
2341
+ }
2342
+ if (manifest.jsDeps.has(typesPackageName(pkg))) return null;
2343
+ return pkg;
2344
+ };
2345
+ const normalizePyName = (name) => name.toLowerCase().replace(/_/g, "-");
2346
+ const checkPyImport = (spec, manifest) => {
2347
+ const root = spec.split(".")[0];
2348
+ if (PYTHON_STDLIB.has(root)) return null;
2349
+ const normalized = normalizePyName(root);
2350
+ if (manifest.pyDeps.has(normalized)) return null;
2351
+ if ((PYTHON_IMPORT_TO_PIP[root] ?? PYTHON_IMPORT_TO_PIP[normalized])?.some((dist) => manifest.pyDeps.has(normalizePyName(dist)))) return null;
2352
+ return root;
2353
+ };
2354
+ const detectHallucinatedImports = async (context) => {
2355
+ const rootPkg = readJson$1(path.join(context.rootDirectory, "package.json"));
2356
+ const workspaceDirs = collectWorkspaceDirs(context.rootDirectory, rootPkg);
2357
+ const manifest = loadManifest(context.rootDirectory);
2358
+ if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
2359
+ const tsAliasMatchers = manifest.hasJsManifest ? collectTsPathAliases(context.rootDirectory, workspaceDirs) : [];
2360
+ const diagnostics = [];
2361
+ const files = getSourceFiles(context);
2362
+ for (const filePath of files) {
2363
+ const ext = path.extname(filePath);
2364
+ const isJs = JS_EXTENSIONS$1.has(ext);
2365
+ const isPy = PY_EXTENSIONS$2.has(ext);
2366
+ if (!isJs && !isPy) continue;
2367
+ if (isJs && !manifest.hasJsManifest) continue;
2368
+ if (isPy && !manifest.hasPyManifest) continue;
2369
+ if (isAutoGenerated(filePath)) continue;
2370
+ let content;
2371
+ try {
2372
+ content = fs.readFileSync(filePath, "utf-8");
2373
+ } catch {
2374
+ continue;
2375
+ }
2376
+ const relPath = path.relative(context.rootDirectory, filePath);
2377
+ if (isNonProductionPath(relPath)) continue;
2378
+ const imports = isJs ? extractJsImports(content) : extractPyImports(content);
2379
+ for (const { spec, line } of imports) {
2380
+ const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
2381
+ if (!hallucinated) continue;
2382
+ const manifestLabel = isJs ? "package.json" : "requirements.txt / pyproject.toml / Pipfile";
2383
+ diagnostics.push({
2384
+ filePath: relPath,
2385
+ engine: "ai-slop",
2386
+ rule: "ai-slop/hallucinated-import",
2387
+ severity: "error",
2388
+ message: `Imports "${hallucinated}" but it's not declared in ${manifestLabel}${isPy ? " and isn't Python stdlib" : ""}`,
2389
+ help: "Most often this is an LLM hallucinating a plausible-sounding package name. Either add the package to your manifest, or correct the import.",
2390
+ line,
2391
+ column: 1,
2392
+ category: "AI Slop",
2393
+ fixable: false
2394
+ });
2395
+ }
2396
+ }
2397
+ return diagnostics;
2398
+ };
2399
+
2400
+ //#endregion
2401
+ //#region src/engines/ai-slop/hardcoded-config.ts
2402
+ const SOURCE_EXTENSIONS = new Set([
2403
+ ".ts",
2404
+ ".tsx",
2405
+ ".js",
2406
+ ".jsx",
2407
+ ".mjs",
2408
+ ".cjs",
2409
+ ".py",
2410
+ ".go",
2411
+ ".rs",
2412
+ ".rb",
2413
+ ".java",
2414
+ ".php"
2415
+ ]);
2416
+ const URL_LITERAL_RE = /(["'`])(https?:\/\/[^"'`\s<>]+)\1/g;
2417
+ const ID_LITERAL_RE = /(["'])([A-Za-z][A-Za-z0-9_-]{15,})\1/g;
2418
+ const ENV_REFERENCE_RE = /\b(?:process\.env|import\.meta\.env|Deno\.env|os\.environ|getenv|env\()\b/i;
2419
+ const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|license|readme|source|svgUrl|pageUrl|href|link|install)\b/i;
2420
+ const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
2421
+ 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;
2422
+ 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;
2423
+ const MIGRATION_PATH_RE$1 = /(?:^|[\\/])(?:migrations?|db[\\/]migrate)[\\/]/i;
2424
+ const PLACEHOLDER_HOSTS = new Set([
2425
+ "example.com",
2426
+ "example.org",
2427
+ "example.net"
2428
+ ]);
2429
+ const LOOPBACK_HOSTS = new Set([
2430
+ "localhost",
2431
+ "127.0.0.1",
2432
+ "0.0.0.0",
2433
+ "::1"
2434
+ ]);
2435
+ const VENDOR_API_DOMAINS = [
2436
+ "github.com",
2437
+ "githubusercontent.com",
2438
+ "googleapis.com",
2439
+ "accounts.google.com",
2440
+ "stripe.com",
2441
+ "openai.com",
2442
+ "anthropic.com",
2443
+ "slack.com",
2444
+ "twilio.com",
2445
+ "sendgrid.com",
2446
+ "mailgun.net",
2447
+ "cloudflare.com",
2448
+ "discord.com",
2449
+ "telegram.org",
2450
+ "login.microsoftonline.com",
2451
+ "graph.microsoft.com",
2452
+ "twitter.com",
2453
+ "x.com",
2454
+ "twimg.com",
2455
+ "t.co",
2456
+ "api.telegram.org"
2457
+ ];
2458
+ const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
2459
+ const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
2460
+ const PROVIDER_ID_RE = /^(?:price|prod|cus|sub|acct|org|app|tenant|workspace|project|client|key|tok|token|sk|pk)_[A-Za-z0-9][A-Za-z0-9_-]{7,}$/i;
2461
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
2462
+ const READABLE_KEY_RE = /^[a-z][a-z0-9]*(?:[_-][a-z0-9]+){2,}$/;
2463
+ const HARDCODED_URL_FINDING = {
2464
+ rule: "ai-slop/hardcoded-url",
2465
+ message: "Hardcoded environment URL in production code",
2466
+ help: "Move deployment-specific URLs to environment variables or a typed config module. Keep only stable documentation/public links inline."
2467
+ };
2468
+ const HARDCODED_ID_FINDING = {
2469
+ rule: "ai-slop/hardcoded-id",
2470
+ message: "Hardcoded provider/project ID in production code",
2471
+ 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."
2472
+ };
2473
+ const makeFinding = (filePath, line, spec) => ({
2474
+ filePath,
2475
+ engine: "ai-slop",
2476
+ rule: spec.rule,
2477
+ severity: "warning",
2478
+ message: spec.message,
2479
+ help: spec.help,
2480
+ line,
2481
+ column: 0,
2482
+ category: "AI Slop",
2483
+ fixable: false
2484
+ });
2485
+ const isCommentOnlyLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*");
2486
+ const commentStartsBefore = (line, index, ext) => {
2487
+ const prefix = line.slice(0, index);
2488
+ if (ext === ".py" || ext === ".rb") return prefix.includes("#");
2489
+ if (ext === ".php") return prefix.includes("//") || prefix.includes("#");
2490
+ return prefix.includes("//") || prefix.includes("/*");
2491
+ };
2492
+ const safeUrlHost = (urlText) => {
2493
+ try {
2494
+ return new URL(urlText).hostname.toLowerCase();
2495
+ } catch {
2496
+ return null;
2497
+ }
2465
2498
  };
2466
- const checkJsImport = (rawSpec, manifest, tsAliasMatchers) => {
2467
- const spec = stripImportQuery(rawSpec);
2468
- if (spec.length === 0) return null;
2469
- if (isJsRelativeOrAbsolute(spec)) return null;
2470
- if (isJsBuiltin(spec)) return null;
2471
- if (isJsVirtualModule(spec, manifest)) return null;
2472
- if (tsAliasMatchers.some((m) => m(spec))) return null;
2473
- const pkg = packageNameFromImport(spec);
2474
- if (manifest.jsDeps.has(pkg)) return null;
2475
- if (pkg.startsWith("@types/")) {
2476
- const realPkg = pkg.slice(7);
2477
- if (manifest.jsDeps.has(realPkg)) return null;
2499
+ const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
2500
+ const TEMPLATE_INTERPOLATION_START = "${";
2501
+ const shouldFlagUrlLiteral = (line, urlText) => {
2502
+ if (isEnvBackedLine(line)) return false;
2503
+ if (urlText.includes(TEMPLATE_INTERPOLATION_START) && /\bnew\s+URL\s*\(/.test(line)) return false;
2504
+ const host = safeUrlHost(urlText);
2505
+ if (!host) return false;
2506
+ if (PLACEHOLDER_HOSTS.has(host)) return false;
2507
+ if (LOOPBACK_HOSTS.has(host)) return false;
2508
+ if (isVendorApiHost(host)) return false;
2509
+ if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
2510
+ return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
2511
+ };
2512
+ const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
2513
+ const hasUsefulIdShape = (value) => {
2514
+ if (PLACEHOLDER_ID_RE.test(value)) return false;
2515
+ if (ENV_VAR_NAME_RE.test(value)) return false;
2516
+ if (/^https?:\/\//i.test(value)) return false;
2517
+ if (/^[A-Za-z]+$/.test(value)) return false;
2518
+ if (READABLE_KEY_RE.test(value) && !PROVIDER_ID_RE.test(value)) return false;
2519
+ if (PROVIDER_ID_RE.test(value)) return true;
2520
+ if (UUID_RE.test(value)) return true;
2521
+ if (!/[0-9]/.test(value)) return false;
2522
+ return value.length >= 24 && !/[_-]/.test(value) && /[a-z]/.test(value) && /[A-Z]/.test(value);
2523
+ };
2524
+ const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
2525
+ const diagnostics = [];
2526
+ if (isCommentOnlyLine(line.trim())) return diagnostics;
2527
+ for (const urlMatch of line.matchAll(URL_LITERAL_RE)) {
2528
+ const urlText = urlMatch[2];
2529
+ if (commentStartsBefore(line, urlMatch.index, ext)) continue;
2530
+ if (!shouldFlagUrlLiteral(line, urlText)) continue;
2531
+ diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_URL_FINDING));
2478
2532
  }
2479
- if (manifest.jsDeps.has(typesPackageName(pkg))) return null;
2480
- return pkg;
2533
+ if (!ID_CONTEXT_RE.test(line) || isEnvBackedLine(line) || DOC_URL_CONTEXT_RE.test(line)) return diagnostics;
2534
+ for (const idMatch of line.matchAll(ID_LITERAL_RE)) {
2535
+ const value = idMatch[2];
2536
+ if (commentStartsBefore(line, idMatch.index, ext)) continue;
2537
+ if (!hasUsefulIdShape(value)) continue;
2538
+ diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_ID_FINDING));
2539
+ }
2540
+ return diagnostics;
2481
2541
  };
2482
- const normalizePyName = (name) => name.toLowerCase().replace(/_/g, "-");
2483
- const checkPyImport = (spec, manifest) => {
2484
- const root = spec.split(".")[0];
2485
- if (PYTHON_STDLIB.has(root)) return null;
2486
- const normalized = normalizePyName(root);
2487
- if (manifest.pyDeps.has(normalized)) return null;
2488
- if ((PYTHON_IMPORT_TO_PIP[root] ?? PYTHON_IMPORT_TO_PIP[normalized])?.some((dist) => manifest.pyDeps.has(normalizePyName(dist)))) return null;
2489
- return root;
2542
+ const scanFileForConfigLiterals = (content, relativePath, ext) => {
2543
+ if (!SOURCE_EXTENSIONS.has(ext)) return [];
2544
+ if (isNonProductionPath(relativePath)) return [];
2545
+ if (MIGRATION_PATH_RE$1.test(relativePath)) return [];
2546
+ return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
2490
2547
  };
2491
- const detectHallucinatedImports = async (context) => {
2492
- const rootPkg = readJson(path.join(context.rootDirectory, "package.json"));
2493
- const workspaceDirs = collectWorkspaceDirs(context.rootDirectory, rootPkg);
2494
- const manifest = loadManifest(context.rootDirectory);
2495
- if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
2496
- const tsAliasMatchers = manifest.hasJsManifest ? collectTsPathAliases(context.rootDirectory, workspaceDirs) : [];
2548
+ const detectHardcodedConfigLiterals = async (context) => {
2497
2549
  const diagnostics = [];
2498
- const files = getSourceFiles(context);
2499
- for (const filePath of files) {
2500
- const ext = path.extname(filePath);
2501
- const isJs = JS_EXTENSIONS$1.has(ext);
2502
- const isPy = PY_EXTENSIONS$2.has(ext);
2503
- if (!isJs && !isPy) continue;
2504
- if (isJs && !manifest.hasJsManifest) continue;
2505
- if (isPy && !manifest.hasPyManifest) continue;
2550
+ for (const filePath of getSourceFiles(context)) {
2506
2551
  if (isAutoGenerated(filePath)) continue;
2507
2552
  let content;
2508
2553
  try {
@@ -2510,30 +2555,18 @@ const detectHallucinatedImports = async (context) => {
2510
2555
  } catch {
2511
2556
  continue;
2512
2557
  }
2513
- const relPath = path.relative(context.rootDirectory, filePath);
2514
- if (isNonProductionPath(relPath)) continue;
2515
- const imports = isJs ? extractJsImports(content) : extractPyImports(content);
2516
- for (const { spec, line } of imports) {
2517
- const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
2518
- if (!hallucinated) continue;
2519
- const manifestLabel = isJs ? "package.json" : "requirements.txt / pyproject.toml / Pipfile";
2520
- diagnostics.push({
2521
- filePath: relPath,
2522
- engine: "ai-slop",
2523
- rule: "ai-slop/hallucinated-import",
2524
- severity: "error",
2525
- message: `Imports "${hallucinated}" but it's not declared in ${manifestLabel}${isPy ? " and isn't Python stdlib" : ""}`,
2526
- help: "Most often this is an LLM hallucinating a plausible-sounding package name. Either add the package to your manifest, or correct the import.",
2527
- line,
2528
- column: 1,
2529
- category: "AI Slop",
2530
- fixable: false
2531
- });
2532
- }
2558
+ const relativePath = path.relative(context.rootDirectory, filePath);
2559
+ const ext = path.extname(filePath);
2560
+ diagnostics.push(...scanFileForConfigLiterals(maskComments(content, ext), relativePath, ext));
2533
2561
  }
2534
2562
  return diagnostics;
2535
2563
  };
2536
2564
 
2565
+ //#endregion
2566
+ //#region src/utils/suppress.ts
2567
+ const DIRECTIVE_RE = /(?:\/\/|\/\*|#|<!--|\*)\s*aislop-ignore-(next-line|line|file)\b([^\n]*)/;
2568
+ const isAislopDirectiveLine = (line) => DIRECTIVE_RE.test(line);
2569
+
2537
2570
  //#endregion
2538
2571
  //#region src/engines/ai-slop/comment-blocks.ts
2539
2572
  const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
@@ -2557,6 +2590,7 @@ const getCommentSyntax = (ext) => {
2557
2590
  };
2558
2591
  const getMatchedLinePrefix = (line, syntax) => {
2559
2592
  const trimmed = line.trimStart();
2593
+ if (isAislopDirectiveLine(trimmed)) return null;
2560
2594
  for (const prefix of syntax.linePrefixes) {
2561
2595
  if (!trimmed.startsWith(prefix)) continue;
2562
2596
  if (prefix === "#" && trimmed.startsWith("#!")) return null;
@@ -3356,9 +3390,7 @@ const isLogOnlyBody = (body) => {
3356
3390
  };
3357
3391
  const detectJsSilentRecovery = (content, relPath) => {
3358
3392
  const out = [];
3359
- CATCH_HEAD_RE.lastIndex = 0;
3360
- let match;
3361
- while ((match = CATCH_HEAD_RE.exec(content)) !== null) {
3393
+ for (const match of content.matchAll(CATCH_HEAD_RE)) {
3362
3394
  const body = extractCatchBody(content, match.index + match[0].length - 1);
3363
3395
  if (body === null) continue;
3364
3396
  if (!isLogOnlyBody(body)) continue;
@@ -3534,18 +3566,22 @@ const extractPyImportedSymbols = (lines) => {
3534
3566
  }
3535
3567
  continue;
3536
3568
  }
3537
- const importMatch = trimmed.match(/^import\s+([\w.]+)(?:\s+as\s+(\w+))?/);
3569
+ const importMatch = trimmed.match(/^import\s+(.+)/);
3538
3570
  if (importMatch) {
3539
3571
  importLines.add(i);
3540
- const alias = importMatch[2];
3541
- if (alias && alias === importMatch[1]) continue;
3542
- const simpleName = (alias ?? importMatch[1]).split(".")[0];
3543
- if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
3544
- name: simpleName,
3545
- line: i + 1,
3546
- isDefault: false,
3547
- isNamespace: true
3548
- });
3572
+ for (const clause of importMatch[1].replace(/#.*$/, "").split(",")) {
3573
+ const clauseMatch = clause.trim().match(/^([\w.]+)(?:\s+as\s+(\w+))?/);
3574
+ if (!clauseMatch) continue;
3575
+ const alias = clauseMatch[2];
3576
+ if (alias && alias === clauseMatch[1]) continue;
3577
+ const simpleName = (alias ?? clauseMatch[1]).split(".")[0];
3578
+ if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
3579
+ name: simpleName,
3580
+ line: i + 1,
3581
+ isDefault: false,
3582
+ isNamespace: true
3583
+ });
3584
+ }
3549
3585
  }
3550
3586
  }
3551
3587
  return {
@@ -3555,8 +3591,7 @@ const extractPyImportedSymbols = (lines) => {
3555
3591
  };
3556
3592
  const isSymbolUsed = (name, content, importLines, lines) => {
3557
3593
  const pattern = new RegExp(`\\b${name}\\b`, "g");
3558
- let match;
3559
- while ((match = pattern.exec(content)) !== null) {
3594
+ for (const match of content.matchAll(pattern)) {
3560
3595
  const lineIndex = content.slice(0, match.index).split("\n").length - 1;
3561
3596
  if (!importLines.has(lineIndex)) return true;
3562
3597
  }
@@ -3657,6 +3692,18 @@ const aiSlopEngine = {
3657
3692
 
3658
3693
  //#endregion
3659
3694
  //#region src/engines/architecture/matchers.ts
3695
+ const REGEX_SPECIAL_CHARS = new Set([
3696
+ ".",
3697
+ "+",
3698
+ "^",
3699
+ "$",
3700
+ "{",
3701
+ "}",
3702
+ "(",
3703
+ ")",
3704
+ "|",
3705
+ "\\"
3706
+ ]);
3660
3707
  const minimatch = (filePath, pattern) => {
3661
3708
  let regex = "";
3662
3709
  let i = 0;
@@ -3681,7 +3728,7 @@ const minimatch = (filePath, pattern) => {
3681
3728
  regex += pattern.slice(i, closeIndex + 1);
3682
3729
  i = closeIndex + 1;
3683
3730
  }
3684
- } else if (".+^${}()|\\".includes(ch)) {
3731
+ } else if (REGEX_SPECIAL_CHARS.has(ch)) {
3685
3732
  regex += `\\${ch}`;
3686
3733
  i++;
3687
3734
  } else {
@@ -3701,27 +3748,15 @@ const extractImports = (content, ext) => {
3701
3748
  ".mjs",
3702
3749
  ".cjs"
3703
3750
  ].includes(ext)) {
3704
- const esPattern = /(?:import|from)\s+["']([^"']+)["']/g;
3705
- let match;
3706
- while ((match = esPattern.exec(content)) !== null) imports.push(match[1]);
3707
- const reqPattern = /require\s*\(\s*["']([^"']+)["']\s*\)/g;
3708
- while ((match = reqPattern.exec(content)) !== null) imports.push(match[1]);
3709
- }
3710
- if (ext === ".py") {
3711
- const pyPattern = /(?:from|import)\s+([\w.]+)/g;
3712
- let match;
3713
- while ((match = pyPattern.exec(content)) !== null) imports.push(match[1]);
3751
+ for (const match of content.matchAll(/(?:import|from)\s+["']([^"']+)["']/g)) imports.push(match[1]);
3752
+ for (const match of content.matchAll(/require\s*\(\s*["']([^"']+)["']\s*\)/g)) imports.push(match[1]);
3714
3753
  }
3754
+ if (ext === ".py") for (const match of content.matchAll(/(?:from|import)\s+([\w.]+)/g)) imports.push(match[1]);
3715
3755
  if (ext === ".go") {
3716
- const goSingleImport = /^\s*import\s+"([^"]+)"/gm;
3717
- let match;
3718
- while ((match = goSingleImport.exec(content)) !== null) imports.push(match[1]);
3719
- const goMultiImport = /import\s*\(([^)]*)\)/gs;
3720
- while ((match = goMultiImport.exec(content)) !== null) {
3756
+ for (const match of content.matchAll(/^\s*import\s+"([^"]+)"/gm)) imports.push(match[1]);
3757
+ for (const match of content.matchAll(/import\s*\(([^)]*)\)/gs)) {
3721
3758
  const block = match[1];
3722
- const pkgPattern = /"([^"]+)"/g;
3723
- let pkgMatch;
3724
- while ((pkgMatch = pkgPattern.exec(block)) !== null) imports.push(pkgMatch[1]);
3759
+ for (const pkgMatch of block.matchAll(/"([^"]+)"/g)) imports.push(pkgMatch[1]);
3725
3760
  }
3726
3761
  }
3727
3762
  return imports;
@@ -3848,10 +3883,10 @@ const architectureEngine = {
3848
3883
  //#endregion
3849
3884
  //#region src/engines/code-quality/function-boundaries.ts
3850
3885
  const PYTHON_CONTROL_FLOW_RE = /^\s*(?:if|for|while|with|try|except|else|elif|finally|def|class)\b/;
3851
- const ARROW_BLOCK_RE = /* @__PURE__ */ new RegExp("=>\\s*\\{");
3852
- const ARROW_END_RE = /* @__PURE__ */ new RegExp("=>\\s*$");
3853
- const BRACE_START_RE = /* @__PURE__ */ new RegExp("^\\s*\\{");
3854
- const NEW_STATEMENT_RE = /* @__PURE__ */ new RegExp("^(?:export\\s+)?(?:const|let|var|function|class)\\s");
3886
+ const ARROW_BLOCK_RE = /=>\s*\{/;
3887
+ const ARROW_END_RE = /=>\s*$/;
3888
+ const BRACE_START_RE = /^\s*\{/;
3889
+ const NEW_STATEMENT_RE = /^(?:export\s+)?(?:const|let|var|function|class)\s/;
3855
3890
  const isControlFlowBrace = (lineText, braceIndex) => {
3856
3891
  const before = lineText.substring(0, braceIndex).trimEnd();
3857
3892
  if (before.endsWith(")")) return true;
@@ -4037,14 +4072,14 @@ const countTemplateLines = (bodyLines) => {
4037
4072
  let templateLineCount = 0;
4038
4073
  for (const line of bodyLines) {
4039
4074
  const startedInside = insideTemplate;
4040
- let escape = false;
4075
+ let escaped = false;
4041
4076
  for (const ch of line) {
4042
- if (escape) {
4043
- escape = false;
4077
+ if (escaped) {
4078
+ escaped = false;
4044
4079
  continue;
4045
4080
  }
4046
4081
  if (ch === "\\") {
4047
- escape = true;
4082
+ escaped = true;
4048
4083
  continue;
4049
4084
  }
4050
4085
  if (ch === "`") insideTemplate = !insideTemplate;
@@ -4435,8 +4470,8 @@ const shouldIncludeIssue = (issueType, filePath) => {
4435
4470
  return !filePath.replace(/\\/g, "/").includes(".github/workflows/");
4436
4471
  };
4437
4472
  const DEPENDENCY_HELP = {
4438
- dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `npx aislop fix`.",
4439
- devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `npx aislop fix`.",
4473
+ dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
4474
+ devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
4440
4475
  unlisted: "This package is imported in code but not declared in package.json. Run `npm install` to add it.",
4441
4476
  unresolved: "This import cannot be resolved. Check for typos or missing packages.",
4442
4477
  binaries: "This binary is used but its package is not in package.json."
@@ -4743,7 +4778,7 @@ const parseBiomeJsonOutput = (output, rootDir) => {
4743
4778
  rule: "formatting",
4744
4779
  severity,
4745
4780
  message,
4746
- help: "Run `npx aislop fix` to auto-format",
4781
+ help: "Run `aislop fix` to auto-format",
4747
4782
  line: entry.location?.start?.line ?? 0,
4748
4783
  column: entry.location?.start?.column ?? 0,
4749
4784
  category: "Format",
@@ -4772,7 +4807,7 @@ const FORMATTERS = {
4772
4807
  rule: "rust-formatting",
4773
4808
  severity: "warning",
4774
4809
  message: "Rust file is not formatted correctly",
4775
- help: "Run `npx aislop fix` to auto-format with rustfmt",
4810
+ help: "Run `aislop fix` to auto-format with rustfmt",
4776
4811
  line: parseInt(match[2], 10),
4777
4812
  column: 0,
4778
4813
  category: "Format",
@@ -4805,7 +4840,7 @@ const FORMATTERS = {
4805
4840
  rule: offense.cop_name ?? "ruby-formatting",
4806
4841
  severity: "warning",
4807
4842
  message: offense.message ?? "Ruby formatting issue",
4808
- help: "Run `npx aislop fix` to auto-format",
4843
+ help: "Run `aislop fix` to auto-format",
4809
4844
  line: offense.location?.start_line ?? 0,
4810
4845
  column: offense.location?.start_column ?? 0,
4811
4846
  category: "Format",
@@ -4836,7 +4871,7 @@ const FORMATTERS = {
4836
4871
  rule: "php-formatting",
4837
4872
  severity: "warning",
4838
4873
  message: "PHP file is not formatted correctly",
4839
- help: "Run `npx aislop fix` to auto-format",
4874
+ help: "Run `aislop fix` to auto-format",
4840
4875
  line: 0,
4841
4876
  column: 0,
4842
4877
  category: "Format",
@@ -4880,7 +4915,7 @@ const runGofmt = async (context) => {
4880
4915
  rule: "go-formatting",
4881
4916
  severity: "warning",
4882
4917
  message: "Go file is not formatted correctly",
4883
- help: "Run `npx aislop fix` to auto-format with gofmt",
4918
+ help: "Run `aislop fix` to auto-format with gofmt",
4884
4919
  line: 0,
4885
4920
  column: 0,
4886
4921
  category: "Format",
@@ -4964,9 +4999,7 @@ const runRuffFormat = async (context) => {
4964
4999
  };
4965
5000
  const parseRuffFormatOutput = (output, rootDir) => {
4966
5001
  const diagnostics = [];
4967
- const filePattern = /^--- (.+)$/gm;
4968
- let match;
4969
- while ((match = filePattern.exec(output)) !== null) {
5002
+ for (const match of output.matchAll(/^--- (.+)$/gm)) {
4970
5003
  const filePath = getRuffDiagnosticPath(rootDir, match[1]);
4971
5004
  diagnostics.push({
4972
5005
  filePath,
@@ -4974,7 +5007,7 @@ const parseRuffFormatOutput = (output, rootDir) => {
4974
5007
  rule: "python-formatting",
4975
5008
  severity: "warning",
4976
5009
  message: "Python file is not formatted correctly",
4977
- help: "Run `npx aislop fix` to auto-format with ruff",
5010
+ help: "Run `aislop fix` to auto-format with ruff",
4978
5011
  line: 0,
4979
5012
  column: 0,
4980
5013
  category: "Format",
@@ -4993,10 +5026,10 @@ const formatEngine = {
4993
5026
  const { languages, installedTools } = context;
4994
5027
  const promises = [];
4995
5028
  if (languages.includes("typescript") || languages.includes("javascript")) promises.push(runBiomeFormat(context));
4996
- if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffFormat(context));
4997
- if (languages.includes("go") && installedTools["gofmt"]) promises.push(runGofmt(context));
4998
- if (languages.includes("rust") && installedTools["rustfmt"]) promises.push(runGenericFormatter(context, "rust"));
4999
- if (languages.includes("ruby") && installedTools["rubocop"]) promises.push(runGenericFormatter(context, "ruby"));
5029
+ if (languages.includes("python") && installedTools.ruff) promises.push(runRuffFormat(context));
5030
+ if (languages.includes("go") && installedTools.gofmt) promises.push(runGofmt(context));
5031
+ if (languages.includes("rust") && installedTools.rustfmt) promises.push(runGenericFormatter(context, "rust"));
5032
+ if (languages.includes("ruby") && installedTools.rubocop) promises.push(runGenericFormatter(context, "ruby"));
5000
5033
  if (languages.includes("php") && installedTools["php-cs-fixer"]) promises.push(runGenericFormatter(context, "php"));
5001
5034
  const results = await Promise.allSettled(promises);
5002
5035
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
@@ -5200,6 +5233,8 @@ const createOxlintConfig = (options) => {
5200
5233
  if (options.mode === "fix") {
5201
5234
  rules["no-unused-vars"] = "off";
5202
5235
  rules["react-hooks/exhaustive-deps"] = "off";
5236
+ rules["jsx-a11y/no-aria-hidden-on-focusable"] = "off";
5237
+ rules["unicorn/no-useless-fallback-in-spread"] = "off";
5203
5238
  }
5204
5239
  const plugins = [
5205
5240
  "import",
@@ -5250,6 +5285,7 @@ const AMBIENT_GLOBAL_DEPS = [
5250
5285
  const SST_PLATFORM_REF_RE = /\/\/\/\s*<reference\s+path=["'][^"']*sst[\\/]+platform[\\/]+config\.d\.ts["']/;
5251
5286
  const ICON_AUTOIMPORT_RE = /^Icon[A-Z]/;
5252
5287
  const NO_UNDEF_IDENT_RE = /^['‘"`]([^'’"`]+)['’"`]/;
5288
+ const SUPABASE_FUNCTION_PATH_RE = /(?:^|\/)supabase\/functions\/[^/]+\/.+\.[cm]?[jt]sx?$/;
5253
5289
  const detectAmbientSources = (rootDir) => {
5254
5290
  const found = /* @__PURE__ */ new Set();
5255
5291
  const skipDirs = new Set([
@@ -5294,6 +5330,36 @@ const detectAmbientSources = (rootDir) => {
5294
5330
  const extractNoUndefIdentifier = (message) => {
5295
5331
  return NO_UNDEF_IDENT_RE.exec(message)?.[1] ?? null;
5296
5332
  };
5333
+ const looksLikeChromeExtensionManifest = (filePath) => {
5334
+ try {
5335
+ const manifest = JSON.parse(fs.readFileSync(filePath, "utf-8"));
5336
+ return typeof manifest.manifest_version === "number" && ("background" in manifest || "content_scripts" in manifest || "permissions" in manifest);
5337
+ } catch {
5338
+ return false;
5339
+ }
5340
+ };
5341
+ const chromeExtensionFileCache = /* @__PURE__ */ new Map();
5342
+ const isChromeExtensionFile = (rootDir, relativeFilePath) => {
5343
+ const cacheKey = `${rootDir}:${relativeFilePath.split(path.sep).join("/")}`;
5344
+ const cached = chromeExtensionFileCache.get(cacheKey);
5345
+ if (cached !== void 0) return cached;
5346
+ const absolute = path.isAbsolute(relativeFilePath) ? relativeFilePath : path.join(rootDir, relativeFilePath);
5347
+ const root = path.resolve(rootDir);
5348
+ let dir = path.dirname(path.resolve(absolute));
5349
+ let matched = false;
5350
+ while (true) {
5351
+ const relativeToRoot = path.relative(root, dir);
5352
+ if (relativeToRoot.startsWith("..") || path.isAbsolute(relativeToRoot)) break;
5353
+ if (looksLikeChromeExtensionManifest(path.join(dir, "manifest.json"))) {
5354
+ matched = true;
5355
+ break;
5356
+ }
5357
+ if (dir === root) break;
5358
+ dir = path.dirname(dir);
5359
+ }
5360
+ chromeExtensionFileCache.set(cacheKey, matched);
5361
+ return matched;
5362
+ };
5297
5363
  const isAmbientFalsePositive = (rule, message, sources) => {
5298
5364
  if (rule !== "eslint/no-undef") return false;
5299
5365
  const ident = extractNoUndefIdentifier(message);
@@ -5302,9 +5368,19 @@ const isAmbientFalsePositive = (rule, message, sources) => {
5302
5368
  if ((sources.has("@types/bun") || sources.has("bun-types")) && ident === "Bun") return true;
5303
5369
  return false;
5304
5370
  };
5371
+ const isRuntimeGlobalFalsePositive = (rule, message, rootDir, relativeFilePath) => {
5372
+ if (rule !== "eslint/no-undef") return false;
5373
+ const ident = extractNoUndefIdentifier(message);
5374
+ if (!ident) return false;
5375
+ const normalized = relativeFilePath.split(path.sep).join("/");
5376
+ if (ident === "Deno" && SUPABASE_FUNCTION_PATH_RE.test(normalized)) return true;
5377
+ if (ident === "chrome" && isChromeExtensionFile(rootDir, relativeFilePath)) return true;
5378
+ return false;
5379
+ };
5305
5380
  const sstReferencedFiles = /* @__PURE__ */ new Map();
5306
5381
  const clearSstReferenceCache = () => {
5307
5382
  sstReferencedFiles.clear();
5383
+ chromeExtensionFileCache.clear();
5308
5384
  };
5309
5385
  const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
5310
5386
  const cached = sstReferencedFiles.get(relativeFilePath);
@@ -5356,6 +5432,32 @@ const collectPackageNames = (dir) => {
5356
5432
  }
5357
5433
  return names;
5358
5434
  };
5435
+ const readJson = (filePath) => {
5436
+ const raw = readTextFile$1(filePath);
5437
+ if (!raw) return null;
5438
+ try {
5439
+ const parsed = JSON.parse(raw);
5440
+ return parsed && typeof parsed === "object" ? parsed : null;
5441
+ } catch {
5442
+ return null;
5443
+ }
5444
+ };
5445
+ const hasBunRuntime = (rootDir, projectFiles) => {
5446
+ if (fs.existsSync(path.join(rootDir, "bun.lock")) || fs.existsSync(path.join(rootDir, "bun.lockb")) || fs.existsSync(path.join(rootDir, "bunfig.toml"))) return true;
5447
+ const hasBunFiles = projectFiles.some((filePath) => /(?:^|\/)bunfig\.toml$|(?:^|\/)bun\.lockb?$/.test(filePath));
5448
+ const pkg = readJson(path.join(rootDir, "package.json"));
5449
+ if (!pkg) return hasBunFiles;
5450
+ if (typeof pkg.packageManager === "string" && /^bun@/i.test(pkg.packageManager)) return true;
5451
+ const scripts = pkg.scripts;
5452
+ if (scripts && typeof scripts === "object") {
5453
+ for (const command of Object.values(scripts)) if (typeof command === "string" && /(?:^|[;&|()\s])bunx?\s/.test(command)) return true;
5454
+ }
5455
+ return hasBunFiles;
5456
+ };
5457
+ const hasDenoRuntime = (rootDir, projectFiles) => {
5458
+ if (fs.existsSync(path.join(rootDir, "deno.json")) || fs.existsSync(path.join(rootDir, "deno.jsonc"))) return true;
5459
+ return projectFiles.some((filePath) => /(?:^|\/)deno\.jsonc?$/.test(filePath));
5460
+ };
5359
5461
  const AMBIENT_GLOBAL_RE = /^\s*(?:declare\s+)?(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/gm;
5360
5462
  const collectAmbientGlobals = (rootDir) => {
5361
5463
  const globals = /* @__PURE__ */ new Set();
@@ -5364,12 +5466,11 @@ const collectAmbientGlobals = (rootDir) => {
5364
5466
  if (!relativePath.endsWith(".d.ts")) continue;
5365
5467
  const content = readTextFile$1(path.join(rootDir, relativePath));
5366
5468
  if (!content) continue;
5367
- AMBIENT_GLOBAL_RE.lastIndex = 0;
5368
- let match;
5369
- while ((match = AMBIENT_GLOBAL_RE.exec(content)) !== null) globals.add(match[1]);
5469
+ for (const match of content.matchAll(AMBIENT_GLOBAL_RE)) globals.add(match[1]);
5370
5470
  }
5371
5471
  const deps = collectPackageNames(rootDir);
5372
- if (deps.has("@types/bun") || deps.has("bun-types")) globals.add("Bun");
5472
+ if (deps.has("@types/bun") || deps.has("bun-types") || hasBunRuntime(rootDir, projectFiles)) globals.add("Bun");
5473
+ if (hasDenoRuntime(rootDir, projectFiles)) globals.add("Deno");
5373
5474
  if (projectFiles.some((filePath) => /(?:^|\/)sst\.config\.ts$/.test(filePath))) for (const name of [
5374
5475
  "$app",
5375
5476
  "$config",
@@ -5467,6 +5568,37 @@ const detectTestFramework = (rootDir) => {
5467
5568
  return null;
5468
5569
  };
5469
5570
  const getOxlintTargets = (context) => getSourceFiles(context).filter((filePath) => OXLINT_EXTENSIONS.has(path.extname(filePath).toLowerCase())).filter((filePath) => !isAutoGenerated(filePath)).map((filePath) => path.relative(context.rootDirectory, filePath).split(path.sep).join("/"));
5571
+ const toDiagnostic = (d) => {
5572
+ const { plugin, rule } = parseRuleCode(d.code);
5573
+ const label = d.labels[0];
5574
+ return {
5575
+ filePath: d.filename,
5576
+ engine: "lint",
5577
+ rule: `${plugin}/${rule}`,
5578
+ severity: d.severity,
5579
+ message: d.message.replace(/\S+\.\w+:\d+:\d+[\s\S]*$/, "").trim() || d.message,
5580
+ help: d.help || "",
5581
+ line: label?.span.line ?? 0,
5582
+ column: label?.span.column ?? 0,
5583
+ category: plugin === "react" ? "React" : plugin === "import" ? "Imports" : "Lint",
5584
+ fixable: false
5585
+ };
5586
+ };
5587
+ const shouldKeepOxlintDiagnostic = (context, ambientSources, seen, d) => {
5588
+ const relativePath = path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath;
5589
+ if (isExcludedFromScan(relativePath)) return false;
5590
+ if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
5591
+ if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
5592
+ if (isRuntimeGlobalFalsePositive(d.rule, d.message, context.rootDirectory, relativePath)) return false;
5593
+ if (isSolidRefFalsePositive(context, d)) return false;
5594
+ if (isContextualTypeScriptFalsePositive(d)) return false;
5595
+ if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
5596
+ if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
5597
+ const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
5598
+ if (seen.has(key)) return false;
5599
+ seen.add(key);
5600
+ return true;
5601
+ };
5470
5602
  const runOxlint = async (context) => {
5471
5603
  const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-${process.pid}.json`);
5472
5604
  const framework = context.frameworks.find((f) => f !== "none");
@@ -5503,34 +5635,7 @@ const runOxlint = async (context) => {
5503
5635
  return [];
5504
5636
  }
5505
5637
  const seen = /* @__PURE__ */ new Set();
5506
- return output.diagnostics.map((d) => {
5507
- const { plugin, rule } = parseRuleCode(d.code);
5508
- const label = d.labels[0];
5509
- return {
5510
- filePath: d.filename,
5511
- engine: "lint",
5512
- rule: `${plugin}/${rule}`,
5513
- severity: d.severity,
5514
- message: d.message.replace(/\S+\.\w+:\d+:\d+[\s\S]*$/, "").trim() || d.message,
5515
- help: d.help || "",
5516
- line: label?.span.line ?? 0,
5517
- column: label?.span.column ?? 0,
5518
- category: plugin === "react" ? "React" : plugin === "import" ? "Imports" : "Lint",
5519
- fixable: false
5520
- };
5521
- }).filter((d) => {
5522
- if (isExcludedFromScan(path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath)) return false;
5523
- if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
5524
- if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
5525
- if (isSolidRefFalsePositive(context, d)) return false;
5526
- if (isContextualTypeScriptFalsePositive(d)) return false;
5527
- if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
5528
- if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
5529
- const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
5530
- if (seen.has(key)) return false;
5531
- seen.add(key);
5532
- return true;
5533
- });
5638
+ return output.diagnostics.map(toDiagnostic).filter((d) => shouldKeepOxlintDiagnostic(context, ambientSources, seen, d));
5534
5639
  } finally {
5535
5640
  if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
5536
5641
  }
@@ -5581,11 +5686,11 @@ const lintEngine = {
5581
5686
  promises.push(runOxlint(context));
5582
5687
  if (context.config.lint.typecheck) promises.push(import("./typecheck-By967nny.js").then((mod) => mod.runTypecheck(context)));
5583
5688
  }
5584
- if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-T4DswmX5.js").then((mod) => mod.runExpoDoctor(context)));
5585
- if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
5689
+ if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-BM2JR6f6.js").then((mod) => mod.runExpoDoctor(context)));
5690
+ if (languages.includes("python") && installedTools.ruff) promises.push(runRuffLint(context));
5586
5691
  if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
5587
- if (languages.includes("rust") && installedTools["cargo"]) promises.push(runGenericLinter(context, "rust"));
5588
- if (languages.includes("ruby") && installedTools["rubocop"]) promises.push(runGenericLinter(context, "ruby"));
5692
+ if (languages.includes("rust") && installedTools.cargo) promises.push(runGenericLinter(context, "rust"));
5693
+ if (languages.includes("ruby") && installedTools.rubocop) promises.push(runGenericLinter(context, "ruby"));
5589
5694
  const results = await Promise.allSettled(promises);
5590
5695
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
5591
5696
  return {
@@ -5599,7 +5704,7 @@ const lintEngine = {
5599
5704
 
5600
5705
  //#endregion
5601
5706
  //#region src/ui/invocation.ts
5602
- const detectInvocation = () => "npx aislop";
5707
+ const detectInvocation = () => "aislop";
5603
5708
 
5604
5709
  //#endregion
5605
5710
  //#region src/engines/security/audit.ts
@@ -5615,7 +5720,7 @@ const runDependencyAudit = async (context) => {
5615
5720
  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));
5616
5721
  }
5617
5722
  if (context.languages.includes("python") && context.installedTools["pip-audit"]) promises.push(runPipAudit(context.rootDirectory, timeout));
5618
- if (context.languages.includes("go") && context.installedTools["govulncheck"]) promises.push(runGovulncheck(context.rootDirectory, timeout));
5723
+ if (context.languages.includes("go") && context.installedTools.govulncheck) promises.push(runGovulncheck(context.rootDirectory, timeout));
5619
5724
  if (context.languages.includes("rust")) promises.push(runCargoAudit(context.rootDirectory, timeout));
5620
5725
  const results = await Promise.allSettled(promises);
5621
5726
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
@@ -5720,9 +5825,12 @@ const parseLegacyAdvisories = (advisories, source) => {
5720
5825
  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 ?? "");
5721
5826
  return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
5722
5827
  };
5828
+ const carriesAdvisory = (vulnerability) => Array.isArray(vulnerability.via) && vulnerability.via.some((entry) => entry !== null && typeof entry === "object");
5723
5829
  const parseModernVulnerabilities = (vulnerabilities, source) => {
5724
5830
  const bucket = /* @__PURE__ */ new Map();
5831
+ const hasRootCauses = Object.values(vulnerabilities).some(carriesAdvisory);
5725
5832
  for (const [packageName, vulnerability] of Object.entries(vulnerabilities)) {
5833
+ if (hasRootCauses && !carriesAdvisory(vulnerability)) continue;
5726
5834
  const severity = (vulnerability.severity ?? "moderate").toLowerCase();
5727
5835
  const fixAvailable = vulnerability.fixAvailable;
5728
5836
  const isDirect = vulnerability.isDirect === true;
@@ -5748,7 +5856,7 @@ const parseJsAudit = (output, source) => {
5748
5856
  rule: "security/dependency-audit-skipped",
5749
5857
  severity: "info",
5750
5858
  message: `Dependency audit skipped (${source}): lockfile is missing`,
5751
- help: error.detail ?? "Generate a lockfile, then re-run `npx aislop scan` for dependency vulnerability checks.",
5859
+ help: error.detail ?? "Generate a lockfile, then re-run `aislop scan` for dependency vulnerability checks.",
5752
5860
  line: 0,
5753
5861
  column: 0,
5754
5862
  category: "Security",
@@ -5865,6 +5973,194 @@ const runCargoAudit = async (rootDir, timeout) => {
5865
5973
  }
5866
5974
  };
5867
5975
 
5976
+ //#endregion
5977
+ //#region src/engines/security/html-safety.ts
5978
+ const SAFE_EMPTY_INNER_HTML_RE = /^\.innerHTML\s*=\s*(?:""|''|``)\s*;?/;
5979
+ const SAFE_SANITIZED_INNER_HTML_RE = /^\.innerHTML\s*=\s*(?:escapeHtml|sanitizeHtml|sanitizeHTML|DOMPurify\.sanitize)\s*\([^;\n]*\)\s*;?(?:\n|$)/;
5980
+ const SANITIZER_EXPR_RE = /^(?:escapeHtml|escapeHTML|sanitizeHtml|sanitizeHTML|DOMPurify\.sanitize)\s*\([^;\n]*\)$/;
5981
+ const IDENT_RE = /^[A-Za-z_$][\w$]*$/;
5982
+ const STATIC_STRING_RE = /^(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\$])*`)$/;
5983
+ const NUMERICISH_EXPR_RE = /^(?:[-+]?\d+(?:\.\d+)?|[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*(?:\s*\|\|\s*[-+]?\d+(?:\.\d+)?)?)$/;
5984
+ const NUMERICISH_NAME_RE = /(?:^|\.)(?:count|length|size|width|height|top|right|bottom|left|duration|elapsed|timestamp|time|ms|port|pid|attempt|attempts|index|total|x|y)$|(?:count|length|size|width|height|duration|elapsed|timestamp|time|port|pid|attempt|index|total)$/i;
5985
+ const SAFE_FORMAT_CALL_RE = /^(?:format[A-Z]\w*|fmt[A-Z]?\w*)\s*\((.*)\)$/;
5986
+ const consumeQuotedLiteral = (content, startIndex, quote) => {
5987
+ let i = startIndex + 1;
5988
+ while (i < content.length) {
5989
+ const char = content[i];
5990
+ if (char === "\\") {
5991
+ i += 2;
5992
+ continue;
5993
+ }
5994
+ if (char === quote) return { endIndex: i };
5995
+ if (char === "\n") return null;
5996
+ i++;
5997
+ }
5998
+ return null;
5999
+ };
6000
+ const consumeTemplateLiteral = (content, startIndex) => {
6001
+ const openIndex = content.indexOf("`", startIndex);
6002
+ if (openIndex === -1) return null;
6003
+ let i = openIndex + 1;
6004
+ while (i < content.length) {
6005
+ const char = content[i];
6006
+ if (char === "\\") {
6007
+ i += 2;
6008
+ continue;
6009
+ }
6010
+ if (char === "`") return {
6011
+ body: content.slice(openIndex + 1, i),
6012
+ endIndex: i
6013
+ };
6014
+ i++;
6015
+ }
6016
+ return null;
6017
+ };
6018
+ const assignmentTailIsClosed = (content, endIndex) => /^\s*(?:;[^\n]*)?(?:\n|$)/.test(content.slice(endIndex + 1));
6019
+ const assignmentRhsStart = (content, matchIndex) => {
6020
+ const match = /^\.innerHTML\s*=\s*/.exec(content.slice(matchIndex));
6021
+ return match ? matchIndex + match[0].length : null;
6022
+ };
6023
+ const templateExpressions = (templateBody) => [...templateBody.matchAll(/\$\{\s*([^}]+?)\s*\}/g)].map((match) => match[1].trim());
6024
+ const staticTernaryRe = /^\s*[^?]+\?\s*(?:"[^"]*"|'[^']*'|`[^`$]*`)\s*:\s*(?:"[^"]*"|'[^']*'|`[^`$]*`)\s*$/;
6025
+ const splitTopLevelTernary = (expr) => {
6026
+ let quote = null;
6027
+ let depth = 0;
6028
+ let question = -1;
6029
+ let colon = -1;
6030
+ for (let i = 0; i < expr.length; i++) {
6031
+ const char = expr[i];
6032
+ if (char === "\\") {
6033
+ i++;
6034
+ continue;
6035
+ }
6036
+ if ((char === "'" || char === "\"" || char === "`") && quote === null) {
6037
+ quote = char;
6038
+ continue;
6039
+ }
6040
+ if (char === quote) {
6041
+ quote = null;
6042
+ continue;
6043
+ }
6044
+ if (quote) continue;
6045
+ if (char === "(" || char === "[" || char === "{") depth++;
6046
+ else if (char === ")" || char === "]" || char === "}") depth = Math.max(0, depth - 1);
6047
+ else if (char === "?" && depth === 0 && question === -1) question = i;
6048
+ else if (char === ":" && depth === 0 && question !== -1) {
6049
+ colon = i;
6050
+ break;
6051
+ }
6052
+ }
6053
+ if (question === -1 || colon === -1) return null;
6054
+ return {
6055
+ whenTrue: expr.slice(question + 1, colon).trim(),
6056
+ whenFalse: expr.slice(colon + 1).trim()
6057
+ };
6058
+ };
6059
+ const isNumericishExpression = (expr) => {
6060
+ const normalized = expr.trim();
6061
+ if (/^(?:Math\.\w+|Number|parseInt|parseFloat)\s*\(/.test(normalized)) return true;
6062
+ if (!NUMERICISH_EXPR_RE.test(normalized)) return false;
6063
+ return /\d/.test(normalized) || NUMERICISH_NAME_RE.test(normalized);
6064
+ };
6065
+ const isSafeTemplateLiteralExpression = (expr, safeNames) => {
6066
+ if (!expr.startsWith("`") || !expr.endsWith("`")) return false;
6067
+ return templateExpressions(expr.slice(1, -1)).every((part) => isSafeHtmlExpression(part, safeNames));
6068
+ };
6069
+ const collectSafeHtmlNames = (content, matchIndex) => {
6070
+ const safeNames = /* @__PURE__ */ new Set();
6071
+ const prefix = content.slice(Math.max(0, matchIndex - 8e3), matchIndex);
6072
+ for (const rawLine of prefix.split("\n")) {
6073
+ const line = rawLine.trim();
6074
+ let match = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(.+?)\s*;?$/.exec(line);
6075
+ if (match) {
6076
+ const [, name, expr] = match;
6077
+ if (isSafeHtmlExpression(expr.trim(), safeNames)) safeNames.add(name);
6078
+ else safeNames.delete(name);
6079
+ continue;
6080
+ }
6081
+ match = /^([A-Za-z_$][\w$]*)\s*\+=\s*(.+?)\s*;?$/.exec(line);
6082
+ if (match) {
6083
+ const [, name, expr] = match;
6084
+ if (safeNames.has(name) && isSafeHtmlExpression(expr.trim(), safeNames)) safeNames.add(name);
6085
+ else safeNames.delete(name);
6086
+ continue;
6087
+ }
6088
+ match = /^([A-Za-z_$][\w$]*)\s*=\s*(.+?)\s*;?$/.exec(line);
6089
+ if (match) {
6090
+ const [, name, expr] = match;
6091
+ if (isSafeHtmlExpression(expr.trim(), safeNames)) safeNames.add(name);
6092
+ else safeNames.delete(name);
6093
+ }
6094
+ }
6095
+ return safeNames;
6096
+ };
6097
+ const isSafeHtmlExpression = (expr, safeNames) => {
6098
+ const normalized = expr.trim();
6099
+ if (SANITIZER_EXPR_RE.test(normalized)) return true;
6100
+ if (STATIC_STRING_RE.test(normalized)) return true;
6101
+ if (staticTernaryRe.test(expr)) return true;
6102
+ if (isNumericishExpression(normalized)) return true;
6103
+ if (IDENT_RE.test(normalized) && safeNames.has(normalized)) return true;
6104
+ if (isSafeTemplateLiteralExpression(normalized, safeNames)) return true;
6105
+ const ternary = splitTopLevelTernary(normalized);
6106
+ if (ternary && isSafeHtmlExpression(ternary.whenTrue, safeNames) && isSafeHtmlExpression(ternary.whenFalse, safeNames)) return true;
6107
+ const formatCall = SAFE_FORMAT_CALL_RE.exec(normalized);
6108
+ if (formatCall) return formatCall[1].split(",").map((arg) => arg.trim()).filter((arg) => arg.length > 0).every((arg) => isNumericishExpression(arg) || IDENT_RE.test(arg) && safeNames.has(arg));
6109
+ return false;
6110
+ };
6111
+ const readSingleLineRhs = (content, rhsStart) => {
6112
+ const lineEnd = content.indexOf("\n", rhsStart);
6113
+ const line = content.slice(rhsStart, lineEnd === -1 ? content.length : lineEnd);
6114
+ let quote = null;
6115
+ for (let i = 0; i < line.length; i++) {
6116
+ const char = line[i];
6117
+ if (char === "\\") {
6118
+ i++;
6119
+ continue;
6120
+ }
6121
+ if ((char === "'" || char === "\"" || char === "`") && quote === null) {
6122
+ quote = char;
6123
+ continue;
6124
+ }
6125
+ if (char === quote) {
6126
+ quote = null;
6127
+ continue;
6128
+ }
6129
+ if (char === ";" && quote === null) return line.slice(0, i).trim();
6130
+ }
6131
+ return line.trim();
6132
+ };
6133
+ const isSafeMapJoinHtmlAssignment = (content, rhsStart) => {
6134
+ const head = content.slice(rhsStart);
6135
+ const mapMatch = /^[A-Za-z_$][\w$.]*\.map\(\s*[A-Za-z_$][\w$]*\s*=>\s*`/.exec(head);
6136
+ if (!mapMatch) return false;
6137
+ const template = consumeTemplateLiteral(content, rhsStart + mapMatch[0].length - 1);
6138
+ if (!template) return false;
6139
+ if (!/^\s*\)\.join\(\s*(?:""|'')\s*\)/.test(content.slice(template.endIndex + 1))) return false;
6140
+ const safeNames = collectSafeHtmlNames(content, rhsStart);
6141
+ return templateExpressions(template.body).every((expr) => isSafeHtmlExpression(expr, safeNames));
6142
+ };
6143
+ const isSafeInnerHtmlAssignment = (content, matchIndex) => {
6144
+ const tail = content.slice(matchIndex);
6145
+ if (SAFE_EMPTY_INNER_HTML_RE.test(tail) || SAFE_SANITIZED_INNER_HTML_RE.test(tail)) return true;
6146
+ const rhsStart = assignmentRhsStart(content, matchIndex);
6147
+ if (rhsStart === null) return false;
6148
+ const first = content[rhsStart];
6149
+ const safeNames = collectSafeHtmlNames(content, matchIndex);
6150
+ if (isSafeHtmlExpression(readSingleLineRhs(content, rhsStart), safeNames)) return true;
6151
+ if (isSafeMapJoinHtmlAssignment(content, rhsStart)) return true;
6152
+ if (first === "'" || first === "\"") {
6153
+ const quoted = consumeQuotedLiteral(content, rhsStart, first);
6154
+ return Boolean(quoted && assignmentTailIsClosed(content, quoted.endIndex));
6155
+ }
6156
+ if (first !== "`") return false;
6157
+ const template = consumeTemplateLiteral(content, rhsStart);
6158
+ if (!template || !assignmentTailIsClosed(content, template.endIndex)) return false;
6159
+ const expressions = templateExpressions(template.body);
6160
+ if (expressions.length === 0) return true;
6161
+ return expressions.every((expr) => isSafeHtmlExpression(expr, safeNames));
6162
+ };
6163
+
5868
6164
  //#endregion
5869
6165
  //#region src/engines/security/risky.ts
5870
6166
  const ev = "eval";
@@ -5990,6 +6286,30 @@ const isStructuredDataScript = (content, matchIndex) => {
5990
6286
  const after = content.slice(matchIndex, Math.min(content.length, matchIndex + 180));
5991
6287
  return /__html\s*:\s*JSON\.stringify\s*\(/.test(after);
5992
6288
  };
6289
+ const isSafeShellSpawnArray = (content, matchIndex) => /^spawn\s*\(\s*\[/.test(content.slice(matchIndex)) && !/^\s*spawn\s*\(\s*\[\s*["'](?:sh|bash|zsh|cmd|cmd\.exe|powershell|pwsh)["']\s*,\s*["'](?:-c|\/c|\/C)["']/i.test(content.slice(matchIndex)) && !/shell\s*:\s*true\b/.test(content.slice(matchIndex, matchIndex + 500));
6290
+ const PLACEHOLDER_EXPR_RE = /^(?:placeholders?|placeholderList|bindMarkers?|bindingMarkers?|bindPlaceholders?|bindingPlaceholders?|parameterPlaceholders?|sqlPlaceholders?)(?:\.\w+\([^)]*\))?$/i;
6291
+ const SQL_PLACEHOLDER_LITERAL_RE = /["'](?:\?|\$\d+|\$\{[^}]+\})["']/;
6292
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6293
+ const isGeneratedPlaceholderList = (content, matchIndex, placeholderExpr) => {
6294
+ const name = placeholderExpr.match(/^([A-Za-z_$][\w$]*)/)?.[1];
6295
+ if (!name) return false;
6296
+ const prefix = content.slice(Math.max(0, matchIndex - 4e3), matchIndex);
6297
+ const declarationRe = new RegExp(`\\b(?:const|let|var)\\s+${escapeRegExp(name)}\\s*=\\s*([^;\\n]+)`, "g");
6298
+ const declaration = [...prefix.matchAll(declarationRe)].at(-1);
6299
+ if (!declaration) return false;
6300
+ const expr = declaration[1];
6301
+ if (!/\.join\s*\(/.test(expr)) return false;
6302
+ return /\.map\s*\(/.test(expr) && /=>/.test(expr) && SQL_PLACEHOLDER_LITERAL_RE.test(expr) || /\.fill\s*\(/.test(expr) && SQL_PLACEHOLDER_LITERAL_RE.test(expr);
6303
+ };
6304
+ const isSafeSqlPlaceholderTemplate = (content, matchIndex) => {
6305
+ const template = consumeTemplateLiteral(content, matchIndex);
6306
+ if (!template) return false;
6307
+ const afterTemplate = content.slice(template.endIndex + 1);
6308
+ if (!(/^\s*,/.test(afterTemplate) || /^\s*\)\s*\.(?:all|get|run|values)\s*\(/.test(afterTemplate))) return false;
6309
+ const expressions = [...template.body.matchAll(/\$\{\s*([^}]+?)\s*\}/g)].map((match) => match[1].trim());
6310
+ if (expressions.length === 0) return false;
6311
+ return expressions.every((expr) => PLACEHOLDER_EXPR_RE.test(expr) && isGeneratedPlaceholderList(content, matchIndex, expr));
6312
+ };
5993
6313
  const detectRiskyConstructs = async (context) => {
5994
6314
  const files = getSourceFiles(context);
5995
6315
  const diagnostics = [];
@@ -6010,13 +6330,15 @@ const detectRiskyConstructs = async (context) => {
6010
6330
  if (!extensions.includes(ext)) continue;
6011
6331
  if (isMigrationOrSeeder && name === "sql-injection") continue;
6012
6332
  const regex = new RegExp(pattern.source, pattern.flags);
6013
- let match;
6014
- while ((match = regex.exec(masked)) !== null) {
6333
+ for (const match of masked.matchAll(regex)) {
6015
6334
  const line = content.slice(0, match.index).split("\n").length;
6016
6335
  if (name === "innerhtml") {
6017
6336
  const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
6337
+ if (isSafeInnerHtmlAssignment(content, match.index)) continue;
6018
6338
  if (/(?:template|tmpl|tpl)$/i.test(beforeMatch.trimEnd()) || /createElement\s*\(\s*['"]template['"]\s*\)$/.test(beforeMatch.trimEnd())) continue;
6019
6339
  }
6340
+ if (name === "sql-injection" && isSafeSqlPlaceholderTemplate(content, match.index)) continue;
6341
+ if (name === "shell-injection" && isSafeShellSpawnArray(content, match.index)) continue;
6020
6342
  if (name === "dangerously-set-innerhtml") {
6021
6343
  if (hasDangerouslySetInnerHtmlIgnore(lines, line - 1)) continue;
6022
6344
  if (isStructuredDataScript(content, match.index)) continue;
@@ -6113,7 +6435,28 @@ const PLACEHOLDER_EXACT = new Set([
6113
6435
  "todo",
6114
6436
  "replace_me"
6115
6437
  ]);
6438
+ const PLACEHOLDER_URL_PARTS = new Set([
6439
+ "example",
6440
+ "host",
6441
+ "localhost",
6442
+ "pass",
6443
+ "password",
6444
+ "pw",
6445
+ "user",
6446
+ "username"
6447
+ ]);
6448
+ const isPlaceholderCredentialUrl = (matchedText) => {
6449
+ const credentialMatch = matchedText.match(/^[a-z]+:\/\/([^:@/\s]+):([^@/\s]+)@/i);
6450
+ if (credentialMatch) return PLACEHOLDER_URL_PARTS.has(credentialMatch[1].toLowerCase()) && PLACEHOLDER_URL_PARTS.has(credentialMatch[2].toLowerCase());
6451
+ try {
6452
+ const parsed = new URL(matchedText);
6453
+ return PLACEHOLDER_URL_PARTS.has(parsed.username.toLowerCase()) && PLACEHOLDER_URL_PARTS.has(parsed.password.toLowerCase()) && PLACEHOLDER_URL_PARTS.has(parsed.hostname.toLowerCase());
6454
+ } catch {
6455
+ return false;
6456
+ }
6457
+ };
6116
6458
  const isPlaceholderValue = (matchedText) => {
6459
+ if (isPlaceholderCredentialUrl(matchedText)) return true;
6117
6460
  if (/env\(/i.test(matchedText)) return true;
6118
6461
  if (matchedText.includes("process.env")) return true;
6119
6462
  if (matchedText.includes("os.environ")) return true;
@@ -6143,8 +6486,7 @@ const scanSecrets = async (context) => {
6143
6486
  const relativePath = path.relative(context.rootDirectory, filePath);
6144
6487
  for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
6145
6488
  const regex = new RegExp(pattern.source, pattern.flags);
6146
- let match;
6147
- while ((match = regex.exec(content)) !== null) {
6489
+ for (const match of content.matchAll(regex)) {
6148
6490
  if (isPlaceholderValue(match[1] ?? match[0])) continue;
6149
6491
  if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
6150
6492
  const line = content.slice(0, match.index).split("\n").length;
@@ -6235,23 +6577,36 @@ const STYLE_RULES = new Set([
6235
6577
  "complexity/function-too-long"
6236
6578
  ]);
6237
6579
  const STYLE_WEIGHT = .5;
6580
+ const COMMENT_STYLE_RULE_CAP = 12;
6581
+ const COMMENT_STYLE_RULES = new Set(["ai-slop/trivial-comment", "ai-slop/narrative-comment"]);
6238
6582
  const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
6239
6583
  if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
6240
6584
  const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
6241
6585
  return Math.max(1, filesWithDiagnostics);
6242
6586
  };
6243
- const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoothing) => {
6587
+ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoothing, maxPerRule) => {
6244
6588
  if (diagnostics.length === 0) return {
6245
6589
  score: PERFECT_SCORE,
6246
6590
  label: "Healthy"
6247
6591
  };
6248
- let deductions = 0;
6592
+ const deductionsByRule = /* @__PURE__ */ new Map();
6249
6593
  for (const d of diagnostics) {
6250
6594
  const engineWeight = weights[d.engine] ?? 1;
6251
6595
  const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
6252
6596
  const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
6253
- deductions += severityPenalty * engineWeight * styleFactor;
6254
- }
6597
+ const key = `${d.engine}:${d.rule}`;
6598
+ deductionsByRule.set(key, (deductionsByRule.get(key) ?? 0) + severityPenalty * engineWeight * styleFactor);
6599
+ }
6600
+ const defaultRuleCap = typeof maxPerRule === "number" && maxPerRule > 0 ? maxPerRule : null;
6601
+ const capForRule = (key) => {
6602
+ const rule = key.slice(key.indexOf(":") + 1);
6603
+ if (COMMENT_STYLE_RULES.has(rule)) return defaultRuleCap ? Math.min(defaultRuleCap, COMMENT_STYLE_RULE_CAP) : COMMENT_STYLE_RULE_CAP;
6604
+ return defaultRuleCap;
6605
+ };
6606
+ const deductions = [...deductionsByRule.entries()].reduce((total, [key, value]) => {
6607
+ const cap = capForRule(key);
6608
+ return total + (cap ? Math.min(value, cap) : value);
6609
+ }, 0);
6255
6610
  const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
6256
6611
  const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
6257
6612
  const issueDensity = Math.min(1, diagnostics.length / (effectiveFileCount + smoothingConstant));
@@ -6265,6 +6620,64 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
6265
6620
 
6266
6621
  //#endregion
6267
6622
  //#region src/utils/discover.ts
6623
+ const UNSUPPORTED_CODE_EXTENSIONS = {
6624
+ ".c": "C/C++",
6625
+ ".h": "C/C++",
6626
+ ".cc": "C/C++",
6627
+ ".cpp": "C/C++",
6628
+ ".cxx": "C/C++",
6629
+ ".hpp": "C/C++",
6630
+ ".hh": "C/C++",
6631
+ ".hxx": "C/C++",
6632
+ ".cs": "C#",
6633
+ ".swift": "Swift",
6634
+ ".kt": "Kotlin",
6635
+ ".kts": "Kotlin",
6636
+ ".m": "Objective-C",
6637
+ ".mm": "Objective-C",
6638
+ ".scala": "Scala",
6639
+ ".dart": "Dart",
6640
+ ".ex": "Elixir",
6641
+ ".exs": "Elixir",
6642
+ ".erl": "Erlang",
6643
+ ".hs": "Haskell",
6644
+ ".clj": "Clojure",
6645
+ ".cljs": "Clojure",
6646
+ ".lua": "Lua",
6647
+ ".jl": "Julia",
6648
+ ".zig": "Zig",
6649
+ ".nim": "Nim",
6650
+ ".ml": "OCaml",
6651
+ ".fs": "F#",
6652
+ ".sol": "Solidity",
6653
+ ".groovy": "Groovy"
6654
+ };
6655
+ const analyzeCoverage = (rootDirectory, excludePatterns = []) => {
6656
+ const allFiles = listProjectFiles(rootDirectory);
6657
+ const supportedFiles = filterProjectFiles(rootDirectory, allFiles, [], excludePatterns).length;
6658
+ const counts = /* @__PURE__ */ new Map();
6659
+ let unsupportedFiles = 0;
6660
+ const candidates = filterProjectFiles(rootDirectory, allFiles, Object.keys(UNSUPPORTED_CODE_EXTENSIONS), excludePatterns);
6661
+ for (const file of candidates) {
6662
+ const lang = UNSUPPORTED_CODE_EXTENSIONS[path.extname(file).toLowerCase()];
6663
+ if (!lang) continue;
6664
+ unsupportedFiles += 1;
6665
+ counts.set(lang, (counts.get(lang) ?? 0) + 1);
6666
+ }
6667
+ let dominantUnsupported = null;
6668
+ let max = 0;
6669
+ for (const [lang, count] of counts) if (count > max) {
6670
+ max = count;
6671
+ dominantUnsupported = lang;
6672
+ }
6673
+ const negligible = supportedFiles === 0 || unsupportedFiles >= 10 && unsupportedFiles > supportedFiles * 3;
6674
+ return {
6675
+ supportedFiles,
6676
+ unsupportedFiles,
6677
+ dominantUnsupported,
6678
+ scoreable: !negligible
6679
+ };
6680
+ };
6268
6681
  const LANGUAGE_SIGNALS = {
6269
6682
  "tsconfig.json": "typescript",
6270
6683
  "go.mod": "go",
@@ -6384,11 +6797,12 @@ const checkInstalledTools = async () => {
6384
6797
  }));
6385
6798
  return results;
6386
6799
  };
6387
- const discoverProject = async (directory) => {
6800
+ const discoverProject = async (directory, excludePatterns = []) => {
6388
6801
  const resolvedDir = path.resolve(directory);
6389
6802
  const languages = detectLanguages(resolvedDir);
6390
6803
  const frameworks = detectFrameworks(resolvedDir);
6391
6804
  const sourceFileCount = countSourceFiles(resolvedDir);
6805
+ const coverage = analyzeCoverage(resolvedDir, excludePatterns);
6392
6806
  const installedTools = await checkInstalledTools();
6393
6807
  return {
6394
6808
  rootDirectory: resolvedDir,
@@ -6396,6 +6810,7 @@ const discoverProject = async (directory) => {
6396
6810
  languages,
6397
6811
  frameworks,
6398
6812
  sourceFileCount,
6813
+ coverage,
6399
6814
  installedTools
6400
6815
  };
6401
6816
  };
@@ -6434,6 +6849,107 @@ const readBaseline = (cwd) => {
6434
6849
  }
6435
6850
  };
6436
6851
 
6852
+ //#endregion
6853
+ //#region src/output/finding-assessment.ts
6854
+ const FINDING_KIND_LABELS = {
6855
+ "confirmed-defect": "confirmed defects",
6856
+ "conservative-security": "conservative security",
6857
+ "style-policy": "style/policy",
6858
+ "ai-slop-indicator": "AI-slop indicators"
6859
+ };
6860
+ const STYLE_POLICY_RULES = new Set([
6861
+ "ai-slop/trivial-comment",
6862
+ "ai-slop/narrative-comment",
6863
+ "ai-slop/meta-comment",
6864
+ "ai-slop/console-leftover",
6865
+ "ai-slop/ts-directive",
6866
+ "complexity/file-too-large",
6867
+ "complexity/function-too-long",
6868
+ "complexity/deep-nesting",
6869
+ "complexity/too-many-params",
6870
+ "code-quality/duplicate-block",
6871
+ "eslint/no-empty",
6872
+ "eslint/no-unused-vars",
6873
+ "eslint/no-useless-escape",
6874
+ "eslint/no-unused-expressions",
6875
+ "unicorn/no-useless-fallback-in-spread",
6876
+ "unicorn/prefer-string-starts-ends-with",
6877
+ "unicorn/no-new-array",
6878
+ "unicorn/no-useless-spread"
6879
+ ]);
6880
+ const CONFIRMED_DEFECT_RULES = new Set([
6881
+ "ai-slop/hallucinated-import",
6882
+ "eslint/no-undef",
6883
+ "eslint/no-unreachable",
6884
+ "security/vulnerable-dependency"
6885
+ ]);
6886
+ const LOW_CONFIDENCE_SECURITY_RULES = new Set(["security/innerhtml", "security/dangerously-set-innerhtml"]);
6887
+ const confidenceFor = (diagnostic, kind) => {
6888
+ if (kind === "confirmed-defect") return "high";
6889
+ if (kind === "style-policy") return "medium";
6890
+ if (kind === "conservative-security") {
6891
+ if (LOW_CONFIDENCE_SECURITY_RULES.has(diagnostic.rule)) return "medium";
6892
+ return diagnostic.severity === "error" ? "high" : "medium";
6893
+ }
6894
+ return diagnostic.severity === "error" ? "high" : "medium";
6895
+ };
6896
+ const classifyKind = (diagnostic) => {
6897
+ if (CONFIRMED_DEFECT_RULES.has(diagnostic.rule)) return "confirmed-defect";
6898
+ if (diagnostic.engine === "security") return "conservative-security";
6899
+ if (STYLE_POLICY_RULES.has(diagnostic.rule)) return "style-policy";
6900
+ if (diagnostic.engine === "format" || diagnostic.engine === "code-quality") return "style-policy";
6901
+ if (diagnostic.engine === "ai-slop") return "ai-slop-indicator";
6902
+ if (diagnostic.severity === "error") return "confirmed-defect";
6903
+ return "style-policy";
6904
+ };
6905
+ const assessDiagnostic = (diagnostic) => {
6906
+ const kind = classifyKind(diagnostic);
6907
+ return {
6908
+ kind,
6909
+ confidence: confidenceFor(diagnostic, kind),
6910
+ label: FINDING_KIND_LABELS[kind]
6911
+ };
6912
+ };
6913
+ const summarizeFindingAssessments = (diagnostics) => {
6914
+ const byKind = {
6915
+ "confirmed-defect": 0,
6916
+ "conservative-security": 0,
6917
+ "style-policy": 0,
6918
+ "ai-slop-indicator": 0
6919
+ };
6920
+ const byConfidence = {
6921
+ high: 0,
6922
+ medium: 0,
6923
+ low: 0
6924
+ };
6925
+ const rows = /* @__PURE__ */ new Map();
6926
+ for (const diagnostic of diagnostics) {
6927
+ const assessment = assessDiagnostic(diagnostic);
6928
+ byKind[assessment.kind]++;
6929
+ byConfidence[assessment.confidence]++;
6930
+ const row = rows.get(assessment.kind) ?? {
6931
+ kind: assessment.kind,
6932
+ label: assessment.label,
6933
+ count: 0,
6934
+ errors: 0,
6935
+ warnings: 0,
6936
+ info: 0,
6937
+ fixable: 0
6938
+ };
6939
+ row.count++;
6940
+ if (diagnostic.severity === "error") row.errors++;
6941
+ else if (diagnostic.severity === "warning") row.warnings++;
6942
+ else row.info++;
6943
+ if (diagnostic.fixable) row.fixable++;
6944
+ rows.set(assessment.kind, row);
6945
+ }
6946
+ return {
6947
+ rows: [...rows.values()].sort((a, b) => b.count - a.count),
6948
+ byKind,
6949
+ byConfidence
6950
+ };
6951
+ };
6952
+
6437
6953
  //#endregion
6438
6954
  //#region src/mcp/tools.ts
6439
6955
  const MAX_FINDINGS = 25;
@@ -6471,27 +6987,32 @@ const summariseDiagnostic = (d, rootDirectory) => ({
6471
6987
  column: d.column,
6472
6988
  rule: d.rule,
6473
6989
  severity: d.severity,
6990
+ assessment: assessDiagnostic(d),
6474
6991
  message: d.message,
6475
6992
  fixable: d.fixable,
6476
6993
  help: d.help || void 0
6477
6994
  });
6478
6995
  const summariseDiagnostics = (diagnostics, rootDirectory) => {
6996
+ const counts = {
6997
+ error: diagnostics.filter((d) => d.severity === "error").length,
6998
+ warning: diagnostics.filter((d) => d.severity === "warning").length,
6999
+ fixable: diagnostics.filter((d) => d.fixable).length,
7000
+ total: diagnostics.length
7001
+ };
7002
+ const findings = diagnostics.slice(0, MAX_FINDINGS).map((d) => summariseDiagnostic(d, rootDirectory));
7003
+ const elided = diagnostics.length > MAX_FINDINGS ? diagnostics.length - MAX_FINDINGS : 0;
6479
7004
  return {
6480
- counts: {
6481
- error: diagnostics.filter((d) => d.severity === "error").length,
6482
- warning: diagnostics.filter((d) => d.severity === "warning").length,
6483
- fixable: diagnostics.filter((d) => d.fixable).length,
6484
- total: diagnostics.length
6485
- },
6486
- findings: diagnostics.slice(0, MAX_FINDINGS).map((d) => summariseDiagnostic(d, rootDirectory)),
6487
- elided: diagnostics.length > MAX_FINDINGS ? diagnostics.length - MAX_FINDINGS : 0
7005
+ counts,
7006
+ findingAssessment: summarizeFindingAssessments(diagnostics),
7007
+ findings,
7008
+ elided
6488
7009
  };
6489
7010
  };
6490
7011
  const runScan = async (cwd) => {
6491
7012
  const project = await discoverProject(cwd);
6492
7013
  const config = loadConfig(cwd);
6493
7014
  const diagnostics = (await runEngines(buildEngineContext(project.rootDirectory, project, config), enabledEnginesFromConfig(config))).flatMap((r) => r.diagnostics);
6494
- const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
7015
+ const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing, config.scoring.maxPerRule);
6495
7016
  const errorCount = diagnostics.filter((d) => d.severity === "error").length;
6496
7017
  const failBelow = config.ci.failBelow;
6497
7018
  return {
@@ -6617,7 +7138,7 @@ const handleAislopBaseline = (input) => {
6617
7138
 
6618
7139
  //#endregion
6619
7140
  //#region src/version.ts
6620
- const APP_VERSION = "0.10.1";
7141
+ const APP_VERSION = "0.11.0";
6621
7142
 
6622
7143
  //#endregion
6623
7144
  //#region src/telemetry/env.ts
@@ -6813,9 +7334,14 @@ const track = (input) => {
6813
7334
  pendingRequests.add(request);
6814
7335
  return { installCreated };
6815
7336
  };
6816
- const flushTelemetry = async () => {
7337
+ const flushTelemetry = async (timeoutMs) => {
6817
7338
  if (pendingRequests.size === 0) return;
6818
- await Promise.all(pendingRequests);
7339
+ const all = Promise.all(pendingRequests);
7340
+ if (timeoutMs == null) {
7341
+ await all;
7342
+ return;
7343
+ }
7344
+ await Promise.race([all, new Promise((resolve) => setTimeout(resolve, timeoutMs))]);
6819
7345
  };
6820
7346
 
6821
7347
  //#endregion