aislop 0.9.2 → 0.9.3

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
@@ -1272,64 +1623,19 @@ const readJson = (filePath) => {
1272
1623
  return JSON.parse(fs.readFileSync(filePath, "utf-8"));
1273
1624
  } catch {
1274
1625
  return null;
1275
- }
1276
- };
1277
- const PKG_DEP_SECTIONS = [
1278
- "dependencies",
1279
- "devDependencies",
1280
- "peerDependencies",
1281
- "optionalDependencies"
1282
- ];
1283
- const addDepsFromPkg = (pkg, jsDeps) => {
1284
- for (const section of PKG_DEP_SECTIONS) {
1285
- const deps = pkg[section];
1286
- if (deps && typeof deps === "object") for (const name of Object.keys(deps)) jsDeps.add(name);
1287
- }
1288
- };
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;
1626
+ }
1321
1627
  };
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;
1628
+ const PKG_DEP_SECTIONS = [
1629
+ "dependencies",
1630
+ "devDependencies",
1631
+ "peerDependencies",
1632
+ "optionalDependencies"
1633
+ ];
1634
+ const addDepsFromPkg = (pkg, jsDeps) => {
1635
+ for (const section of PKG_DEP_SECTIONS) {
1636
+ const deps = pkg[section];
1637
+ if (deps && typeof deps === "object") for (const name of Object.keys(deps)) jsDeps.add(name);
1638
+ }
1333
1639
  };
1334
1640
  const SKIP_DIRS = new Set([
1335
1641
  "node_modules",
@@ -1372,7 +1678,7 @@ const collectJsDeps = (rootDir, jsDeps) => {
1372
1678
  if (!pkg || typeof pkg !== "object") return false;
1373
1679
  addDepsFromPkg(pkg, jsDeps);
1374
1680
  if (typeof pkg.name === "string") jsDeps.add(pkg.name);
1375
- const workspaceDirs = expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, pkg));
1681
+ const workspaceDirs = collectWorkspaceDirs(rootDir, pkg);
1376
1682
  for (const wsDir of workspaceDirs) {
1377
1683
  const wsPkg = readJson(path.join(wsDir, "package.json"));
1378
1684
  if (!wsPkg) continue;
@@ -1382,43 +1688,6 @@ const collectJsDeps = (rootDir, jsDeps) => {
1382
1688
  collectNestedManifests(rootDir, jsDeps);
1383
1689
  return true;
1384
1690
  };
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
- }
1415
- };
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;
1421
- };
1422
1691
  const loadManifest = (rootDir) => {
1423
1692
  const jsDeps = /* @__PURE__ */ new Set();
1424
1693
  const hasJsManifest = collectJsDeps(rootDir, jsDeps);
@@ -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;
2040
+ };
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));
1851
2045
  };
1852
- const DOC_INDICATOR_RE = /`[^`]+`|\|\s*[-:]+\s*\||```|\b(?:note|warning|warn|caveat|example|caution|see):|\(e\.g\.[^)]+\)|\(i\.e\.[^)]+\)/i;
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
  };
@@ -2162,7 +2367,23 @@ const isTestFile = (relPath) => {
2162
2367
  if (segments.some((s) => TEST_CRATE_SEGMENT_RE.test(s))) return true;
2163
2368
  const basename = segments[segments.length - 1] ?? "";
2164
2369
  if (TEST_BASENAMES.has(basename)) return true;
2165
- return basename.endsWith("_tests.rs") || basename.endsWith("_testutil.rs");
2370
+ return basename.endsWith("_tests.rs") || basename.endsWith("_test.rs") || basename.endsWith("_testutil.rs");
2371
+ };
2372
+ const buildBlockCommentRanges = (lines) => {
2373
+ const ranges = [];
2374
+ let openLine = -1;
2375
+ for (let i = 0; i < lines.length; i++) {
2376
+ const line = lines[i];
2377
+ if (openLine === -1) {
2378
+ const openIdx = line.indexOf("/*");
2379
+ if (openIdx !== -1 && line.indexOf("*/", openIdx + 2) === -1) openLine = i;
2380
+ } else if (line.indexOf("*/") !== -1) {
2381
+ ranges.push([openLine, i]);
2382
+ openLine = -1;
2383
+ }
2384
+ }
2385
+ if (openLine !== -1) ranges.push([openLine, lines.length - 1]);
2386
+ return ranges;
2166
2387
  };
2167
2388
  const isExampleFile = (relPath) => relPath.split(path.sep).some((seg) => seg === "examples" || seg === "benches");
2168
2389
  const UNWRAP_INTENT_LOOKBACK = 2;
@@ -2193,11 +2414,12 @@ const buildTestRanges = (lines) => {
2193
2414
  return ranges;
2194
2415
  };
2195
2416
  const isInRange = (ranges, lineIdx) => ranges.some(([start, end]) => lineIdx >= start && lineIdx <= end);
2196
- const flagNonTestUnwrap = (lines, relPath, testRanges, out) => {
2417
+ const flagNonTestUnwrap = (lines, relPath, testRanges, blockCommentRanges, out) => {
2197
2418
  for (let i = 0; i < lines.length; i++) {
2198
2419
  const line = lines[i];
2199
2420
  if (COMMENT_LINE_RE.test(line)) continue;
2200
2421
  if (isInRange(testRanges, i)) continue;
2422
+ if (isInRange(blockCommentRanges, i)) continue;
2201
2423
  if (!UNWRAP_CALL_RE.test(line)) continue;
2202
2424
  if (WRITELN_UNWRAP_RE.test(line)) continue;
2203
2425
  if (hasIntentComment(lines, i)) continue;
@@ -2254,7 +2476,7 @@ const detectRustPatterns = async (context) => {
2254
2476
  flagTodoMacro(lines, relPath, diagnostics);
2255
2477
  continue;
2256
2478
  }
2257
- flagNonTestUnwrap(lines, relPath, buildTestRanges(lines), diagnostics);
2479
+ flagNonTestUnwrap(lines, relPath, buildTestRanges(lines), buildBlockCommentRanges(lines), diagnostics);
2258
2480
  flagTodoMacro(lines, relPath, diagnostics);
2259
2481
  }
2260
2482
  return diagnostics;
@@ -2339,7 +2561,9 @@ const extractPyImportedSymbols = (lines) => {
2339
2561
  const cleaned = importPart.replace(/[()]/g, "");
2340
2562
  for (const item of cleaned.split(",")) {
2341
2563
  const parts = item.trim().split(/\s+as\s+/);
2342
- const localName = parts.length > 1 ? parts[1].trim() : parts[0].trim();
2564
+ const original = parts[0].trim();
2565
+ const localName = parts.length > 1 ? parts[1].trim() : original;
2566
+ if (parts.length > 1 && original === localName) continue;
2343
2567
  if (localName && /^\w+$/.test(localName)) symbols.push({
2344
2568
  name: localName,
2345
2569
  line: i + 1,
@@ -2352,7 +2576,9 @@ const extractPyImportedSymbols = (lines) => {
2352
2576
  const importMatch = trimmed.match(/^import\s+([\w.]+)(?:\s+as\s+(\w+))?/);
2353
2577
  if (importMatch) {
2354
2578
  importLines.add(i);
2355
- const simpleName = (importMatch[2] ?? importMatch[1]).split(".")[0];
2579
+ const alias = importMatch[2];
2580
+ if (alias && alias === importMatch[1]) continue;
2581
+ const simpleName = (alias ?? importMatch[1]).split(".")[0];
2356
2582
  if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
2357
2583
  name: simpleName,
2358
2584
  line: i + 1,
@@ -3008,9 +3234,17 @@ const isTrivialLine = (line) => {
3008
3234
  if (trimmed.startsWith("/*") || trimmed.startsWith("*")) return true;
3009
3235
  return false;
3010
3236
  };
3237
+ 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)=/;
3238
+ const DATA_LITERAL_RE = /^\s*(?:[A-Za-z_$][\w$-]*:\s*(?:["'`[{]|\d|true\b|false\b|null\b)|["'`][^"'`]*["'`],?\s*$|[{}\]],?\s*$|\),?\s*$)/;
3011
3239
  const SUPPRESS_RE = /aislop[- ]ignore(?:-next-block|-file)?\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
3012
3240
  const FILE_SUPPRESS_RE = /aislop[- ]ignore-file\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
3013
3241
  const fileHasSuppression = (content) => FILE_SUPPRESS_RE.test(content);
3242
+ const isLowSignalMarkupWindow = (lines) => {
3243
+ return lines.filter((line) => SVG_MARKUP_RE.test(line)).length >= Math.ceil(WINDOW_SIZE / 2);
3244
+ };
3245
+ const isLowSignalDataWindow = (lines) => {
3246
+ return lines.filter((line) => DATA_LITERAL_RE.test(line)).length >= WINDOW_SIZE - 1;
3247
+ };
3014
3248
  const findSuppressedLines = (lines) => {
3015
3249
  const suppressed = /* @__PURE__ */ new Set();
3016
3250
  for (let i = 0; i < lines.length; i++) {
@@ -3037,6 +3271,8 @@ const collectMeaningfulLines = (content) => {
3037
3271
  if (suppressed.has(i + 1)) continue;
3038
3272
  const window = lines.slice(i, i + WINDOW_SIZE);
3039
3273
  if (window.some((l) => !MEANINGFUL_LINE.test(l))) continue;
3274
+ if (isLowSignalMarkupWindow(window)) continue;
3275
+ if (isLowSignalDataWindow(window)) continue;
3040
3276
  if (window.every(isTrivialLine)) continue;
3041
3277
  const normalised = window.map(normaliseLine);
3042
3278
  if (normalised.filter((n) => n.length > 0 && n !== "}" && n !== "{").length < WINDOW_SIZE - 1) continue;
@@ -3915,7 +4151,20 @@ const createOxlintConfig = (options) => {
3915
4151
  ...buildFrameworkPlugins(options.framework)
3916
4152
  ];
3917
4153
  const globals = buildTestGlobals(options.testFramework ?? null);
3918
- if (options.framework === "expo" || options.framework === "react") globals.__DEV__ = "readonly";
4154
+ for (const name of [
4155
+ "__DEV__",
4156
+ "__TEST__",
4157
+ "__BROWSER__",
4158
+ "__NODE__",
4159
+ "__GLOBAL__",
4160
+ "__SSR__",
4161
+ "__ESM_BROWSER__",
4162
+ "__ESM_BUNDLER__",
4163
+ "__VERSION__",
4164
+ "__COMMIT__",
4165
+ "__BUILD__"
4166
+ ]) globals[name] = "readonly";
4167
+ for (const globalName of options.globals ?? []) globals[globalName] = "readonly";
3919
4168
  if (options.framework === "astro") {
3920
4169
  globals.Astro = "readonly";
3921
4170
  rules["no-undef"] = "off";
@@ -3935,19 +4184,7 @@ const createOxlintConfig = (options) => {
3935
4184
  };
3936
4185
 
3937
4186
  //#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);
4187
+ //#region src/engines/lint/oxlint-context-filters.ts
3951
4188
  const AMBIENT_GLOBAL_DEPS = [
3952
4189
  "unplugin-icons",
3953
4190
  "@types/bun",
@@ -4009,6 +4246,9 @@ const isAmbientFalsePositive = (rule, message, sources) => {
4009
4246
  return false;
4010
4247
  };
4011
4248
  const sstReferencedFiles = /* @__PURE__ */ new Map();
4249
+ const clearSstReferenceCache = () => {
4250
+ sstReferencedFiles.clear();
4251
+ };
4012
4252
  const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
4013
4253
  const cached = sstReferencedFiles.get(relativeFilePath);
4014
4254
  if (cached !== void 0) return cached;
@@ -4029,12 +4269,114 @@ const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
4029
4269
  sstReferencedFiles.set(relativeFilePath, referenced);
4030
4270
  return referenced;
4031
4271
  };
4272
+
4273
+ //#endregion
4274
+ //#region src/engines/lint/oxlint-globals.ts
4275
+ const readTextFile$1 = (filePath) => {
4276
+ try {
4277
+ return fs.readFileSync(filePath, "utf-8");
4278
+ } catch {
4279
+ return null;
4280
+ }
4281
+ };
4282
+ const collectPackageNames = (dir) => {
4283
+ const names = /* @__PURE__ */ new Set();
4284
+ const raw = readTextFile$1(path.join(dir, "package.json"));
4285
+ if (!raw) return names;
4286
+ try {
4287
+ const pkg = JSON.parse(raw);
4288
+ for (const section of [
4289
+ "dependencies",
4290
+ "devDependencies",
4291
+ "peerDependencies",
4292
+ "optionalDependencies"
4293
+ ]) {
4294
+ const deps = pkg[section];
4295
+ if (deps && typeof deps === "object") for (const name of Object.keys(deps)) names.add(name);
4296
+ }
4297
+ } catch {
4298
+ return names;
4299
+ }
4300
+ return names;
4301
+ };
4302
+ const AMBIENT_GLOBAL_RE = /^\s*(?:declare\s+)?(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/gm;
4303
+ const collectAmbientGlobals = (rootDir) => {
4304
+ const globals = /* @__PURE__ */ new Set();
4305
+ const projectFiles = listProjectFiles(rootDir);
4306
+ for (const relativePath of projectFiles) {
4307
+ if (!relativePath.endsWith(".d.ts")) continue;
4308
+ const content = readTextFile$1(path.join(rootDir, relativePath));
4309
+ if (!content) continue;
4310
+ AMBIENT_GLOBAL_RE.lastIndex = 0;
4311
+ let match;
4312
+ while ((match = AMBIENT_GLOBAL_RE.exec(content)) !== null) globals.add(match[1]);
4313
+ }
4314
+ const deps = collectPackageNames(rootDir);
4315
+ if (deps.has("@types/bun") || deps.has("bun-types")) globals.add("Bun");
4316
+ if (projectFiles.some((filePath) => /(?:^|\/)sst\.config\.ts$/.test(filePath))) for (const name of [
4317
+ "$app",
4318
+ "$config",
4319
+ "$dev",
4320
+ "$interpolate",
4321
+ "$resolve",
4322
+ "$jsonParse",
4323
+ "$jsonStringify",
4324
+ "aws",
4325
+ "cloudflare",
4326
+ "docker",
4327
+ "random",
4328
+ "sst",
4329
+ "vercel",
4330
+ "pulumi"
4331
+ ]) globals.add(name);
4332
+ return [...globals];
4333
+ };
4334
+
4335
+ //#endregion
4336
+ //#region src/engines/lint/oxlint.ts
4337
+ const esmRequire = createRequire(import.meta.url);
4338
+ const OXLINT_EXTENSIONS = new Set([
4339
+ ".ts",
4340
+ ".tsx",
4341
+ ".js",
4342
+ ".jsx",
4343
+ ".mjs",
4344
+ ".cjs"
4345
+ ]);
4346
+ const resolveOxlintBinary = () => {
4347
+ try {
4348
+ const oxlintMainPath = esmRequire.resolve("oxlint");
4349
+ const oxlintDir = path.resolve(path.dirname(oxlintMainPath), "..");
4350
+ return path.join(oxlintDir, "bin", "oxlint");
4351
+ } catch {
4352
+ return "oxlint";
4353
+ }
4354
+ };
4355
+ const VITE_QUERY_RE = /["'][^"']*\?(worker|sharedworker|worker-url|url|raw|inline|init)\b/;
4356
+ const isViteVirtualImportFalsePositive = (rule, message) => rule.startsWith("import/") && VITE_QUERY_RE.test(message);
4032
4357
  const UNUSED_VAR_IDENT_RE = /(?:Variable|Parameter|Catch parameter) '([^']+)' (?:is declared but never used|is caught but never used)/;
4033
4358
  const isUnderscoreUnusedVar = (rule, message) => {
4034
4359
  if (rule !== "eslint/no-unused-vars") return false;
4035
4360
  const match = UNUSED_VAR_IDENT_RE.exec(message);
4036
4361
  return match ? match[1].startsWith("_") : false;
4037
4362
  };
4363
+ const readTextFile = (filePath) => {
4364
+ try {
4365
+ return fs.readFileSync(filePath, "utf-8");
4366
+ } catch {
4367
+ return null;
4368
+ }
4369
+ };
4370
+ const isSolidRefFalsePositive = (context, diagnostic) => {
4371
+ if (diagnostic.rule !== "eslint/no-unassigned-vars") return false;
4372
+ const name = diagnostic.message.match(/^'([^']+)' is always 'undefined'/)?.[1];
4373
+ if (!name) return false;
4374
+ const content = readTextFile(path.isAbsolute(diagnostic.filePath) ? diagnostic.filePath : path.join(context.rootDirectory, diagnostic.filePath));
4375
+ if (!content) return false;
4376
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4377
+ return new RegExp(`\\bref=\\{\\s*${escaped}\\s*\\}`).test(content);
4378
+ };
4379
+ const isContextualTypeScriptFalsePositive = (diagnostic) => diagnostic.rule === "typescript-eslint/triple-slash-reference" && (diagnostic.filePath.endsWith(".d.ts") || /(?:^|\/)sst\.config\.ts$/.test(diagnostic.filePath));
4038
4380
  const parseRuleCode = (code) => {
4039
4381
  if (!code) return {
4040
4382
  plugin: "eslint",
@@ -4067,14 +4409,20 @@ const detectTestFramework = (rootDir) => {
4067
4409
  } catch {}
4068
4410
  return null;
4069
4411
  };
4412
+ 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
4413
  const runOxlint = async (context) => {
4071
4414
  const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-${process.pid}.json`);
4415
+ const framework = context.frameworks.find((f) => f !== "none");
4416
+ const testFramework = detectTestFramework(context.rootDirectory);
4417
+ const targets = getOxlintTargets(context);
4418
+ if (targets.length === 0) return [];
4072
4419
  const config = createOxlintConfig({
4073
- framework: context.frameworks.find((f) => f !== "none"),
4074
- testFramework: detectTestFramework(context.rootDirectory)
4420
+ framework,
4421
+ testFramework,
4422
+ globals: collectAmbientGlobals(context.rootDirectory)
4075
4423
  });
4076
4424
  const ambientSources = detectAmbientSources(context.rootDirectory);
4077
- sstReferencedFiles.clear();
4425
+ clearSstReferenceCache();
4078
4426
  try {
4079
4427
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
4080
4428
  const args = [
@@ -4085,7 +4433,7 @@ const runOxlint = async (context) => {
4085
4433
  "json"
4086
4434
  ];
4087
4435
  if (context.languages.includes("typescript") && fs.existsSync(path.join(context.rootDirectory, "tsconfig.json"))) args.push("--tsconfig", "./tsconfig.json");
4088
- args.push(".");
4436
+ args.push(...targets);
4089
4437
  const result = await runSubprocess(process.execPath, args, {
4090
4438
  cwd: context.rootDirectory,
4091
4439
  timeout: 12e4
@@ -4117,6 +4465,8 @@ const runOxlint = async (context) => {
4117
4465
  if (isExcludedFromScan(path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath)) return false;
4118
4466
  if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
4119
4467
  if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
4468
+ if (isSolidRefFalsePositive(context, d)) return false;
4469
+ if (isContextualTypeScriptFalsePositive(d)) return false;
4120
4470
  if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
4121
4471
  if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
4122
4472
  const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
@@ -4672,7 +5022,7 @@ const DB_RECEIVER = "(?:db|database|knex|client|connection|conn|pool|sql|prisma|
4672
5022
  const DB_METHOD = "(?:query|execute|exec|raw|\\$queryRaw|\\$queryRawUnsafe|\\$executeRaw|\\$executeRawUnsafe)";
4673
5023
  const RISKY_PATTERNS = [
4674
5024
  {
4675
- pattern: new RegExp(`\\b${ev}\\s*\\(`, "g"),
5025
+ pattern: new RegExp(`(?<![\\w.>:\\\\])\\b${ev}\\s*\\(`, "g"),
4676
5026
  extensions: [
4677
5027
  ".ts",
4678
5028
  ".tsx",
@@ -4779,6 +5129,16 @@ const RISKY_PATTERNS = [
4779
5129
  help: "Use parameterized queries or an ORM instead of string concatenation"
4780
5130
  }
4781
5131
  ];
5132
+ const hasDangerouslySetInnerHtmlIgnore = (lines, lineIndex) => {
5133
+ const start = Math.max(0, lineIndex - 2);
5134
+ return lines.slice(start, lineIndex + 1).some((line) => /(?:biome-ignore|eslint-disable|aislop-ignore).*(?:noDangerouslySetInnerHtml|dangerouslySetInnerHTML|dangerously-set-innerhtml)/i.test(line));
5135
+ };
5136
+ const isStructuredDataScript = (content, matchIndex) => {
5137
+ const before = content.slice(Math.max(0, matchIndex - 300), matchIndex);
5138
+ if (/type=["']application\/ld\+json["']/.test(before)) return true;
5139
+ const after = content.slice(matchIndex, Math.min(content.length, matchIndex + 180));
5140
+ return /__html\s*:\s*JSON\.stringify\s*\(/.test(after);
5141
+ };
4782
5142
  const detectRiskyConstructs = async (context) => {
4783
5143
  const files = getSourceFiles(context);
4784
5144
  const diagnostics = [];
@@ -4794,6 +5154,7 @@ const detectRiskyConstructs = async (context) => {
4794
5154
  const normalizedPath = relativePath.split(path.sep).join("/");
4795
5155
  const isMigrationOrSeeder = /(?:^|\/)(migrations|seeders|seeds|migrate)\//.test(normalizedPath);
4796
5156
  const masked = maskStringsAndComments(content, ext);
5157
+ const lines = content.split("\n");
4797
5158
  for (const { pattern, extensions, name, message, help } of RISKY_PATTERNS) {
4798
5159
  if (!extensions.includes(ext)) continue;
4799
5160
  if (isMigrationOrSeeder && name === "sql-injection") continue;
@@ -4805,6 +5166,10 @@ const detectRiskyConstructs = async (context) => {
4805
5166
  const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
4806
5167
  if (/(?:template|tmpl|tpl)$/i.test(beforeMatch.trimEnd()) || /createElement\s*\(\s*['"]template['"]\s*\)$/.test(beforeMatch.trimEnd())) continue;
4807
5168
  }
5169
+ if (name === "dangerously-set-innerhtml") {
5170
+ if (hasDangerouslySetInnerHtmlIgnore(lines, line - 1)) continue;
5171
+ if (isStructuredDataScript(content, match.index)) continue;
5172
+ }
4808
5173
  diagnostics.push({
4809
5174
  filePath: relativePath,
4810
5175
  engine: "security",
@@ -4828,7 +5193,8 @@ const detectRiskyConstructs = async (context) => {
4828
5193
  const SECRET_PATTERNS = [
4829
5194
  {
4830
5195
  pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
4831
- name: "API key"
5196
+ name: "API key",
5197
+ keywordPrefixed: true
4832
5198
  },
4833
5199
  {
4834
5200
  pattern: /AKIA[0-9A-Z]{16}/g,
@@ -4836,11 +5202,13 @@ const SECRET_PATTERNS = [
4836
5202
  },
4837
5203
  {
4838
5204
  pattern: /(?:aws[_-]?secret|secret[_-]?key)\s*[:=]\s*["']([A-Za-z0-9/+=]{40})["']/gi,
4839
- name: "AWS Secret Key"
5205
+ name: "AWS Secret Key",
5206
+ keywordPrefixed: true
4840
5207
  },
4841
5208
  {
4842
5209
  pattern: /(?:password|passwd|pwd|secret)\s*[:=]\s*["']([^"']{8,})["']/gi,
4843
- name: "Hardcoded password/secret"
5210
+ name: "Hardcoded password/secret",
5211
+ keywordPrefixed: true
4844
5212
  },
4845
5213
  {
4846
5214
  pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
@@ -4852,7 +5220,8 @@ const SECRET_PATTERNS = [
4852
5220
  },
4853
5221
  {
4854
5222
  pattern: /(?:token|bearer)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
4855
- name: "Authentication token"
5223
+ name: "Authentication token",
5224
+ keywordPrefixed: true
4856
5225
  },
4857
5226
  {
4858
5227
  pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/g,
@@ -4867,6 +5236,24 @@ const SECRET_PATTERNS = [
4867
5236
  name: "Database connection string with credentials"
4868
5237
  }
4869
5238
  ];
5239
+ const isInsideStringLiteral = (content, matchIndex) => {
5240
+ const lineStart = content.lastIndexOf("\n", matchIndex - 1) + 1;
5241
+ const prefix = content.slice(lineStart, matchIndex);
5242
+ let inDouble = false;
5243
+ let inSingle = false;
5244
+ let inBacktick = false;
5245
+ for (let i = 0; i < prefix.length; i++) {
5246
+ const ch = prefix[i];
5247
+ if (ch === "\\") {
5248
+ i++;
5249
+ continue;
5250
+ }
5251
+ if (ch === "\"" && !inSingle && !inBacktick) inDouble = !inDouble;
5252
+ else if (ch === "'" && !inDouble && !inBacktick) inSingle = !inSingle;
5253
+ else if (ch === "`" && !inDouble && !inSingle) inBacktick = !inBacktick;
5254
+ }
5255
+ return inDouble || inSingle || inBacktick;
5256
+ };
4870
5257
  const PLACEHOLDER_EXACT = new Set([
4871
5258
  "changeme",
4872
5259
  "password",
@@ -4902,11 +5289,12 @@ const scanSecrets = async (context) => {
4902
5289
  continue;
4903
5290
  }
4904
5291
  const relativePath = path.relative(context.rootDirectory, filePath);
4905
- for (const { pattern, name } of SECRET_PATTERNS) {
5292
+ for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
4906
5293
  const regex = new RegExp(pattern.source, pattern.flags);
4907
5294
  let match;
4908
5295
  while ((match = regex.exec(content)) !== null) {
4909
5296
  if (isPlaceholderValue(match[1] ?? match[0])) continue;
5297
+ if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
4910
5298
  const line = content.slice(0, match.index).split("\n").length;
4911
5299
  diagnostics.push({
4912
5300
  filePath: relativePath,
@@ -5369,7 +5757,7 @@ const handleAislopBaseline = (input) => {
5369
5757
 
5370
5758
  //#endregion
5371
5759
  //#region src/version.ts
5372
- const APP_VERSION = "0.9.2";
5760
+ const APP_VERSION = "0.9.3";
5373
5761
 
5374
5762
  //#endregion
5375
5763
  //#region src/telemetry/env.ts