aislop 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-BOJR1S8l.js";
1
+ import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-B9ZchFMv.js";
2
2
  import { n as runSubprocess, t as isToolInstalled } from "./subprocess-CQUJDGgn.js";
3
3
  import { r as runGenericLinter, t as fixRubyLint } from "./generic-BrcWMW7E.js";
4
4
  import { n as runExpoDoctor } from "./expo-doctor-Bz0LZhQ6.js";
5
- import { createRequire } from "node:module";
5
+ import { createRequire, isBuiltin } from "node:module";
6
6
  import fs from "node:fs";
7
7
  import path from "node:path";
8
8
  import YAML from "yaml";
@@ -41,6 +41,7 @@ const DEFAULT_CONFIG = {
41
41
  maxNesting: 5,
42
42
  maxParams: 6
43
43
  },
44
+ lint: { typecheck: false },
44
45
  security: {
45
46
  audit: true,
46
47
  auditTimeout: 25e3
@@ -169,6 +170,7 @@ const QualitySchema = z.object({
169
170
  maxNesting: z.number().positive().default(5),
170
171
  maxParams: z.number().positive().default(6)
171
172
  });
173
+ const LintConfigSchema = z.object({ typecheck: z.boolean().default(false) });
172
174
  const SecurityConfigSchema = z.object({
173
175
  audit: z.boolean().default(true),
174
176
  auditTimeout: z.number().positive().default(25e3)
@@ -206,6 +208,7 @@ const AislopConfigSchema = z.object({
206
208
  maxNesting: 5,
207
209
  maxParams: 6
208
210
  })),
211
+ lint: LintConfigSchema.default(() => ({ typecheck: false })),
209
212
  security: SecurityConfigSchema.default(() => ({
210
213
  audit: true,
211
214
  auditTimeout: 25e3
@@ -606,7 +609,7 @@ const hasAllowedExtension = (filePath, extraExtensions) => {
606
609
  return SOURCE_EXTENSIONS.has(extension) || extraExtensions.has(extension);
607
610
  };
608
611
  const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
609
- const isTestFile = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
612
+ const isTestFile$2 = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
610
613
  const getIgnoredPaths = (rootDirectory, files) => {
611
614
  if (files.length === 0) return /* @__PURE__ */ new Set();
612
615
  const result = spawnSync("git", [
@@ -680,7 +683,7 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude
680
683
  return micromatch.isMatch(relativePath, normalizedExcludePatterns, { dot: true });
681
684
  };
682
685
  return normalizedFiles.filter(({ absolutePath, relativePath }) => {
683
- return hasAllowedExtension(relativePath, extraSet) && !isExcludedPath(relativePath) && !isTestFile(relativePath) && !ignoredPaths.has(relativePath) && !isUserExcluded(relativePath) && fs.existsSync(absolutePath);
686
+ return hasAllowedExtension(relativePath, extraSet) && !isExcludedPath(relativePath) && !isTestFile$2(relativePath) && !ignoredPaths.has(relativePath) && !isUserExcluded(relativePath) && fs.existsSync(absolutePath);
684
687
  }).map(({ absolutePath }) => absolutePath);
685
688
  };
686
689
  const filterExplicitFiles = (rootDirectory, files, extraExtensions = []) => {
@@ -989,16 +992,29 @@ const planFormat = (ctx) => {
989
992
  skipReason: "no supported language"
990
993
  };
991
994
  };
992
- const planLint = (ctx) => {
993
- const { languages, frameworks, installedTools } = ctx.projectInfo;
994
- if (frameworks.includes("expo")) return {
995
- tool: "expo-doctor + oxlint (bundled)",
995
+ const findLocalTsc = (root) => {
996
+ const candidate = path.join(root, "node_modules", ".bin", "tsc");
997
+ return fs.existsSync(candidate) ? candidate : null;
998
+ };
999
+ const withTypecheckSuffix = (baseTool, ctx) => {
1000
+ if (!ctx.config.lint?.typecheck) return {
1001
+ tool: baseTool,
996
1002
  status: "ok"
997
1003
  };
998
- if (hasJsLike(languages)) return {
999
- tool: "oxlint (bundled)",
1004
+ if (findLocalTsc(ctx.rootDirectory)) return {
1005
+ tool: `${baseTool} + tsc`,
1000
1006
  status: "ok"
1001
1007
  };
1008
+ return {
1009
+ tool: `${baseTool} + tsc not found`,
1010
+ status: "missing",
1011
+ remediation: "Install TypeScript locally (pnpm add -D typescript), or set lint.typecheck: false in .aislop/config.yml."
1012
+ };
1013
+ };
1014
+ const planLint = (ctx) => {
1015
+ const { languages, frameworks, installedTools } = ctx.projectInfo;
1016
+ if (frameworks.includes("expo")) return withTypecheckSuffix("expo-doctor + oxlint (bundled)", ctx);
1017
+ if (hasJsLike(languages)) return withTypecheckSuffix("oxlint (bundled)", ctx);
1002
1018
  return firstMatching(languages, installedTools, LINT_SPECS) ?? {
1003
1019
  tool: "no linter",
1004
1020
  status: "skipped",
@@ -1258,13 +1274,14 @@ const detectOverAbstraction = async (context) => {
1258
1274
 
1259
1275
  //#endregion
1260
1276
  //#region src/engines/ai-slop/comments.ts
1277
+ const NON_PRODUCTION_DIR_PATTERN$2 = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3)\//i;
1261
1278
  const TRIVIAL_VERB_STEMS = "Import|Defin|Initializ|Setting|Set\\s+up|Setup|Return|Check|Loop|Iterat|Creat|Updat|Delet|Remov|Handl|Get|Fetch|Increment|Decrement|Writ|Runn|Run|Pars|Execut|Extract|Sav|Load|Build|Start|Stopp|Stop|Clean(?:up|\\s+up)?|Configur|Validat|Process|Queue|Fire|Emit|Dispatch|Log|Print|Render";
1262
1279
  const TRIVIAL_JS_COMMENT_PATTERNS = [/\/\/\s*This (?:function|method|class|variable|constant) (?:will |is used to |is responsible for )?/i, new RegExp(`\\/\\/\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
1263
1280
  const TRIVIAL_PYTHON_COMMENT_PATTERNS = [/^#\s*This (?:function|method|class) (?:will |is used to )?/i, new RegExp(`^#\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
1264
1281
  const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?)\b/i;
1265
1282
  const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
1266
1283
  const MAX_TRIVIAL_COMMENT_LENGTH = 60;
1267
- const isJsComment = (trimmed) => trimmed.startsWith("//");
1284
+ const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
1268
1285
  const isPythonComment = (trimmed) => trimmed.startsWith("#") && !trimmed.startsWith("#!");
1269
1286
  /**
1270
1287
  * Extract just the comment text after the comment marker.
@@ -1313,13 +1330,14 @@ const detectTrivialComments = async (context) => {
1313
1330
  const diagnostics = [];
1314
1331
  for (const filePath of files) {
1315
1332
  if (isAutoGenerated(filePath)) continue;
1333
+ const relativePath = path.relative(context.rootDirectory, filePath);
1334
+ if (NON_PRODUCTION_DIR_PATTERN$2.test(relativePath)) continue;
1316
1335
  let content;
1317
1336
  try {
1318
1337
  content = fs.readFileSync(filePath, "utf-8");
1319
1338
  } catch {
1320
1339
  continue;
1321
1340
  }
1322
- const relativePath = path.relative(context.rootDirectory, filePath);
1323
1341
  diagnostics.push(...scanFileForTrivialComments(content, relativePath));
1324
1342
  }
1325
1343
  return diagnostics;
@@ -1327,7 +1345,7 @@ const detectTrivialComments = async (context) => {
1327
1345
 
1328
1346
  //#endregion
1329
1347
  //#region src/engines/ai-slop/dead-patterns.ts
1330
- const JS_EXTENSIONS$1 = new Set([
1348
+ const JS_EXTENSIONS$4 = new Set([
1331
1349
  ".ts",
1332
1350
  ".tsx",
1333
1351
  ".js",
@@ -1349,11 +1367,11 @@ const slop = (filePath, line, rule, severity, message, help, fixable) => ({
1349
1367
  fixable
1350
1368
  });
1351
1369
  const LOGGER_FILE_PATTERN = /(?:^|\/)(?:logger|logging|log)\.[^/]+$/i;
1352
- const SCRIPT_DIR_PATTERN = /(?:^|\/)(scripts|bin)\//;
1370
+ const NON_PRODUCTION_DIR_PATTERN$1 = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|cli|cli-[\w-]+|[\w-]+-cli)\//;
1353
1371
  const detectConsoleLeftovers = (content, relativePath, ext) => {
1354
- if (!JS_EXTENSIONS$1.has(ext)) return [];
1372
+ if (!JS_EXTENSIONS$4.has(ext)) return [];
1355
1373
  if (LOGGER_FILE_PATTERN.test(relativePath)) return [];
1356
- if (SCRIPT_DIR_PATTERN.test(relativePath)) return [];
1374
+ if (NON_PRODUCTION_DIR_PATTERN$1.test(relativePath)) return [];
1357
1375
  const diagnostics = [];
1358
1376
  const lines = content.split("\n");
1359
1377
  for (let i = 0; i < lines.length; i++) {
@@ -1393,9 +1411,9 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
1393
1411
  for (let i = 0; i < lines.length; i++) {
1394
1412
  const trimmed = lines[i].trim();
1395
1413
  const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
1396
- if (JS_EXTENSIONS$1.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));
1414
+ if (JS_EXTENSIONS$4.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));
1397
1415
  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));
1398
- if (JS_EXTENSIONS$1.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));
1416
+ if (JS_EXTENSIONS$4.has(ext) && /(?:function\s+\w+|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ")) diagnostics.push(slop(relativePath, i + 1, "ai-slop/empty-function", "info", "Empty function body — possible stub or unfinished implementation", "Implement the function body or add a comment explaining why it's empty", false));
1399
1417
  }
1400
1418
  return diagnostics;
1401
1419
  };
@@ -1403,6 +1421,7 @@ const asAnyPattern = new RegExp(`\\bas\\s+any\\b`);
1403
1421
  const doubleAssertPattern = new RegExp(`\\bas\\s+unknown\\s+as\\s+`);
1404
1422
  const detectUnsafeTypePatterns = (content, relativePath, ext) => {
1405
1423
  if (ext !== ".ts" && ext !== ".tsx") return [];
1424
+ if (NON_PRODUCTION_DIR_PATTERN$1.test(relativePath)) return [];
1406
1425
  const diagnostics = [];
1407
1426
  const lines = content.split("\n");
1408
1427
  for (let i = 0; i < lines.length; i++) {
@@ -1439,6 +1458,74 @@ const detectDeadPatterns = async (context) => {
1439
1458
  return diagnostics;
1440
1459
  };
1441
1460
 
1461
+ //#endregion
1462
+ //#region src/engines/ai-slop/duplicate-imports.ts
1463
+ const JS_EXTENSIONS$3 = new Set([
1464
+ ".ts",
1465
+ ".tsx",
1466
+ ".js",
1467
+ ".jsx",
1468
+ ".mjs",
1469
+ ".cjs"
1470
+ ]);
1471
+ const IMPORT_FROM_RE$1 = /^\s*import\s+[^;]*?from\s+["']([^"']+)["']/;
1472
+ const extractImportLines = (content) => {
1473
+ const lines = content.split("\n");
1474
+ const results = [];
1475
+ for (let i = 0; i < lines.length; i++) {
1476
+ const line = lines[i];
1477
+ const match = IMPORT_FROM_RE$1.exec(line);
1478
+ if (!match) continue;
1479
+ results.push({
1480
+ spec: match[1],
1481
+ line: i + 1
1482
+ });
1483
+ }
1484
+ return results;
1485
+ };
1486
+ const detectDuplicateImports = async (context) => {
1487
+ const diagnostics = [];
1488
+ const files = getSourceFiles(context);
1489
+ for (const filePath of files) {
1490
+ if (!JS_EXTENSIONS$3.has(path.extname(filePath))) continue;
1491
+ if (isAutoGenerated(filePath)) continue;
1492
+ let content;
1493
+ try {
1494
+ content = fs.readFileSync(filePath, "utf-8");
1495
+ } catch {
1496
+ continue;
1497
+ }
1498
+ const imports = extractImportLines(content);
1499
+ if (imports.length < 2) continue;
1500
+ const bySpec = /* @__PURE__ */ new Map();
1501
+ for (const imp of imports) {
1502
+ const list = bySpec.get(imp.spec) ?? [];
1503
+ list.push(imp);
1504
+ bySpec.set(imp.spec, list);
1505
+ }
1506
+ const relPath = path.relative(context.rootDirectory, filePath);
1507
+ for (const [spec, occurrences] of bySpec) {
1508
+ if (occurrences.length < 2) continue;
1509
+ for (const dup of occurrences.slice(1)) {
1510
+ const firstLine = occurrences[0].line;
1511
+ diagnostics.push({
1512
+ filePath: relPath,
1513
+ engine: "ai-slop",
1514
+ rule: "ai-slop/duplicate-import",
1515
+ severity: "warning",
1516
+ message: `"${spec}" is also imported on line ${firstLine}. Merge into a single import statement.`,
1517
+ help: "Two imports from the same module split readers' attention and grow the import block. Run aislop fix to merge them automatically.",
1518
+ line: dup.line,
1519
+ column: 1,
1520
+ category: "AI Slop",
1521
+ fixable: true
1522
+ });
1523
+ }
1524
+ }
1525
+ }
1526
+ return diagnostics;
1527
+ };
1528
+
1442
1529
  //#endregion
1443
1530
  //#region src/engines/ai-slop/exceptions.ts
1444
1531
  const SWALLOWED_EXCEPTION_PATTERNS = [
@@ -1529,6 +1616,600 @@ const detectSwallowedExceptions = async (context) => {
1529
1616
  return diagnostics;
1530
1617
  };
1531
1618
 
1619
+ //#endregion
1620
+ //#region src/engines/ai-slop/go-patterns.ts
1621
+ const GO_EXTENSIONS = new Set([".go"]);
1622
+ const PACKAGE_DECL_RE = /^\s*package\s+(\w+)/;
1623
+ const PANIC_CALL_RE = /\bpanic\s*\(/;
1624
+ const COMMENT_LINE_RE$1 = /^\s*\/\//;
1625
+ const NIL_GUARD_RE = /^\s*if\s+[\w.]+(?:\(\))?\s*==\s*nil\s*\{?\s*$/;
1626
+ const SHORT_STRING_PANIC_RE = /\bpanic\s*\(\s*"[^"]{1,40}"\s*\)/;
1627
+ const detectPackageName = (lines) => {
1628
+ for (const line of lines) {
1629
+ const m = PACKAGE_DECL_RE.exec(line);
1630
+ if (m) return m[1];
1631
+ }
1632
+ return null;
1633
+ };
1634
+ const PANIC_INTENT_LOOKBACK = 3;
1635
+ const hasIntentComment$1 = (lines, panicLineIdx) => {
1636
+ for (let j = panicLineIdx - 1; j >= Math.max(0, panicLineIdx - PANIC_INTENT_LOOKBACK); j--) if (COMMENT_LINE_RE$1.test(lines[j])) return true;
1637
+ return false;
1638
+ };
1639
+ const isNilGuardPanic = (lines, panicLineIdx, line) => {
1640
+ if (!SHORT_STRING_PANIC_RE.test(line)) return false;
1641
+ for (let j = panicLineIdx - 1; j >= Math.max(0, panicLineIdx - 2); j--) {
1642
+ const prev = lines[j];
1643
+ if (prev.trim() === "") continue;
1644
+ return NIL_GUARD_RE.test(prev);
1645
+ }
1646
+ return false;
1647
+ };
1648
+ const flagLibraryPanic = (lines, relPath, pkg, out) => {
1649
+ if (pkg === "main") return;
1650
+ for (let i = 0; i < lines.length; i++) {
1651
+ const line = lines[i];
1652
+ if (COMMENT_LINE_RE$1.test(line)) continue;
1653
+ PANIC_CALL_RE.lastIndex = 0;
1654
+ if (!PANIC_CALL_RE.test(line)) continue;
1655
+ if (hasIntentComment$1(lines, i)) continue;
1656
+ if (isNilGuardPanic(lines, i, line)) continue;
1657
+ out.push({
1658
+ filePath: relPath,
1659
+ engine: "ai-slop",
1660
+ rule: "ai-slop/go-library-panic",
1661
+ severity: "warning",
1662
+ message: `\`panic()\` in package \`${pkg}\` (non-main, non-test). Library code should return errors, not unwind the goroutine.`,
1663
+ help: "Convert to `return fmt.Errorf(...)` (or a wrapped error) and let the caller decide. Reserve `panic` for genuinely-impossible states (corrupt internal invariants), and mark those with a comment so future readers know it's intentional.",
1664
+ line: i + 1,
1665
+ column: 1,
1666
+ category: "AI Slop",
1667
+ fixable: false
1668
+ });
1669
+ }
1670
+ };
1671
+ const detectGoPatterns = async (context) => {
1672
+ const diagnostics = [];
1673
+ const files = getSourceFiles(context);
1674
+ for (const filePath of files) {
1675
+ if (!GO_EXTENSIONS.has(path.extname(filePath))) continue;
1676
+ if (isAutoGenerated(filePath)) continue;
1677
+ if (filePath.endsWith("_test.go")) continue;
1678
+ let content;
1679
+ try {
1680
+ content = fs.readFileSync(filePath, "utf-8");
1681
+ } catch {
1682
+ continue;
1683
+ }
1684
+ const lines = content.split("\n");
1685
+ const pkg = detectPackageName(lines);
1686
+ if (!pkg) continue;
1687
+ flagLibraryPanic(lines, path.relative(context.rootDirectory, filePath), pkg, diagnostics);
1688
+ }
1689
+ return diagnostics;
1690
+ };
1691
+
1692
+ //#endregion
1693
+ //#region src/engines/ai-slop/python-data.ts
1694
+ const PYTHON_STDLIB = new Set([
1695
+ "__future__",
1696
+ "_thread",
1697
+ "abc",
1698
+ "argparse",
1699
+ "array",
1700
+ "ast",
1701
+ "asyncio",
1702
+ "atexit",
1703
+ "base64",
1704
+ "binascii",
1705
+ "bisect",
1706
+ "builtins",
1707
+ "bz2",
1708
+ "calendar",
1709
+ "codecs",
1710
+ "collections",
1711
+ "concurrent",
1712
+ "configparser",
1713
+ "contextlib",
1714
+ "contextvars",
1715
+ "copy",
1716
+ "csv",
1717
+ "ctypes",
1718
+ "dataclasses",
1719
+ "datetime",
1720
+ "decimal",
1721
+ "difflib",
1722
+ "dis",
1723
+ "doctest",
1724
+ "email",
1725
+ "encodings",
1726
+ "enum",
1727
+ "errno",
1728
+ "faulthandler",
1729
+ "filecmp",
1730
+ "fileinput",
1731
+ "fnmatch",
1732
+ "fractions",
1733
+ "functools",
1734
+ "gc",
1735
+ "getopt",
1736
+ "getpass",
1737
+ "gettext",
1738
+ "glob",
1739
+ "graphlib",
1740
+ "gzip",
1741
+ "hashlib",
1742
+ "heapq",
1743
+ "hmac",
1744
+ "html",
1745
+ "http",
1746
+ "imaplib",
1747
+ "importlib",
1748
+ "inspect",
1749
+ "io",
1750
+ "ipaddress",
1751
+ "itertools",
1752
+ "json",
1753
+ "keyword",
1754
+ "linecache",
1755
+ "locale",
1756
+ "logging",
1757
+ "lzma",
1758
+ "mailbox",
1759
+ "math",
1760
+ "mimetypes",
1761
+ "mmap",
1762
+ "multiprocessing",
1763
+ "numbers",
1764
+ "operator",
1765
+ "os",
1766
+ "pathlib",
1767
+ "pdb",
1768
+ "pickle",
1769
+ "platform",
1770
+ "plistlib",
1771
+ "pprint",
1772
+ "profile",
1773
+ "pstats",
1774
+ "pty",
1775
+ "queue",
1776
+ "quopri",
1777
+ "random",
1778
+ "re",
1779
+ "readline",
1780
+ "reprlib",
1781
+ "resource",
1782
+ "secrets",
1783
+ "select",
1784
+ "selectors",
1785
+ "shelve",
1786
+ "shlex",
1787
+ "shutil",
1788
+ "signal",
1789
+ "site",
1790
+ "smtplib",
1791
+ "socket",
1792
+ "socketserver",
1793
+ "sqlite3",
1794
+ "ssl",
1795
+ "stat",
1796
+ "statistics",
1797
+ "string",
1798
+ "stringprep",
1799
+ "struct",
1800
+ "subprocess",
1801
+ "sunau",
1802
+ "symtable",
1803
+ "sys",
1804
+ "sysconfig",
1805
+ "syslog",
1806
+ "tarfile",
1807
+ "telnetlib",
1808
+ "tempfile",
1809
+ "termios",
1810
+ "test",
1811
+ "textwrap",
1812
+ "threading",
1813
+ "time",
1814
+ "timeit",
1815
+ "tkinter",
1816
+ "token",
1817
+ "tokenize",
1818
+ "tomllib",
1819
+ "trace",
1820
+ "traceback",
1821
+ "tracemalloc",
1822
+ "tty",
1823
+ "turtle",
1824
+ "types",
1825
+ "typing",
1826
+ "unicodedata",
1827
+ "unittest",
1828
+ "urllib",
1829
+ "uu",
1830
+ "uuid",
1831
+ "venv",
1832
+ "warnings",
1833
+ "wave",
1834
+ "weakref",
1835
+ "webbrowser",
1836
+ "winreg",
1837
+ "winsound",
1838
+ "wsgiref",
1839
+ "xml",
1840
+ "xmlrpc",
1841
+ "zipapp",
1842
+ "zipfile",
1843
+ "zipimport",
1844
+ "zlib",
1845
+ "zoneinfo"
1846
+ ]);
1847
+ const PYTHON_IMPORT_TO_PIP = {
1848
+ yaml: "pyyaml",
1849
+ PIL: "pillow",
1850
+ dateutil: "python-dateutil",
1851
+ cv2: "opencv-python",
1852
+ sklearn: "scikit-learn",
1853
+ bs4: "beautifulsoup4",
1854
+ typing_extensions: "typing-extensions",
1855
+ google: "google-api-python-client",
1856
+ jose: "python-jose",
1857
+ jwt: "pyjwt",
1858
+ OpenSSL: "pyopenssl",
1859
+ magic: "python-magic",
1860
+ docx: "python-docx",
1861
+ pptx: "python-pptx",
1862
+ git: "gitpython",
1863
+ socks: "pysocks",
1864
+ redis: "redis"
1865
+ };
1866
+
1867
+ //#endregion
1868
+ //#region src/engines/ai-slop/hallucinated-imports.ts
1869
+ const JS_EXTENSIONS$2 = new Set([
1870
+ ".ts",
1871
+ ".tsx",
1872
+ ".js",
1873
+ ".jsx",
1874
+ ".mjs",
1875
+ ".cjs"
1876
+ ]);
1877
+ const PY_EXTENSIONS$2 = new Set([".py"]);
1878
+ const readJson = (filePath) => {
1879
+ try {
1880
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
1881
+ } catch {
1882
+ return null;
1883
+ }
1884
+ };
1885
+ const PKG_DEP_SECTIONS = [
1886
+ "dependencies",
1887
+ "devDependencies",
1888
+ "peerDependencies",
1889
+ "optionalDependencies"
1890
+ ];
1891
+ const addDepsFromPkg = (pkg, jsDeps) => {
1892
+ for (const section of PKG_DEP_SECTIONS) {
1893
+ const deps = pkg[section];
1894
+ if (deps && typeof deps === "object") for (const name of Object.keys(deps)) jsDeps.add(name);
1895
+ }
1896
+ };
1897
+ const readWorkspaceGlobs = (rootDir, rootPkg) => {
1898
+ const globs = [];
1899
+ if (rootPkg && typeof rootPkg === "object") {
1900
+ const ws = rootPkg.workspaces;
1901
+ if (Array.isArray(ws)) {
1902
+ for (const g of ws) if (typeof g === "string") globs.push(g);
1903
+ } else if (ws && typeof ws === "object") {
1904
+ const pkgs = ws.packages;
1905
+ if (Array.isArray(pkgs)) {
1906
+ for (const g of pkgs) if (typeof g === "string") globs.push(g);
1907
+ }
1908
+ }
1909
+ }
1910
+ const lerna = readJson(path.join(rootDir, "lerna.json"));
1911
+ if (lerna && Array.isArray(lerna.packages)) {
1912
+ for (const g of lerna.packages) if (typeof g === "string") globs.push(g);
1913
+ }
1914
+ try {
1915
+ const pnpmWs = fs.readFileSync(path.join(rootDir, "pnpm-workspace.yaml"), "utf-8");
1916
+ let inPackages = false;
1917
+ for (const rawLine of pnpmWs.split("\n")) {
1918
+ if (/^packages\s*:\s*$/.test(rawLine)) {
1919
+ inPackages = true;
1920
+ continue;
1921
+ }
1922
+ if (!inPackages) continue;
1923
+ if (/^\S/.test(rawLine)) break;
1924
+ const m = rawLine.match(/^\s*-\s*["']?([^"'\n]+?)["']?\s*$/);
1925
+ if (m) globs.push(m[1].trim());
1926
+ }
1927
+ } catch {}
1928
+ return globs;
1929
+ };
1930
+ const expandWorkspaceDirs = (rootDir, globs) => {
1931
+ const dirs = [];
1932
+ for (const glob of globs) if (glob.endsWith("/*")) {
1933
+ const parent = path.join(rootDir, glob.slice(0, -2));
1934
+ try {
1935
+ for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
1936
+ } catch {
1937
+ continue;
1938
+ }
1939
+ } else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
1940
+ return dirs;
1941
+ };
1942
+ const SKIP_DIRS = new Set([
1943
+ "node_modules",
1944
+ ".git",
1945
+ "dist",
1946
+ "build",
1947
+ "out",
1948
+ "target",
1949
+ "coverage"
1950
+ ]);
1951
+ const NESTED_PKG_JSON_DEPTH = 4;
1952
+ const collectNestedManifests = (rootDir, jsDeps) => {
1953
+ const walk = (dir, depth) => {
1954
+ if (depth > NESTED_PKG_JSON_DEPTH) return;
1955
+ let entries;
1956
+ try {
1957
+ entries = fs.readdirSync(dir, { withFileTypes: true });
1958
+ } catch {
1959
+ return;
1960
+ }
1961
+ for (const entry of entries) {
1962
+ if (entry.name.startsWith(".") && entry.name !== ".github") continue;
1963
+ if (SKIP_DIRS.has(entry.name)) continue;
1964
+ const full = path.join(dir, entry.name);
1965
+ if (entry.isDirectory()) walk(full, depth + 1);
1966
+ else if (entry.name === "package.json" && depth > 0) {
1967
+ const wsPkg = readJson(full);
1968
+ if (!wsPkg) continue;
1969
+ if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
1970
+ addDepsFromPkg(wsPkg, jsDeps);
1971
+ }
1972
+ }
1973
+ };
1974
+ walk(rootDir, 0);
1975
+ };
1976
+ const collectJsDeps = (rootDir, jsDeps) => {
1977
+ const pkgPath = path.join(rootDir, "package.json");
1978
+ if (!fs.existsSync(pkgPath)) return false;
1979
+ const pkg = readJson(pkgPath);
1980
+ if (!pkg || typeof pkg !== "object") return false;
1981
+ addDepsFromPkg(pkg, jsDeps);
1982
+ if (typeof pkg.name === "string") jsDeps.add(pkg.name);
1983
+ const workspaceDirs = expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, pkg));
1984
+ for (const wsDir of workspaceDirs) {
1985
+ const wsPkg = readJson(path.join(wsDir, "package.json"));
1986
+ if (!wsPkg) continue;
1987
+ if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
1988
+ addDepsFromPkg(wsPkg, jsDeps);
1989
+ }
1990
+ collectNestedManifests(rootDir, jsDeps);
1991
+ return true;
1992
+ };
1993
+ const addPyDep = (pyDeps, name) => {
1994
+ const normalized = name.toLowerCase().replace(/_/g, "-");
1995
+ pyDeps.add(normalized);
1996
+ };
1997
+ const collectFromRequirementsTxt = (rootDir, pyDeps) => {
1998
+ const reqPath = path.join(rootDir, "requirements.txt");
1999
+ if (!fs.existsSync(reqPath)) return false;
2000
+ try {
2001
+ const content = fs.readFileSync(reqPath, "utf-8");
2002
+ for (const line of content.split("\n")) {
2003
+ const trimmed = line.trim();
2004
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
2005
+ const match = trimmed.match(/^([a-zA-Z0-9_\-.]+)/);
2006
+ if (match) addPyDep(pyDeps, match[1]);
2007
+ }
2008
+ return true;
2009
+ } catch {
2010
+ return false;
2011
+ }
2012
+ };
2013
+ const collectFromPyproject = (rootDir, pyDeps) => {
2014
+ const pyprojPath = path.join(rootDir, "pyproject.toml");
2015
+ if (!fs.existsSync(pyprojPath)) return false;
2016
+ try {
2017
+ const content = fs.readFileSync(pyprojPath, "utf-8");
2018
+ const projectNameMatch = content.match(/\[project\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
2019
+ if (projectNameMatch) addPyDep(pyDeps, projectNameMatch[1]);
2020
+ const poetryNameMatch = content.match(/\[tool\.poetry\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
2021
+ if (poetryNameMatch) addPyDep(pyDeps, poetryNameMatch[1]);
2022
+ const pep621 = content.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
2023
+ if (pep621) for (const line of pep621[1].split("\n")) {
2024
+ const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
2025
+ if (m) addPyDep(pyDeps, m[1]);
2026
+ }
2027
+ const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
2028
+ let match = poetryRe.exec(content);
2029
+ while (match !== null) {
2030
+ for (const line of match[1].split("\n")) {
2031
+ const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
2032
+ if (m && m[1] !== "python") addPyDep(pyDeps, m[1]);
2033
+ }
2034
+ match = poetryRe.exec(content);
2035
+ }
2036
+ return true;
2037
+ } catch {
2038
+ return false;
2039
+ }
2040
+ };
2041
+ const collectFromPipfile = (rootDir, pyDeps) => {
2042
+ const pipfilePath = path.join(rootDir, "Pipfile");
2043
+ if (!fs.existsSync(pipfilePath)) return false;
2044
+ try {
2045
+ const content = fs.readFileSync(pipfilePath, "utf-8");
2046
+ const sectionRe = /\[(packages|dev-packages)\]([\s\S]*?)(?=\n\[|$)/g;
2047
+ let match = sectionRe.exec(content);
2048
+ while (match !== null) {
2049
+ for (const line of match[2].split("\n")) {
2050
+ const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
2051
+ if (m) addPyDep(pyDeps, m[1]);
2052
+ }
2053
+ match = sectionRe.exec(content);
2054
+ }
2055
+ return true;
2056
+ } catch {
2057
+ return false;
2058
+ }
2059
+ };
2060
+ const loadManifest = (rootDir) => {
2061
+ const jsDeps = /* @__PURE__ */ new Set();
2062
+ const pyDeps = /* @__PURE__ */ new Set();
2063
+ const hasJsManifest = collectJsDeps(rootDir, jsDeps);
2064
+ const hasReq = collectFromRequirementsTxt(rootDir, pyDeps);
2065
+ const hasPyproject = collectFromPyproject(rootDir, pyDeps);
2066
+ const hasPipfile = collectFromPipfile(rootDir, pyDeps);
2067
+ return {
2068
+ jsDeps,
2069
+ pyDeps,
2070
+ hasJsManifest,
2071
+ hasPyManifest: hasReq || hasPyproject || hasPipfile
2072
+ };
2073
+ };
2074
+ const isJsRelativeOrAbsolute = (spec) => spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("~/");
2075
+ const isJsBuiltin = (spec) => {
2076
+ return isBuiltin(spec.startsWith("node:") ? spec.slice(5) : spec) || isBuiltin(spec);
2077
+ };
2078
+ const VIRTUAL_MODULE_PREFIXES = [
2079
+ "astro:",
2080
+ "virtual:",
2081
+ "bun:"
2082
+ ];
2083
+ const isJsVirtualModule = (spec) => VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p));
2084
+ const TEMPLATE_PLACEHOLDER_RE = /\$\{/;
2085
+ const isLikelyRealImportSpec = (spec) => {
2086
+ if (spec.length === 0) return false;
2087
+ if (TEMPLATE_PLACEHOLDER_RE.test(spec)) return false;
2088
+ if (spec.includes("\\")) return false;
2089
+ if (/\s/.test(spec)) return false;
2090
+ return true;
2091
+ };
2092
+ const packageNameFromImport = (spec) => {
2093
+ if (spec.startsWith("@")) {
2094
+ const parts = spec.split("/");
2095
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : spec;
2096
+ }
2097
+ return spec.split("/")[0];
2098
+ };
2099
+ const STATIC_IMPORT_RE = /^\s*import\s+(?:[\w*{},\s]+\s+from\s+)?["']([^"']+)["']/;
2100
+ const DYNAMIC_IMPORT_RE = /(?:import|require)\s*\(\s*["']([^"']+)["']/g;
2101
+ const extractJsImports = (content) => {
2102
+ const lines = content.split("\n");
2103
+ const results = [];
2104
+ for (let i = 0; i < lines.length; i++) {
2105
+ const line = lines[i];
2106
+ const trimmed = line.trim();
2107
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
2108
+ const staticMatch = STATIC_IMPORT_RE.exec(line);
2109
+ if (staticMatch && isLikelyRealImportSpec(staticMatch[1])) results.push({
2110
+ spec: staticMatch[1],
2111
+ line: i + 1
2112
+ });
2113
+ DYNAMIC_IMPORT_RE.lastIndex = 0;
2114
+ let dyn = DYNAMIC_IMPORT_RE.exec(line);
2115
+ while (dyn !== null) {
2116
+ if (isLikelyRealImportSpec(dyn[1])) results.push({
2117
+ spec: dyn[1],
2118
+ line: i + 1
2119
+ });
2120
+ dyn = DYNAMIC_IMPORT_RE.exec(line);
2121
+ }
2122
+ }
2123
+ return results;
2124
+ };
2125
+ const extractPyImports = (content) => {
2126
+ const lines = content.split("\n");
2127
+ const results = [];
2128
+ for (let i = 0; i < lines.length; i++) {
2129
+ const line = lines[i].trim();
2130
+ if (line.startsWith("#")) continue;
2131
+ const fromMatch = line.match(/^from\s+([\w.]+)\s+import\b/);
2132
+ if (fromMatch && !fromMatch[1].startsWith(".")) {
2133
+ results.push({
2134
+ spec: fromMatch[1],
2135
+ line: i + 1
2136
+ });
2137
+ continue;
2138
+ }
2139
+ const importMatch = line.match(/^import\s+([\w.,\s]+?)(?:\s+as\s+\w+)?\s*$/);
2140
+ if (importMatch) for (const raw of importMatch[1].split(",")) {
2141
+ const cleaned = raw.trim().split(/\s+as\s+/)[0];
2142
+ if (cleaned && !cleaned.startsWith(".")) results.push({
2143
+ spec: cleaned,
2144
+ line: i + 1
2145
+ });
2146
+ }
2147
+ }
2148
+ return results;
2149
+ };
2150
+ const checkJsImport = (spec, manifest) => {
2151
+ if (isJsRelativeOrAbsolute(spec)) return null;
2152
+ if (isJsBuiltin(spec)) return null;
2153
+ if (isJsVirtualModule(spec)) return null;
2154
+ const pkg = packageNameFromImport(spec);
2155
+ if (manifest.jsDeps.has(pkg)) return null;
2156
+ if (pkg.startsWith("@types/")) {
2157
+ const realPkg = pkg.slice(7);
2158
+ if (manifest.jsDeps.has(realPkg)) return null;
2159
+ }
2160
+ return pkg;
2161
+ };
2162
+ const checkPyImport = (spec, manifest) => {
2163
+ const root = spec.split(".")[0];
2164
+ if (PYTHON_STDLIB.has(root)) return null;
2165
+ const normalized = root.toLowerCase().replace(/_/g, "-");
2166
+ if (manifest.pyDeps.has(normalized)) return null;
2167
+ const pipName = PYTHON_IMPORT_TO_PIP[root];
2168
+ if (pipName && manifest.pyDeps.has(pipName)) return null;
2169
+ return root;
2170
+ };
2171
+ const detectHallucinatedImports = async (context) => {
2172
+ const manifest = loadManifest(context.rootDirectory);
2173
+ if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
2174
+ const diagnostics = [];
2175
+ const files = getSourceFiles(context);
2176
+ for (const filePath of files) {
2177
+ const ext = path.extname(filePath);
2178
+ const isJs = JS_EXTENSIONS$2.has(ext);
2179
+ const isPy = PY_EXTENSIONS$2.has(ext);
2180
+ if (!isJs && !isPy) continue;
2181
+ if (isJs && !manifest.hasJsManifest) continue;
2182
+ if (isPy && !manifest.hasPyManifest) continue;
2183
+ if (isAutoGenerated(filePath)) continue;
2184
+ let content;
2185
+ try {
2186
+ content = fs.readFileSync(filePath, "utf-8");
2187
+ } catch {
2188
+ continue;
2189
+ }
2190
+ const relPath = path.relative(context.rootDirectory, filePath);
2191
+ const imports = isJs ? extractJsImports(content) : extractPyImports(content);
2192
+ for (const { spec, line } of imports) {
2193
+ const hallucinated = isJs ? checkJsImport(spec, manifest) : checkPyImport(spec, manifest);
2194
+ if (!hallucinated) continue;
2195
+ const manifestLabel = isJs ? "package.json" : "requirements.txt / pyproject.toml / Pipfile";
2196
+ diagnostics.push({
2197
+ filePath: relPath,
2198
+ engine: "ai-slop",
2199
+ rule: "ai-slop/hallucinated-import",
2200
+ severity: "error",
2201
+ message: `Imports "${hallucinated}" but it's not declared in ${manifestLabel}${isPy ? " and isn't Python stdlib" : ""}`,
2202
+ help: "Most often this is an LLM hallucinating a plausible-sounding package name. Either add the package to your manifest, or correct the import.",
2203
+ line,
2204
+ column: 1,
2205
+ category: "AI Slop",
2206
+ fixable: false
2207
+ });
2208
+ }
2209
+ }
2210
+ return diagnostics;
2211
+ };
2212
+
1532
2213
  //#endregion
1533
2214
  //#region src/engines/ai-slop/narrative-comments-patterns.ts
1534
2215
  const DECORATIVE_SEPARATOR = /^[-=─━~_*#]{6,}$/;
@@ -1645,6 +2326,7 @@ const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|re
1645
2326
 
1646
2327
  //#endregion
1647
2328
  //#region src/engines/ai-slop/narrative-comments.ts
2329
+ const NON_PRODUCTION_DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3)\//i;
1648
2330
  const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
1649
2331
  const stripLineComment = (line) => line.replace(/^\s*(?:(?:\/\/)|#)\s?/, "");
1650
2332
  const getCommentSyntax = (ext) => {
@@ -1673,6 +2355,10 @@ const getMatchedLinePrefix = (line, syntax) => {
1673
2355
  }
1674
2356
  return null;
1675
2357
  };
2358
+ const isRustDocCommentLine = (line) => {
2359
+ const trimmed = line.trimStart();
2360
+ return trimmed.startsWith("///") || trimmed.startsWith("//!");
2361
+ };
1676
2362
  const collectBlocks = (sourceLines, syntax) => {
1677
2363
  const blocks = [];
1678
2364
  let i = 0;
@@ -1688,6 +2374,8 @@ const collectBlocks = (sourceLines, syntax) => {
1688
2374
  }
1689
2375
  let next = i;
1690
2376
  while (next < sourceLines.length && sourceLines[next].trim() === "") next += 1;
2377
+ const docCandidates = raw.filter((l) => l.trim().length > 0);
2378
+ const isRustDoc = docCandidates.length > 0 && docCandidates.every((l) => isRustDocCommentLine(l));
1691
2379
  blocks.push({
1692
2380
  kind: "line",
1693
2381
  startLine: start + 1,
@@ -1695,6 +2383,7 @@ const collectBlocks = (sourceLines, syntax) => {
1695
2383
  rawLines: raw,
1696
2384
  prose: raw.map(stripLineComment),
1697
2385
  hasMeaningfulJsdocTag: false,
2386
+ isRustDoc,
1698
2387
  nextNonBlankLine: next < sourceLines.length ? sourceLines[next] : null
1699
2388
  });
1700
2389
  continue;
@@ -1728,6 +2417,7 @@ const collectBlocks = (sourceLines, syntax) => {
1728
2417
  rawLines: raw,
1729
2418
  prose,
1730
2419
  hasMeaningfulJsdocTag: hasMeaningful,
2420
+ isRustDoc: false,
1731
2421
  nextNonBlankLine: next < sourceLines.length ? sourceLines[next] : null
1732
2422
  });
1733
2423
  continue;
@@ -1778,6 +2468,22 @@ const nextLineLooksLikeDataEntry = (nextLine) => {
1778
2468
  return false;
1779
2469
  };
1780
2470
  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));
2471
+ const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
2472
+ const looksLikeGoDocComment = (block, ext) => {
2473
+ if (ext !== ".go" || block.kind !== "line") return false;
2474
+ const next = block.nextNonBlankLine;
2475
+ if (!next) return false;
2476
+ const declMatch = GO_DECL_NAME_RE.exec(next.trim());
2477
+ if (!declMatch) return false;
2478
+ return ((block.prose.find((l) => l.length > 0) ?? "").split(/\s+/)[0] ?? "") === declMatch[1];
2479
+ };
2480
+ const DOC_INDICATOR_RE = /`[^`]+`|\|\s*[-:]+\s*\||```|\b(?:note|warning|warn|caveat|example|caution|see):/i;
2481
+ const hasDocIndicator = (block) => {
2482
+ const joined = block.prose.join(" ");
2483
+ if (DOC_INDICATOR_RE.test(joined)) return true;
2484
+ for (const l of block.prose) if (/^[-]\s/.test(l)) return true;
2485
+ return false;
2486
+ };
1781
2487
  const detectNarrativeInBlock = (block, ext) => {
1782
2488
  if (looksLikeLicenseHeader(block)) return {
1783
2489
  matched: false,
@@ -1791,6 +2497,14 @@ const detectNarrativeInBlock = (block, ext) => {
1791
2497
  matched: false,
1792
2498
  reason: ""
1793
2499
  };
2500
+ if (block.isRustDoc) return {
2501
+ matched: false,
2502
+ reason: ""
2503
+ };
2504
+ if (looksLikeGoDocComment(block, ext)) return {
2505
+ matched: false,
2506
+ reason: ""
2507
+ };
1794
2508
  if (block.kind === "line" && block.prose.some((l) => DECORATIVE_SEPARATOR.test(l) || DECORATIVE_SECTION_HEADER.test(l))) return {
1795
2509
  matched: true,
1796
2510
  reason: "decorative separator"
@@ -1803,11 +2517,16 @@ const detectNarrativeInBlock = (block, ext) => {
1803
2517
  matched: true,
1804
2518
  reason: "bare section label"
1805
2519
  };
2520
+ const joined = block.prose.join(" ");
2521
+ const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
2522
+ if ((hasWhyMarker || hasDocIndicator(block)) && block.kind === "jsdoc") return {
2523
+ matched: false,
2524
+ reason: ""
2525
+ };
1806
2526
  if (block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
1807
2527
  matched: true,
1808
2528
  reason: block.kind === "jsdoc" ? "JSDoc preamble before declaration" : "multi-line preamble before declaration"
1809
2529
  };
1810
- const joined = block.prose.join(" ");
1811
2530
  if (CROSS_REFERENCE_PHRASES.some((re) => re.test(joined))) return {
1812
2531
  matched: true,
1813
2532
  reason: "cross-reference commentary"
@@ -1823,8 +2542,6 @@ const detectNarrativeInBlock = (block, ext) => {
1823
2542
  reason: "explanatory preamble"
1824
2543
  };
1825
2544
  const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
1826
- const joinedProse = block.prose.join(" ");
1827
- const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joinedProse);
1828
2545
  if (nonEmptyProseCount >= 5) return {
1829
2546
  matched: true,
1830
2547
  reason: "long narrative block"
@@ -1847,6 +2564,8 @@ const detectNarrativeComments = async (context) => {
1847
2564
  if (isAutoGenerated(filePath)) continue;
1848
2565
  const syntax = getCommentSyntax(ext);
1849
2566
  if (!syntax) continue;
2567
+ const relativePath = path.relative(context.rootDirectory, filePath);
2568
+ if (NON_PRODUCTION_DIR_PATTERN.test(relativePath)) continue;
1850
2569
  let content;
1851
2570
  try {
1852
2571
  content = fs.readFileSync(filePath, "utf-8");
@@ -1854,7 +2573,6 @@ const detectNarrativeComments = async (context) => {
1854
2573
  continue;
1855
2574
  }
1856
2575
  const blocks = collectBlocks(content.split("\n"), syntax);
1857
- const relativePath = filePath.replace(`${context.rootDirectory}/`, "");
1858
2576
  for (const block of blocks) {
1859
2577
  const { matched, reason } = detectNarrativeInBlock(block, ext);
1860
2578
  if (!matched) continue;
@@ -1874,48 +2592,293 @@ const detectNarrativeComments = async (context) => {
1874
2592
  }
1875
2593
  return diagnostics;
1876
2594
  };
1877
- const fixNarrativeComments = async (context) => {
1878
- const diagnostics = await detectNarrativeComments(context);
1879
- if (diagnostics.length === 0) return;
1880
- const byFile = /* @__PURE__ */ new Map();
1881
- for (const d of diagnostics) {
1882
- const abs = d.filePath.startsWith("/") ? d.filePath : `${context.rootDirectory}/${d.filePath}`;
1883
- const list = byFile.get(abs) ?? [];
1884
- list.push(d);
1885
- byFile.set(abs, list);
2595
+
2596
+ //#endregion
2597
+ //#region src/engines/ai-slop/python-patterns.ts
2598
+ const PY_EXTENSIONS$1 = new Set([".py"]);
2599
+ const BARE_EXCEPT_RE = /^\s*except\s*:\s*(?:#.*)?$/;
2600
+ const BROAD_EXCEPT_RE = /^\s*except\s+(Exception|BaseException)\s*(?:as\s+\w+)?\s*:\s*(?:#.*)?$/;
2601
+ const PRINT_RE = /^\s*print\s*\(/;
2602
+ const DEF_RE = /^\s*(?:async\s+)?def\s+\w+\s*\(/;
2603
+ const MUTABLE_DEFAULT_RE = /(\w+)\s*(?::\s*[^,)=]+)?\s*=\s*(\[\s*\]|\{\s*\}|set\(\s*\))/;
2604
+ 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");
2605
+ const isScriptOrEntrypoint = (basename) => basename === "__main__.py" || basename === "manage.py" || basename === "setup.py";
2606
+ const SCRIPT_DIR_NAMES = new Set([
2607
+ "scripts",
2608
+ "bin",
2609
+ ".github",
2610
+ "action",
2611
+ "docs",
2612
+ "docs_src",
2613
+ "examples",
2614
+ "example"
2615
+ ]);
2616
+ const isInScriptDir = (relPath) => relPath.split(path.sep).some((seg) => SCRIPT_DIR_NAMES.has(seg));
2617
+ const isTutorialFile = (basename) => basename.startsWith("tutorial") && basename.endsWith(".py");
2618
+ const MAIN_GUARD_RE = /^\s*if\s+__name__\s*==\s*["']__main__["']\s*:/;
2619
+ const hasMainGuard = (lines) => lines.some((l) => MAIN_GUARD_RE.test(l));
2620
+ const buildDocstringRanges = (lines) => {
2621
+ const inside = /* @__PURE__ */ new Set();
2622
+ let openDelim = null;
2623
+ for (let i = 0; i < lines.length; i++) {
2624
+ const line = lines[i];
2625
+ if (openDelim) {
2626
+ inside.add(i);
2627
+ if (line.includes(openDelim)) openDelim = null;
2628
+ continue;
2629
+ }
2630
+ for (const delim of ["\"\"\"", "'''"]) {
2631
+ const first = line.indexOf(delim);
2632
+ if (first === -1) continue;
2633
+ if (line.indexOf(delim, first + 3) === -1) {
2634
+ openDelim = delim;
2635
+ inside.add(i);
2636
+ break;
2637
+ }
2638
+ }
1886
2639
  }
1887
- for (const [filePath, diags] of byFile) {
1888
- const syntax = getCommentSyntax(path.extname(filePath));
1889
- if (!syntax) continue;
2640
+ return inside;
2641
+ };
2642
+ const pushFinding = (out, a) => {
2643
+ out.push({
2644
+ filePath: a.relPath,
2645
+ engine: "ai-slop",
2646
+ rule: a.rule,
2647
+ severity: a.severity,
2648
+ message: a.message,
2649
+ help: a.help,
2650
+ line: a.line,
2651
+ column: 1,
2652
+ category: "AI Slop",
2653
+ fixable: false
2654
+ });
2655
+ };
2656
+ const flagBareExcept = (lines, relPath, out) => {
2657
+ for (let i = 0; i < lines.length; i++) {
2658
+ if (!BARE_EXCEPT_RE.test(lines[i])) continue;
2659
+ pushFinding(out, {
2660
+ relPath,
2661
+ rule: "ai-slop/python-bare-except",
2662
+ severity: "warning",
2663
+ message: "Bare `except:` swallows every exception including KeyboardInterrupt and SystemExit.",
2664
+ help: "Catch the specific exception type you actually expect (`except ValueError:`, `except (KeyError, IndexError):`). If you genuinely want everything, `except BaseException:` plus a re-raise or log makes the intent explicit.",
2665
+ line: i + 1
2666
+ });
2667
+ }
2668
+ };
2669
+ const flagBroadExceptWithSilentBody = (lines, relPath, out) => {
2670
+ for (let i = 0; i < lines.length; i++) {
2671
+ const match = BROAD_EXCEPT_RE.exec(lines[i]);
2672
+ if (!match) continue;
2673
+ const trimmedNext = (lines[i + 1] ?? "").trim();
2674
+ if (!(trimmedNext === "pass" || trimmedNext.startsWith("#") && (lines[i + 2] ?? "").trim() === "pass")) continue;
2675
+ pushFinding(out, {
2676
+ relPath,
2677
+ rule: "ai-slop/python-broad-except",
2678
+ severity: "warning",
2679
+ message: `\`except ${match[1]}: pass\` silently drops every exception. Failures vanish without a trace.`,
2680
+ help: "Either narrow the exception class (`except ValueError:`), log the error, or re-raise. If you genuinely intend to swallow, add a comment naming the specific failure mode you're handling — auditors will thank you.",
2681
+ line: i + 1
2682
+ });
2683
+ }
2684
+ };
2685
+ const flagMutableDefaults = (lines, relPath, out) => {
2686
+ let i = 0;
2687
+ while (i < lines.length) {
2688
+ if (!DEF_RE.test(lines[i])) {
2689
+ i++;
2690
+ continue;
2691
+ }
2692
+ const startLine = i;
2693
+ let signature = lines[i];
2694
+ let parenDepth = 0;
2695
+ for (const ch of signature) if (ch === "(") parenDepth++;
2696
+ else if (ch === ")") parenDepth--;
2697
+ while (parenDepth > 0 && i + 1 < lines.length) {
2698
+ i++;
2699
+ signature += `\n${lines[i]}`;
2700
+ for (const ch of lines[i]) if (ch === "(") parenDepth++;
2701
+ else if (ch === ")") parenDepth--;
2702
+ }
2703
+ MUTABLE_DEFAULT_RE.lastIndex = 0;
2704
+ const found = MUTABLE_DEFAULT_RE.exec(signature);
2705
+ if (found) pushFinding(out, {
2706
+ relPath,
2707
+ rule: "ai-slop/python-mutable-default",
2708
+ severity: "warning",
2709
+ message: `Mutable default argument \`${found[1]}=${found[2]}\`. The default is shared across all calls — bugs that look like state-leakage.`,
2710
+ help: "Use `None` as the default and create the mutable value inside the body: `def f(items=None): items = items if items is not None else []`. Standard Python idiom; anything else is the AI agent shortcutting.",
2711
+ line: startLine + 1
2712
+ });
2713
+ i++;
2714
+ }
2715
+ };
2716
+ const flagPrintInProduction = (lines, relPath, basename, out) => {
2717
+ if (isTestFile$1(relPath, basename) || isScriptOrEntrypoint(basename)) return;
2718
+ if (isInScriptDir(relPath)) return;
2719
+ if (isTutorialFile(basename)) return;
2720
+ if (hasMainGuard(lines)) return;
2721
+ const docstringLines = buildDocstringRanges(lines);
2722
+ for (let i = 0; i < lines.length; i++) {
2723
+ const line = lines[i];
2724
+ if (!PRINT_RE.test(line)) continue;
2725
+ if (line.trim().startsWith("#")) continue;
2726
+ if (docstringLines.has(i)) continue;
2727
+ pushFinding(out, {
2728
+ relPath,
2729
+ rule: "ai-slop/python-print-debug",
2730
+ severity: "warning",
2731
+ message: "`print()` in production code — usually a leftover debug statement.",
2732
+ help: "Use the project's logger (`logging.getLogger(__name__).info(...)`). If this file is genuinely a CLI entry point (typer/click/argparse), it's safe to ignore — but rename to `__main__.py` or move under `scripts/` so the rule skips it next time.",
2733
+ line: i + 1
2734
+ });
2735
+ }
2736
+ };
2737
+ const detectPythonPatterns = async (context) => {
2738
+ const diagnostics = [];
2739
+ const files = getSourceFiles(context);
2740
+ for (const filePath of files) {
2741
+ if (!PY_EXTENSIONS$1.has(path.extname(filePath))) continue;
2742
+ if (isAutoGenerated(filePath)) continue;
1890
2743
  let content;
1891
2744
  try {
1892
2745
  content = fs.readFileSync(filePath, "utf-8");
1893
2746
  } catch {
1894
2747
  continue;
1895
2748
  }
2749
+ const relPath = path.relative(context.rootDirectory, filePath);
2750
+ const basename = path.basename(filePath);
1896
2751
  const lines = content.split("\n");
1897
- const blocks = collectBlocks(lines, syntax);
1898
- const toRemove = /* @__PURE__ */ new Set();
1899
- for (const d of diags) {
1900
- const block = blocks.find((b) => b.startLine === d.line);
1901
- if (!block) continue;
1902
- for (let ln = block.startLine; ln <= block.endLine; ln += 1) toRemove.add(ln);
1903
- const prev = block.startLine - 1;
1904
- const next = block.endLine + 1;
1905
- const prevIsBlank = prev >= 1 && lines[prev - 1]?.trim() === "";
1906
- const nextIsBlank = next <= lines.length && lines[next - 1]?.trim() === "";
1907
- if (prevIsBlank && nextIsBlank) toRemove.add(prev);
2752
+ flagBareExcept(lines, relPath, diagnostics);
2753
+ flagBroadExceptWithSilentBody(lines, relPath, diagnostics);
2754
+ flagMutableDefaults(lines, relPath, diagnostics);
2755
+ flagPrintInProduction(lines, relPath, basename, diagnostics);
2756
+ }
2757
+ return diagnostics;
2758
+ };
2759
+
2760
+ //#endregion
2761
+ //#region src/engines/ai-slop/rust-patterns.ts
2762
+ const RUST_EXTENSIONS = new Set([".rs"]);
2763
+ const UNWRAP_CALL_RE = /\.unwrap\s*\(\s*\)/;
2764
+ const TODO_MACRO_RE = /\b(todo|unimplemented)\s*!\s*\(/;
2765
+ const COMMENT_LINE_RE = /^\s*\/\//;
2766
+ const TEST_ATTR_RE = /^\s*#\s*\[\s*(?:cfg\s*\(\s*test\s*\)|test|tokio::test)/;
2767
+ const WRITELN_UNWRAP_RE = /\b(?:writeln|write)\s*!\s*\([^)]*\)\s*\.unwrap\s*\(\s*\)/;
2768
+ const TEST_BASENAMES = new Set([
2769
+ "tests.rs",
2770
+ "testutil.rs",
2771
+ "test_util.rs",
2772
+ "test_utils.rs",
2773
+ "build.rs"
2774
+ ]);
2775
+ const TEST_CRATE_SEGMENT_RE = /(?:^|[-_])tests?(?:$|[-_])/;
2776
+ const isTestFile = (relPath) => {
2777
+ const segments = relPath.split(path.sep);
2778
+ if (segments.some((s) => TEST_CRATE_SEGMENT_RE.test(s))) return true;
2779
+ const basename = segments[segments.length - 1] ?? "";
2780
+ if (TEST_BASENAMES.has(basename)) return true;
2781
+ return basename.endsWith("_tests.rs") || basename.endsWith("_testutil.rs");
2782
+ };
2783
+ const isExampleFile = (relPath) => relPath.split(path.sep).some((seg) => seg === "examples" || seg === "benches");
2784
+ const UNWRAP_INTENT_LOOKBACK = 2;
2785
+ const hasIntentComment = (lines, lineIdx) => {
2786
+ for (let j = lineIdx - 1; j >= Math.max(0, lineIdx - UNWRAP_INTENT_LOOKBACK); j--) if (COMMENT_LINE_RE.test(lines[j])) return true;
2787
+ return false;
2788
+ };
2789
+ const buildTestRanges = (lines) => {
2790
+ const ranges = [];
2791
+ for (let i = 0; i < lines.length; i++) {
2792
+ if (!TEST_ATTR_RE.test(lines[i])) continue;
2793
+ const openLine = i;
2794
+ let depth = 0;
2795
+ let started = false;
2796
+ for (let j = i; j < lines.length; j++) {
2797
+ const line = lines[j];
2798
+ for (const ch of line) if (ch === "{") {
2799
+ depth++;
2800
+ started = true;
2801
+ } else if (ch === "}") depth--;
2802
+ if (started && depth === 0) {
2803
+ ranges.push([openLine, j]);
2804
+ i = j;
2805
+ break;
2806
+ }
1908
2807
  }
1909
- const kept = [];
1910
- for (let i = 0; i < lines.length; i += 1) if (!toRemove.has(i + 1)) kept.push(lines[i]);
1911
- const newContent = kept.join("\n");
1912
- if (newContent !== content) fs.writeFileSync(filePath, newContent);
2808
+ }
2809
+ return ranges;
2810
+ };
2811
+ const isInRange = (ranges, lineIdx) => ranges.some(([start, end]) => lineIdx >= start && lineIdx <= end);
2812
+ const flagNonTestUnwrap = (lines, relPath, testRanges, out) => {
2813
+ for (let i = 0; i < lines.length; i++) {
2814
+ const line = lines[i];
2815
+ if (COMMENT_LINE_RE.test(line)) continue;
2816
+ if (isInRange(testRanges, i)) continue;
2817
+ if (!UNWRAP_CALL_RE.test(line)) continue;
2818
+ if (WRITELN_UNWRAP_RE.test(line)) continue;
2819
+ if (hasIntentComment(lines, i)) continue;
2820
+ out.push({
2821
+ filePath: relPath,
2822
+ engine: "ai-slop",
2823
+ rule: "ai-slop/rust-non-test-unwrap",
2824
+ severity: "warning",
2825
+ message: "`.unwrap()` in non-test code panics on None/Err. Surfaces as a hard crash for the caller.",
2826
+ help: "Use `?` to propagate, `.expect(\"context\")` if you really mean it (and the message names the invariant), or pattern-match the variant you care about. Reserve raw `.unwrap()` for tests and prototypes.",
2827
+ line: i + 1,
2828
+ column: 1,
2829
+ category: "AI Slop",
2830
+ fixable: false
2831
+ });
1913
2832
  }
1914
2833
  };
2834
+ const flagTodoMacro = (lines, relPath, out) => {
2835
+ for (let i = 0; i < lines.length; i++) {
2836
+ const line = lines[i];
2837
+ if (COMMENT_LINE_RE.test(line)) continue;
2838
+ const match = TODO_MACRO_RE.exec(line);
2839
+ if (!match) continue;
2840
+ out.push({
2841
+ filePath: relPath,
2842
+ engine: "ai-slop",
2843
+ rule: "ai-slop/rust-todo-stub",
2844
+ severity: "warning",
2845
+ message: `\`${match[1]}!()\` panics at runtime — almost certainly a stub the agent forgot to fill in.`,
2846
+ help: "Implement the missing path or remove it. If the work is genuinely deferred, file a ticket and put the number in a comment next to the macro so it doesn't ship invisibly.",
2847
+ line: i + 1,
2848
+ column: 1,
2849
+ category: "AI Slop",
2850
+ fixable: false
2851
+ });
2852
+ }
2853
+ };
2854
+ const detectRustPatterns = async (context) => {
2855
+ const diagnostics = [];
2856
+ const files = getSourceFiles(context);
2857
+ for (const filePath of files) {
2858
+ if (!RUST_EXTENSIONS.has(path.extname(filePath))) continue;
2859
+ if (isAutoGenerated(filePath)) continue;
2860
+ let content;
2861
+ try {
2862
+ content = fs.readFileSync(filePath, "utf-8");
2863
+ } catch {
2864
+ continue;
2865
+ }
2866
+ const relPath = path.relative(context.rootDirectory, filePath);
2867
+ const lines = content.split("\n");
2868
+ if (isExampleFile(relPath)) continue;
2869
+ if (isTestFile(relPath)) {
2870
+ flagTodoMacro(lines, relPath, diagnostics);
2871
+ continue;
2872
+ }
2873
+ flagNonTestUnwrap(lines, relPath, buildTestRanges(lines), diagnostics);
2874
+ flagTodoMacro(lines, relPath, diagnostics);
2875
+ }
2876
+ return diagnostics;
2877
+ };
1915
2878
 
1916
2879
  //#endregion
1917
2880
  //#region src/engines/ai-slop/unused-imports.ts
1918
- const JS_EXTENSIONS = new Set([
2881
+ const JS_EXTENSIONS$1 = new Set([
1919
2882
  ".ts",
1920
2883
  ".tsx",
1921
2884
  ".js",
@@ -2045,7 +3008,7 @@ const analyzeFile = (filePath) => {
2045
3008
  const lines = content.split("\n");
2046
3009
  let symbols;
2047
3010
  let importLines;
2048
- if (JS_EXTENSIONS.has(ext)) {
3011
+ if (JS_EXTENSIONS$1.has(ext)) {
2049
3012
  const result = extractJsImportedSymbols(lines);
2050
3013
  symbols = result.symbols;
2051
3014
  importLines = result.importLines;
@@ -2101,7 +3064,12 @@ const aiSlopEngine = {
2101
3064
  detectOverAbstraction(context),
2102
3065
  detectDeadPatterns(context),
2103
3066
  detectUnusedImports(context),
2104
- detectNarrativeComments(context)
3067
+ detectNarrativeComments(context),
3068
+ detectDuplicateImports(context),
3069
+ detectPythonPatterns(context),
3070
+ detectGoPatterns(context),
3071
+ detectRustPatterns(context),
3072
+ detectHallucinatedImports(context)
2105
3073
  ]);
2106
3074
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
2107
3075
  return {
@@ -2485,6 +3453,12 @@ const isDataFile = (content) => {
2485
3453
  const dataLinePattern = /^\s*[{}[\]"']/;
2486
3454
  return nonEmpty.filter((l) => dataLinePattern.test(l)).length / nonEmpty.length > .8;
2487
3455
  };
3456
+ const TEST_PATH_RE = /(?:^|\/)(?:tests?|spec|specs|__tests__|__spec__|src\/test)\//i;
3457
+ const TEST_BASENAME_RE = /(?:^|[/.])(?:test_[\w-]+\.(?:py|rb)|[\w-]+_(?:test|spec)\.(?:py|rb|go|rs)|[\w-]+\.(?:test|spec)\.(?:[jt]sx?|mjs|cjs)|conftest\.py|[A-Z]\w*Tests?\.(?:java|cs|php))$/;
3458
+ const MIGRATION_PATH_RE = /(?:^|\/)(?:migrations?|migrate|prisma\/migrations|db\/migrate)\//i;
3459
+ const FIXTURE_PATH_RE = /(?:^|\/)(?:__fixtures__|__snapshots__|__mocks__|fixtures?|snapshots?|seeds?|stubs?)\//i;
3460
+ const GENERATED_PATH_RE = /(?:^|\/)(?:generated|gen|build|dist|out|target|coverage|node_modules|vendor|\.next|\.nuxt|\.svelte-kit)\//i;
3461
+ const isExemptFromComplexity = (relativePath) => TEST_PATH_RE.test(relativePath) || TEST_BASENAME_RE.test(relativePath) || MIGRATION_PATH_RE.test(relativePath) || FIXTURE_PATH_RE.test(relativePath) || GENERATED_PATH_RE.test(relativePath);
2488
3462
  const analyzeFunctions = (content, ext) => {
2489
3463
  const lines = content.split("\n");
2490
3464
  const functions = [];
@@ -2513,13 +3487,13 @@ const checkFileDiagnostics = (relativePath, content, limits) => {
2513
3487
  const lineCount = content.split("\n").length;
2514
3488
  const ext = path.extname(relativePath).toLowerCase();
2515
3489
  if (isDataFile(content)) return results;
2516
- const effectiveMax = ext === ".jsx" || ext === ".tsx" ? Math.ceil(limits.maxFileLoc * JSX_FILE_LOC_MULTIPLIER) : limits.maxFileLoc;
2517
- if (lineCount > effectiveMax) results.push({
3490
+ const configuredMax = ext === ".jsx" || ext === ".tsx" ? Math.ceil(limits.maxFileLoc * JSX_FILE_LOC_MULTIPLIER) : limits.maxFileLoc;
3491
+ if (lineCount > Math.ceil(configuredMax * 1.1)) results.push({
2518
3492
  filePath: relativePath,
2519
3493
  engine: "code-quality",
2520
3494
  rule: "complexity/file-too-large",
2521
3495
  severity: "warning",
2522
- message: `File has ${lineCount} lines (max: ${effectiveMax})`,
3496
+ message: `File has ${lineCount} lines (max: ${configuredMax})`,
2523
3497
  help: "Consider splitting this file into smaller modules",
2524
3498
  line: 0,
2525
3499
  column: 0,
@@ -2569,13 +3543,14 @@ const checkFunctionDiagnostics = (relativePath, fn, limits) => {
2569
3543
  return results;
2570
3544
  };
2571
3545
  const checkFileComplexity = (filePath, rootDirectory, limits) => {
3546
+ const relativePath = path.relative(rootDirectory, filePath);
3547
+ if (isExemptFromComplexity(relativePath)) return [];
2572
3548
  let content;
2573
3549
  try {
2574
3550
  content = fs.readFileSync(filePath, "utf-8");
2575
3551
  } catch {
2576
3552
  return [];
2577
3553
  }
2578
- const relativePath = path.relative(rootDirectory, filePath);
2579
3554
  const ext = path.extname(filePath).toLowerCase();
2580
3555
  const diagnostics = checkFileDiagnostics(relativePath, content, limits);
2581
3556
  for (const fn of analyzeFunctions(content, ext)) diagnostics.push(...checkFunctionDiagnostics(relativePath, fn, limits));
@@ -3957,7 +4932,10 @@ const lintEngine = {
3957
4932
  const diagnostics = [];
3958
4933
  const { languages, installedTools } = context;
3959
4934
  const promises = [];
3960
- if (languages.includes("typescript") || languages.includes("javascript")) promises.push(runOxlint(context));
4935
+ if (languages.includes("typescript") || languages.includes("javascript")) {
4936
+ promises.push(runOxlint(context));
4937
+ if (context.config.lint.typecheck) promises.push(import("./typecheck-XJMuCczG.js").then((mod) => mod.runTypecheck(context)));
4938
+ }
3961
4939
  if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-Bz0LZhQ6.js").then((n) => n.t).then((mod) => mod.runExpoDoctor(context)));
3962
4940
  if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
3963
4941
  if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
@@ -5315,6 +6293,7 @@ const scanCommand = async (directory, config, options) => {
5315
6293
  const engineConfig = {
5316
6294
  quality: config.quality,
5317
6295
  security: config.security,
6296
+ lint: config.lint,
5318
6297
  architectureRulesPath: config.engines.architecture ? rulesPath : void 0
5319
6298
  };
5320
6299
  const gridRows = ALL_ENGINE_NAMES.filter((engine) => config.engines[engine] !== false).map((engine) => ({
@@ -5382,7 +6361,7 @@ const scanCommand = async (directory, config, options) => {
5382
6361
  });
5383
6362
  }
5384
6363
  if (options.json) {
5385
- const { buildJsonOutput } = await import("./json-DwAcCqqG.js");
6364
+ const { buildJsonOutput } = await import("./json-BJGLCIK-.js");
5386
6365
  const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
5387
6366
  console.log(JSON.stringify(jsonOut, null, 2));
5388
6367
  return { exitCode };
@@ -5737,6 +6716,212 @@ const applyEditsAndCollapse = (lines, linesToRemove, lineReplacements) => {
5737
6716
  return collapsed.join("\n");
5738
6717
  };
5739
6718
 
6719
+ //#endregion
6720
+ //#region src/engines/ai-slop/duplicate-imports-fix.ts
6721
+ const JS_EXTENSIONS = new Set([
6722
+ ".ts",
6723
+ ".tsx",
6724
+ ".js",
6725
+ ".jsx",
6726
+ ".mjs",
6727
+ ".cjs"
6728
+ ]);
6729
+ const IMPORT_FROM_RE = /^\s*import\s+(.*?)\s+from\s+["']([^"']+)["']\s*;?\s*$/;
6730
+ const SIDE_EFFECT_RE = /^\s*import\s+["']([^"']+)["']\s*;?\s*$/;
6731
+ const parseNamedClause = (clause) => {
6732
+ const inner = clause.trim().slice(1, -1).trim();
6733
+ if (inner.length === 0) return [];
6734
+ const items = [];
6735
+ for (const part of inner.split(",")) {
6736
+ const trimmed = part.trim();
6737
+ if (!trimmed) continue;
6738
+ let isType = false;
6739
+ let working = trimmed;
6740
+ if (/^type\s+/.test(working)) {
6741
+ isType = true;
6742
+ working = working.replace(/^type\s+/, "");
6743
+ }
6744
+ const aliasMatch = working.match(/^(\w+)\s+as\s+(\w+)$/);
6745
+ if (aliasMatch) {
6746
+ items.push({
6747
+ name: aliasMatch[1],
6748
+ alias: aliasMatch[2],
6749
+ isType
6750
+ });
6751
+ continue;
6752
+ }
6753
+ if (/^\w+$/.test(working)) items.push({
6754
+ name: working,
6755
+ isType
6756
+ });
6757
+ }
6758
+ return items;
6759
+ };
6760
+ const parseImportClause = (clause) => {
6761
+ let rest = clause.trim();
6762
+ let isTypeOnly = false;
6763
+ if (/^type\s+/.test(rest)) {
6764
+ isTypeOnly = true;
6765
+ rest = rest.replace(/^type\s+/, "");
6766
+ }
6767
+ const out = {
6768
+ named: [],
6769
+ isTypeOnly
6770
+ };
6771
+ const defMatch = rest.match(/^([A-Za-z_$][\w$]*)\s*(?:,\s*(.+))?$/);
6772
+ if (defMatch && !rest.startsWith("{") && !rest.startsWith("*")) {
6773
+ out.default = defMatch[1];
6774
+ rest = defMatch[2]?.trim() ?? "";
6775
+ }
6776
+ if (rest.startsWith("*")) {
6777
+ const nsMatch = rest.match(/^\*\s+as\s+(\w+)/);
6778
+ if (nsMatch) out.namespace = nsMatch[1];
6779
+ return out;
6780
+ }
6781
+ if (rest.startsWith("{")) out.named = parseNamedClause(rest);
6782
+ return out;
6783
+ };
6784
+ const parseImportLine = (line, lineIndex) => {
6785
+ const sideEffect = line.match(SIDE_EFFECT_RE);
6786
+ if (sideEffect) return {
6787
+ lineIndex,
6788
+ module: sideEffect[1],
6789
+ named: [],
6790
+ isTypeOnly: false,
6791
+ isSideEffect: true
6792
+ };
6793
+ const m = line.match(IMPORT_FROM_RE);
6794
+ if (!m) return null;
6795
+ return {
6796
+ lineIndex,
6797
+ module: m[2],
6798
+ isSideEffect: false,
6799
+ ...parseImportClause(m[1])
6800
+ };
6801
+ };
6802
+ const formatNamed = (n, stripType) => {
6803
+ const prefix = n.isType && !stripType ? "type " : "";
6804
+ const suffix = n.alias ? ` as ${n.alias}` : "";
6805
+ return `${prefix}${n.name}${suffix}`;
6806
+ };
6807
+ const mergeImports = (group) => {
6808
+ if (group.some((s) => s.isSideEffect)) return null;
6809
+ if (group.some((s) => s.namespace !== void 0)) return null;
6810
+ if (group.some((s) => s.isTypeOnly && s.default !== void 0)) return null;
6811
+ const defaults = group.map((s) => s.default).filter((d) => d !== void 0);
6812
+ const uniqueDefaults = Array.from(new Set(defaults));
6813
+ if (uniqueDefaults.length > 1) return null;
6814
+ const defaultName = uniqueDefaults[0];
6815
+ const merged = /* @__PURE__ */ new Map();
6816
+ for (const stmt of group) for (const nm of stmt.named) {
6817
+ const key = nm.alias ?? nm.name;
6818
+ const isType = nm.isType || stmt.isTypeOnly;
6819
+ const existing = merged.get(key);
6820
+ if (!existing) merged.set(key, {
6821
+ ...nm,
6822
+ isType
6823
+ });
6824
+ else existing.isType = existing.isType && isType;
6825
+ }
6826
+ const insertionOrder = Array.from(merged.values());
6827
+ const namedList = [...insertionOrder.filter((n) => !n.isType), ...insertionOrder.filter((n) => n.isType)];
6828
+ const allTypeOnly = namedList.length > 0 && namedList.every((n) => n.isType);
6829
+ const module = group[0].module;
6830
+ if (!defaultName && namedList.length === 0) return null;
6831
+ if (!defaultName && allTypeOnly) return `import type { ${namedList.map((n) => formatNamed(n, true)).join(", ")} } from "${module}";`;
6832
+ const parts = [];
6833
+ if (defaultName) parts.push(defaultName);
6834
+ if (namedList.length > 0) {
6835
+ const items = namedList.map((n) => formatNamed(n, false)).join(", ");
6836
+ parts.push(`{ ${items} }`);
6837
+ }
6838
+ return `import ${parts.join(", ")} from "${module}";`;
6839
+ };
6840
+ const fixDuplicateImports = async (context) => {
6841
+ const files = getSourceFiles(context);
6842
+ for (const filePath of files) {
6843
+ if (!JS_EXTENSIONS.has(path.extname(filePath))) continue;
6844
+ if (isAutoGenerated(filePath)) continue;
6845
+ let content;
6846
+ try {
6847
+ content = fs.readFileSync(filePath, "utf-8");
6848
+ } catch {
6849
+ continue;
6850
+ }
6851
+ const lines = content.split("\n");
6852
+ const imports = [];
6853
+ for (let i = 0; i < lines.length; i++) {
6854
+ const stmt = parseImportLine(lines[i], i);
6855
+ if (stmt) imports.push(stmt);
6856
+ }
6857
+ if (imports.length < 2) continue;
6858
+ const groups = /* @__PURE__ */ new Map();
6859
+ for (const stmt of imports) {
6860
+ const list = groups.get(stmt.module) ?? [];
6861
+ list.push(stmt);
6862
+ groups.set(stmt.module, list);
6863
+ }
6864
+ const linesToRemove = /* @__PURE__ */ new Set();
6865
+ const replacements = /* @__PURE__ */ new Map();
6866
+ let modified = false;
6867
+ for (const group of groups.values()) {
6868
+ if (group.length < 2) continue;
6869
+ const merged = mergeImports(group);
6870
+ if (!merged) continue;
6871
+ replacements.set(group[0].lineIndex, merged);
6872
+ for (const stmt of group.slice(1)) linesToRemove.add(stmt.lineIndex);
6873
+ modified = true;
6874
+ }
6875
+ if (!modified) continue;
6876
+ const next = [...lines];
6877
+ for (const [idx, replacement] of replacements) next[idx] = replacement;
6878
+ const sortedRemove = Array.from(linesToRemove).sort((a, b) => b - a);
6879
+ for (const idx of sortedRemove) next.splice(idx, 1);
6880
+ fs.writeFileSync(filePath, next.join("\n"));
6881
+ }
6882
+ };
6883
+
6884
+ //#endregion
6885
+ //#region src/engines/ai-slop/narrative-comments-fix.ts
6886
+ const fixNarrativeComments = async (context) => {
6887
+ const diagnostics = await detectNarrativeComments(context);
6888
+ if (diagnostics.length === 0) return;
6889
+ const byFile = /* @__PURE__ */ new Map();
6890
+ for (const d of diagnostics) {
6891
+ const abs = d.filePath.startsWith("/") ? d.filePath : `${context.rootDirectory}/${d.filePath}`;
6892
+ const list = byFile.get(abs) ?? [];
6893
+ list.push(d);
6894
+ byFile.set(abs, list);
6895
+ }
6896
+ for (const [filePath, diags] of byFile) {
6897
+ const syntax = getCommentSyntax(path.extname(filePath));
6898
+ if (!syntax) continue;
6899
+ let content;
6900
+ try {
6901
+ content = fs.readFileSync(filePath, "utf-8");
6902
+ } catch {
6903
+ continue;
6904
+ }
6905
+ const lines = content.split("\n");
6906
+ const blocks = collectBlocks(lines, syntax);
6907
+ const toRemove = /* @__PURE__ */ new Set();
6908
+ for (const d of diags) {
6909
+ const block = blocks.find((b) => b.startLine === d.line);
6910
+ if (!block) continue;
6911
+ for (let ln = block.startLine; ln <= block.endLine; ln += 1) toRemove.add(ln);
6912
+ const prev = block.startLine - 1;
6913
+ const next = block.endLine + 1;
6914
+ const prevIsBlank = prev >= 1 && lines[prev - 1]?.trim() === "";
6915
+ const nextIsBlank = next <= lines.length && lines[next - 1]?.trim() === "";
6916
+ if (prevIsBlank && nextIsBlank) toRemove.add(prev);
6917
+ }
6918
+ const kept = [];
6919
+ for (let i = 0; i < lines.length; i += 1) if (!toRemove.has(i + 1)) kept.push(lines[i]);
6920
+ const newContent = kept.join("\n");
6921
+ if (newContent !== content) fs.writeFileSync(filePath, newContent);
6922
+ }
6923
+ };
6924
+
5740
6925
  //#endregion
5741
6926
  //#region src/engines/ai-slop/unused-imports-fix.ts
5742
6927
  const fixUnusedImports = async (context) => {
@@ -5758,9 +6943,9 @@ const fixUnusedImports = async (context) => {
5758
6943
  for (const [lineNo, syms] of symbolsByLine) {
5759
6944
  const lineIdx = lineNo - 1;
5760
6945
  const allUnused = syms.every((s) => unusedNames.has(s.name));
5761
- const importSpan = JS_EXTENSIONS.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
6946
+ const importSpan = JS_EXTENSIONS$1.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
5762
6947
  if (allUnused) for (const idx of importSpan) linesToRemove.add(idx);
5763
- else if (JS_EXTENSIONS.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
6948
+ else if (JS_EXTENSIONS$1.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
5764
6949
  else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, syms, unusedNames);
5765
6950
  }
5766
6951
  if (linesToRemove.size === 0 && unused.length === 0) continue;
@@ -6363,6 +7548,7 @@ const hasJsOrTs = (projectInfo) => projectInfo.languages.includes("typescript")
6363
7548
  const runAiSlopSteps = async (deps) => {
6364
7549
  if (!deps.config.engines["ai-slop"]) return;
6365
7550
  await deps.runStep("Unused imports", () => detectUnusedImports(deps.context), () => fixUnusedImports(deps.context));
7551
+ await deps.runStep("Duplicate imports", () => detectDuplicateImports(deps.context), () => fixDuplicateImports(deps.context));
6366
7552
  const detectFixableSlop = async () => {
6367
7553
  const [comments, dead, narrative] = await Promise.all([
6368
7554
  detectTrivialComments(deps.context),
@@ -6468,7 +7654,8 @@ const createEngineContext = (rootDirectory, projectInfo, config) => ({
6468
7654
  installedTools: projectInfo.installedTools,
6469
7655
  config: {
6470
7656
  quality: config.quality,
6471
- security: config.security
7657
+ security: config.security,
7658
+ lint: config.lint
6472
7659
  }
6473
7660
  });
6474
7661
  const fixCommand = async (directory, config, options = {
@@ -6531,6 +7718,7 @@ const fixCommand = async (directory, config, options = {
6531
7718
  const engineConfig = {
6532
7719
  quality: config.quality,
6533
7720
  security: config.security,
7721
+ lint: config.lint,
6534
7722
  architectureRulesPath: config.engines.architecture ? rulesPath : void 0
6535
7723
  };
6536
7724
  rail.start("Verifying results");
@@ -6835,8 +8023,10 @@ const buildRulesRender = (input) => {
6835
8023
  const AI_SLOP_FIXABLE = new Set([
6836
8024
  "ai-slop/trivial-comment",
6837
8025
  "ai-slop/unused-import",
6838
- "ai-slop/narrative-comment"
8026
+ "ai-slop/narrative-comment",
8027
+ "ai-slop/duplicate-import"
6839
8028
  ]);
8029
+ const AI_SLOP_ERRORS = new Set(["ai-slop/hallucinated-import"]);
6840
8030
  const BUILTIN_RULES = [
6841
8031
  {
6842
8032
  engine: "format",
@@ -6857,7 +8047,8 @@ const BUILTIN_RULES = [
6857
8047
  "ruff/*",
6858
8048
  "go/*",
6859
8049
  "clippy/*",
6860
- "rubocop/*"
8050
+ "rubocop/*",
8051
+ "typescript/*"
6861
8052
  ]
6862
8053
  },
6863
8054
  {
@@ -6893,7 +8084,16 @@ const BUILTIN_RULES = [
6893
8084
  "ai-slop/unsafe-type-assertion",
6894
8085
  "ai-slop/double-type-assertion",
6895
8086
  "ai-slop/ts-directive",
6896
- "ai-slop/narrative-comment"
8087
+ "ai-slop/narrative-comment",
8088
+ "ai-slop/duplicate-import",
8089
+ "ai-slop/python-bare-except",
8090
+ "ai-slop/python-broad-except",
8091
+ "ai-slop/python-mutable-default",
8092
+ "ai-slop/python-print-debug",
8093
+ "ai-slop/go-library-panic",
8094
+ "ai-slop/rust-non-test-unwrap",
8095
+ "ai-slop/rust-todo-stub",
8096
+ "ai-slop/hallucinated-import"
6897
8097
  ]
6898
8098
  },
6899
8099
  {
@@ -6924,7 +8124,7 @@ const toRuleEntry = (engine, ruleId) => {
6924
8124
  if (engine === "ai-slop") return {
6925
8125
  id: ruleId,
6926
8126
  engine,
6927
- severity: "warning",
8127
+ severity: AI_SLOP_ERRORS.has(ruleId) ? "error" : "warning",
6928
8128
  fixable: AI_SLOP_FIXABLE.has(ruleId)
6929
8129
  };
6930
8130
  return {