aislop 0.9.2 → 0.9.4

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
@@ -278,6 +278,7 @@ const EXCLUDED_DIRS = [
278
278
  "build",
279
279
  ".git",
280
280
  ".agents",
281
+ ".pnpm-store",
281
282
  "vendor",
282
283
  "examples",
283
284
  "example",
@@ -314,6 +315,7 @@ const FIND_PRUNE_DIRS = [
314
315
  "build",
315
316
  ".git",
316
317
  ".agents",
318
+ ".pnpm-store",
317
319
  "vendor",
318
320
  "examples",
319
321
  "example",
@@ -337,7 +339,11 @@ const FIND_PRUNE_DIRS = [
337
339
  ".turbo",
338
340
  "public"
339
341
  ];
340
- const BUILD_CACHE_FILE_PATTERNS = [/\.timestamp-\d+-[a-z0-9]+\.[mc]?js$/i];
342
+ const BUILD_CACHE_FILE_PATTERNS = [
343
+ /\.timestamp-\d+-[a-z0-9]+\.[mc]?js$/i,
344
+ /\.min\.(?:js|css|mjs|cjs)$/i,
345
+ /\.bundle\.(?:js|css|mjs|cjs)$/i
346
+ ];
341
347
  const isBuildCacheFile = (filePath) => BUILD_CACHE_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
342
348
  const TEST_FILE_PATTERNS = [
343
349
  /(?:^|\/).*\.test\.[^/]+$/i,
@@ -365,6 +371,17 @@ const hasAllowedExtension = (filePath, extraExtensions) => {
365
371
  const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
366
372
  const isExcludedFromScan = (relativePath) => isExcludedPath(relativePath) || isBuildCacheFile(relativePath);
367
373
  const isTestFile$2 = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
374
+ const readBiomeExcludePatterns = (rootDirectory) => {
375
+ const biomePath = path.join(rootDirectory, "biome.json");
376
+ if (!fs.existsSync(biomePath)) return [];
377
+ try {
378
+ const includes = JSON.parse(fs.readFileSync(biomePath, "utf-8")).files?.includes;
379
+ if (!Array.isArray(includes)) return [];
380
+ return includes.filter((entry) => typeof entry === "string").filter((entry) => entry.startsWith("!") && entry.length > 1).map((entry) => entry.slice(1));
381
+ } catch {
382
+ return [];
383
+ }
384
+ };
368
385
  const getIgnoredPaths = (rootDirectory, files) => {
369
386
  if (files.length === 0) return /* @__PURE__ */ new Set();
370
387
  const result = spawnSync("git", [
@@ -432,7 +449,8 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude
432
449
  };
433
450
  }).filter(({ relativePath }) => isWithinProject(relativePath));
434
451
  const ignoredPaths = getIgnoredPaths(rootDirectory, normalizedFiles.map(({ relativePath }) => relativePath));
435
- const normalizedExcludePatterns = exclude.length ? normalizeExcludePatterns(exclude) : [];
452
+ const excludePatterns = [...readBiomeExcludePatterns(rootDirectory), ...exclude];
453
+ const normalizedExcludePatterns = excludePatterns.length ? normalizeExcludePatterns(excludePatterns) : [];
436
454
  const isUserExcluded = (relativePath) => {
437
455
  if (!normalizedExcludePatterns.length) return false;
438
456
  return micromatch.isMatch(relativePath, normalizedExcludePatterns, { dot: true });
@@ -487,10 +505,27 @@ const getSourceFilesWithExtras = (context, extraExtensions) => {
487
505
 
488
506
  //#endregion
489
507
  //#region src/engines/ai-slop/abstractions.ts
508
+ const JS_EXTS$1 = new Set([
509
+ ".ts",
510
+ ".tsx",
511
+ ".js",
512
+ ".jsx",
513
+ ".mjs",
514
+ ".cjs"
515
+ ]);
490
516
  const THIN_WRAPPER_PATTERNS = [
491
- /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
492
- /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
493
- /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm
517
+ {
518
+ pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
519
+ extensions: JS_EXTS$1
520
+ },
521
+ {
522
+ pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
523
+ extensions: JS_EXTS$1
524
+ },
525
+ {
526
+ pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
527
+ extensions: new Set([".py"])
528
+ }
494
529
  ];
495
530
  const AI_NAMING_PATTERNS = [/(?:helper|util|handler|process|do|handle|execute|perform)_?\d+/i, /(?:data|temp|result|value|item|obj|arr|str|num|val)\d+/];
496
531
  const FRAMEWORK_METHOD_NAMES = /^(?:setUp|tearDown|setUpClass|tearDownClass|setUpModule|tearDownModule)$/;
@@ -505,10 +540,11 @@ const hasHardcodedArgs = (matchText) => {
505
540
  return /['"`]\w+['"`]|(?<!\w)\d+(?!\w)/.test(innerCallMatch[1]);
506
541
  };
507
542
  const isUseContextWrapper = (matchText) => /\buse\w+/.test(matchText) && /useContext\s*\(/.test(matchText);
508
- const detectThinWrappers = (content, relativePath) => {
543
+ const detectThinWrappers = (content, relativePath, ext) => {
509
544
  const diagnostics = [];
510
545
  const lines = content.split("\n");
511
- for (const pattern of THIN_WRAPPER_PATTERNS) {
546
+ for (const { pattern, extensions } of THIN_WRAPPER_PATTERNS) {
547
+ if (!extensions.has(ext)) continue;
512
548
  const regex = new RegExp(pattern.source, pattern.flags);
513
549
  let match;
514
550
  while ((match = regex.exec(content)) !== null) {
@@ -574,12 +610,133 @@ const detectOverAbstraction = async (context) => {
574
610
  continue;
575
611
  }
576
612
  const relativePath = path.relative(context.rootDirectory, filePath);
577
- diagnostics.push(...detectThinWrappers(content, relativePath));
613
+ const ext = path.extname(filePath);
614
+ diagnostics.push(...detectThinWrappers(content, relativePath, ext));
578
615
  diagnostics.push(...detectAiNaming(content, relativePath));
579
616
  }
580
617
  return diagnostics;
581
618
  };
582
619
 
620
+ //#endregion
621
+ //#region src/engines/ai-slop/narrative-comments-patterns.ts
622
+ const DECORATIVE_SEPARATOR = /^[-=─━~_*#]{6,}$/;
623
+ const DECORATIVE_SECTION_HEADER = /^[-=─━~_*#]{3,}[\s\S]+?[-=─━~_*#]{3,}$/;
624
+ const SECTION_HEADER = /^(Phase|Step|Section|Part)\s+\d+[:.-]/i;
625
+ const CROSS_REFERENCE_PHRASES = [
626
+ /\bwill then be\b/i,
627
+ /\bused by\b/i,
628
+ /\bcalled from\b/i,
629
+ /\bcalled later\b/i,
630
+ /\bsee (?:above|below|later|earlier)\b/i,
631
+ /\breplaces the\b/i,
632
+ /\bmatches the one\b/i,
633
+ /\bwe moved\b/i,
634
+ /\bwe used to\b/i,
635
+ /\brefactor(?:ed)? from\b/i,
636
+ /\bcombined with\b.*\bthis\b/i
637
+ ];
638
+ const JUSTIFICATION_OPENERS = [
639
+ /^(The idea here|The trick is|This was needed|Originally,?)/i,
640
+ /^This\s+(?:function|method|class|module|component|hook|util|helper|handler|service)\b/i,
641
+ /^It\s+(?:does|handles|takes|returns|processes|reads|writes|sends|fetches|loads|creates|deletes|updates|parses|validates)\b/i,
642
+ /^(?:First|Then|Finally|Next|Lastly|Subsequently),?\s+(?:it|we|the\s+(?:function|method|class))\b/i
643
+ ];
644
+ const EXPLANATORY_OPENERS = /^(Matches|Detects|Represents|Holds|Stores|Tracks|Handles|Manages|Controls|Contains|Captures|Encapsulates|Wraps|Describes)\s+[A-Za-z`'"]/;
645
+ const STEP_COMMENT_VERB_RE = /^(?:Render|Enable|Disable|Initialize|Init|Setup|Set|Get|Fetch|Load|Save|Build|Create|Delete|Remove|Add|Update|Process|Execute|Run|Start|Stop|Clean|Cleanup|Configure|Validate|Check|Verify|Parse|Extract|Apply|Wait|Sleep|Skip|Allow|Deny|Lock|Unlock|Refresh|Reload|Reset|Clear|Send|Receive|Read|Write|Print|Log|Emit|Dispatch|Fire|Open|Close|Bind|Connect|Disconnect|Register|Unregister|Push|Pop|Insert|Append|Prepend|Sort|Filter|Find|Search|Replace|Encode|Decode|Convert|Transform|Map|Reduce|Iterate|Loop|Walk|Visit|Mark|Unmark|Toggle|Switch|Restart|Resume|Pause|Abort|Cancel|Compute|Calculate|Resolve|Reject|Ignore|Handle|Track|Trace|Increment|Decrement|Round|Truncate|Resize|Move|Copy|Clone|Merge|Split|Join|Wrap|Unwrap|Bump|Drain|Flush|Sync|Persist|Commit|Rollback|Yield|Return|Discard|Defer|Pin|Unpin|Mount|Unmount|Spawn|Kill|Restore)(?:\s|$)/;
646
+ const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:|to\s+avoid|to\s+ensure|to\s+prevent|in\s+order\s+to|necessary|guarantee[sd]?|prevents?|regardless\s+of|required\s+(?:for|to|by)|for\s+example|e\.g\.|i\.e\.|useful\s+(?:for|when)|intended\s+to|on\s+purpose|by\s+design)\b/i;
647
+ const MEANINGFUL_JSDOC_TAGS = new Set([
648
+ "deprecated",
649
+ "see",
650
+ "example",
651
+ "type",
652
+ "returns",
653
+ "return",
654
+ "param",
655
+ "throws",
656
+ "typedef",
657
+ "callback",
658
+ "override",
659
+ "template",
660
+ "internal",
661
+ "public",
662
+ "private",
663
+ "protected",
664
+ "experimental",
665
+ "alpha",
666
+ "beta",
667
+ "since",
668
+ "todo",
669
+ "link",
670
+ "license",
671
+ "preserve",
672
+ "swagger",
673
+ "openapi",
674
+ "route",
675
+ "group",
676
+ "summary",
677
+ "description",
678
+ "operationid",
679
+ "response",
680
+ "responses",
681
+ "request",
682
+ "requestbody",
683
+ "security",
684
+ "tag",
685
+ "tags",
686
+ "path",
687
+ "body",
688
+ "query",
689
+ "queryparam",
690
+ "header",
691
+ "headers",
692
+ "produces",
693
+ "accept",
694
+ "middleware",
695
+ "api",
696
+ "apiname",
697
+ "apidefine",
698
+ "apigroup",
699
+ "apiparam",
700
+ "apiquery",
701
+ "apibody",
702
+ "apiheader",
703
+ "apisuccess",
704
+ "apierror",
705
+ "apiexample",
706
+ "apiversion",
707
+ "apidescription",
708
+ "apipermission",
709
+ "apiuse",
710
+ "apiignore",
711
+ "apiprivate",
712
+ "namespace",
713
+ "category"
714
+ ]);
715
+ const SUPPORTED_EXTS = new Set([
716
+ ".ts",
717
+ ".tsx",
718
+ ".js",
719
+ ".jsx",
720
+ ".mjs",
721
+ ".cjs",
722
+ ".py",
723
+ ".go",
724
+ ".rs",
725
+ ".rb",
726
+ ".java",
727
+ ".php"
728
+ ]);
729
+ const DECL_START = /^(\s*)(export\s+)?(async\s+)?(const|let|var|function|class|type|interface|enum|abstract\s+class)\s+/;
730
+ const EXPORT_DEFAULT = /^\s*export\s+default\b/;
731
+ const TS_MEMBER_DECL_START = /^\s*(?:readonly\s+|static\s+|public\s+|private\s+|protected\s+|abstract\s+|override\s+)*[\w$]+\??\s*:/;
732
+ const PY_DECL_START = /^\s*(async\s+def|def|class)\s+/;
733
+ const GO_DECL_START = /^\s*(func|type|var|const|import)\b/;
734
+ const RUST_DECL_START = /^\s*(pub\s+)?(async\s+)?(fn|struct|enum|trait|impl|const|static|type|mod)\s+/;
735
+ const RUBY_DECL_START = /^\s*(class|module|def)\s+/;
736
+ const JAVA_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|sealed|non-sealed|\s)+(?:class|interface|enum|record|@interface|\w[^(){};=]*\s+\w+\s*\()/;
737
+ const JAVA_DECL_START_FALLBACK = /^\s*(class|interface|enum|record|@interface)\s+/;
738
+ const PHP_DECL_START = /^\s*(?:(?:public|private|protected|static|final|abstract|readonly)\s+)*(function|class|interface|trait|enum|const)\s+/;
739
+
583
740
  //#endregion
584
741
  //#region src/engines/ai-slop/non-production-paths.ts
585
742
  const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
@@ -618,11 +775,37 @@ const isTrivialComment = (trimmed, nextLine) => {
618
775
  if (/^-{3,}|─{3,}/.test(commentBody)) return false;
619
776
  return (isJs ? TRIVIAL_JS_COMMENT_PATTERNS : TRIVIAL_PYTHON_COMMENT_PATTERNS).some((pattern) => pattern.test(trimmed));
620
777
  };
621
- const scanFileForTrivialComments = (content, relativePath) => {
778
+ const declStartForExt = (ext) => {
779
+ switch (ext) {
780
+ case ".rb": return [RUBY_DECL_START];
781
+ case ".java": return [JAVA_DECL_START, JAVA_DECL_START_FALLBACK];
782
+ case ".php": return [PHP_DECL_START];
783
+ default: return [];
784
+ }
785
+ };
786
+ const isCommentLineForExt = (line, ext) => {
787
+ const trimmed = line.trim();
788
+ if (ext === ".rb") return trimmed.startsWith("#") && !trimmed.startsWith("#!");
789
+ if (ext === ".java" || ext === ".php") return trimmed.startsWith("//") || trimmed.startsWith("#");
790
+ return false;
791
+ };
792
+ const isDocCommentForDeclaration = (lines, lineIdx, ext) => {
793
+ const patterns = declStartForExt(ext);
794
+ if (patterns.length === 0) return false;
795
+ for (let j = lineIdx + 1; j < lines.length; j++) {
796
+ const candidate = lines[j];
797
+ if (candidate.trim() === "") continue;
798
+ if (isCommentLineForExt(candidate, ext)) continue;
799
+ return patterns.some((re) => re.test(candidate));
800
+ }
801
+ return false;
802
+ };
803
+ const scanFileForTrivialComments = (content, relativePath, ext) => {
622
804
  const diagnostics = [];
623
805
  const lines = content.split("\n");
624
806
  for (let i = 0; i < lines.length; i++) {
625
807
  if (!isTrivialComment(lines[i].trim(), i + 1 < lines.length ? lines[i + 1] : void 0)) continue;
808
+ if (isDocCommentForDeclaration(lines, i, ext)) continue;
626
809
  diagnostics.push({
627
810
  filePath: relativePath,
628
811
  engine: "ai-slop",
@@ -651,7 +834,7 @@ const detectTrivialComments = async (context) => {
651
834
  } catch {
652
835
  continue;
653
836
  }
654
- diagnostics.push(...scanFileForTrivialComments(content, relativePath));
837
+ diagnostics.push(...scanFileForTrivialComments(content, relativePath, path.extname(filePath)));
655
838
  }
656
839
  return diagnostics;
657
840
  };
@@ -707,6 +890,19 @@ const TODO_PATTERN = new RegExp(`\\b(?:${[
707
890
  "PLACEHOLDER",
708
891
  "STUB"
709
892
  ].join("|")})[:\\s]`);
893
+ const isBlockCloserAfterReturn = (line) => line.startsWith("}") || line.startsWith("};") || line.startsWith("),") || line.startsWith(");") || line.startsWith("],") || line.startsWith("]);");
894
+ const isGuardedSingleLineExit = (lines, lineIndex) => {
895
+ const contextLines = [];
896
+ for (let i = lineIndex - 1; i >= 0 && contextLines.length < 16; i--) {
897
+ const trimmed = lines[i].trim();
898
+ if (!trimmed || trimmed.startsWith("//")) continue;
899
+ contextLines.unshift(trimmed);
900
+ if (/^(?:if|else\s+if|for|while)\b/.test(trimmed) || /^}\s*else\s+if\b/.test(trimmed)) break;
901
+ if (/;\s*$/.test(trimmed)) break;
902
+ }
903
+ const control = contextLines.join(" ");
904
+ return /(?:^|[}\s])(?:if|else\s+if|for|while)\s*\(/.test(control) && !/{\s*$/.test(control);
905
+ };
710
906
  const detectTodoStubs = (content, relativePath) => {
711
907
  const diagnostics = [];
712
908
  const lines = content.split("\n");
@@ -723,7 +919,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
723
919
  for (let i = 0; i < lines.length; i++) {
724
920
  const trimmed = lines[i].trim();
725
921
  const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
726
- if (JS_EXTENSIONS$3.has(ext) && /^(?:return|throw)\b/.test(trimmed) && trimmed.endsWith(";") && nextLine && nextLine.length > 0 && !nextLine.startsWith("}") && !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));
922
+ 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));
727
923
  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));
728
924
  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));
729
925
  }
@@ -891,6 +1087,25 @@ const SWALLOWED_EXCEPTION_PATTERNS = [
891
1087
  message: "Empty catch block swallows errors silently"
892
1088
  }
893
1089
  ];
1090
+ const INTENTIONAL_IGNORE_NAMES = new Set([
1091
+ "ignored",
1092
+ "ignore",
1093
+ "tolerated",
1094
+ "expected",
1095
+ "unused",
1096
+ "_",
1097
+ "_e",
1098
+ "_err",
1099
+ "_ex",
1100
+ "_t"
1101
+ ]);
1102
+ const CATCH_PARAM_RE = /catch\s*\(\s*(?:\w+\s+)?([\w$]+)/;
1103
+ const RESCUE_PARAM_RE = /rescue(?:\s+[\w:]+)?\s*=>\s*([\w$]+)/;
1104
+ const isIntentionalIgnore = (matchText, ext) => {
1105
+ const m = (ext === ".rb" ? RESCUE_PARAM_RE : CATCH_PARAM_RE).exec(matchText);
1106
+ if (!m) return false;
1107
+ return INTENTIONAL_IGNORE_NAMES.has(m[1].toLowerCase());
1108
+ };
894
1109
  const detectSwallowedExceptions = async (context) => {
895
1110
  const files = getSourceFiles(context);
896
1111
  const diagnostics = [];
@@ -909,6 +1124,7 @@ const detectSwallowedExceptions = async (context) => {
909
1124
  let match;
910
1125
  const regex = new RegExp(pattern.source, pattern.flags + (pattern.flags.includes("g") ? "" : "g"));
911
1126
  while ((match = regex.exec(content)) !== null) {
1127
+ if (isIntentionalIgnore(match[0], ext)) continue;
912
1128
  const line = content.slice(0, match.index).split("\n").length;
913
1129
  diagnostics.push({
914
1130
  filePath: relativePath,
@@ -1001,6 +1217,117 @@ const detectGoPatterns = async (context) => {
1001
1217
  return diagnostics;
1002
1218
  };
1003
1219
 
1220
+ //#endregion
1221
+ //#region src/engines/ai-slop/js-import-aliases.ts
1222
+ const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
1223
+ const JS_RESOLUTION_EXTENSIONS = [
1224
+ "",
1225
+ ".ts",
1226
+ ".tsx",
1227
+ ".js",
1228
+ ".jsx",
1229
+ ".mjs",
1230
+ ".cjs",
1231
+ ".json",
1232
+ "/index.ts",
1233
+ "/index.tsx",
1234
+ "/index.js",
1235
+ "/index.jsx"
1236
+ ];
1237
+ const readJson$2 = (filePath) => {
1238
+ try {
1239
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
1240
+ } catch {
1241
+ return null;
1242
+ }
1243
+ };
1244
+ const buildAliasMatcher = (key) => {
1245
+ const starIdx = key.indexOf("*");
1246
+ if (starIdx === -1) return (spec) => spec === key;
1247
+ const before = key.slice(0, starIdx);
1248
+ const after = key.slice(starIdx + 1);
1249
+ return (spec) => spec.length >= before.length + after.length && spec.startsWith(before) && spec.endsWith(after);
1250
+ };
1251
+ const collectAliasMatchersFromConfig = (configPath, matchers) => {
1252
+ const opts = readJson$2(configPath)?.compilerOptions;
1253
+ if (!opts || typeof opts !== "object") return;
1254
+ const configDir = path.dirname(configPath);
1255
+ const paths = opts.paths;
1256
+ if (paths && typeof paths === "object") for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
1257
+ const baseUrl = opts.baseUrl;
1258
+ if (typeof baseUrl === "string") {
1259
+ const baseDir = path.resolve(configDir, baseUrl);
1260
+ matchers.push((spec) => {
1261
+ if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("@")) return false;
1262
+ return JS_RESOLUTION_EXTENSIONS.some((suffix) => fs.existsSync(path.join(baseDir, `${spec}${suffix}`)));
1263
+ });
1264
+ }
1265
+ };
1266
+ const collectTsPathAliases = (rootDir, workspaceDirs) => {
1267
+ const matchers = [];
1268
+ const dirs = [rootDir, ...workspaceDirs];
1269
+ for (const dir of dirs) for (const fname of TS_CONFIG_FILES) collectAliasMatchersFromConfig(path.join(dir, fname), matchers);
1270
+ return matchers;
1271
+ };
1272
+
1273
+ //#endregion
1274
+ //#region src/engines/ai-slop/js-workspaces.ts
1275
+ const readJson$1 = (filePath) => {
1276
+ try {
1277
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
1278
+ } catch {
1279
+ return null;
1280
+ }
1281
+ };
1282
+ const readWorkspaceGlobs = (rootDir, rootPkg) => {
1283
+ const globs = [];
1284
+ if (rootPkg && typeof rootPkg === "object") {
1285
+ const ws = rootPkg.workspaces;
1286
+ if (Array.isArray(ws)) {
1287
+ for (const g of ws) if (typeof g === "string") globs.push(g);
1288
+ } else if (ws && typeof ws === "object") {
1289
+ const pkgs = ws.packages;
1290
+ if (Array.isArray(pkgs)) {
1291
+ for (const g of pkgs) if (typeof g === "string") globs.push(g);
1292
+ }
1293
+ }
1294
+ }
1295
+ const lerna = readJson$1(path.join(rootDir, "lerna.json"));
1296
+ if (lerna && Array.isArray(lerna.packages)) {
1297
+ for (const g of lerna.packages) if (typeof g === "string") globs.push(g);
1298
+ }
1299
+ try {
1300
+ const pnpmWs = fs.readFileSync(path.join(rootDir, "pnpm-workspace.yaml"), "utf-8");
1301
+ let inPackages = false;
1302
+ for (const rawLine of pnpmWs.split("\n")) {
1303
+ if (/^packages\s*:\s*$/.test(rawLine)) {
1304
+ inPackages = true;
1305
+ continue;
1306
+ }
1307
+ if (!inPackages) continue;
1308
+ if (/^\S/.test(rawLine)) break;
1309
+ const m = rawLine.match(/^\s*-\s*["']?([^"'\n]+?)["']?\s*$/);
1310
+ if (m) globs.push(m[1].trim());
1311
+ }
1312
+ } catch {
1313
+ return globs;
1314
+ }
1315
+ return globs;
1316
+ };
1317
+ const expandWorkspaceDirs = (rootDir, globs) => {
1318
+ const dirs = [];
1319
+ for (const glob of globs) if (glob.endsWith("/*")) {
1320
+ const parent = path.join(rootDir, glob.slice(0, -2));
1321
+ try {
1322
+ for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
1323
+ } catch {
1324
+ continue;
1325
+ }
1326
+ } else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
1327
+ return dirs;
1328
+ };
1329
+ const collectWorkspaceDirs = (rootDir, rootPkg) => expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, rootPkg));
1330
+
1004
1331
  //#endregion
1005
1332
  //#region src/engines/ai-slop/python-data.ts
1006
1333
  const PYTHON_STDLIB = new Set([
@@ -1245,11 +1572,35 @@ const collectFromPipfile = (rootDir, pyDeps) => {
1245
1572
  return false;
1246
1573
  }
1247
1574
  };
1575
+ const LOCAL_PACKAGE_ROOTS = [
1576
+ "",
1577
+ "src",
1578
+ "lib"
1579
+ ];
1580
+ const collectLocalPythonPackages = (rootDir, pyDeps) => {
1581
+ for (const sub of LOCAL_PACKAGE_ROOTS) {
1582
+ const dir = sub ? path.join(rootDir, sub) : rootDir;
1583
+ let entries;
1584
+ try {
1585
+ entries = fs.readdirSync(dir, { withFileTypes: true });
1586
+ } catch {
1587
+ continue;
1588
+ }
1589
+ for (const entry of entries) {
1590
+ if (!entry.isDirectory()) continue;
1591
+ if (entry.name.startsWith(".")) continue;
1592
+ if (entry.name === "node_modules" || entry.name === "__pycache__") continue;
1593
+ const initPath = path.join(dir, entry.name, "__init__.py");
1594
+ if (fs.existsSync(initPath)) addPyDep(pyDeps, entry.name);
1595
+ }
1596
+ }
1597
+ };
1248
1598
  const collectPythonDeps = (rootDir) => {
1249
1599
  const pyDeps = /* @__PURE__ */ new Set();
1250
1600
  const hasReq = collectFromRequirementsTxt(rootDir, pyDeps);
1251
1601
  const hasPyproject = collectFromPyproject(rootDir, pyDeps);
1252
1602
  const hasPipfile = collectFromPipfile(rootDir, pyDeps);
1603
+ collectLocalPythonPackages(rootDir, pyDeps);
1253
1604
  return {
1254
1605
  pyDeps,
1255
1606
  hasPyManifest: hasReq || hasPyproject || hasPipfile
@@ -1286,51 +1637,6 @@ const addDepsFromPkg = (pkg, jsDeps) => {
1286
1637
  if (deps && typeof deps === "object") for (const name of Object.keys(deps)) jsDeps.add(name);
1287
1638
  }
1288
1639
  };
1289
- const readWorkspaceGlobs = (rootDir, rootPkg) => {
1290
- const globs = [];
1291
- if (rootPkg && typeof rootPkg === "object") {
1292
- const ws = rootPkg.workspaces;
1293
- if (Array.isArray(ws)) {
1294
- for (const g of ws) if (typeof g === "string") globs.push(g);
1295
- } else if (ws && typeof ws === "object") {
1296
- const pkgs = ws.packages;
1297
- if (Array.isArray(pkgs)) {
1298
- for (const g of pkgs) if (typeof g === "string") globs.push(g);
1299
- }
1300
- }
1301
- }
1302
- const lerna = readJson(path.join(rootDir, "lerna.json"));
1303
- if (lerna && Array.isArray(lerna.packages)) {
1304
- for (const g of lerna.packages) if (typeof g === "string") globs.push(g);
1305
- }
1306
- try {
1307
- const pnpmWs = fs.readFileSync(path.join(rootDir, "pnpm-workspace.yaml"), "utf-8");
1308
- let inPackages = false;
1309
- for (const rawLine of pnpmWs.split("\n")) {
1310
- if (/^packages\s*:\s*$/.test(rawLine)) {
1311
- inPackages = true;
1312
- continue;
1313
- }
1314
- if (!inPackages) continue;
1315
- if (/^\S/.test(rawLine)) break;
1316
- const m = rawLine.match(/^\s*-\s*["']?([^"'\n]+?)["']?\s*$/);
1317
- if (m) globs.push(m[1].trim());
1318
- }
1319
- } catch {}
1320
- return globs;
1321
- };
1322
- const expandWorkspaceDirs = (rootDir, globs) => {
1323
- const dirs = [];
1324
- for (const glob of globs) if (glob.endsWith("/*")) {
1325
- const parent = path.join(rootDir, glob.slice(0, -2));
1326
- try {
1327
- for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
1328
- } catch {
1329
- continue;
1330
- }
1331
- } else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
1332
- return dirs;
1333
- };
1334
1640
  const SKIP_DIRS = new Set([
1335
1641
  "node_modules",
1336
1642
  ".git",
@@ -1362,62 +1668,25 @@ const collectNestedManifests = (rootDir, jsDeps) => {
1362
1668
  addDepsFromPkg(wsPkg, jsDeps);
1363
1669
  }
1364
1670
  }
1365
- };
1366
- walk(rootDir, 0);
1367
- };
1368
- const collectJsDeps = (rootDir, jsDeps) => {
1369
- const pkgPath = path.join(rootDir, "package.json");
1370
- if (!fs.existsSync(pkgPath)) return false;
1371
- const pkg = readJson(pkgPath);
1372
- if (!pkg || typeof pkg !== "object") return false;
1373
- addDepsFromPkg(pkg, jsDeps);
1374
- if (typeof pkg.name === "string") jsDeps.add(pkg.name);
1375
- const workspaceDirs = expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, pkg));
1376
- for (const wsDir of workspaceDirs) {
1377
- const wsPkg = readJson(path.join(wsDir, "package.json"));
1378
- if (!wsPkg) continue;
1379
- if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
1380
- addDepsFromPkg(wsPkg, jsDeps);
1381
- }
1382
- collectNestedManifests(rootDir, jsDeps);
1383
- return true;
1384
- };
1385
- const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
1386
- const buildAliasMatcher = (key) => {
1387
- const starIdx = key.indexOf("*");
1388
- if (starIdx === -1) return (spec) => spec === key;
1389
- const before = key.slice(0, starIdx);
1390
- const after = key.slice(starIdx + 1);
1391
- return (spec) => spec.length >= before.length + after.length && spec.startsWith(before) && spec.endsWith(after);
1392
- };
1393
- const collectAliasMatchersFromConfig = (configPath, matchers) => {
1394
- const opts = readJson(configPath)?.compilerOptions;
1395
- if (!opts) return;
1396
- const paths = opts.paths;
1397
- if (paths && typeof paths === "object") for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
1398
- const baseUrl = opts.baseUrl;
1399
- if (typeof baseUrl === "string") {
1400
- const baseUrlDir = path.resolve(path.dirname(configPath), baseUrl);
1401
- let entries;
1402
- try {
1403
- entries = fs.readdirSync(baseUrlDir);
1404
- } catch {
1405
- return;
1406
- }
1407
- const baseSpecifiers = /* @__PURE__ */ new Set();
1408
- for (const entry of entries) {
1409
- if (entry.startsWith(".") || entry === "node_modules") continue;
1410
- const base = entry.replace(/\.(?:[jt]sx?|mjs|cjs|d\.ts)$/i, "");
1411
- if (base.length > 0) baseSpecifiers.add(base);
1412
- }
1413
- for (const name of baseSpecifiers) matchers.push((spec) => spec === name || spec.startsWith(`${name}/`));
1414
- }
1671
+ };
1672
+ walk(rootDir, 0);
1415
1673
  };
1416
- const collectTsPathAliases = (rootDir) => {
1417
- const matchers = [];
1418
- const dirs = [rootDir, ...expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, readJson(path.join(rootDir, "package.json"))))];
1419
- for (const dir of dirs) for (const fname of TS_CONFIG_FILES) collectAliasMatchersFromConfig(path.join(dir, fname), matchers);
1420
- return matchers;
1674
+ const collectJsDeps = (rootDir, jsDeps) => {
1675
+ const pkgPath = path.join(rootDir, "package.json");
1676
+ if (!fs.existsSync(pkgPath)) return false;
1677
+ const pkg = readJson(pkgPath);
1678
+ if (!pkg || typeof pkg !== "object") return false;
1679
+ addDepsFromPkg(pkg, jsDeps);
1680
+ if (typeof pkg.name === "string") jsDeps.add(pkg.name);
1681
+ const workspaceDirs = collectWorkspaceDirs(rootDir, pkg);
1682
+ for (const wsDir of workspaceDirs) {
1683
+ const wsPkg = readJson(path.join(wsDir, "package.json"));
1684
+ if (!wsPkg) continue;
1685
+ if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
1686
+ addDepsFromPkg(wsPkg, jsDeps);
1687
+ }
1688
+ collectNestedManifests(rootDir, jsDeps);
1689
+ return true;
1421
1690
  };
1422
1691
  const loadManifest = (rootDir) => {
1423
1692
  const jsDeps = /* @__PURE__ */ new Set();
@@ -1440,14 +1709,19 @@ const VIRTUAL_MODULE_PREFIXES = [
1440
1709
  "astro:",
1441
1710
  "virtual:",
1442
1711
  "bun:",
1443
- "~icons/"
1712
+ "file:"
1444
1713
  ];
1445
- const isJsVirtualModule = (spec) => VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p));
1714
+ const isJsVirtualModule = (spec, manifest) => {
1715
+ if (VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p))) return true;
1716
+ if (spec === "bun") return true;
1717
+ if (spec === "unfonts.css" && manifest.jsDeps.has("unplugin-fonts")) return true;
1718
+ if (spec.startsWith("~icons/") && manifest.jsDeps.has("unplugin-icons")) return true;
1719
+ return false;
1720
+ };
1446
1721
  const stripImportQuery = (spec) => {
1447
1722
  const idx = spec.indexOf("?");
1448
1723
  return idx === -1 ? spec : spec.slice(0, idx);
1449
1724
  };
1450
- const VIRTUAL_ASSET_FILES = { "unfonts.css": "unplugin-fonts" };
1451
1725
  const TEMPLATE_PLACEHOLDER_RE = /\$\{/;
1452
1726
  const isLikelyRealImportSpec = (spec) => {
1453
1727
  if (spec.length === 0) return false;
@@ -1519,9 +1793,7 @@ const checkJsImport = (rawSpec, manifest, tsAliasMatchers) => {
1519
1793
  if (spec.length === 0) return null;
1520
1794
  if (isJsRelativeOrAbsolute(spec)) return null;
1521
1795
  if (isJsBuiltin(spec)) return null;
1522
- if (isJsVirtualModule(spec)) return null;
1523
- const virtualOwner = VIRTUAL_ASSET_FILES[spec];
1524
- if (virtualOwner && manifest.jsDeps.has(virtualOwner)) return null;
1796
+ if (isJsVirtualModule(spec, manifest)) return null;
1525
1797
  if (tsAliasMatchers.some((m) => m(spec))) return null;
1526
1798
  const pkg = packageNameFromImport(spec);
1527
1799
  if (manifest.jsDeps.has(pkg)) return null;
@@ -1541,9 +1813,11 @@ const checkPyImport = (spec, manifest) => {
1541
1813
  return root;
1542
1814
  };
1543
1815
  const detectHallucinatedImports = async (context) => {
1816
+ const rootPkg = readJson(path.join(context.rootDirectory, "package.json"));
1817
+ const workspaceDirs = collectWorkspaceDirs(context.rootDirectory, rootPkg);
1544
1818
  const manifest = loadManifest(context.rootDirectory);
1545
1819
  if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
1546
- const tsAliasMatchers = manifest.hasJsManifest ? collectTsPathAliases(context.rootDirectory) : [];
1820
+ const tsAliasMatchers = manifest.hasJsManifest ? collectTsPathAliases(context.rootDirectory, workspaceDirs) : [];
1547
1821
  const diagnostics = [];
1548
1822
  const files = getSourceFiles(context);
1549
1823
  for (const filePath of files) {
@@ -1584,121 +1858,7 @@ const detectHallucinatedImports = async (context) => {
1584
1858
  };
1585
1859
 
1586
1860
  //#endregion
1587
- //#region src/engines/ai-slop/narrative-comments-patterns.ts
1588
- const DECORATIVE_SEPARATOR = /^[-=─━~_*#]{6,}$/;
1589
- const DECORATIVE_SECTION_HEADER = /^[-=─━~_*#]{3,}[\s\S]+?[-=─━~_*#]{3,}$/;
1590
- const SECTION_HEADER = /^(Phase|Step|Section|Part)\s+\d+[:.-]/i;
1591
- const CROSS_REFERENCE_PHRASES = [
1592
- /\bwill then be\b/i,
1593
- /\bused by\b/i,
1594
- /\bcalled from\b/i,
1595
- /\bcalled later\b/i,
1596
- /\bsee (?:above|below|later|earlier)\b/i,
1597
- /\breplaces the\b/i,
1598
- /\bmatches the one\b/i,
1599
- /\bwe moved\b/i,
1600
- /\bwe used to\b/i,
1601
- /\brefactor(?:ed)? from\b/i,
1602
- /\bcombined with\b.*\bthis\b/i
1603
- ];
1604
- const JUSTIFICATION_OPENERS = [/^(The idea here|The trick is|This was needed|Originally,?)/i];
1605
- const EXPLANATORY_OPENERS = /^(Matches|Detects|Represents|Holds|Stores|Tracks|Handles|Manages|Controls|Contains|Captures|Encapsulates|Wraps|Describes)\s+[A-Za-z`'"]/;
1606
- const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:)\b/i;
1607
- const MEANINGFUL_JSDOC_TAGS = new Set([
1608
- "deprecated",
1609
- "see",
1610
- "example",
1611
- "type",
1612
- "returns",
1613
- "return",
1614
- "param",
1615
- "throws",
1616
- "typedef",
1617
- "callback",
1618
- "override",
1619
- "template",
1620
- "internal",
1621
- "public",
1622
- "private",
1623
- "protected",
1624
- "experimental",
1625
- "alpha",
1626
- "beta",
1627
- "since",
1628
- "todo",
1629
- "link",
1630
- "license",
1631
- "preserve",
1632
- "swagger",
1633
- "openapi",
1634
- "route",
1635
- "group",
1636
- "summary",
1637
- "description",
1638
- "operationid",
1639
- "response",
1640
- "responses",
1641
- "request",
1642
- "requestbody",
1643
- "security",
1644
- "tag",
1645
- "tags",
1646
- "path",
1647
- "body",
1648
- "query",
1649
- "queryparam",
1650
- "header",
1651
- "headers",
1652
- "produces",
1653
- "accept",
1654
- "middleware",
1655
- "api",
1656
- "apiname",
1657
- "apidefine",
1658
- "apigroup",
1659
- "apiparam",
1660
- "apiquery",
1661
- "apibody",
1662
- "apiheader",
1663
- "apisuccess",
1664
- "apierror",
1665
- "apiexample",
1666
- "apiversion",
1667
- "apidescription",
1668
- "apipermission",
1669
- "apiuse",
1670
- "apiignore",
1671
- "apiprivate",
1672
- "namespace",
1673
- "category"
1674
- ]);
1675
- const SUPPORTED_EXTS = new Set([
1676
- ".ts",
1677
- ".tsx",
1678
- ".js",
1679
- ".jsx",
1680
- ".mjs",
1681
- ".cjs",
1682
- ".py",
1683
- ".go",
1684
- ".rs",
1685
- ".rb",
1686
- ".java",
1687
- ".php"
1688
- ]);
1689
- const DECL_START = /^(\s*)(export\s+)?(async\s+)?(const|let|var|function|class|type|interface|enum|abstract\s+class)\s+/;
1690
- const EXPORT_DEFAULT = /^\s*export\s+default\b/;
1691
- const TS_MEMBER_DECL_START = /^\s*(?:readonly\s+|static\s+|public\s+|private\s+|protected\s+|abstract\s+|override\s+)*[\w$]+\??\s*:/;
1692
- const PY_DECL_START = /^\s*(async\s+def|def|class)\s+/;
1693
- const GO_DECL_START = /^\s*(func|type|var|const)\s+/;
1694
- const RUST_DECL_START = /^\s*(pub\s+)?(async\s+)?(fn|struct|enum|trait|impl|const|static|type|mod)\s+/;
1695
- const RUBY_DECL_START = /^\s*(class|module|def)\s+/;
1696
- const JAVA_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|sealed|non-sealed|\s)+(?:class|interface|enum|record|@interface|\w[^(){};=]*\s+\w+\s*\()/;
1697
- const JAVA_DECL_START_FALLBACK = /^\s*(class|interface|enum|record|@interface)\s+/;
1698
- const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|readonly\s+)*(function|class|interface|trait|enum|const)\s+/;
1699
-
1700
- //#endregion
1701
- //#region src/engines/ai-slop/narrative-comments.ts
1861
+ //#region src/engines/ai-slop/comment-blocks.ts
1702
1862
  const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
1703
1863
  const stripLineComment = (line) => line.replace(/^\s*(?:(?:\/\/)|#)\s?/, "");
1704
1864
  const getCommentSyntax = (ext) => {
@@ -1798,6 +1958,9 @@ const collectBlocks = (sourceLines, syntax) => {
1798
1958
  }
1799
1959
  return blocks;
1800
1960
  };
1961
+
1962
+ //#endregion
1963
+ //#region src/engines/ai-slop/narrative-comments.ts
1801
1964
  const looksLikeDeclarationPreamble = (nextLine, ext) => {
1802
1965
  if (nextLine === null) return false;
1803
1966
  if (DECL_START.test(nextLine) || EXPORT_DEFAULT.test(nextLine)) return true;
@@ -1827,6 +1990,7 @@ const isBareSectionLabel = (prose) => {
1827
1990
  if (!BARE_LABEL_RE.test(prose)) return false;
1828
1991
  if (prose.endsWith(".")) return false;
1829
1992
  if (prose.split(/\s+/).length > 3) return false;
1993
+ if (STEP_COMMENT_VERB_RE.test(prose)) return false;
1830
1994
  return true;
1831
1995
  };
1832
1996
  const DATA_ENTRY_START = /^\s*(?:\{|\[|["'`]|\d|\w+:\s|case\s)/;
@@ -1841,15 +2005,45 @@ const nextLineLooksLikeDataEntry = (nextLine) => {
1841
2005
  };
1842
2006
  const looksLikeSuppressDirective = (block) => block.rawLines.some((l) => /\b(biome-ignore|eslint-disable|ts-ignore|ts-expect-error|@ts-\w+|noqa|pylint:\s*disable|rubocop:disable|noinspection|phpcs:disable)\b/.test(l));
1843
2007
  const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
2008
+ const GO_FIELD_LEAD_RE = /^(\w+)\s+/;
2009
+ const GO_KEYWORDS = new Set([
2010
+ "return",
2011
+ "if",
2012
+ "for",
2013
+ "switch",
2014
+ "case",
2015
+ "default",
2016
+ "go",
2017
+ "select",
2018
+ "defer",
2019
+ "else",
2020
+ "break",
2021
+ "continue",
2022
+ "goto",
2023
+ "package",
2024
+ "import",
2025
+ "map",
2026
+ "chan",
2027
+ "range"
2028
+ ]);
1844
2029
  const looksLikeGoDocComment = (block, ext) => {
1845
2030
  if (ext !== ".go" || block.kind !== "line") return false;
1846
2031
  const next = block.nextNonBlankLine;
1847
2032
  if (!next) return false;
1848
- const declMatch = GO_DECL_NAME_RE.exec(next.trim());
1849
- if (!declMatch) return false;
1850
- return ((block.prose.find((l) => l.length > 0) ?? "").split(/\s+/)[0] ?? "") === declMatch[1];
2033
+ const trimmedNext = next.trim();
2034
+ const firstWord = (block.prose.find((l) => l.length > 0) ?? "").split(/\s+/)[0] ?? "";
2035
+ const declMatch = GO_DECL_NAME_RE.exec(trimmedNext);
2036
+ if (declMatch && firstWord === declMatch[1]) return true;
2037
+ const fieldMatch = GO_FIELD_LEAD_RE.exec(trimmedNext);
2038
+ if (fieldMatch && !GO_KEYWORDS.has(fieldMatch[1]) && firstWord === fieldMatch[1]) return true;
2039
+ return false;
1851
2040
  };
1852
- const DOC_INDICATOR_RE = /`[^`]+`|\|\s*[-:]+\s*\||```|\b(?:note|warning|warn|caveat|example|caution|see):|\(e\.g\.[^)]+\)|\(i\.e\.[^)]+\)/i;
2041
+ const RUBY_DOC_INDICATORS = /^\s*#\s*(?:#|@\w+|:[\w-]+:|=begin|=end)/;
2042
+ const looksLikeRubyDocBlock = (block, ext) => {
2043
+ if (ext !== ".rb" || block.kind !== "line") return false;
2044
+ return block.rawLines.some((line) => RUBY_DOC_INDICATORS.test(line));
2045
+ };
2046
+ const DOC_INDICATOR_RE = /`[^`]+`|\|\s*[-:]+\s*\||```|\b(?:note|warning|warn|caveat|example|caution|see|todo|fixme|hack|reason|deprecated|deprecation|migration|legacy|historical|context):|\(e\.g\.[^)]+\)|\(i\.e\.[^)]+\)|\b\w+\.\w+(?:\.\w+)+\b|\[[\w/.-]+\]/i;
1853
2047
  const hasDocIndicator = (block) => {
1854
2048
  const joined = block.prose.join(" ");
1855
2049
  if (DOC_INDICATOR_RE.test(joined)) return true;
@@ -1885,6 +2079,10 @@ const detectNarrativeInBlock = (block, ext) => {
1885
2079
  matched: false,
1886
2080
  reason: ""
1887
2081
  };
2082
+ if (looksLikeRubyDocBlock(block, ext)) return {
2083
+ matched: false,
2084
+ reason: ""
2085
+ };
1888
2086
  if (block.kind === "line" && block.prose.some((l) => DECORATIVE_SEPARATOR.test(l) || DECORATIVE_SECTION_HEADER.test(l))) return {
1889
2087
  matched: true,
1890
2088
  reason: "decorative separator"
@@ -1893,17 +2091,17 @@ const detectNarrativeInBlock = (block, ext) => {
1893
2091
  matched: true,
1894
2092
  reason: "phase/section header"
1895
2093
  };
1896
- if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine)) return {
2094
+ if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine) && !looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
1897
2095
  matched: true,
1898
2096
  reason: "bare section label"
1899
2097
  };
1900
2098
  const joined = block.prose.join(" ");
1901
2099
  const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
1902
- if ((hasWhyMarker || hasDocIndicator(block)) && block.kind === "jsdoc") return {
2100
+ if (hasWhyMarker || hasDocIndicator(block)) return {
1903
2101
  matched: false,
1904
2102
  reason: ""
1905
2103
  };
1906
- if (block.kind === "line" && block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
2104
+ if (block.kind === "line" && block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext) && hasPreambleSlopSignal(block)) return {
1907
2105
  matched: true,
1908
2106
  reason: "multi-line preamble before declaration"
1909
2107
  };
@@ -1926,11 +2124,18 @@ const detectNarrativeInBlock = (block, ext) => {
1926
2124
  reason: "explanatory preamble"
1927
2125
  };
1928
2126
  const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
1929
- if (nonEmptyProseCount >= 5) return {
1930
- matched: true,
1931
- reason: "long narrative block"
1932
- };
1933
- if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line") return {
2127
+ const isAboveDeclaration = looksLikeDeclarationPreamble(block.nextNonBlankLine, ext);
2128
+ if (nonEmptyProseCount >= 5) {
2129
+ if (isAboveDeclaration) return {
2130
+ matched: false,
2131
+ reason: ""
2132
+ };
2133
+ return {
2134
+ matched: true,
2135
+ reason: "long narrative block"
2136
+ };
2137
+ }
2138
+ if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration) return {
1934
2139
  matched: true,
1935
2140
  reason: "multi-line narrative prose"
1936
2141
  };
@@ -1985,6 +2190,11 @@ const BROAD_EXCEPT_RE = /^\s*except\s+(Exception|BaseException)\s*(?:as\s+\w+)?\
1985
2190
  const PRINT_RE = /^\s*print\s*\(/;
1986
2191
  const DEF_RE = /^\s*(?:async\s+)?def\s+\w+\s*\(/;
1987
2192
  const MUTABLE_DEFAULT_RE = /(\w+)\s*(?::\s*[^,)=]+)?\s*=\s*(\[\s*\]|\{\s*\}|set\(\s*\))/;
2193
+ const RANGE_LEN_LOOP_RE = /^\s*for\s+([A-Za-z_]\w*)\s+in\s+range\s*\(\s*len\s*\(\s*([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*\)\s*\)\s*:\s*(?:#.*)?$/;
2194
+ const CHAINED_DICT_GET_RE = /\.get\s*\([^)]*,\s*\{\s*\}\s*\)\s*\.get\s*\(/;
2195
+ const SAME_VALUE_BRANCH_RE = /^(\s*)(?:if|elif)\s+([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*==\s*["'][^"']+["']\s*:/;
2196
+ const INSTANCE_BRANCH_RE = /^(\s*)(?:if|elif)\s+isinstance\s*\(\s*([A-Za-z_]\w*)\s*,\s*[^)]+\)\s*:/;
2197
+ const BRANCH_LADDER_THRESHOLD = 4;
1988
2198
  const isTestFile$1 = (relPath, basename) => basename.startsWith("test_") || basename.endsWith("_test.py") || basename === "conftest.py" || relPath.split(path.sep).some((seg) => seg === "tests" || seg === "test");
1989
2199
  const isScriptOrEntrypoint = (basename) => basename === "__main__.py" || basename === "manage.py" || basename === "setup.py";
1990
2200
  const SCRIPT_DIR_NAMES = new Set([
@@ -2037,6 +2247,13 @@ const pushFinding = (out, a) => {
2037
2247
  fixable: false
2038
2248
  });
2039
2249
  };
2250
+ const pushLineFinding = (out, relPath, line, finding) => {
2251
+ pushFinding(out, {
2252
+ relPath,
2253
+ line,
2254
+ ...finding
2255
+ });
2256
+ };
2040
2257
  const flagBareExcept = (lines, relPath, out) => {
2041
2258
  for (let i = 0; i < lines.length; i++) {
2042
2259
  if (!BARE_EXCEPT_RE.test(lines[i])) continue;
@@ -2118,6 +2335,76 @@ const flagPrintInProduction = (lines, relPath, basename, out) => {
2118
2335
  });
2119
2336
  }
2120
2337
  };
2338
+ const flagRangeLenLoops = (lines, relPath, out) => {
2339
+ for (let i = 0; i < lines.length; i++) {
2340
+ const match = RANGE_LEN_LOOP_RE.exec(lines[i]);
2341
+ if (!match) continue;
2342
+ pushLineFinding(out, relPath, i + 1, {
2343
+ rule: "ai-slop/python-range-len-loop",
2344
+ severity: "info",
2345
+ message: `\`range(len(${match[2]}))\` loop — usually a hand-rolled iteration pattern.`,
2346
+ help: "Prefer direct iteration (`for item in items`) or `enumerate(items)` when the index is needed. Keeping index plumbing out of the loop reduces checkpoint-to-checkpoint bloat."
2347
+ });
2348
+ }
2349
+ };
2350
+ const flagChainedDictGets = (lines, relPath, out) => {
2351
+ for (let i = 0; i < lines.length; i++) {
2352
+ if (!CHAINED_DICT_GET_RE.test(lines[i])) continue;
2353
+ pushLineFinding(out, relPath, i + 1, {
2354
+ rule: "ai-slop/python-chained-dict-get",
2355
+ severity: "warning",
2356
+ message: "Chained `.get(..., {})` defaults hide missing-data cases.",
2357
+ help: "Normalize the input at the boundary, use a typed object, or split the lookup into explicit steps. Empty-dict fallback chains are a common agent shortcut that becomes brittle as schemas evolve."
2358
+ });
2359
+ }
2360
+ };
2361
+ const countBranchLadder = (lines, start, pattern, selector, indent) => {
2362
+ let count = 1;
2363
+ for (let i = start + 1; i < lines.length; i++) {
2364
+ const line = lines[i];
2365
+ const trimmed = line.trim();
2366
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
2367
+ const match = pattern.exec(line);
2368
+ if (match?.[1] === indent && match[2] === selector) {
2369
+ count++;
2370
+ continue;
2371
+ }
2372
+ if (line.startsWith(`${indent}elif `)) break;
2373
+ if (line.length - line.trimStart().length <= indent.length && !line.startsWith(`${indent}else`)) break;
2374
+ }
2375
+ return count;
2376
+ };
2377
+ const flagBranchLadders = (lines, relPath, out) => {
2378
+ const reported = /* @__PURE__ */ new Set();
2379
+ for (let i = 0; i < lines.length; i++) {
2380
+ if (reported.has(i)) continue;
2381
+ const valueMatch = SAME_VALUE_BRANCH_RE.exec(lines[i]);
2382
+ if (valueMatch) {
2383
+ const count = countBranchLadder(lines, i, SAME_VALUE_BRANCH_RE, valueMatch[2], valueMatch[1]);
2384
+ if (count >= BRANCH_LADDER_THRESHOLD) {
2385
+ reported.add(i);
2386
+ pushLineFinding(out, relPath, i + 1, {
2387
+ rule: "ai-slop/python-repetitive-dispatch",
2388
+ severity: "warning",
2389
+ message: `${count} repeated branches dispatch on \`${valueMatch[2]}\`.`,
2390
+ help: "Use a table, set membership, or handler map when branches share the same shape. SlopCodeBench highlights these selector ladders as code that keeps growing instead of absorbing new cases cleanly."
2391
+ });
2392
+ }
2393
+ continue;
2394
+ }
2395
+ const instanceMatch = INSTANCE_BRANCH_RE.exec(lines[i]);
2396
+ if (!instanceMatch) continue;
2397
+ const count = countBranchLadder(lines, i, INSTANCE_BRANCH_RE, instanceMatch[2], instanceMatch[1]);
2398
+ if (count < BRANCH_LADDER_THRESHOLD) continue;
2399
+ reported.add(i);
2400
+ pushLineFinding(out, relPath, i + 1, {
2401
+ rule: "ai-slop/python-isinstance-ladder",
2402
+ severity: "warning",
2403
+ message: `${count} repeated \`isinstance(${instanceMatch[2]}, ...)\` branches.`,
2404
+ help: "Prefer a handler map, protocol, or normalized intermediate representation when each type branch has the same role. Repeated type ladders are one of the maintainability smells SCBench-style checks look for."
2405
+ });
2406
+ }
2407
+ };
2121
2408
  const detectPythonPatterns = async (context) => {
2122
2409
  const diagnostics = [];
2123
2410
  const files = getSourceFiles(context);
@@ -2137,6 +2424,9 @@ const detectPythonPatterns = async (context) => {
2137
2424
  flagBroadExceptWithSilentBody(lines, relPath, diagnostics);
2138
2425
  flagMutableDefaults(lines, relPath, diagnostics);
2139
2426
  flagPrintInProduction(lines, relPath, basename, diagnostics);
2427
+ flagRangeLenLoops(lines, relPath, diagnostics);
2428
+ flagChainedDictGets(lines, relPath, diagnostics);
2429
+ flagBranchLadders(lines, relPath, diagnostics);
2140
2430
  }
2141
2431
  return diagnostics;
2142
2432
  };
@@ -2162,7 +2452,23 @@ const isTestFile = (relPath) => {
2162
2452
  if (segments.some((s) => TEST_CRATE_SEGMENT_RE.test(s))) return true;
2163
2453
  const basename = segments[segments.length - 1] ?? "";
2164
2454
  if (TEST_BASENAMES.has(basename)) return true;
2165
- return basename.endsWith("_tests.rs") || basename.endsWith("_testutil.rs");
2455
+ return basename.endsWith("_tests.rs") || basename.endsWith("_test.rs") || basename.endsWith("_testutil.rs");
2456
+ };
2457
+ const buildBlockCommentRanges = (lines) => {
2458
+ const ranges = [];
2459
+ let openLine = -1;
2460
+ for (let i = 0; i < lines.length; i++) {
2461
+ const line = lines[i];
2462
+ if (openLine === -1) {
2463
+ const openIdx = line.indexOf("/*");
2464
+ if (openIdx !== -1 && line.indexOf("*/", openIdx + 2) === -1) openLine = i;
2465
+ } else if (line.indexOf("*/") !== -1) {
2466
+ ranges.push([openLine, i]);
2467
+ openLine = -1;
2468
+ }
2469
+ }
2470
+ if (openLine !== -1) ranges.push([openLine, lines.length - 1]);
2471
+ return ranges;
2166
2472
  };
2167
2473
  const isExampleFile = (relPath) => relPath.split(path.sep).some((seg) => seg === "examples" || seg === "benches");
2168
2474
  const UNWRAP_INTENT_LOOKBACK = 2;
@@ -2193,11 +2499,12 @@ const buildTestRanges = (lines) => {
2193
2499
  return ranges;
2194
2500
  };
2195
2501
  const isInRange = (ranges, lineIdx) => ranges.some(([start, end]) => lineIdx >= start && lineIdx <= end);
2196
- const flagNonTestUnwrap = (lines, relPath, testRanges, out) => {
2502
+ const flagNonTestUnwrap = (lines, relPath, testRanges, blockCommentRanges, out) => {
2197
2503
  for (let i = 0; i < lines.length; i++) {
2198
2504
  const line = lines[i];
2199
2505
  if (COMMENT_LINE_RE.test(line)) continue;
2200
2506
  if (isInRange(testRanges, i)) continue;
2507
+ if (isInRange(blockCommentRanges, i)) continue;
2201
2508
  if (!UNWRAP_CALL_RE.test(line)) continue;
2202
2509
  if (WRITELN_UNWRAP_RE.test(line)) continue;
2203
2510
  if (hasIntentComment(lines, i)) continue;
@@ -2254,7 +2561,7 @@ const detectRustPatterns = async (context) => {
2254
2561
  flagTodoMacro(lines, relPath, diagnostics);
2255
2562
  continue;
2256
2563
  }
2257
- flagNonTestUnwrap(lines, relPath, buildTestRanges(lines), diagnostics);
2564
+ flagNonTestUnwrap(lines, relPath, buildTestRanges(lines), buildBlockCommentRanges(lines), diagnostics);
2258
2565
  flagTodoMacro(lines, relPath, diagnostics);
2259
2566
  }
2260
2567
  return diagnostics;
@@ -2339,7 +2646,9 @@ const extractPyImportedSymbols = (lines) => {
2339
2646
  const cleaned = importPart.replace(/[()]/g, "");
2340
2647
  for (const item of cleaned.split(",")) {
2341
2648
  const parts = item.trim().split(/\s+as\s+/);
2342
- const localName = parts.length > 1 ? parts[1].trim() : parts[0].trim();
2649
+ const original = parts[0].trim();
2650
+ const localName = parts.length > 1 ? parts[1].trim() : original;
2651
+ if (parts.length > 1 && original === localName) continue;
2343
2652
  if (localName && /^\w+$/.test(localName)) symbols.push({
2344
2653
  name: localName,
2345
2654
  line: i + 1,
@@ -2352,7 +2661,9 @@ const extractPyImportedSymbols = (lines) => {
2352
2661
  const importMatch = trimmed.match(/^import\s+([\w.]+)(?:\s+as\s+(\w+))?/);
2353
2662
  if (importMatch) {
2354
2663
  importLines.add(i);
2355
- const simpleName = (importMatch[2] ?? importMatch[1]).split(".")[0];
2664
+ const alias = importMatch[2];
2665
+ if (alias && alias === importMatch[1]) continue;
2666
+ const simpleName = (alias ?? importMatch[1]).split(".")[0];
2356
2667
  if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
2357
2668
  name: simpleName,
2358
2669
  line: i + 1,
@@ -3008,9 +3319,17 @@ const isTrivialLine = (line) => {
3008
3319
  if (trimmed.startsWith("/*") || trimmed.startsWith("*")) return true;
3009
3320
  return false;
3010
3321
  };
3322
+ const SVG_MARKUP_RE = /<\/?(?:svg|path|polyline|line|circle|rect|g)\b|(?:xmlns|viewBox|stroke(?:-width|-linecap|-linejoin)?|fill|fill-opacity|d|points|x1|x2|y1|y2)=/;
3323
+ const DATA_LITERAL_RE = /^\s*(?:[A-Za-z_$][\w$-]*:\s*(?:["'`[{]|\d|true\b|false\b|null\b)|["'`][^"'`]*["'`],?\s*$|[{}\]],?\s*$|\),?\s*$)/;
3011
3324
  const SUPPRESS_RE = /aislop[- ]ignore(?:-next-block|-file)?\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
3012
3325
  const FILE_SUPPRESS_RE = /aislop[- ]ignore-file\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
3013
3326
  const fileHasSuppression = (content) => FILE_SUPPRESS_RE.test(content);
3327
+ const isLowSignalMarkupWindow = (lines) => {
3328
+ return lines.filter((line) => SVG_MARKUP_RE.test(line)).length >= Math.ceil(WINDOW_SIZE / 2);
3329
+ };
3330
+ const isLowSignalDataWindow = (lines) => {
3331
+ return lines.filter((line) => DATA_LITERAL_RE.test(line)).length >= WINDOW_SIZE - 1;
3332
+ };
3014
3333
  const findSuppressedLines = (lines) => {
3015
3334
  const suppressed = /* @__PURE__ */ new Set();
3016
3335
  for (let i = 0; i < lines.length; i++) {
@@ -3037,6 +3356,8 @@ const collectMeaningfulLines = (content) => {
3037
3356
  if (suppressed.has(i + 1)) continue;
3038
3357
  const window = lines.slice(i, i + WINDOW_SIZE);
3039
3358
  if (window.some((l) => !MEANINGFUL_LINE.test(l))) continue;
3359
+ if (isLowSignalMarkupWindow(window)) continue;
3360
+ if (isLowSignalDataWindow(window)) continue;
3040
3361
  if (window.every(isTrivialLine)) continue;
3041
3362
  const normalised = window.map(normaliseLine);
3042
3363
  if (normalised.filter((n) => n.length > 0 && n !== "}" && n !== "{").length < WINDOW_SIZE - 1) continue;
@@ -3915,7 +4236,20 @@ const createOxlintConfig = (options) => {
3915
4236
  ...buildFrameworkPlugins(options.framework)
3916
4237
  ];
3917
4238
  const globals = buildTestGlobals(options.testFramework ?? null);
3918
- if (options.framework === "expo" || options.framework === "react") globals.__DEV__ = "readonly";
4239
+ for (const name of [
4240
+ "__DEV__",
4241
+ "__TEST__",
4242
+ "__BROWSER__",
4243
+ "__NODE__",
4244
+ "__GLOBAL__",
4245
+ "__SSR__",
4246
+ "__ESM_BROWSER__",
4247
+ "__ESM_BUNDLER__",
4248
+ "__VERSION__",
4249
+ "__COMMIT__",
4250
+ "__BUILD__"
4251
+ ]) globals[name] = "readonly";
4252
+ for (const globalName of options.globals ?? []) globals[globalName] = "readonly";
3919
4253
  if (options.framework === "astro") {
3920
4254
  globals.Astro = "readonly";
3921
4255
  rules["no-undef"] = "off";
@@ -3935,19 +4269,7 @@ const createOxlintConfig = (options) => {
3935
4269
  };
3936
4270
 
3937
4271
  //#endregion
3938
- //#region src/engines/lint/oxlint.ts
3939
- const esmRequire = createRequire(import.meta.url);
3940
- const resolveOxlintBinary = () => {
3941
- try {
3942
- const oxlintMainPath = esmRequire.resolve("oxlint");
3943
- const oxlintDir = path.resolve(path.dirname(oxlintMainPath), "..");
3944
- return path.join(oxlintDir, "bin", "oxlint");
3945
- } catch {
3946
- return "oxlint";
3947
- }
3948
- };
3949
- const VITE_QUERY_RE = /["'][^"']*\?(worker|sharedworker|worker-url|url|raw|inline|init)\b/;
3950
- const isViteVirtualImportFalsePositive = (rule, message) => rule.startsWith("import/") && VITE_QUERY_RE.test(message);
4272
+ //#region src/engines/lint/oxlint-context-filters.ts
3951
4273
  const AMBIENT_GLOBAL_DEPS = [
3952
4274
  "unplugin-icons",
3953
4275
  "@types/bun",
@@ -4009,6 +4331,9 @@ const isAmbientFalsePositive = (rule, message, sources) => {
4009
4331
  return false;
4010
4332
  };
4011
4333
  const sstReferencedFiles = /* @__PURE__ */ new Map();
4334
+ const clearSstReferenceCache = () => {
4335
+ sstReferencedFiles.clear();
4336
+ };
4012
4337
  const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
4013
4338
  const cached = sstReferencedFiles.get(relativeFilePath);
4014
4339
  if (cached !== void 0) return cached;
@@ -4029,12 +4354,114 @@ const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
4029
4354
  sstReferencedFiles.set(relativeFilePath, referenced);
4030
4355
  return referenced;
4031
4356
  };
4357
+
4358
+ //#endregion
4359
+ //#region src/engines/lint/oxlint-globals.ts
4360
+ const readTextFile$1 = (filePath) => {
4361
+ try {
4362
+ return fs.readFileSync(filePath, "utf-8");
4363
+ } catch {
4364
+ return null;
4365
+ }
4366
+ };
4367
+ const collectPackageNames = (dir) => {
4368
+ const names = /* @__PURE__ */ new Set();
4369
+ const raw = readTextFile$1(path.join(dir, "package.json"));
4370
+ if (!raw) return names;
4371
+ try {
4372
+ const pkg = JSON.parse(raw);
4373
+ for (const section of [
4374
+ "dependencies",
4375
+ "devDependencies",
4376
+ "peerDependencies",
4377
+ "optionalDependencies"
4378
+ ]) {
4379
+ const deps = pkg[section];
4380
+ if (deps && typeof deps === "object") for (const name of Object.keys(deps)) names.add(name);
4381
+ }
4382
+ } catch {
4383
+ return names;
4384
+ }
4385
+ return names;
4386
+ };
4387
+ const AMBIENT_GLOBAL_RE = /^\s*(?:declare\s+)?(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/gm;
4388
+ const collectAmbientGlobals = (rootDir) => {
4389
+ const globals = /* @__PURE__ */ new Set();
4390
+ const projectFiles = listProjectFiles(rootDir);
4391
+ for (const relativePath of projectFiles) {
4392
+ if (!relativePath.endsWith(".d.ts")) continue;
4393
+ const content = readTextFile$1(path.join(rootDir, relativePath));
4394
+ if (!content) continue;
4395
+ AMBIENT_GLOBAL_RE.lastIndex = 0;
4396
+ let match;
4397
+ while ((match = AMBIENT_GLOBAL_RE.exec(content)) !== null) globals.add(match[1]);
4398
+ }
4399
+ const deps = collectPackageNames(rootDir);
4400
+ if (deps.has("@types/bun") || deps.has("bun-types")) globals.add("Bun");
4401
+ if (projectFiles.some((filePath) => /(?:^|\/)sst\.config\.ts$/.test(filePath))) for (const name of [
4402
+ "$app",
4403
+ "$config",
4404
+ "$dev",
4405
+ "$interpolate",
4406
+ "$resolve",
4407
+ "$jsonParse",
4408
+ "$jsonStringify",
4409
+ "aws",
4410
+ "cloudflare",
4411
+ "docker",
4412
+ "random",
4413
+ "sst",
4414
+ "vercel",
4415
+ "pulumi"
4416
+ ]) globals.add(name);
4417
+ return [...globals];
4418
+ };
4419
+
4420
+ //#endregion
4421
+ //#region src/engines/lint/oxlint.ts
4422
+ const esmRequire = createRequire(import.meta.url);
4423
+ const OXLINT_EXTENSIONS = new Set([
4424
+ ".ts",
4425
+ ".tsx",
4426
+ ".js",
4427
+ ".jsx",
4428
+ ".mjs",
4429
+ ".cjs"
4430
+ ]);
4431
+ const resolveOxlintBinary = () => {
4432
+ try {
4433
+ const oxlintMainPath = esmRequire.resolve("oxlint");
4434
+ const oxlintDir = path.resolve(path.dirname(oxlintMainPath), "..");
4435
+ return path.join(oxlintDir, "bin", "oxlint");
4436
+ } catch {
4437
+ return "oxlint";
4438
+ }
4439
+ };
4440
+ const VITE_QUERY_RE = /["'][^"']*\?(worker|sharedworker|worker-url|url|raw|inline|init)\b/;
4441
+ const isViteVirtualImportFalsePositive = (rule, message) => rule.startsWith("import/") && VITE_QUERY_RE.test(message);
4032
4442
  const UNUSED_VAR_IDENT_RE = /(?:Variable|Parameter|Catch parameter) '([^']+)' (?:is declared but never used|is caught but never used)/;
4033
4443
  const isUnderscoreUnusedVar = (rule, message) => {
4034
4444
  if (rule !== "eslint/no-unused-vars") return false;
4035
4445
  const match = UNUSED_VAR_IDENT_RE.exec(message);
4036
4446
  return match ? match[1].startsWith("_") : false;
4037
4447
  };
4448
+ const readTextFile = (filePath) => {
4449
+ try {
4450
+ return fs.readFileSync(filePath, "utf-8");
4451
+ } catch {
4452
+ return null;
4453
+ }
4454
+ };
4455
+ const isSolidRefFalsePositive = (context, diagnostic) => {
4456
+ if (diagnostic.rule !== "eslint/no-unassigned-vars") return false;
4457
+ const name = diagnostic.message.match(/^'([^']+)' is always 'undefined'/)?.[1];
4458
+ if (!name) return false;
4459
+ const content = readTextFile(path.isAbsolute(diagnostic.filePath) ? diagnostic.filePath : path.join(context.rootDirectory, diagnostic.filePath));
4460
+ if (!content) return false;
4461
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4462
+ return new RegExp(`\\bref=\\{\\s*${escaped}\\s*\\}`).test(content);
4463
+ };
4464
+ const isContextualTypeScriptFalsePositive = (diagnostic) => diagnostic.rule === "typescript-eslint/triple-slash-reference" && (diagnostic.filePath.endsWith(".d.ts") || /(?:^|\/)sst\.config\.ts$/.test(diagnostic.filePath));
4038
4465
  const parseRuleCode = (code) => {
4039
4466
  if (!code) return {
4040
4467
  plugin: "eslint",
@@ -4067,14 +4494,20 @@ const detectTestFramework = (rootDir) => {
4067
4494
  } catch {}
4068
4495
  return null;
4069
4496
  };
4497
+ 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("/"));
4070
4498
  const runOxlint = async (context) => {
4071
4499
  const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-${process.pid}.json`);
4500
+ const framework = context.frameworks.find((f) => f !== "none");
4501
+ const testFramework = detectTestFramework(context.rootDirectory);
4502
+ const targets = getOxlintTargets(context);
4503
+ if (targets.length === 0) return [];
4072
4504
  const config = createOxlintConfig({
4073
- framework: context.frameworks.find((f) => f !== "none"),
4074
- testFramework: detectTestFramework(context.rootDirectory)
4505
+ framework,
4506
+ testFramework,
4507
+ globals: collectAmbientGlobals(context.rootDirectory)
4075
4508
  });
4076
4509
  const ambientSources = detectAmbientSources(context.rootDirectory);
4077
- sstReferencedFiles.clear();
4510
+ clearSstReferenceCache();
4078
4511
  try {
4079
4512
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
4080
4513
  const args = [
@@ -4085,7 +4518,7 @@ const runOxlint = async (context) => {
4085
4518
  "json"
4086
4519
  ];
4087
4520
  if (context.languages.includes("typescript") && fs.existsSync(path.join(context.rootDirectory, "tsconfig.json"))) args.push("--tsconfig", "./tsconfig.json");
4088
- args.push(".");
4521
+ args.push(...targets);
4089
4522
  const result = await runSubprocess(process.execPath, args, {
4090
4523
  cwd: context.rootDirectory,
4091
4524
  timeout: 12e4
@@ -4117,6 +4550,8 @@ const runOxlint = async (context) => {
4117
4550
  if (isExcludedFromScan(path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath)) return false;
4118
4551
  if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
4119
4552
  if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
4553
+ if (isSolidRefFalsePositive(context, d)) return false;
4554
+ if (isContextualTypeScriptFalsePositive(d)) return false;
4120
4555
  if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
4121
4556
  if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
4122
4557
  const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
@@ -4672,7 +5107,7 @@ const DB_RECEIVER = "(?:db|database|knex|client|connection|conn|pool|sql|prisma|
4672
5107
  const DB_METHOD = "(?:query|execute|exec|raw|\\$queryRaw|\\$queryRawUnsafe|\\$executeRaw|\\$executeRawUnsafe)";
4673
5108
  const RISKY_PATTERNS = [
4674
5109
  {
4675
- pattern: new RegExp(`\\b${ev}\\s*\\(`, "g"),
5110
+ pattern: new RegExp(`(?<![\\w.>:\\\\])\\b${ev}\\s*\\(`, "g"),
4676
5111
  extensions: [
4677
5112
  ".ts",
4678
5113
  ".tsx",
@@ -4779,6 +5214,16 @@ const RISKY_PATTERNS = [
4779
5214
  help: "Use parameterized queries or an ORM instead of string concatenation"
4780
5215
  }
4781
5216
  ];
5217
+ const hasDangerouslySetInnerHtmlIgnore = (lines, lineIndex) => {
5218
+ const start = Math.max(0, lineIndex - 2);
5219
+ return lines.slice(start, lineIndex + 1).some((line) => /(?:biome-ignore|eslint-disable|aislop-ignore).*(?:noDangerouslySetInnerHtml|dangerouslySetInnerHTML|dangerously-set-innerhtml)/i.test(line));
5220
+ };
5221
+ const isStructuredDataScript = (content, matchIndex) => {
5222
+ const before = content.slice(Math.max(0, matchIndex - 300), matchIndex);
5223
+ if (/type=["']application\/ld\+json["']/.test(before)) return true;
5224
+ const after = content.slice(matchIndex, Math.min(content.length, matchIndex + 180));
5225
+ return /__html\s*:\s*JSON\.stringify\s*\(/.test(after);
5226
+ };
4782
5227
  const detectRiskyConstructs = async (context) => {
4783
5228
  const files = getSourceFiles(context);
4784
5229
  const diagnostics = [];
@@ -4794,6 +5239,7 @@ const detectRiskyConstructs = async (context) => {
4794
5239
  const normalizedPath = relativePath.split(path.sep).join("/");
4795
5240
  const isMigrationOrSeeder = /(?:^|\/)(migrations|seeders|seeds|migrate)\//.test(normalizedPath);
4796
5241
  const masked = maskStringsAndComments(content, ext);
5242
+ const lines = content.split("\n");
4797
5243
  for (const { pattern, extensions, name, message, help } of RISKY_PATTERNS) {
4798
5244
  if (!extensions.includes(ext)) continue;
4799
5245
  if (isMigrationOrSeeder && name === "sql-injection") continue;
@@ -4805,6 +5251,10 @@ const detectRiskyConstructs = async (context) => {
4805
5251
  const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
4806
5252
  if (/(?:template|tmpl|tpl)$/i.test(beforeMatch.trimEnd()) || /createElement\s*\(\s*['"]template['"]\s*\)$/.test(beforeMatch.trimEnd())) continue;
4807
5253
  }
5254
+ if (name === "dangerously-set-innerhtml") {
5255
+ if (hasDangerouslySetInnerHtmlIgnore(lines, line - 1)) continue;
5256
+ if (isStructuredDataScript(content, match.index)) continue;
5257
+ }
4808
5258
  diagnostics.push({
4809
5259
  filePath: relativePath,
4810
5260
  engine: "security",
@@ -4828,7 +5278,8 @@ const detectRiskyConstructs = async (context) => {
4828
5278
  const SECRET_PATTERNS = [
4829
5279
  {
4830
5280
  pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
4831
- name: "API key"
5281
+ name: "API key",
5282
+ keywordPrefixed: true
4832
5283
  },
4833
5284
  {
4834
5285
  pattern: /AKIA[0-9A-Z]{16}/g,
@@ -4836,11 +5287,13 @@ const SECRET_PATTERNS = [
4836
5287
  },
4837
5288
  {
4838
5289
  pattern: /(?:aws[_-]?secret|secret[_-]?key)\s*[:=]\s*["']([A-Za-z0-9/+=]{40})["']/gi,
4839
- name: "AWS Secret Key"
5290
+ name: "AWS Secret Key",
5291
+ keywordPrefixed: true
4840
5292
  },
4841
5293
  {
4842
5294
  pattern: /(?:password|passwd|pwd|secret)\s*[:=]\s*["']([^"']{8,})["']/gi,
4843
- name: "Hardcoded password/secret"
5295
+ name: "Hardcoded password/secret",
5296
+ keywordPrefixed: true
4844
5297
  },
4845
5298
  {
4846
5299
  pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
@@ -4852,7 +5305,8 @@ const SECRET_PATTERNS = [
4852
5305
  },
4853
5306
  {
4854
5307
  pattern: /(?:token|bearer)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
4855
- name: "Authentication token"
5308
+ name: "Authentication token",
5309
+ keywordPrefixed: true
4856
5310
  },
4857
5311
  {
4858
5312
  pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/g,
@@ -4867,6 +5321,24 @@ const SECRET_PATTERNS = [
4867
5321
  name: "Database connection string with credentials"
4868
5322
  }
4869
5323
  ];
5324
+ const isInsideStringLiteral = (content, matchIndex) => {
5325
+ const lineStart = content.lastIndexOf("\n", matchIndex - 1) + 1;
5326
+ const prefix = content.slice(lineStart, matchIndex);
5327
+ let inDouble = false;
5328
+ let inSingle = false;
5329
+ let inBacktick = false;
5330
+ for (let i = 0; i < prefix.length; i++) {
5331
+ const ch = prefix[i];
5332
+ if (ch === "\\") {
5333
+ i++;
5334
+ continue;
5335
+ }
5336
+ if (ch === "\"" && !inSingle && !inBacktick) inDouble = !inDouble;
5337
+ else if (ch === "'" && !inDouble && !inBacktick) inSingle = !inSingle;
5338
+ else if (ch === "`" && !inDouble && !inSingle) inBacktick = !inBacktick;
5339
+ }
5340
+ return inDouble || inSingle || inBacktick;
5341
+ };
4870
5342
  const PLACEHOLDER_EXACT = new Set([
4871
5343
  "changeme",
4872
5344
  "password",
@@ -4902,11 +5374,12 @@ const scanSecrets = async (context) => {
4902
5374
  continue;
4903
5375
  }
4904
5376
  const relativePath = path.relative(context.rootDirectory, filePath);
4905
- for (const { pattern, name } of SECRET_PATTERNS) {
5377
+ for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
4906
5378
  const regex = new RegExp(pattern.source, pattern.flags);
4907
5379
  let match;
4908
5380
  while ((match = regex.exec(content)) !== null) {
4909
5381
  if (isPlaceholderValue(match[1] ?? match[0])) continue;
5382
+ if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
4910
5383
  const line = content.slice(0, match.index).split("\n").length;
4911
5384
  diagnostics.push({
4912
5385
  filePath: relativePath,
@@ -5369,7 +5842,7 @@ const handleAislopBaseline = (input) => {
5369
5842
 
5370
5843
  //#endregion
5371
5844
  //#region src/version.ts
5372
- const APP_VERSION = "0.9.2";
5845
+ const APP_VERSION = "0.9.4";
5373
5846
 
5374
5847
  //#endregion
5375
5848
  //#region src/telemetry/env.ts