aislop 0.6.2 → 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-AmNwcw_U.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
@@ -103,6 +104,48 @@ const DEFAULT_RULES_YAML = `# Architecture rules (BYO)
103
104
  # severity: error
104
105
  `;
105
106
 
107
+ //#endregion
108
+ //#region src/config/extends.ts
109
+ const MAX_DEPTH = 5;
110
+ const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
111
+ const deepMerge = (...sources) => {
112
+ const result = {};
113
+ for (const source of sources) for (const key of Object.keys(source)) {
114
+ const a = result[key];
115
+ const b = source[key];
116
+ result[key] = isPlainObject(a) && isPlainObject(b) ? deepMerge(a, b) : b;
117
+ }
118
+ return result;
119
+ };
120
+ const resolveExtendsRef = (ref, fromDir) => {
121
+ if (ref.startsWith("http://") || ref.startsWith("https://")) throw new Error(`URL-based extends not yet supported: ${ref}`);
122
+ if (ref.startsWith("./") || ref.startsWith("../") || path.isAbsolute(ref)) return path.resolve(fromDir, ref);
123
+ throw new Error(`Package-name extends not yet supported: ${ref} (use a relative path for now)`);
124
+ };
125
+ const normalizeExtends = (raw) => {
126
+ if (raw === void 0 || raw === null) return [];
127
+ if (typeof raw === "string") return [raw];
128
+ if (Array.isArray(raw) && raw.every((s) => typeof s === "string")) return raw;
129
+ throw new Error("`extends` must be a string or array of strings");
130
+ };
131
+ const loadConfigChain = (configPath, visited = /* @__PURE__ */ new Set(), depth = 0) => {
132
+ if (depth > MAX_DEPTH) throw new Error(`extends depth exceeded ${MAX_DEPTH} (cycle or runaway chain): ${configPath}`);
133
+ const absPath = path.resolve(configPath);
134
+ if (visited.has(absPath)) throw new Error(`circular extends detected: ${absPath}`);
135
+ if (!fs.existsSync(absPath)) throw new Error(`extends target not found: ${absPath}`);
136
+ const nextVisited = new Set(visited);
137
+ nextVisited.add(absPath);
138
+ const raw = fs.readFileSync(absPath, "utf-8");
139
+ const parsed = YAML.parse(raw) ?? {};
140
+ const refs = normalizeExtends(parsed.extends);
141
+ const fromDir = path.dirname(absPath);
142
+ const parents = refs.map((ref) => {
143
+ return loadConfigChain(resolveExtendsRef(ref, fromDir), nextVisited, depth + 1);
144
+ });
145
+ const { extends: _drop, ...own } = parsed;
146
+ return deepMerge(...parents, own);
147
+ };
148
+
106
149
  //#endregion
107
150
  //#region src/config/schema.ts
108
151
  const DEFAULT_WEIGHTS = {
@@ -127,6 +170,7 @@ const QualitySchema = z.object({
127
170
  maxNesting: z.number().positive().default(5),
128
171
  maxParams: z.number().positive().default(6)
129
172
  });
173
+ const LintConfigSchema = z.object({ typecheck: z.boolean().default(false) });
130
174
  const SecurityConfigSchema = z.object({
131
175
  audit: z.boolean().default(true),
132
176
  auditTimeout: z.number().positive().default(25e3)
@@ -164,6 +208,7 @@ const AislopConfigSchema = z.object({
164
208
  maxNesting: 5,
165
209
  maxParams: 6
166
210
  })),
211
+ lint: LintConfigSchema.default(() => ({ typecheck: false })),
167
212
  security: SecurityConfigSchema.default(() => ({
168
213
  audit: true,
169
214
  auditTimeout: 25e3
@@ -237,8 +282,7 @@ const loadConfig = (directory) => {
237
282
  const configPath = path.join(configDir, CONFIG_FILE);
238
283
  if (!fs.existsSync(configPath)) return DEFAULT_CONFIG;
239
284
  try {
240
- const raw = fs.readFileSync(configPath, "utf-8");
241
- return parseConfig(YAML.parse(raw));
285
+ return parseConfig(loadConfigChain(configPath));
242
286
  } catch (error) {
243
287
  const msg = error instanceof Error ? error.message : String(error);
244
288
  process.stderr.write(` ⚠ Failed to parse ${configPath}: ${msg}\n ⚠ Using default configuration.\n`);
@@ -565,7 +609,7 @@ const hasAllowedExtension = (filePath, extraExtensions) => {
565
609
  return SOURCE_EXTENSIONS.has(extension) || extraExtensions.has(extension);
566
610
  };
567
611
  const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
568
- const isTestFile = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
612
+ const isTestFile$2 = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
569
613
  const getIgnoredPaths = (rootDirectory, files) => {
570
614
  if (files.length === 0) return /* @__PURE__ */ new Set();
571
615
  const result = spawnSync("git", [
@@ -639,7 +683,7 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude
639
683
  return micromatch.isMatch(relativePath, normalizedExcludePatterns, { dot: true });
640
684
  };
641
685
  return normalizedFiles.filter(({ absolutePath, relativePath }) => {
642
- 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);
643
687
  }).map(({ absolutePath }) => absolutePath);
644
688
  };
645
689
  const filterExplicitFiles = (rootDirectory, files, extraExtensions = []) => {
@@ -948,16 +992,29 @@ const planFormat = (ctx) => {
948
992
  skipReason: "no supported language"
949
993
  };
950
994
  };
951
- const planLint = (ctx) => {
952
- const { languages, frameworks, installedTools } = ctx.projectInfo;
953
- if (frameworks.includes("expo")) return {
954
- 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,
955
1002
  status: "ok"
956
1003
  };
957
- if (hasJsLike(languages)) return {
958
- tool: "oxlint (bundled)",
1004
+ if (findLocalTsc(ctx.rootDirectory)) return {
1005
+ tool: `${baseTool} + tsc`,
959
1006
  status: "ok"
960
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);
961
1018
  return firstMatching(languages, installedTools, LINT_SPECS) ?? {
962
1019
  tool: "no linter",
963
1020
  status: "skipped",
@@ -1217,13 +1274,14 @@ const detectOverAbstraction = async (context) => {
1217
1274
 
1218
1275
  //#endregion
1219
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;
1220
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";
1221
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")];
1222
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")];
1223
1281
  const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?)\b/i;
1224
1282
  const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
1225
1283
  const MAX_TRIVIAL_COMMENT_LENGTH = 60;
1226
- const isJsComment = (trimmed) => trimmed.startsWith("//");
1284
+ const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
1227
1285
  const isPythonComment = (trimmed) => trimmed.startsWith("#") && !trimmed.startsWith("#!");
1228
1286
  /**
1229
1287
  * Extract just the comment text after the comment marker.
@@ -1272,13 +1330,14 @@ const detectTrivialComments = async (context) => {
1272
1330
  const diagnostics = [];
1273
1331
  for (const filePath of files) {
1274
1332
  if (isAutoGenerated(filePath)) continue;
1333
+ const relativePath = path.relative(context.rootDirectory, filePath);
1334
+ if (NON_PRODUCTION_DIR_PATTERN$2.test(relativePath)) continue;
1275
1335
  let content;
1276
1336
  try {
1277
1337
  content = fs.readFileSync(filePath, "utf-8");
1278
1338
  } catch {
1279
1339
  continue;
1280
1340
  }
1281
- const relativePath = path.relative(context.rootDirectory, filePath);
1282
1341
  diagnostics.push(...scanFileForTrivialComments(content, relativePath));
1283
1342
  }
1284
1343
  return diagnostics;
@@ -1286,7 +1345,7 @@ const detectTrivialComments = async (context) => {
1286
1345
 
1287
1346
  //#endregion
1288
1347
  //#region src/engines/ai-slop/dead-patterns.ts
1289
- const JS_EXTENSIONS$1 = new Set([
1348
+ const JS_EXTENSIONS$4 = new Set([
1290
1349
  ".ts",
1291
1350
  ".tsx",
1292
1351
  ".js",
@@ -1308,11 +1367,11 @@ const slop = (filePath, line, rule, severity, message, help, fixable) => ({
1308
1367
  fixable
1309
1368
  });
1310
1369
  const LOGGER_FILE_PATTERN = /(?:^|\/)(?:logger|logging|log)\.[^/]+$/i;
1311
- 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)\//;
1312
1371
  const detectConsoleLeftovers = (content, relativePath, ext) => {
1313
- if (!JS_EXTENSIONS$1.has(ext)) return [];
1372
+ if (!JS_EXTENSIONS$4.has(ext)) return [];
1314
1373
  if (LOGGER_FILE_PATTERN.test(relativePath)) return [];
1315
- if (SCRIPT_DIR_PATTERN.test(relativePath)) return [];
1374
+ if (NON_PRODUCTION_DIR_PATTERN$1.test(relativePath)) return [];
1316
1375
  const diagnostics = [];
1317
1376
  const lines = content.split("\n");
1318
1377
  for (let i = 0; i < lines.length; i++) {
@@ -1352,9 +1411,9 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
1352
1411
  for (let i = 0; i < lines.length; i++) {
1353
1412
  const trimmed = lines[i].trim();
1354
1413
  const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
1355
- 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));
1356
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));
1357
- 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));
1358
1417
  }
1359
1418
  return diagnostics;
1360
1419
  };
@@ -1362,6 +1421,7 @@ const asAnyPattern = new RegExp(`\\bas\\s+any\\b`);
1362
1421
  const doubleAssertPattern = new RegExp(`\\bas\\s+unknown\\s+as\\s+`);
1363
1422
  const detectUnsafeTypePatterns = (content, relativePath, ext) => {
1364
1423
  if (ext !== ".ts" && ext !== ".tsx") return [];
1424
+ if (NON_PRODUCTION_DIR_PATTERN$1.test(relativePath)) return [];
1365
1425
  const diagnostics = [];
1366
1426
  const lines = content.split("\n");
1367
1427
  for (let i = 0; i < lines.length; i++) {
@@ -1398,6 +1458,74 @@ const detectDeadPatterns = async (context) => {
1398
1458
  return diagnostics;
1399
1459
  };
1400
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
+
1401
1529
  //#endregion
1402
1530
  //#region src/engines/ai-slop/exceptions.ts
1403
1531
  const SWALLOWED_EXCEPTION_PATTERNS = [
@@ -1488,6 +1616,600 @@ const detectSwallowedExceptions = async (context) => {
1488
1616
  return diagnostics;
1489
1617
  };
1490
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
+
1491
2213
  //#endregion
1492
2214
  //#region src/engines/ai-slop/narrative-comments-patterns.ts
1493
2215
  const DECORATIVE_SEPARATOR = /^[-=─━~_*#]{6,}$/;
@@ -1604,6 +2326,7 @@ const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|re
1604
2326
 
1605
2327
  //#endregion
1606
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;
1607
2330
  const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
1608
2331
  const stripLineComment = (line) => line.replace(/^\s*(?:(?:\/\/)|#)\s?/, "");
1609
2332
  const getCommentSyntax = (ext) => {
@@ -1632,6 +2355,10 @@ const getMatchedLinePrefix = (line, syntax) => {
1632
2355
  }
1633
2356
  return null;
1634
2357
  };
2358
+ const isRustDocCommentLine = (line) => {
2359
+ const trimmed = line.trimStart();
2360
+ return trimmed.startsWith("///") || trimmed.startsWith("//!");
2361
+ };
1635
2362
  const collectBlocks = (sourceLines, syntax) => {
1636
2363
  const blocks = [];
1637
2364
  let i = 0;
@@ -1647,6 +2374,8 @@ const collectBlocks = (sourceLines, syntax) => {
1647
2374
  }
1648
2375
  let next = i;
1649
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));
1650
2379
  blocks.push({
1651
2380
  kind: "line",
1652
2381
  startLine: start + 1,
@@ -1654,6 +2383,7 @@ const collectBlocks = (sourceLines, syntax) => {
1654
2383
  rawLines: raw,
1655
2384
  prose: raw.map(stripLineComment),
1656
2385
  hasMeaningfulJsdocTag: false,
2386
+ isRustDoc,
1657
2387
  nextNonBlankLine: next < sourceLines.length ? sourceLines[next] : null
1658
2388
  });
1659
2389
  continue;
@@ -1687,6 +2417,7 @@ const collectBlocks = (sourceLines, syntax) => {
1687
2417
  rawLines: raw,
1688
2418
  prose,
1689
2419
  hasMeaningfulJsdocTag: hasMeaningful,
2420
+ isRustDoc: false,
1690
2421
  nextNonBlankLine: next < sourceLines.length ? sourceLines[next] : null
1691
2422
  });
1692
2423
  continue;
@@ -1737,6 +2468,22 @@ const nextLineLooksLikeDataEntry = (nextLine) => {
1737
2468
  return false;
1738
2469
  };
1739
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
+ };
1740
2487
  const detectNarrativeInBlock = (block, ext) => {
1741
2488
  if (looksLikeLicenseHeader(block)) return {
1742
2489
  matched: false,
@@ -1750,6 +2497,14 @@ const detectNarrativeInBlock = (block, ext) => {
1750
2497
  matched: false,
1751
2498
  reason: ""
1752
2499
  };
2500
+ if (block.isRustDoc) return {
2501
+ matched: false,
2502
+ reason: ""
2503
+ };
2504
+ if (looksLikeGoDocComment(block, ext)) return {
2505
+ matched: false,
2506
+ reason: ""
2507
+ };
1753
2508
  if (block.kind === "line" && block.prose.some((l) => DECORATIVE_SEPARATOR.test(l) || DECORATIVE_SECTION_HEADER.test(l))) return {
1754
2509
  matched: true,
1755
2510
  reason: "decorative separator"
@@ -1762,11 +2517,16 @@ const detectNarrativeInBlock = (block, ext) => {
1762
2517
  matched: true,
1763
2518
  reason: "bare section label"
1764
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
+ };
1765
2526
  if (block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
1766
2527
  matched: true,
1767
2528
  reason: block.kind === "jsdoc" ? "JSDoc preamble before declaration" : "multi-line preamble before declaration"
1768
2529
  };
1769
- const joined = block.prose.join(" ");
1770
2530
  if (CROSS_REFERENCE_PHRASES.some((re) => re.test(joined))) return {
1771
2531
  matched: true,
1772
2532
  reason: "cross-reference commentary"
@@ -1782,8 +2542,6 @@ const detectNarrativeInBlock = (block, ext) => {
1782
2542
  reason: "explanatory preamble"
1783
2543
  };
1784
2544
  const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
1785
- const joinedProse = block.prose.join(" ");
1786
- const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joinedProse);
1787
2545
  if (nonEmptyProseCount >= 5) return {
1788
2546
  matched: true,
1789
2547
  reason: "long narrative block"
@@ -1806,6 +2564,8 @@ const detectNarrativeComments = async (context) => {
1806
2564
  if (isAutoGenerated(filePath)) continue;
1807
2565
  const syntax = getCommentSyntax(ext);
1808
2566
  if (!syntax) continue;
2567
+ const relativePath = path.relative(context.rootDirectory, filePath);
2568
+ if (NON_PRODUCTION_DIR_PATTERN.test(relativePath)) continue;
1809
2569
  let content;
1810
2570
  try {
1811
2571
  content = fs.readFileSync(filePath, "utf-8");
@@ -1813,7 +2573,6 @@ const detectNarrativeComments = async (context) => {
1813
2573
  continue;
1814
2574
  }
1815
2575
  const blocks = collectBlocks(content.split("\n"), syntax);
1816
- const relativePath = filePath.replace(`${context.rootDirectory}/`, "");
1817
2576
  for (const block of blocks) {
1818
2577
  const { matched, reason } = detectNarrativeInBlock(block, ext);
1819
2578
  if (!matched) continue;
@@ -1833,48 +2592,293 @@ const detectNarrativeComments = async (context) => {
1833
2592
  }
1834
2593
  return diagnostics;
1835
2594
  };
1836
- const fixNarrativeComments = async (context) => {
1837
- const diagnostics = await detectNarrativeComments(context);
1838
- if (diagnostics.length === 0) return;
1839
- const byFile = /* @__PURE__ */ new Map();
1840
- for (const d of diagnostics) {
1841
- const abs = d.filePath.startsWith("/") ? d.filePath : `${context.rootDirectory}/${d.filePath}`;
1842
- const list = byFile.get(abs) ?? [];
1843
- list.push(d);
1844
- 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
+ }
1845
2639
  }
1846
- for (const [filePath, diags] of byFile) {
1847
- const syntax = getCommentSyntax(path.extname(filePath));
1848
- 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;
1849
2743
  let content;
1850
2744
  try {
1851
2745
  content = fs.readFileSync(filePath, "utf-8");
1852
2746
  } catch {
1853
2747
  continue;
1854
2748
  }
2749
+ const relPath = path.relative(context.rootDirectory, filePath);
2750
+ const basename = path.basename(filePath);
1855
2751
  const lines = content.split("\n");
1856
- const blocks = collectBlocks(lines, syntax);
1857
- const toRemove = /* @__PURE__ */ new Set();
1858
- for (const d of diags) {
1859
- const block = blocks.find((b) => b.startLine === d.line);
1860
- if (!block) continue;
1861
- for (let ln = block.startLine; ln <= block.endLine; ln += 1) toRemove.add(ln);
1862
- const prev = block.startLine - 1;
1863
- const next = block.endLine + 1;
1864
- const prevIsBlank = prev >= 1 && lines[prev - 1]?.trim() === "";
1865
- const nextIsBlank = next <= lines.length && lines[next - 1]?.trim() === "";
1866
- 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
+ }
1867
2807
  }
1868
- const kept = [];
1869
- for (let i = 0; i < lines.length; i += 1) if (!toRemove.has(i + 1)) kept.push(lines[i]);
1870
- const newContent = kept.join("\n");
1871
- 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
+ });
2832
+ }
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
+ });
1872
2852
  }
1873
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
+ };
1874
2878
 
1875
2879
  //#endregion
1876
2880
  //#region src/engines/ai-slop/unused-imports.ts
1877
- const JS_EXTENSIONS = new Set([
2881
+ const JS_EXTENSIONS$1 = new Set([
1878
2882
  ".ts",
1879
2883
  ".tsx",
1880
2884
  ".js",
@@ -2004,7 +3008,7 @@ const analyzeFile = (filePath) => {
2004
3008
  const lines = content.split("\n");
2005
3009
  let symbols;
2006
3010
  let importLines;
2007
- if (JS_EXTENSIONS.has(ext)) {
3011
+ if (JS_EXTENSIONS$1.has(ext)) {
2008
3012
  const result = extractJsImportedSymbols(lines);
2009
3013
  symbols = result.symbols;
2010
3014
  importLines = result.importLines;
@@ -2060,7 +3064,12 @@ const aiSlopEngine = {
2060
3064
  detectOverAbstraction(context),
2061
3065
  detectDeadPatterns(context),
2062
3066
  detectUnusedImports(context),
2063
- detectNarrativeComments(context)
3067
+ detectNarrativeComments(context),
3068
+ detectDuplicateImports(context),
3069
+ detectPythonPatterns(context),
3070
+ detectGoPatterns(context),
3071
+ detectRustPatterns(context),
3072
+ detectHallucinatedImports(context)
2064
3073
  ]);
2065
3074
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
2066
3075
  return {
@@ -2444,6 +3453,12 @@ const isDataFile = (content) => {
2444
3453
  const dataLinePattern = /^\s*[{}[\]"']/;
2445
3454
  return nonEmpty.filter((l) => dataLinePattern.test(l)).length / nonEmpty.length > .8;
2446
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);
2447
3462
  const analyzeFunctions = (content, ext) => {
2448
3463
  const lines = content.split("\n");
2449
3464
  const functions = [];
@@ -2472,13 +3487,13 @@ const checkFileDiagnostics = (relativePath, content, limits) => {
2472
3487
  const lineCount = content.split("\n").length;
2473
3488
  const ext = path.extname(relativePath).toLowerCase();
2474
3489
  if (isDataFile(content)) return results;
2475
- const effectiveMax = ext === ".jsx" || ext === ".tsx" ? Math.ceil(limits.maxFileLoc * JSX_FILE_LOC_MULTIPLIER) : limits.maxFileLoc;
2476
- 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({
2477
3492
  filePath: relativePath,
2478
3493
  engine: "code-quality",
2479
3494
  rule: "complexity/file-too-large",
2480
3495
  severity: "warning",
2481
- message: `File has ${lineCount} lines (max: ${effectiveMax})`,
3496
+ message: `File has ${lineCount} lines (max: ${configuredMax})`,
2482
3497
  help: "Consider splitting this file into smaller modules",
2483
3498
  line: 0,
2484
3499
  column: 0,
@@ -2528,13 +3543,14 @@ const checkFunctionDiagnostics = (relativePath, fn, limits) => {
2528
3543
  return results;
2529
3544
  };
2530
3545
  const checkFileComplexity = (filePath, rootDirectory, limits) => {
3546
+ const relativePath = path.relative(rootDirectory, filePath);
3547
+ if (isExemptFromComplexity(relativePath)) return [];
2531
3548
  let content;
2532
3549
  try {
2533
3550
  content = fs.readFileSync(filePath, "utf-8");
2534
3551
  } catch {
2535
3552
  return [];
2536
3553
  }
2537
- const relativePath = path.relative(rootDirectory, filePath);
2538
3554
  const ext = path.extname(filePath).toLowerCase();
2539
3555
  const diagnostics = checkFileDiagnostics(relativePath, content, limits);
2540
3556
  for (const fn of analyzeFunctions(content, ext)) diagnostics.push(...checkFunctionDiagnostics(relativePath, fn, limits));
@@ -3916,7 +4932,10 @@ const lintEngine = {
3916
4932
  const diagnostics = [];
3917
4933
  const { languages, installedTools } = context;
3918
4934
  const promises = [];
3919
- 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
+ }
3920
4939
  if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-Bz0LZhQ6.js").then((n) => n.t).then((mod) => mod.runExpoDoctor(context)));
3921
4940
  if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
3922
4941
  if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
@@ -5274,6 +6293,7 @@ const scanCommand = async (directory, config, options) => {
5274
6293
  const engineConfig = {
5275
6294
  quality: config.quality,
5276
6295
  security: config.security,
6296
+ lint: config.lint,
5277
6297
  architectureRulesPath: config.engines.architecture ? rulesPath : void 0
5278
6298
  };
5279
6299
  const gridRows = ALL_ENGINE_NAMES.filter((engine) => config.engines[engine] !== false).map((engine) => ({
@@ -5341,7 +6361,7 @@ const scanCommand = async (directory, config, options) => {
5341
6361
  });
5342
6362
  }
5343
6363
  if (options.json) {
5344
- const { buildJsonOutput } = await import("./json-ZItDVIZL.js");
6364
+ const { buildJsonOutput } = await import("./json-BJGLCIK-.js");
5345
6365
  const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
5346
6366
  console.log(JSON.stringify(jsonOut, null, 2));
5347
6367
  return { exitCode };
@@ -5696,6 +6716,212 @@ const applyEditsAndCollapse = (lines, linesToRemove, lineReplacements) => {
5696
6716
  return collapsed.join("\n");
5697
6717
  };
5698
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
+
5699
6925
  //#endregion
5700
6926
  //#region src/engines/ai-slop/unused-imports-fix.ts
5701
6927
  const fixUnusedImports = async (context) => {
@@ -5717,9 +6943,9 @@ const fixUnusedImports = async (context) => {
5717
6943
  for (const [lineNo, syms] of symbolsByLine) {
5718
6944
  const lineIdx = lineNo - 1;
5719
6945
  const allUnused = syms.every((s) => unusedNames.has(s.name));
5720
- const importSpan = JS_EXTENSIONS.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
6946
+ const importSpan = JS_EXTENSIONS$1.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
5721
6947
  if (allUnused) for (const idx of importSpan) linesToRemove.add(idx);
5722
- 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);
5723
6949
  else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, syms, unusedNames);
5724
6950
  }
5725
6951
  if (linesToRemove.size === 0 && unused.length === 0) continue;
@@ -6322,6 +7548,7 @@ const hasJsOrTs = (projectInfo) => projectInfo.languages.includes("typescript")
6322
7548
  const runAiSlopSteps = async (deps) => {
6323
7549
  if (!deps.config.engines["ai-slop"]) return;
6324
7550
  await deps.runStep("Unused imports", () => detectUnusedImports(deps.context), () => fixUnusedImports(deps.context));
7551
+ await deps.runStep("Duplicate imports", () => detectDuplicateImports(deps.context), () => fixDuplicateImports(deps.context));
6325
7552
  const detectFixableSlop = async () => {
6326
7553
  const [comments, dead, narrative] = await Promise.all([
6327
7554
  detectTrivialComments(deps.context),
@@ -6427,7 +7654,8 @@ const createEngineContext = (rootDirectory, projectInfo, config) => ({
6427
7654
  installedTools: projectInfo.installedTools,
6428
7655
  config: {
6429
7656
  quality: config.quality,
6430
- security: config.security
7657
+ security: config.security,
7658
+ lint: config.lint
6431
7659
  }
6432
7660
  });
6433
7661
  const fixCommand = async (directory, config, options = {
@@ -6490,6 +7718,7 @@ const fixCommand = async (directory, config, options = {
6490
7718
  const engineConfig = {
6491
7719
  quality: config.quality,
6492
7720
  security: config.security,
7721
+ lint: config.lint,
6493
7722
  architectureRulesPath: config.engines.architecture ? rulesPath : void 0
6494
7723
  };
6495
7724
  rail.start("Verifying results");
@@ -6794,8 +8023,10 @@ const buildRulesRender = (input) => {
6794
8023
  const AI_SLOP_FIXABLE = new Set([
6795
8024
  "ai-slop/trivial-comment",
6796
8025
  "ai-slop/unused-import",
6797
- "ai-slop/narrative-comment"
8026
+ "ai-slop/narrative-comment",
8027
+ "ai-slop/duplicate-import"
6798
8028
  ]);
8029
+ const AI_SLOP_ERRORS = new Set(["ai-slop/hallucinated-import"]);
6799
8030
  const BUILTIN_RULES = [
6800
8031
  {
6801
8032
  engine: "format",
@@ -6816,7 +8047,8 @@ const BUILTIN_RULES = [
6816
8047
  "ruff/*",
6817
8048
  "go/*",
6818
8049
  "clippy/*",
6819
- "rubocop/*"
8050
+ "rubocop/*",
8051
+ "typescript/*"
6820
8052
  ]
6821
8053
  },
6822
8054
  {
@@ -6852,7 +8084,16 @@ const BUILTIN_RULES = [
6852
8084
  "ai-slop/unsafe-type-assertion",
6853
8085
  "ai-slop/double-type-assertion",
6854
8086
  "ai-slop/ts-directive",
6855
- "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"
6856
8097
  ]
6857
8098
  },
6858
8099
  {
@@ -6883,7 +8124,7 @@ const toRuleEntry = (engine, ruleId) => {
6883
8124
  if (engine === "ai-slop") return {
6884
8125
  id: ruleId,
6885
8126
  engine,
6886
- severity: "warning",
8127
+ severity: AI_SLOP_ERRORS.has(ruleId) ? "error" : "warning",
6887
8128
  fixable: AI_SLOP_FIXABLE.has(ruleId)
6888
8129
  };
6889
8130
  return {