aislop 0.9.3 → 0.9.5

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,7 +1,8 @@
1
- import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-BNO_Lw7E.js";
1
+ import { n as getEngineLabel, t as ENGINE_INFO } from "./engine-info-DCvIfZ0f.js";
2
2
  import { n as runSubprocess, t as isToolInstalled } from "./subprocess-CQUJDGgn.js";
3
- import { r as runGenericLinter, t as fixRubyLint } from "./generic-BrcWMW7E.js";
4
- import { n as runExpoDoctor } from "./expo-doctor-Bz0LZhQ6.js";
3
+ import { t as APP_VERSION } from "./version-ls3wZmOU.js";
4
+ import { r as runGenericLinter, t as fixRubyLint } from "./generic-D_T4cUaC.js";
5
+ import { n as runExpoDoctor } from "./expo-doctor-BcIkOte5.js";
5
6
  import { createRequire, isBuiltin } from "node:module";
6
7
  import fs from "node:fs";
7
8
  import path from "node:path";
@@ -13,8 +14,8 @@ import { spawnSync } from "node:child_process";
13
14
  import micromatch from "micromatch";
14
15
  import { fileURLToPath } from "node:url";
15
16
  import { performance } from "node:perf_hooks";
16
- import os from "node:os";
17
17
  import ts from "typescript";
18
+ import os from "node:os";
18
19
  import { randomUUID } from "node:crypto";
19
20
  import { isCancel, multiselect, select, text } from "@clack/prompts";
20
21
 
@@ -67,7 +68,8 @@ const DEFAULT_CONFIG = {
67
68
  failBelow: 70,
68
69
  format: "json"
69
70
  },
70
- telemetry: { enabled: true }
71
+ telemetry: { enabled: true },
72
+ rules: {}
71
73
  };
72
74
  const GITHUB_WORKFLOW_DIR = ".github/workflows";
73
75
  const GITHUB_WORKFLOW_FILE = "aislop.yml";
@@ -194,6 +196,12 @@ const CiSchema = z.object({
194
196
  format: z.enum(["json"]).default("json")
195
197
  });
196
198
  const TelemetrySchema = z.object({ enabled: z.boolean().default(true) });
199
+ const RuleSeverityOverride = z.enum([
200
+ "error",
201
+ "warning",
202
+ "off"
203
+ ]);
204
+ const RulesSchema = z.record(z.string(), RuleSeverityOverride).default(() => ({}));
197
205
  const AislopConfigSchema = z.object({
198
206
  version: z.number().default(1),
199
207
  engines: EnginesSchema.default(() => ({
@@ -228,6 +236,7 @@ const AislopConfigSchema = z.object({
228
236
  format: "json"
229
237
  })),
230
238
  telemetry: TelemetrySchema.default(() => ({ enabled: true })),
239
+ rules: RulesSchema,
231
240
  exclude: z.array(z.string()).default(() => [
232
241
  "node_modules",
233
242
  ".git",
@@ -544,7 +553,7 @@ const padStart = (s, target, fill = " ") => {
544
553
  //#endregion
545
554
  //#region src/utils/source-files.ts
546
555
  const MAX_BUFFER$1 = 50 * 1024 * 1024;
547
- const SOURCE_EXTENSIONS = new Set([
556
+ const SOURCE_EXTENSIONS$1 = new Set([
548
557
  ".ts",
549
558
  ".tsx",
550
559
  ".js",
@@ -652,7 +661,7 @@ const toProjectPath = (rootDirectory, filePath) => {
652
661
  const isWithinProject = (relativePath) => relativePath.length > 0 && !relativePath.startsWith("..");
653
662
  const hasAllowedExtension = (filePath, extraExtensions) => {
654
663
  const extension = path.extname(filePath);
655
- return SOURCE_EXTENSIONS.has(extension) || extraExtensions.has(extension);
664
+ return SOURCE_EXTENSIONS$1.has(extension) || extraExtensions.has(extension);
656
665
  };
657
666
  const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
658
667
  const isExcludedFromScan = (relativePath) => isExcludedPath(relativePath) || isBuildCacheFile(relativePath);
@@ -1246,7 +1255,7 @@ const doctorCommand = async (directory, options = {}) => {
1246
1255
 
1247
1256
  //#endregion
1248
1257
  //#region src/engines/ai-slop/abstractions.ts
1249
- const JS_EXTS$1 = new Set([
1258
+ const JS_EXTS$2 = new Set([
1250
1259
  ".ts",
1251
1260
  ".tsx",
1252
1261
  ".js",
@@ -1257,11 +1266,11 @@ const JS_EXTS$1 = new Set([
1257
1266
  const THIN_WRAPPER_PATTERNS = [
1258
1267
  {
1259
1268
  pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
1260
- extensions: JS_EXTS$1
1269
+ extensions: JS_EXTS$2
1261
1270
  },
1262
1271
  {
1263
1272
  pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
1264
- extensions: JS_EXTS$1
1273
+ extensions: JS_EXTS$2
1265
1274
  },
1266
1275
  {
1267
1276
  pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
@@ -1494,6 +1503,12 @@ const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
1494
1503
  const MAX_TRIVIAL_COMMENT_LENGTH = 60;
1495
1504
  const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
1496
1505
  const isPythonComment = (trimmed) => trimmed.startsWith("#") && !trimmed.startsWith("#!");
1506
+ const isLineComment = (trimmed) => isJsComment(trimmed) || isPythonComment(trimmed);
1507
+ const isInMultiLineCommentRun = (lines, index) => {
1508
+ const prev = index > 0 ? lines[index - 1].trim() : "";
1509
+ const next = index + 1 < lines.length ? lines[index + 1].trim() : "";
1510
+ return isLineComment(prev) || isLineComment(next);
1511
+ };
1497
1512
  /**
1498
1513
  * Extract just the comment text after the comment marker.
1499
1514
  */
@@ -1546,6 +1561,7 @@ const scanFileForTrivialComments = (content, relativePath, ext) => {
1546
1561
  const lines = content.split("\n");
1547
1562
  for (let i = 0; i < lines.length; i++) {
1548
1563
  if (!isTrivialComment(lines[i].trim(), i + 1 < lines.length ? lines[i + 1] : void 0)) continue;
1564
+ if (isInMultiLineCommentRun(lines, i)) continue;
1549
1565
  if (isDocCommentForDeclaration(lines, i, ext)) continue;
1550
1566
  diagnostics.push({
1551
1567
  filePath: relativePath,
@@ -1707,6 +1723,177 @@ const detectDeadPatterns = async (context) => {
1707
1723
  return diagnostics;
1708
1724
  };
1709
1725
 
1726
+ //#endregion
1727
+ //#region src/engines/ai-slop/defensive-patterns.ts
1728
+ const JS_TS_EXTENSIONS = new Set([
1729
+ ".ts",
1730
+ ".tsx",
1731
+ ".js",
1732
+ ".jsx",
1733
+ ".mjs",
1734
+ ".cjs"
1735
+ ]);
1736
+ const TS_EXTENSIONS = new Set([".ts", ".tsx"]);
1737
+ const COERCION_CTORS = {
1738
+ string: "String",
1739
+ number: "Number",
1740
+ boolean: "Boolean"
1741
+ };
1742
+ const scriptKindFor = (ext) => {
1743
+ switch (ext) {
1744
+ case ".tsx": return ts.ScriptKind.TSX;
1745
+ case ".jsx": return ts.ScriptKind.JSX;
1746
+ case ".js":
1747
+ case ".mjs":
1748
+ case ".cjs": return ts.ScriptKind.JS;
1749
+ default: return ts.ScriptKind.TS;
1750
+ }
1751
+ };
1752
+ const lineFor = (sourceFile, node) => sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
1753
+ const makeDiagnostic = (filePath, line, rule, message, help) => ({
1754
+ filePath,
1755
+ engine: "ai-slop",
1756
+ rule,
1757
+ severity: "warning",
1758
+ message,
1759
+ help,
1760
+ line,
1761
+ column: 0,
1762
+ category: "AI Slop",
1763
+ fixable: false
1764
+ });
1765
+ const isFunctionNode = (node) => ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node) || ts.isConstructorDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node);
1766
+ const primitiveKindOf = (node) => {
1767
+ if (!node) return null;
1768
+ switch (node.kind) {
1769
+ case ts.SyntaxKind.StringKeyword: return "string";
1770
+ case ts.SyntaxKind.NumberKeyword: return "number";
1771
+ case ts.SyntaxKind.BooleanKeyword: return "boolean";
1772
+ default: return null;
1773
+ }
1774
+ };
1775
+ const hasExportModifier = (node) => node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
1776
+ const primitiveParamsOf = (node) => {
1777
+ const params = /* @__PURE__ */ new Map();
1778
+ for (const param of node.parameters) {
1779
+ if (!ts.isIdentifier(param.name)) continue;
1780
+ const kind = primitiveKindOf(param.type);
1781
+ if (!kind) continue;
1782
+ params.set(param.name.text, kind);
1783
+ }
1784
+ return params;
1785
+ };
1786
+ const isRethrowStatement = (statement, errorName) => ts.isThrowStatement(statement) && statement.expression !== void 0 && ts.isIdentifier(statement.expression) && statement.expression.text === errorName;
1787
+ const isPromiseRejectRethrow = (statement, errorName) => {
1788
+ if (!ts.isReturnStatement(statement) || !statement.expression) return false;
1789
+ const expression = statement.expression;
1790
+ if (!ts.isCallExpression(expression) || expression.arguments.length !== 1) return false;
1791
+ const [arg] = expression.arguments;
1792
+ if (!ts.isIdentifier(arg) || arg.text !== errorName) return false;
1793
+ if (!ts.isPropertyAccessExpression(expression.expression)) return false;
1794
+ const target = expression.expression;
1795
+ return ts.isIdentifier(target.expression) && target.expression.text === "Promise" && target.name.text === "reject";
1796
+ };
1797
+ const detectRedundantTryCatch = (sourceFile, relativePath) => {
1798
+ const diagnostics = [];
1799
+ const visit = (node) => {
1800
+ if (ts.isTryStatement(node) && node.catchClause && !node.finallyBlock) {
1801
+ const catchNameNode = node.catchClause.variableDeclaration?.name;
1802
+ const [onlyStatement] = node.catchClause.block.statements;
1803
+ if (catchNameNode && ts.isIdentifier(catchNameNode) && node.catchClause.block.statements.length === 1 && onlyStatement && (isRethrowStatement(onlyStatement, catchNameNode.text) || isPromiseRejectRethrow(onlyStatement, catchNameNode.text))) diagnostics.push(makeDiagnostic(relativePath, lineFor(sourceFile, node.catchClause), "ai-slop/redundant-try-catch", "Catch block only rethrows the same error", "Remove the try/catch or add useful context, cleanup, or recovery. Rethrowing unchanged errors is usually defensive agent noise."));
1804
+ }
1805
+ ts.forEachChild(node, visit);
1806
+ };
1807
+ visit(sourceFile);
1808
+ return diagnostics;
1809
+ };
1810
+ const detectPrimitiveCoercions = (sourceFile, relativePath) => {
1811
+ const diagnostics = [];
1812
+ const scanFunctionBody = (node, params) => {
1813
+ const body = node.body;
1814
+ if (!body || params.size === 0) return;
1815
+ const visitBody = (child) => {
1816
+ if (child !== body && isFunctionNode(child)) return;
1817
+ if (ts.isCallExpression(child) && ts.isIdentifier(child.expression)) {
1818
+ const [arg] = child.arguments;
1819
+ if (arg && ts.isIdentifier(arg)) {
1820
+ const primitive = params.get(arg.text);
1821
+ if (primitive && child.expression.text === COERCION_CTORS[primitive]) diagnostics.push(makeDiagnostic(relativePath, lineFor(sourceFile, child), "ai-slop/redundant-type-coercion", `Parameter '${arg.text}' is already typed as ${primitive} but is coerced again`, "Trust the typed boundary or validate unknown input before this function. Re-coercing already typed parameters is usually defensive agent noise."));
1822
+ }
1823
+ }
1824
+ ts.forEachChild(child, visitBody);
1825
+ };
1826
+ visitBody(body);
1827
+ };
1828
+ const visit = (node) => {
1829
+ if (isFunctionNode(node)) scanFunctionBody(node, primitiveParamsOf(node));
1830
+ ts.forEachChild(node, visit);
1831
+ };
1832
+ visit(sourceFile);
1833
+ return diagnostics;
1834
+ };
1835
+ const normalizedTypeDeclaration = (sourceFile, node) => sourceFile.text.slice(node.getStart(sourceFile), node.getEnd()).replace(/\bexport\b/g, "").replace(/\bdeclare\b/g, "").replace(/\s+/g, " ").trim();
1836
+ const exportedTypesOf = (parsed) => {
1837
+ const declarations = [];
1838
+ const visit = (node) => {
1839
+ if ((ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) && hasExportModifier(node)) declarations.push({
1840
+ name: node.name.text,
1841
+ signature: normalizedTypeDeclaration(parsed.sourceFile, node),
1842
+ filePath: parsed.relativePath,
1843
+ line: lineFor(parsed.sourceFile, node)
1844
+ });
1845
+ ts.forEachChild(node, visit);
1846
+ };
1847
+ visit(parsed.sourceFile);
1848
+ return declarations;
1849
+ };
1850
+ const duplicateTypeKeyOf = (declaration) => `${declaration.name}\0${declaration.signature}`;
1851
+ const detectDuplicateExportedTypes = (parsedSources) => {
1852
+ const diagnostics = [];
1853
+ const seen = /* @__PURE__ */ new Map();
1854
+ for (const parsed of parsedSources) {
1855
+ if (!TS_EXTENSIONS.has(parsed.ext)) continue;
1856
+ for (const declaration of exportedTypesOf(parsed)) {
1857
+ const key = duplicateTypeKeyOf(declaration);
1858
+ const previous = seen.get(key);
1859
+ if (!previous) {
1860
+ seen.set(key, declaration);
1861
+ continue;
1862
+ }
1863
+ if (previous.filePath === declaration.filePath) continue;
1864
+ diagnostics.push(makeDiagnostic(declaration.filePath, declaration.line, "ai-slop/duplicate-type-declaration", `Exported type '${declaration.name}' duplicates an existing declaration`, `Reuse or import the existing type from ${previous.filePath} instead of re-declaring the same shape in another file.`));
1865
+ }
1866
+ }
1867
+ return diagnostics;
1868
+ };
1869
+ const detectDefensivePatterns = async (context) => {
1870
+ const diagnostics = [];
1871
+ const parsedSources = [];
1872
+ for (const filePath of getSourceFiles(context)) {
1873
+ if (isAutoGenerated(filePath)) continue;
1874
+ let content;
1875
+ try {
1876
+ content = fs.readFileSync(filePath, "utf-8");
1877
+ } catch {
1878
+ continue;
1879
+ }
1880
+ const relativePath = path.relative(context.rootDirectory, filePath);
1881
+ if (isNonProductionPath(relativePath)) continue;
1882
+ const ext = path.extname(filePath);
1883
+ if (!JS_TS_EXTENSIONS.has(ext)) continue;
1884
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKindFor(ext));
1885
+ parsedSources.push({
1886
+ sourceFile,
1887
+ relativePath,
1888
+ ext
1889
+ });
1890
+ diagnostics.push(...detectRedundantTryCatch(sourceFile, relativePath));
1891
+ if (TS_EXTENSIONS.has(ext)) diagnostics.push(...detectPrimitiveCoercions(sourceFile, relativePath));
1892
+ }
1893
+ diagnostics.push(...detectDuplicateExportedTypes(parsedSources));
1894
+ return diagnostics;
1895
+ };
1896
+
1710
1897
  //#endregion
1711
1898
  //#region src/engines/ai-slop/duplicate-imports.ts
1712
1899
  const JS_EXTENSIONS$3 = new Set([
@@ -1717,7 +1904,16 @@ const JS_EXTENSIONS$3 = new Set([
1717
1904
  ".mjs",
1718
1905
  ".cjs"
1719
1906
  ]);
1720
- const IMPORT_FROM_RE$1 = /^\s*import\s+[^;]*?from\s+["']([^"']+)["']/;
1907
+ const IMPORT_FROM_RE$1 = /^\s*import\s+([^;]*?)\s+from\s+["']([^"']+)["']/;
1908
+ const TYPE_ONLY_RE = /^\s*type\b/;
1909
+ const VALUE_BINDING_RE = /\{([^}]*)\}/;
1910
+ const isTypeOnly = (clause) => {
1911
+ if (TYPE_ONLY_RE.test(clause)) return true;
1912
+ const braces = VALUE_BINDING_RE.exec(clause);
1913
+ if (!braces) return false;
1914
+ const members = braces[1].split(",").map((member) => member.trim()).filter((member) => member.length > 0);
1915
+ return members.length > 0 && members.every((member) => /^type\b/.test(member));
1916
+ };
1721
1917
  const extractImportLines = (content) => {
1722
1918
  const lines = content.split("\n");
1723
1919
  const results = [];
@@ -1726,8 +1922,9 @@ const extractImportLines = (content) => {
1726
1922
  const match = IMPORT_FROM_RE$1.exec(line);
1727
1923
  if (!match) continue;
1728
1924
  results.push({
1729
- spec: match[1],
1730
- line: i + 1
1925
+ spec: match[2],
1926
+ line: i + 1,
1927
+ typeOnly: isTypeOnly(match[1])
1731
1928
  });
1732
1929
  }
1733
1930
  return results;
@@ -1746,14 +1943,16 @@ const detectDuplicateImports = async (context) => {
1746
1943
  }
1747
1944
  const imports = extractImportLines(content);
1748
1945
  if (imports.length < 2) continue;
1749
- const bySpec = /* @__PURE__ */ new Map();
1946
+ const byBucket = /* @__PURE__ */ new Map();
1750
1947
  for (const imp of imports) {
1751
- const list = bySpec.get(imp.spec) ?? [];
1948
+ const key = `${imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
1949
+ const list = byBucket.get(key) ?? [];
1752
1950
  list.push(imp);
1753
- bySpec.set(imp.spec, list);
1951
+ byBucket.set(key, list);
1754
1952
  }
1755
1953
  const relPath = path.relative(context.rootDirectory, filePath);
1756
- for (const [spec, occurrences] of bySpec) {
1954
+ for (const occurrences of byBucket.values()) {
1955
+ const { spec } = occurrences[0];
1757
1956
  if (occurrences.length < 2) continue;
1758
1957
  for (const dup of occurrences.slice(1)) {
1759
1958
  const firstLine = occurrences[0].line;
@@ -1958,6 +2157,130 @@ const detectGoPatterns = async (context) => {
1958
2157
  return diagnostics;
1959
2158
  };
1960
2159
 
2160
+ //#endregion
2161
+ //#region src/engines/ai-slop/hardcoded-config.ts
2162
+ const SOURCE_EXTENSIONS = new Set([
2163
+ ".ts",
2164
+ ".tsx",
2165
+ ".js",
2166
+ ".jsx",
2167
+ ".mjs",
2168
+ ".cjs",
2169
+ ".py",
2170
+ ".go",
2171
+ ".rs",
2172
+ ".rb",
2173
+ ".java",
2174
+ ".php"
2175
+ ]);
2176
+ const URL_LITERAL_RE = /(["'`])(https?:\/\/[^"'`\s<>]+)\1/g;
2177
+ const ID_LITERAL_RE = /(["'])([A-Za-z][A-Za-z0-9_-]{15,})\1/g;
2178
+ const ENV_REFERENCE_RE = /\b(?:process\.env|import\.meta\.env|Deno\.env|os\.environ|getenv|env\()\b/i;
2179
+ const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|license|readme|source|svgUrl|pageUrl|href|link|install)\b/i;
2180
+ const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
2181
+ const ENVIRONMENT_HOST_RE = /(?:^|[.-])(?:api|app|admin|auth|staging|stage|prod|dev|sandbox|webhook|internal)(?:[.-]|$)|^(?:localhost|127\.0\.0\.1|0\.0\.0\.0)$/i;
2182
+ const ID_CONTEXT_RE = /(?:^|[^A-Za-z0-9])(?:api[_-]?key|client[_-]?id|project[_-]?id|org(?:anization)?[_-]?id|workspace[_-]?id|tenant[_-]?id|price[_-]?id|product[_-]?id|customer[_-]?id|subscription[_-]?id|account[_-]?id|app[_-]?id|key|token|secret)(?:$|[^A-Za-z0-9])/i;
2183
+ const PLACEHOLDER_HOSTS = new Set([
2184
+ "example.com",
2185
+ "example.org",
2186
+ "example.net"
2187
+ ]);
2188
+ const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
2189
+ const HARDCODED_URL_FINDING = {
2190
+ rule: "ai-slop/hardcoded-url",
2191
+ message: "Hardcoded environment URL in production code",
2192
+ help: "Move deployment-specific URLs to environment variables or a typed config module. Keep only stable documentation/public links inline."
2193
+ };
2194
+ const HARDCODED_ID_FINDING = {
2195
+ rule: "ai-slop/hardcoded-id",
2196
+ message: "Hardcoded provider/project ID in production code",
2197
+ help: "Move provider IDs, tenant IDs, price IDs, and similar deployment-specific identifiers to env/config so agents do not bake one environment into source."
2198
+ };
2199
+ const makeFinding = (filePath, line, spec) => ({
2200
+ filePath,
2201
+ engine: "ai-slop",
2202
+ rule: spec.rule,
2203
+ severity: "warning",
2204
+ message: spec.message,
2205
+ help: spec.help,
2206
+ line,
2207
+ column: 0,
2208
+ category: "AI Slop",
2209
+ fixable: false
2210
+ });
2211
+ const isCommentOnlyLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*");
2212
+ const commentStartsBefore = (line, index, ext) => {
2213
+ const prefix = line.slice(0, index);
2214
+ if (ext === ".py" || ext === ".rb") return prefix.includes("#");
2215
+ if (ext === ".php") return prefix.includes("//") || prefix.includes("#");
2216
+ return prefix.includes("//") || prefix.includes("/*");
2217
+ };
2218
+ const safeUrlHost = (urlText) => {
2219
+ try {
2220
+ return new URL(urlText).hostname.toLowerCase();
2221
+ } catch {
2222
+ return null;
2223
+ }
2224
+ };
2225
+ const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
2226
+ const shouldFlagUrlLiteral = (line, urlText) => {
2227
+ if (isEnvBackedLine(line)) return false;
2228
+ const host = safeUrlHost(urlText);
2229
+ if (!host) return false;
2230
+ if (PLACEHOLDER_HOSTS.has(host)) return false;
2231
+ if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
2232
+ return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
2233
+ };
2234
+ const hasUsefulIdShape = (value) => {
2235
+ if (PLACEHOLDER_ID_RE.test(value)) return false;
2236
+ if (/^https?:\/\//i.test(value)) return false;
2237
+ if (/^[A-Za-z]+$/.test(value)) return false;
2238
+ return /[0-9_-]/.test(value);
2239
+ };
2240
+ const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
2241
+ const diagnostics = [];
2242
+ if (isCommentOnlyLine(line.trim())) return diagnostics;
2243
+ URL_LITERAL_RE.lastIndex = 0;
2244
+ let urlMatch;
2245
+ while ((urlMatch = URL_LITERAL_RE.exec(line)) !== null) {
2246
+ const urlText = urlMatch[2];
2247
+ if (commentStartsBefore(line, urlMatch.index, ext)) continue;
2248
+ if (!shouldFlagUrlLiteral(line, urlText)) continue;
2249
+ diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_URL_FINDING));
2250
+ }
2251
+ if (!ID_CONTEXT_RE.test(line) || isEnvBackedLine(line) || DOC_URL_CONTEXT_RE.test(line)) return diagnostics;
2252
+ ID_LITERAL_RE.lastIndex = 0;
2253
+ let idMatch;
2254
+ while ((idMatch = ID_LITERAL_RE.exec(line)) !== null) {
2255
+ const value = idMatch[2];
2256
+ if (commentStartsBefore(line, idMatch.index, ext)) continue;
2257
+ if (!hasUsefulIdShape(value)) continue;
2258
+ diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_ID_FINDING));
2259
+ }
2260
+ return diagnostics;
2261
+ };
2262
+ const scanFileForConfigLiterals = (content, relativePath, ext) => {
2263
+ if (!SOURCE_EXTENSIONS.has(ext)) return [];
2264
+ if (isNonProductionPath(relativePath)) return [];
2265
+ return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
2266
+ };
2267
+ const detectHardcodedConfigLiterals = async (context) => {
2268
+ const diagnostics = [];
2269
+ for (const filePath of getSourceFiles(context)) {
2270
+ if (isAutoGenerated(filePath)) continue;
2271
+ let content;
2272
+ try {
2273
+ content = fs.readFileSync(filePath, "utf-8");
2274
+ } catch {
2275
+ continue;
2276
+ }
2277
+ const relativePath = path.relative(context.rootDirectory, filePath);
2278
+ const ext = path.extname(filePath);
2279
+ diagnostics.push(...scanFileForConfigLiterals(content, relativePath, ext));
2280
+ }
2281
+ return diagnostics;
2282
+ };
2283
+
1961
2284
  //#endregion
1962
2285
  //#region src/engines/ai-slop/js-import-aliases.ts
1963
2286
  const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
@@ -2225,23 +2548,87 @@ const PYTHON_STDLIB = new Set([
2225
2548
  "zoneinfo"
2226
2549
  ]);
2227
2550
  const PYTHON_IMPORT_TO_PIP = {
2228
- yaml: "pyyaml",
2229
- PIL: "pillow",
2230
- dateutil: "python-dateutil",
2231
- cv2: "opencv-python",
2232
- sklearn: "scikit-learn",
2233
- bs4: "beautifulsoup4",
2234
- typing_extensions: "typing-extensions",
2235
- google: "google-api-python-client",
2236
- jose: "python-jose",
2237
- jwt: "pyjwt",
2238
- OpenSSL: "pyopenssl",
2239
- magic: "python-magic",
2240
- docx: "python-docx",
2241
- pptx: "python-pptx",
2242
- git: "gitpython",
2243
- socks: "pysocks",
2244
- redis: "redis"
2551
+ yaml: ["pyyaml"],
2552
+ PIL: ["pillow"],
2553
+ dateutil: ["python-dateutil"],
2554
+ cv2: [
2555
+ "opencv-python",
2556
+ "opencv-python-headless",
2557
+ "opencv-contrib-python"
2558
+ ],
2559
+ sklearn: ["scikit-learn"],
2560
+ bs4: ["beautifulsoup4"],
2561
+ typing_extensions: ["typing-extensions"],
2562
+ dotenv: ["python-dotenv"],
2563
+ genai: ["google-genai"],
2564
+ google: [
2565
+ "google-genai",
2566
+ "google-generativeai",
2567
+ "google-api-python-client",
2568
+ "google-cloud-storage",
2569
+ "google-cloud-aiplatform",
2570
+ "google-auth",
2571
+ "protobuf"
2572
+ ],
2573
+ jose: ["python-jose"],
2574
+ jwt: ["pyjwt"],
2575
+ OpenSSL: ["pyopenssl"],
2576
+ Crypto: ["pycryptodome", "pycryptodomex"],
2577
+ Cryptodome: ["pycryptodomex", "pycryptodome"],
2578
+ magic: ["python-magic"],
2579
+ docx: ["python-docx"],
2580
+ pptx: ["python-pptx"],
2581
+ git: ["gitpython"],
2582
+ socks: ["pysocks"],
2583
+ redis: ["redis"],
2584
+ cairo: ["pycairo"],
2585
+ serial: ["pyserial"],
2586
+ usb: ["pyusb"],
2587
+ gi: ["pygobject"],
2588
+ Xlib: ["python-xlib"],
2589
+ ldap: ["python-ldap"],
2590
+ slugify: ["python-slugify"],
2591
+ memcache: ["python-memcached"],
2592
+ dns: ["dnspython"],
2593
+ attr: ["attrs"],
2594
+ attrs: ["attrs"],
2595
+ zoneinfo_data: ["tzdata"],
2596
+ pkg_resources: ["setuptools"],
2597
+ setuptools: ["setuptools"],
2598
+ wx: ["wxpython"],
2599
+ skimage: ["scikit-image"],
2600
+ OpenGL: ["pyopengl"],
2601
+ win32api: ["pywin32"],
2602
+ win32con: ["pywin32"],
2603
+ win32com: ["pywin32"],
2604
+ pythoncom: ["pywin32"],
2605
+ pywintypes: ["pywin32"],
2606
+ rest_framework: ["djangorestframework"],
2607
+ allauth: ["django-allauth"],
2608
+ corsheaders: ["django-cors-headers"],
2609
+ debug_toolbar: ["django-debug-toolbar"],
2610
+ environ: ["django-environ"],
2611
+ flask_cors: ["flask-cors"],
2612
+ flask_sqlalchemy: ["flask-sqlalchemy"],
2613
+ flask_migrate: ["flask-migrate"],
2614
+ flask_login: ["flask-login"],
2615
+ jwt_extended: ["flask-jwt-extended"],
2616
+ dateparser: ["dateparser"],
2617
+ yaml_include: ["pyyaml-include"],
2618
+ lxml_html_clean: ["lxml-html-clean"],
2619
+ grpc: ["grpcio"],
2620
+ grpc_status: ["grpcio-status"],
2621
+ google_crc32c: ["google-crc32c"],
2622
+ pkg_about: ["pkg-about"],
2623
+ mpl_toolkits: ["matplotlib"],
2624
+ dotmap: ["dotmap"],
2625
+ pydantic_settings: ["pydantic-settings"],
2626
+ telegram: ["python-telegram-bot"],
2627
+ discord: ["discord-py"],
2628
+ nacl: ["pynacl"],
2629
+ jwcrypto: ["jwcrypto"],
2630
+ humanfriendly: ["humanfriendly"],
2631
+ multipart: ["python-multipart"]
2245
2632
  };
2246
2633
 
2247
2634
  //#endregion
@@ -2280,6 +2667,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
2280
2667
  const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
2281
2668
  if (m) addPyDep(pyDeps, m[1]);
2282
2669
  }
2670
+ const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
2671
+ if (extras) for (const m of extras[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
2283
2672
  const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
2284
2673
  let match = poetryRe.exec(content);
2285
2674
  while (match !== null) {
@@ -2478,6 +2867,11 @@ const packageNameFromImport = (spec) => {
2478
2867
  }
2479
2868
  return spec.split("/")[0];
2480
2869
  };
2870
+ const typesPackageName = (pkg) => {
2871
+ if (pkg.startsWith("@types/")) return pkg;
2872
+ if (pkg.startsWith("@")) return `@types/${pkg.slice(1).replace("/", "__")}`;
2873
+ return `@types/${pkg}`;
2874
+ };
2481
2875
  const STATIC_IMPORT_RE = /^\s*import\s+(?:[\w*{},\s]+\s+from\s+)?["']([^"']+)["']/;
2482
2876
  const DYNAMIC_IMPORT_RE = /(?:import|require)\s*\(\s*["']([^"']+)["']/g;
2483
2877
  const extractJsImports = (content) => {
@@ -2542,15 +2936,16 @@ const checkJsImport = (rawSpec, manifest, tsAliasMatchers) => {
2542
2936
  const realPkg = pkg.slice(7);
2543
2937
  if (manifest.jsDeps.has(realPkg)) return null;
2544
2938
  }
2939
+ if (manifest.jsDeps.has(typesPackageName(pkg))) return null;
2545
2940
  return pkg;
2546
2941
  };
2942
+ const normalizePyName = (name) => name.toLowerCase().replace(/_/g, "-");
2547
2943
  const checkPyImport = (spec, manifest) => {
2548
2944
  const root = spec.split(".")[0];
2549
2945
  if (PYTHON_STDLIB.has(root)) return null;
2550
- const normalized = root.toLowerCase().replace(/_/g, "-");
2946
+ const normalized = normalizePyName(root);
2551
2947
  if (manifest.pyDeps.has(normalized)) return null;
2552
- const pipName = PYTHON_IMPORT_TO_PIP[root];
2553
- if (pipName && manifest.pyDeps.has(pipName)) return null;
2948
+ if ((PYTHON_IMPORT_TO_PIP[root] ?? PYTHON_IMPORT_TO_PIP[normalized])?.some((dist) => manifest.pyDeps.has(normalizePyName(dist)))) return null;
2554
2949
  return root;
2555
2950
  };
2556
2951
  const detectHallucinatedImports = async (context) => {
@@ -2700,6 +3095,85 @@ const collectBlocks = (sourceLines, syntax) => {
2700
3095
  return blocks;
2701
3096
  };
2702
3097
 
3098
+ //#endregion
3099
+ //#region src/engines/ai-slop/meta-comment.ts
3100
+ const PLAN_REFERENCE_RES = [
3101
+ /\b(?:stage|step|phase)\s+\d+\b(?!\s*[:.]?\s*(?:bytes|ms|seconds|of\s+\d))/i,
3102
+ /\bstep\s+\d+\s+of\s+the\s+plan\b/i,
3103
+ /\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
3104
+ /\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
3105
+ /\bfrom\s+the\s+(?:task|todo|plan|spec|ticket|prompt|requirements?)\b/i,
3106
+ /\bimplement(?:ing|s|ed)?\s+use\s*case\s+\d*/i,
3107
+ /\b(?:requirements?\s+doc|requirement\s+\d+)\b/i,
3108
+ /\bas\s+(?:instructed|specified|outlined)\s+(?:above|below|in\s+the)\b/i
3109
+ ];
3110
+ const BEFORE_AFTER_RES = [
3111
+ /\bpreviously[,:]?\s+(?:this|we|it|the)\b/i,
3112
+ /\bused\s+to\s+(?:be|use|call|return|do|have|rely)\b/i,
3113
+ /\bchanged\s+(?:\w+\s+){0,3}from\s+.+\bto\b/i,
3114
+ /\bno\s+longer\s+(?:needed|used|required|necessary|calls?|returns?|does)\b/i,
3115
+ /\bthis\s+was\s+.+\bbut\s+now\b/i,
3116
+ /\bwe\s+(?:now|used\s+to)\s+(?:no\s+longer\s+)?(?:use|call|return|do|have)\b/i,
3117
+ /\breplaced\s+the\s+(?:old|previous|former)\b/i,
3118
+ /\b(?:was|were)\s+(?:renamed|moved|removed|refactored|extracted)\s+(?:from|to|out\s+of)\b/i
3119
+ ];
3120
+ const WHY_OR_TODO_RE = /\b(?:because|since|otherwise|todo|fixme|hack|note:|reason:|workaround|see\s+(?:issue|#))\b/i;
3121
+ const looksLikeLicenseHeader$1 = (block) => {
3122
+ if (block.startLine !== 1) return false;
3123
+ const text = block.rawLines.join(" ").toLowerCase();
3124
+ return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
3125
+ };
3126
+ const looksLikeSuppressDirective$1 = (block) => block.rawLines.some((line) => /\b(?:biome-ignore|eslint-disable|ts-ignore|ts-expect-error|@ts-\w+|noqa|pylint:\s*disable|rubocop:disable|noinspection|phpcs:disable)\b/.test(line));
3127
+ const matchMetaSignal = (block) => {
3128
+ if (looksLikeLicenseHeader$1(block)) return null;
3129
+ if (looksLikeSuppressDirective$1(block)) return null;
3130
+ if (block.kind === "jsdoc" && block.hasMeaningfulJsdocTag) return null;
3131
+ if (block.isRustDoc) return null;
3132
+ const joined = block.prose.join(" ");
3133
+ if (joined.trim().length === 0) return null;
3134
+ if (WHY_OR_TODO_RE.test(joined)) return null;
3135
+ if (PLAN_REFERENCE_RES.some((re) => re.test(joined))) return "plan/process reference";
3136
+ if (BEFORE_AFTER_RES.some((re) => re.test(joined))) return "before/after state narration";
3137
+ return null;
3138
+ };
3139
+ const detectMetaComments = async (context) => {
3140
+ const files = getSourceFiles(context);
3141
+ const diagnostics = [];
3142
+ for (const filePath of files) {
3143
+ const ext = path.extname(filePath);
3144
+ if (!SUPPORTED_EXTS.has(ext)) continue;
3145
+ if (isAutoGenerated(filePath)) continue;
3146
+ const syntax = getCommentSyntax(ext);
3147
+ if (!syntax) continue;
3148
+ const relativePath = path.relative(context.rootDirectory, filePath);
3149
+ if (isNonProductionPath(relativePath)) continue;
3150
+ let content;
3151
+ try {
3152
+ content = fs.readFileSync(filePath, "utf-8");
3153
+ } catch {
3154
+ continue;
3155
+ }
3156
+ const blocks = collectBlocks(content.split("\n"), syntax);
3157
+ for (const block of blocks) {
3158
+ const reason = matchMetaSignal(block);
3159
+ if (!reason) continue;
3160
+ diagnostics.push({
3161
+ filePath: relativePath,
3162
+ engine: "ai-slop",
3163
+ rule: "ai-slop/meta-comment",
3164
+ severity: "warning",
3165
+ message: `Meta/plan comment (${reason})`,
3166
+ help: "Remove — references to the build plan or before/after code state belong in PR descriptions and commit messages, not source.",
3167
+ line: block.startLine,
3168
+ column: 0,
3169
+ category: "Comments",
3170
+ fixable: false
3171
+ });
3172
+ }
3173
+ }
3174
+ return diagnostics;
3175
+ };
3176
+
2703
3177
  //#endregion
2704
3178
  //#region src/engines/ai-slop/narrative-comments.ts
2705
3179
  const looksLikeDeclarationPreamble = (nextLine, ext) => {
@@ -2931,6 +3405,11 @@ const BROAD_EXCEPT_RE = /^\s*except\s+(Exception|BaseException)\s*(?:as\s+\w+)?\
2931
3405
  const PRINT_RE = /^\s*print\s*\(/;
2932
3406
  const DEF_RE = /^\s*(?:async\s+)?def\s+\w+\s*\(/;
2933
3407
  const MUTABLE_DEFAULT_RE = /(\w+)\s*(?::\s*[^,)=]+)?\s*=\s*(\[\s*\]|\{\s*\}|set\(\s*\))/;
3408
+ const RANGE_LEN_LOOP_RE = /^\s*for\s+([A-Za-z_]\w*)\s+in\s+range\s*\(\s*len\s*\(\s*([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*\)\s*\)\s*:\s*(?:#.*)?$/;
3409
+ const CHAINED_DICT_GET_RE = /\.get\s*\([^)]*,\s*\{\s*\}\s*\)\s*\.get\s*\(/;
3410
+ const SAME_VALUE_BRANCH_RE = /^(\s*)(?:if|elif)\s+([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*==\s*["'][^"']+["']\s*:/;
3411
+ const INSTANCE_BRANCH_RE = /^(\s*)(?:if|elif)\s+isinstance\s*\(\s*([A-Za-z_]\w*)\s*,\s*[^)]+\)\s*:/;
3412
+ const BRANCH_LADDER_THRESHOLD = 4;
2934
3413
  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");
2935
3414
  const isScriptOrEntrypoint = (basename) => basename === "__main__.py" || basename === "manage.py" || basename === "setup.py";
2936
3415
  const SCRIPT_DIR_NAMES = new Set([
@@ -2983,6 +3462,13 @@ const pushFinding = (out, a) => {
2983
3462
  fixable: false
2984
3463
  });
2985
3464
  };
3465
+ const pushLineFinding = (out, relPath, line, finding) => {
3466
+ pushFinding(out, {
3467
+ relPath,
3468
+ line,
3469
+ ...finding
3470
+ });
3471
+ };
2986
3472
  const flagBareExcept = (lines, relPath, out) => {
2987
3473
  for (let i = 0; i < lines.length; i++) {
2988
3474
  if (!BARE_EXCEPT_RE.test(lines[i])) continue;
@@ -3064,6 +3550,76 @@ const flagPrintInProduction = (lines, relPath, basename, out) => {
3064
3550
  });
3065
3551
  }
3066
3552
  };
3553
+ const flagRangeLenLoops = (lines, relPath, out) => {
3554
+ for (let i = 0; i < lines.length; i++) {
3555
+ const match = RANGE_LEN_LOOP_RE.exec(lines[i]);
3556
+ if (!match) continue;
3557
+ pushLineFinding(out, relPath, i + 1, {
3558
+ rule: "ai-slop/python-range-len-loop",
3559
+ severity: "info",
3560
+ message: `\`range(len(${match[2]}))\` loop — usually a hand-rolled iteration pattern.`,
3561
+ help: "Prefer direct iteration (`for item in items`) or `enumerate(items)` when the index is needed. Keeping index plumbing out of the loop reduces checkpoint-to-checkpoint bloat."
3562
+ });
3563
+ }
3564
+ };
3565
+ const flagChainedDictGets = (lines, relPath, out) => {
3566
+ for (let i = 0; i < lines.length; i++) {
3567
+ if (!CHAINED_DICT_GET_RE.test(lines[i])) continue;
3568
+ pushLineFinding(out, relPath, i + 1, {
3569
+ rule: "ai-slop/python-chained-dict-get",
3570
+ severity: "warning",
3571
+ message: "Chained `.get(..., {})` defaults hide missing-data cases.",
3572
+ help: "Normalize the input at the boundary, use a typed object, or split the lookup into explicit steps. Empty-dict fallback chains are a common agent shortcut that becomes brittle as schemas evolve."
3573
+ });
3574
+ }
3575
+ };
3576
+ const countBranchLadder = (lines, start, pattern, selector, indent) => {
3577
+ let count = 1;
3578
+ for (let i = start + 1; i < lines.length; i++) {
3579
+ const line = lines[i];
3580
+ const trimmed = line.trim();
3581
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
3582
+ const match = pattern.exec(line);
3583
+ if (match?.[1] === indent && match[2] === selector) {
3584
+ count++;
3585
+ continue;
3586
+ }
3587
+ if (line.startsWith(`${indent}elif `)) break;
3588
+ if (line.length - line.trimStart().length <= indent.length && !line.startsWith(`${indent}else`)) break;
3589
+ }
3590
+ return count;
3591
+ };
3592
+ const flagBranchLadders = (lines, relPath, out) => {
3593
+ const reported = /* @__PURE__ */ new Set();
3594
+ for (let i = 0; i < lines.length; i++) {
3595
+ if (reported.has(i)) continue;
3596
+ const valueMatch = SAME_VALUE_BRANCH_RE.exec(lines[i]);
3597
+ if (valueMatch) {
3598
+ const count = countBranchLadder(lines, i, SAME_VALUE_BRANCH_RE, valueMatch[2], valueMatch[1]);
3599
+ if (count >= BRANCH_LADDER_THRESHOLD) {
3600
+ reported.add(i);
3601
+ pushLineFinding(out, relPath, i + 1, {
3602
+ rule: "ai-slop/python-repetitive-dispatch",
3603
+ severity: "warning",
3604
+ message: `${count} repeated branches dispatch on \`${valueMatch[2]}\`.`,
3605
+ help: "Use a table, set membership, or handler map when branches share the same shape. SlopCodeBench highlights these selector ladders as code that keeps growing instead of absorbing new cases cleanly."
3606
+ });
3607
+ }
3608
+ continue;
3609
+ }
3610
+ const instanceMatch = INSTANCE_BRANCH_RE.exec(lines[i]);
3611
+ if (!instanceMatch) continue;
3612
+ const count = countBranchLadder(lines, i, INSTANCE_BRANCH_RE, instanceMatch[2], instanceMatch[1]);
3613
+ if (count < BRANCH_LADDER_THRESHOLD) continue;
3614
+ reported.add(i);
3615
+ pushLineFinding(out, relPath, i + 1, {
3616
+ rule: "ai-slop/python-isinstance-ladder",
3617
+ severity: "warning",
3618
+ message: `${count} repeated \`isinstance(${instanceMatch[2]}, ...)\` branches.`,
3619
+ help: "Prefer a handler map, protocol, or normalized intermediate representation when each type branch has the same role. Repeated type ladders are one of the maintainability smells SCBench-style checks look for."
3620
+ });
3621
+ }
3622
+ };
3067
3623
  const detectPythonPatterns = async (context) => {
3068
3624
  const diagnostics = [];
3069
3625
  const files = getSourceFiles(context);
@@ -3083,6 +3639,9 @@ const detectPythonPatterns = async (context) => {
3083
3639
  flagBroadExceptWithSilentBody(lines, relPath, diagnostics);
3084
3640
  flagMutableDefaults(lines, relPath, diagnostics);
3085
3641
  flagPrintInProduction(lines, relPath, basename, diagnostics);
3642
+ flagRangeLenLoops(lines, relPath, diagnostics);
3643
+ flagChainedDictGets(lines, relPath, diagnostics);
3644
+ flagBranchLadders(lines, relPath, diagnostics);
3086
3645
  }
3087
3646
  return diagnostics;
3088
3647
  };
@@ -3223,6 +3782,143 @@ const detectRustPatterns = async (context) => {
3223
3782
  return diagnostics;
3224
3783
  };
3225
3784
 
3785
+ //#endregion
3786
+ //#region src/engines/ai-slop/silent-recovery.ts
3787
+ const JS_EXTS$1 = new Set([
3788
+ ".ts",
3789
+ ".tsx",
3790
+ ".js",
3791
+ ".jsx",
3792
+ ".mjs",
3793
+ ".cjs"
3794
+ ]);
3795
+ const CATCH_HEAD_RE = /\bcatch\s*(?:\([^)]*\))?\s*\{/g;
3796
+ const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
3797
+ const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
3798
+ const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
3799
+ const extractCatchBody = (content, openBraceIndex) => {
3800
+ let depth = 0;
3801
+ let inString = null;
3802
+ for (let i = openBraceIndex; i < content.length; i += 1) {
3803
+ const ch = content[i];
3804
+ const prev = content[i - 1];
3805
+ if (inString) {
3806
+ if (ch === inString && prev !== "\\") inString = null;
3807
+ continue;
3808
+ }
3809
+ if (ch === "\"" || ch === "'" || ch === "`") {
3810
+ inString = ch;
3811
+ continue;
3812
+ }
3813
+ if (ch === "{") depth += 1;
3814
+ else if (ch === "}") {
3815
+ depth -= 1;
3816
+ if (depth === 0) return content.slice(openBraceIndex + 1, i);
3817
+ }
3818
+ }
3819
+ return null;
3820
+ };
3821
+ const isLogOnlyBody = (body) => {
3822
+ const statements = stripBlockComments(body).split("\n").map((line) => line.replace(/\/\/.*$/, "").trim()).filter((line) => line.length > 0 && line !== ";");
3823
+ if (statements.length === 0) return false;
3824
+ if (statements.some((line) => HANDLING_TOKEN_RE.test(line))) return false;
3825
+ let sawLog = false;
3826
+ for (const statement of statements) {
3827
+ const normalized = statement.replace(/;+$/, "");
3828
+ if (LOG_STATEMENT_RE.test(normalized)) {
3829
+ sawLog = true;
3830
+ continue;
3831
+ }
3832
+ if (/^[\w$"'`{[(),.\s+:-]+$/.test(normalized) && !/[=(]\s*(?:async\s+)?\(/.test(normalized)) continue;
3833
+ return false;
3834
+ }
3835
+ return sawLog;
3836
+ };
3837
+ const detectJsSilentRecovery = (content, relPath) => {
3838
+ const out = [];
3839
+ CATCH_HEAD_RE.lastIndex = 0;
3840
+ let match;
3841
+ while ((match = CATCH_HEAD_RE.exec(content)) !== null) {
3842
+ const body = extractCatchBody(content, match.index + match[0].length - 1);
3843
+ if (body === null) continue;
3844
+ if (!isLogOnlyBody(body)) continue;
3845
+ const line = content.slice(0, match.index).split("\n").length;
3846
+ out.push({
3847
+ filePath: relPath,
3848
+ engine: "ai-slop",
3849
+ rule: "ai-slop/silent-recovery",
3850
+ severity: "warning",
3851
+ message: "Catch only logs then continues, leaving execution in a possibly broken state",
3852
+ help: "Handle the error: rethrow, return an error value, or recover explicitly. Logging alone lets the program proceed as if nothing failed.",
3853
+ line,
3854
+ column: 0,
3855
+ category: "AI Slop",
3856
+ fixable: false
3857
+ });
3858
+ }
3859
+ return out;
3860
+ };
3861
+ const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
3862
+ const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
3863
+ const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
3864
+ const detectPySilentRecovery = (content, relPath) => {
3865
+ const out = [];
3866
+ const lines = content.split("\n");
3867
+ for (let i = 0; i < lines.length; i += 1) {
3868
+ const exceptMatch = PY_EXCEPT_RE.exec(lines[i]);
3869
+ if (!exceptMatch) continue;
3870
+ const indent = exceptMatch[1].length;
3871
+ const bodyLines = [];
3872
+ let j = i + 1;
3873
+ for (; j < lines.length; j += 1) {
3874
+ const raw = lines[j];
3875
+ if (raw.trim() === "") continue;
3876
+ if (raw.length - raw.trimStart().length <= indent) break;
3877
+ bodyLines.push(raw.trim());
3878
+ }
3879
+ if (bodyLines.length === 0) continue;
3880
+ if (bodyLines.some((line) => PY_HANDLING_TOKEN_RE.test(line))) continue;
3881
+ if (bodyLines.some((line) => line === "pass")) continue;
3882
+ const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
3883
+ const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
3884
+ if (!allLogs || !sawLog) continue;
3885
+ out.push({
3886
+ filePath: relPath,
3887
+ engine: "ai-slop",
3888
+ rule: "ai-slop/silent-recovery",
3889
+ severity: "warning",
3890
+ message: "except only logs then continues, leaving execution in a possibly broken state",
3891
+ help: "Handle the error: re-raise, return an error value, or recover explicitly. Logging alone lets the program proceed as if nothing failed.",
3892
+ line: i + 1,
3893
+ column: 0,
3894
+ category: "AI Slop",
3895
+ fixable: false
3896
+ });
3897
+ }
3898
+ return out;
3899
+ };
3900
+ const detectSilentRecovery = async (context) => {
3901
+ const files = getSourceFiles(context);
3902
+ const diagnostics = [];
3903
+ for (const filePath of files) {
3904
+ if (isAutoGenerated(filePath)) continue;
3905
+ const ext = path.extname(filePath);
3906
+ const isJs = JS_EXTS$1.has(ext);
3907
+ if (!isJs && !(ext === ".py")) continue;
3908
+ const relPath = path.relative(context.rootDirectory, filePath);
3909
+ if (isNonProductionPath(relPath)) continue;
3910
+ let content;
3911
+ try {
3912
+ content = fs.readFileSync(filePath, "utf-8");
3913
+ } catch {
3914
+ continue;
3915
+ }
3916
+ if (isJs) diagnostics.push(...detectJsSilentRecovery(content, relPath));
3917
+ else diagnostics.push(...detectPySilentRecovery(content, relPath));
3918
+ }
3919
+ return diagnostics;
3920
+ };
3921
+
3226
3922
  //#endregion
3227
3923
  //#region src/engines/ai-slop/unused-imports.ts
3228
3924
  const JS_EXTENSIONS$1 = new Set([
@@ -3412,15 +4108,19 @@ const aiSlopEngine = {
3412
4108
  const results = await Promise.allSettled([
3413
4109
  detectTrivialComments(context),
3414
4110
  detectSwallowedExceptions(context),
4111
+ detectDefensivePatterns(context),
3415
4112
  detectOverAbstraction(context),
3416
4113
  detectDeadPatterns(context),
3417
4114
  detectUnusedImports(context),
3418
4115
  detectNarrativeComments(context),
3419
4116
  detectDuplicateImports(context),
4117
+ detectHardcodedConfigLiterals(context),
3420
4118
  detectPythonPatterns(context),
3421
4119
  detectGoPatterns(context),
3422
4120
  detectRustPatterns(context),
3423
- detectHallucinatedImports(context)
4121
+ detectHallucinatedImports(context),
4122
+ detectSilentRecovery(context),
4123
+ detectMetaComments(context)
3424
4124
  ]);
3425
4125
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
3426
4126
  return {
@@ -5556,9 +6256,9 @@ const lintEngine = {
5556
6256
  const promises = [];
5557
6257
  if (languages.includes("typescript") || languages.includes("javascript")) {
5558
6258
  promises.push(runOxlint(context));
5559
- if (context.config.lint.typecheck) promises.push(import("./typecheck-XJMuCczG.js").then((mod) => mod.runTypecheck(context)));
6259
+ if (context.config.lint.typecheck) promises.push(import("./typecheck-DQSzG8fX.js").then((mod) => mod.runTypecheck(context)));
5560
6260
  }
5561
- if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-Bz0LZhQ6.js").then((n) => n.t).then((mod) => mod.runExpoDoctor(context)));
6261
+ if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-BcIkOte5.js").then((n) => n.t).then((mod) => mod.runExpoDoctor(context)));
5562
6262
  if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
5563
6263
  if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
5564
6264
  if (languages.includes("rust") && installedTools["cargo"]) promises.push(runGenericLinter(context, "rust"));
@@ -6109,7 +6809,7 @@ const RISKY_PATTERNS = [
6109
6809
  help: "Use JSON, MessagePack, or other safe serialization formats for untrusted data"
6110
6810
  },
6111
6811
  {
6112
- pattern: new RegExp(`\\bexec\\s*\\(`, "g"),
6812
+ pattern: new RegExp(`(?<![\\w.>:\\\\])\\bexec\\s*\\(`, "g"),
6113
6813
  extensions: [".py"],
6114
6814
  name: "python-exec",
6115
6815
  message: "Use of exec() can execute arbitrary code",
@@ -6677,8 +7377,8 @@ const redactProperties = (props) => {
6677
7377
 
6678
7378
  //#endregion
6679
7379
  //#region src/telemetry/client.ts
6680
- const POSTHOG_HOST = "https://eu.i.posthog.com";
6681
- const POSTHOG_KEY = "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
7380
+ const POSTHOG_HOST = process.env.AISLOP_POSTHOG_HOST ?? "https://eu.i.posthog.com";
7381
+ const POSTHOG_KEY = process.env.AISLOP_POSTHOG_KEY ?? "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
6682
7382
  const SCHEMA_VERSION = "v2";
6683
7383
  const REQUEST_TIMEOUT_MS = 3e3;
6684
7384
  const isTelemetryDisabled = (config) => {
@@ -7017,6 +7717,30 @@ const printEngineStatus = (result) => {
7017
7717
  }
7018
7718
  };
7019
7719
 
7720
+ //#endregion
7721
+ //#region src/scoring/rule-severity.ts
7722
+ /**
7723
+ * Apply per-rule severity overrides from config: "off" drops the diagnostic,
7724
+ * "error"/"warning" rewrite its severity before scoring and rendering.
7725
+ */
7726
+ const applyRuleSeverities = (diagnostics, overrides) => {
7727
+ if (Object.keys(overrides).length === 0) return diagnostics;
7728
+ const result = [];
7729
+ for (const diagnostic of diagnostics) {
7730
+ const override = overrides[diagnostic.rule];
7731
+ if (!override) {
7732
+ result.push(diagnostic);
7733
+ continue;
7734
+ }
7735
+ if (override === "off") continue;
7736
+ result.push(override === diagnostic.severity ? diagnostic : {
7737
+ ...diagnostic,
7738
+ severity: override
7739
+ });
7740
+ }
7741
+ return result;
7742
+ };
7743
+
7020
7744
  //#endregion
7021
7745
  //#region src/ui/live-grid.ts
7022
7746
  const SPINNER = [
@@ -7153,6 +7877,11 @@ const RULE_LABELS = {
7153
7877
  "knip/types": "Unused type",
7154
7878
  "ai-slop/trivial-comment": "Trivial restating comment",
7155
7879
  "ai-slop/swallowed-exception": "Empty catch (swallowed error)",
7880
+ "ai-slop/silent-recovery": "Catch logs then continues",
7881
+ "ai-slop/meta-comment": "Meta/plan comment",
7882
+ "ai-slop/redundant-try-catch": "Redundant try/catch",
7883
+ "ai-slop/redundant-type-coercion": "Redundant type coercion",
7884
+ "ai-slop/duplicate-type-declaration": "Duplicate exported type",
7156
7885
  "ai-slop/thin-wrapper": "Thin function wrapper",
7157
7886
  "ai-slop/generic-naming": "Generic/vague identifier name",
7158
7887
  "ai-slop/unused-import": "Unused import",
@@ -7166,10 +7895,16 @@ const RULE_LABELS = {
7166
7895
  "ai-slop/ts-directive": "@ts-ignore / @ts-expect-error",
7167
7896
  "ai-slop/narrative-comment": "Narrative comment block",
7168
7897
  "ai-slop/duplicate-import": "Duplicate import statement",
7898
+ "ai-slop/hardcoded-url": "Hardcoded URL",
7899
+ "ai-slop/hardcoded-id": "Hardcoded provider ID",
7169
7900
  "ai-slop/python-bare-except": "Bare except",
7170
7901
  "ai-slop/python-broad-except": "Broad except",
7171
7902
  "ai-slop/python-mutable-default": "Mutable default argument",
7172
7903
  "ai-slop/python-print-debug": "print() left in code",
7904
+ "ai-slop/python-range-len-loop": "range(len(...)) loop",
7905
+ "ai-slop/python-chained-dict-get": "Chained dict get",
7906
+ "ai-slop/python-repetitive-dispatch": "Repetitive dispatch ladder",
7907
+ "ai-slop/python-isinstance-ladder": "isinstance ladder",
7173
7908
  "ai-slop/go-library-panic": "panic() in Go library code",
7174
7909
  "ai-slop/rust-non-test-unwrap": "Rust .unwrap() in production code",
7175
7910
  "ai-slop/rust-todo-stub": "Rust todo!() stub",
@@ -7273,6 +8008,9 @@ const renderSummary = (input, deps = {}) => {
7273
8008
  }
7274
8009
  return lines.join("\n");
7275
8010
  };
8011
+ const renderStarCta = (deps = {}) => {
8012
+ return `\n ${style(deps.theme ?? theme, "muted", "★ Found this useful? Star us at github.com/scanaislop/aislop")}\n`;
8013
+ };
7276
8014
  const renderCleanRun = (input, deps = {}) => {
7277
8015
  const t = deps.theme ?? theme;
7278
8016
  const s = deps.symbols ?? symbols;
@@ -7331,8 +8069,35 @@ const getStagedFiles = (cwd) => {
7331
8069
  return result.stdout.split("\n").filter((f) => f.length > 0).map((f) => path.resolve(cwd, f));
7332
8070
  };
7333
8071
 
8072
+ //#endregion
8073
+ //#region src/utils/history.ts
8074
+ const HISTORY_FILE = "history.jsonl";
8075
+ const isHistoryDisabled = (env = process.env) => env.AISLOP_NO_HISTORY === "1";
8076
+ const historyPath = (directory) => path.join(path.resolve(directory), CONFIG_DIR, HISTORY_FILE);
8077
+ /**
8078
+ * Append a compact scan record to .aislop/history.jsonl. Best-effort: never
8079
+ * throws, so a read-only checkout or missing config dir can't break a scan.
8080
+ */
8081
+ const appendHistory = (input) => {
8082
+ if (isHistoryDisabled()) return;
8083
+ const configDir = path.join(path.resolve(input.directory), CONFIG_DIR);
8084
+ if (!fs.existsSync(configDir)) return;
8085
+ const record = {
8086
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8087
+ score: input.score,
8088
+ errors: input.errors,
8089
+ warnings: input.warnings,
8090
+ files: input.files,
8091
+ cliVersion: APP_VERSION
8092
+ };
8093
+ try {
8094
+ fs.appendFileSync(historyPath(input.directory), `${JSON.stringify(record)}\n`);
8095
+ } catch {}
8096
+ };
8097
+
7334
8098
  //#endregion
7335
8099
  //#region src/commands/scan.ts
8100
+ const isMachineOutput = (options) => Boolean(options.json) || Boolean(options.sarif);
7336
8101
  const shouldUseSpinner = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
7337
8102
  const ALL_ENGINE_NAMES = Object.keys(ENGINE_INFO);
7338
8103
  const BREAKDOWN_TOP_N = 10;
@@ -7388,11 +8153,12 @@ const buildScanRender = (input) => {
7388
8153
  const warnings = input.diagnostics.filter((d) => d.severity === "warning").length;
7389
8154
  const fixable = input.diagnostics.filter((d) => d.fixable).length;
7390
8155
  const hasVulnerableDeps = input.diagnostics.some((d) => d.rule === "security/vulnerable-dependency");
8156
+ const starCta = input.printBrand !== false ? renderStarCta(deps) : "";
7391
8157
  if (input.diagnostics.length === 0 && input.score.score === 100) return `${header}${renderCleanRun({
7392
8158
  score: input.score.score,
7393
8159
  label: input.score.label,
7394
8160
  elapsedMs: input.elapsedMs
7395
- }, deps)}`;
8161
+ }, deps)}${starCta}`;
7396
8162
  const diagBlock = input.diagnostics.length === 0 ? "" : renderDiagnostics(input.diagnostics, input.verbose);
7397
8163
  const nextSteps = [];
7398
8164
  if (fixable > 0) nextSteps.push({
@@ -7419,7 +8185,7 @@ const buildScanRender = (input) => {
7419
8185
  nextSteps,
7420
8186
  breakdown: computeBreakdown(input.diagnostics),
7421
8187
  thresholds: input.thresholds
7422
- }, deps)}`;
8188
+ }, deps)}${starCta}`;
7423
8189
  };
7424
8190
  const scanCommand = async (directory, config, options) => {
7425
8191
  const resolvedDir = path.resolve(directory);
@@ -7446,17 +8212,18 @@ const scanCommand = async (directory, config, options) => {
7446
8212
  const runScanBody = async (resolvedDir, config, options, projectInfo) => {
7447
8213
  const startTime = performance.now();
7448
8214
  const showHeader = options.showHeader !== false;
7449
- const useLiveProgress = !options.json && shouldUseSpinner();
8215
+ const machineOutput = isMachineOutput(options);
8216
+ const useLiveProgress = !machineOutput && shouldUseSpinner();
7450
8217
  let files;
7451
8218
  if (options.staged) {
7452
8219
  files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir), [], config.exclude);
7453
- if (!options.json) log.muted(`Scope: ${files.length} staged file(s)`);
8220
+ if (!machineOutput) log.muted(`Scope: ${files.length} staged file(s)`);
7454
8221
  } else if (options.changes) {
7455
8222
  files = filterProjectFiles(resolvedDir, getChangedFiles(resolvedDir), [], config.exclude);
7456
- if (!options.json) log.muted(`Scope: ${files.length} changed file(s)`);
8223
+ if (!machineOutput) log.muted(`Scope: ${files.length} changed file(s)`);
7457
8224
  } else {
7458
8225
  files = filterProjectFiles(resolvedDir, listProjectFiles(resolvedDir), [], config.exclude);
7459
- if (!options.json) log.muted(`Scope: ${files.length} file(s) after exclusions`);
8226
+ if (!machineOutput) log.muted(`Scope: ${files.length} file(s) after exclusions`);
7460
8227
  }
7461
8228
  const configDir = findConfigDir(resolvedDir);
7462
8229
  const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
@@ -7473,7 +8240,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
7473
8240
  }));
7474
8241
  const progressRenderer = useLiveProgress ? new LiveGrid(gridRows) : null;
7475
8242
  progressRenderer?.start();
7476
- const results = await runEngines({
8243
+ const rawResults = await runEngines({
7477
8244
  rootDirectory: resolvedDir,
7478
8245
  languages: projectInfo.languages,
7479
8246
  frameworks: projectInfo.frameworks,
@@ -7506,9 +8273,13 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
7506
8273
  elapsedMs: result.elapsed
7507
8274
  });
7508
8275
  }
7509
- if (!options.json && !progressRenderer) printEngineStatus(result);
8276
+ if (!machineOutput && !progressRenderer) printEngineStatus(result);
7510
8277
  });
7511
8278
  progressRenderer?.stop();
8279
+ const results = rawResults.map((result) => ({
8280
+ ...result,
8281
+ diagnostics: applyRuleSeverities(result.diagnostics, config.rules)
8282
+ }));
7512
8283
  const allDiagnostics = results.flatMap((r) => r.diagnostics);
7513
8284
  const elapsedMs = performance.now() - startTime;
7514
8285
  const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
@@ -7529,12 +8300,24 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
7529
8300
  engineIssues,
7530
8301
  engineTimings
7531
8302
  };
8303
+ if (options.sarif) {
8304
+ const { buildSarifLog } = await import("./sarif-Cneulb6L.js");
8305
+ console.log(JSON.stringify(buildSarifLog(results), null, 2));
8306
+ return completion;
8307
+ }
7532
8308
  if (options.json) {
7533
- const { buildJsonOutput } = await import("./json-BhO1Ufj3.js");
8309
+ const { buildJsonOutput } = await import("./json-CZU3lEfE.js");
7534
8310
  const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
7535
8311
  console.log(JSON.stringify(jsonOut, null, 2));
7536
8312
  return completion;
7537
8313
  }
8314
+ if (!options.staged && !options.changes && options.command !== "ci" && !isCiEnv()) appendHistory({
8315
+ directory: resolvedDir,
8316
+ score: scoreResult.score,
8317
+ errors: completion.errorCount,
8318
+ warnings: completion.warningCount,
8319
+ files: projectInfo.sourceFileCount
8320
+ });
7538
8321
  const projectName = projectInfo.projectName ?? "project";
7539
8322
  const language = projectInfo.languages[0] ?? "unknown";
7540
8323
  process.stdout.write(buildScanRender({
@@ -7614,6 +8397,16 @@ const AGENT_CONFIGS = {
7614
8397
  bin: "goose",
7615
8398
  args: (p) => ["run", p]
7616
8399
  },
8400
+ pi: {
8401
+ type: "cli",
8402
+ bin: "pi",
8403
+ args: (p) => ["-p", p]
8404
+ },
8405
+ crush: {
8406
+ type: "cli",
8407
+ bin: "crush",
8408
+ args: (p) => ["run", p]
8409
+ },
7617
8410
  cursor: {
7618
8411
  type: "editor",
7619
8412
  bin: "cursor"
@@ -8812,7 +9605,7 @@ const runLintSteps = async (deps) => {
8812
9605
  if (hasJsOrTs(deps.projectInfo)) await deps.runStep("Lint fixes (js/ts)", () => runOxlint(deps.context), () => fixOxlint(deps.context, { force: deps.force }));
8813
9606
  if (deps.projectInfo.languages.includes("python") && deps.projectInfo.installedTools.ruff) await deps.runStep("Lint fixes (python)", () => runRuffLint(deps.context), () => deps.force ? fixRuffLintForce(deps.resolvedDir) : fixRuffLint(deps.resolvedDir));
8814
9607
  else if (deps.projectInfo.languages.includes("python")) log.warn("Python detected but ruff is not installed; skipping Python lint fixes.");
8815
- if (deps.projectInfo.languages.includes("ruby") && deps.projectInfo.installedTools.rubocop) await deps.runStep("Lint fixes (ruby)", () => import("./generic-BrcWMW7E.js").then((n) => n.n).then((mod) => mod.runGenericLinter(deps.context, "ruby")), () => fixRubyLint(deps.resolvedDir));
9608
+ if (deps.projectInfo.languages.includes("ruby") && deps.projectInfo.installedTools.rubocop) await deps.runStep("Lint fixes (ruby)", () => import("./generic-D_T4cUaC.js").then((n) => n.n).then((mod) => mod.runGenericLinter(deps.context, "ruby")), () => fixRubyLint(deps.resolvedDir));
8816
9609
  else if (deps.projectInfo.languages.includes("ruby")) log.warn("Ruby detected but rubocop is not installed; skipping Ruby lint fixes.");
8817
9610
  };
8818
9611
  const runDependencyStep = async (deps) => {
@@ -9338,6 +10131,11 @@ const BUILTIN_RULES = [
9338
10131
  rules: [
9339
10132
  "ai-slop/trivial-comment",
9340
10133
  "ai-slop/swallowed-exception",
10134
+ "ai-slop/silent-recovery",
10135
+ "ai-slop/meta-comment",
10136
+ "ai-slop/redundant-try-catch",
10137
+ "ai-slop/redundant-type-coercion",
10138
+ "ai-slop/duplicate-type-declaration",
9341
10139
  "ai-slop/thin-wrapper",
9342
10140
  "ai-slop/generic-naming",
9343
10141
  "ai-slop/unused-import",
@@ -9351,10 +10149,16 @@ const BUILTIN_RULES = [
9351
10149
  "ai-slop/ts-directive",
9352
10150
  "ai-slop/narrative-comment",
9353
10151
  "ai-slop/duplicate-import",
10152
+ "ai-slop/hardcoded-url",
10153
+ "ai-slop/hardcoded-id",
9354
10154
  "ai-slop/python-bare-except",
9355
10155
  "ai-slop/python-broad-except",
9356
10156
  "ai-slop/python-mutable-default",
9357
10157
  "ai-slop/python-print-debug",
10158
+ "ai-slop/python-range-len-loop",
10159
+ "ai-slop/python-chained-dict-get",
10160
+ "ai-slop/python-repetitive-dispatch",
10161
+ "ai-slop/python-isinstance-ladder",
9358
10162
  "ai-slop/go-library-panic",
9359
10163
  "ai-slop/rust-non-test-unwrap",
9360
10164
  "ai-slop/rust-todo-stub",