aislop 0.9.5 → 0.10.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/cli.js CHANGED
@@ -34,7 +34,7 @@ var __exportAll = (all, no_symbols) => {
34
34
 
35
35
  //#endregion
36
36
  //#region src/version.ts
37
- const APP_VERSION = "0.9.5";
37
+ const APP_VERSION = "0.10.0";
38
38
 
39
39
  //#endregion
40
40
  //#region src/telemetry/env.ts
@@ -1111,14 +1111,16 @@ const THIN_WRAPPER_PATTERNS = [
1111
1111
  const AI_NAMING_PATTERNS = [/(?:helper|util|handler|process|do|handle|execute|perform)_?\d+/i, /(?:data|temp|result|value|item|obj|arr|str|num|val)\d+/];
1112
1112
  const FRAMEWORK_METHOD_NAMES = /^(?:setUp|tearDown|setUpClass|tearDownClass|setUpModule|tearDownModule)$/;
1113
1113
  const DUNDER_PATTERN = /^__\w+__$/;
1114
- const hasHardcodedArgs = (matchText) => {
1115
- const innerCallMatch = matchText.match(/=>\s*\w+\(([^)]*)\)\s*;?\s*$/);
1116
- if (!innerCallMatch) {
1117
- const returnCallMatch = matchText.match(/return\s+\w+\(([^)]*)\)\s*;?\s*\}/);
1118
- if (!returnCallMatch) return false;
1119
- return /['"`]\w+['"`]|(?<!\w)\d+(?!\w)/.test(returnCallMatch[1]);
1120
- }
1121
- return /['"`]\w+['"`]|(?<!\w)\d+(?!\w)/.test(innerCallMatch[1]);
1114
+ const stripParam = (p) => p.trim().split(/[:=]/)[0].trim().replace(/^[*&]+/, "");
1115
+ const paramNames = (paramsText) => new Set(paramsText.split(",").map(stripParam).filter((p) => p && p !== "self" && p !== "cls"));
1116
+ const isIdentityForward = (matchText) => {
1117
+ const paramsMatch = matchText.match(/\(([^)]*)\)/);
1118
+ const innerMatch = matchText.match(/(?:return\s+\w+|=>\s*\w+)\s*\(([^)]*)\)/);
1119
+ if (!paramsMatch || !innerMatch) return false;
1120
+ const params = paramNames(paramsMatch[1]);
1121
+ const args = innerMatch[1].split(",").map((a) => a.trim()).filter((a) => a.length > 0);
1122
+ if (args.length === 0) return false;
1123
+ return args.every((a) => /^[A-Za-z_$][\w$]*$/.test(a) && params.has(a));
1122
1124
  };
1123
1125
  const isUseContextWrapper = (matchText) => /\buse\w+/.test(matchText) && /useContext\s*\(/.test(matchText);
1124
1126
  const detectThinWrappers = (content, relativePath, ext) => {
@@ -1138,7 +1140,7 @@ const detectThinWrappers = (content, relativePath, ext) => {
1138
1140
  const prevLine = lines[lineNumber - 2]?.trim();
1139
1141
  if (prevLine && prevLine.startsWith("@")) continue;
1140
1142
  }
1141
- if (hasHardcodedArgs(matchText)) continue;
1143
+ if (!isIdentityForward(matchText)) continue;
1142
1144
  if (isUseContextWrapper(matchText)) continue;
1143
1145
  diagnostics.push({
1144
1146
  filePath: relativePath,
@@ -1223,8 +1225,7 @@ const JUSTIFICATION_OPENERS = [
1223
1225
  /^(?:First|Then|Finally|Next|Lastly|Subsequently),?\s+(?:it|we|the\s+(?:function|method|class))\b/i
1224
1226
  ];
1225
1227
  const EXPLANATORY_OPENERS = /^(Matches|Detects|Represents|Holds|Stores|Tracks|Handles|Manages|Controls|Contains|Captures|Encapsulates|Wraps|Describes)\s+[A-Za-z`'"]/;
1226
- const STEP_COMMENT_VERB_RE = /^(?:Render|Enable|Disable|Initialize|Init|Setup|Set|Get|Fetch|Load|Save|Build|Create|Delete|Remove|Add|Update|Process|Execute|Run|Start|Stop|Clean|Cleanup|Configure|Validate|Check|Verify|Parse|Extract|Apply|Wait|Sleep|Skip|Allow|Deny|Lock|Unlock|Refresh|Reload|Reset|Clear|Send|Receive|Read|Write|Print|Log|Emit|Dispatch|Fire|Open|Close|Bind|Connect|Disconnect|Register|Unregister|Push|Pop|Insert|Append|Prepend|Sort|Filter|Find|Search|Replace|Encode|Decode|Convert|Transform|Map|Reduce|Iterate|Loop|Walk|Visit|Mark|Unmark|Toggle|Switch|Restart|Resume|Pause|Abort|Cancel|Compute|Calculate|Resolve|Reject|Ignore|Handle|Track|Trace|Increment|Decrement|Round|Truncate|Resize|Move|Copy|Clone|Merge|Split|Join|Wrap|Unwrap|Bump|Drain|Flush|Sync|Persist|Commit|Rollback|Yield|Return|Discard|Defer|Pin|Unpin|Mount|Unmount|Spawn|Kill|Restore)(?:\s|$)/;
1227
- const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:|to\s+avoid|to\s+ensure|to\s+prevent|in\s+order\s+to|necessary|guarantee[sd]?|prevents?|regardless\s+of|required\s+(?:for|to|by)|for\s+example|e\.g\.|i\.e\.|useful\s+(?:for|when)|intended\s+to|on\s+purpose|by\s+design)\b/i;
1228
+ const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:|to\s+avoid|to\s+ensure|to\s+prevent|in\s+order\s+to|necessary|guarantee[sd]?|prevents?|regardless\s+of|required\s+(?:for|to|by)|for\s+example|e\.g\.|i\.e\.|useful\s+(?:for|when)|intended\s+to|on\s+purpose|by\s+design|ideally|however|although|even\s+though|despite|whereas|unfortunately|trade-?off|first\s+need)\b/i;
1228
1229
  const MEANINGFUL_JSDOC_TAGS = new Set([
1229
1230
  "deprecated",
1230
1231
  "see",
@@ -1320,7 +1321,7 @@ const PHP_DECL_START = /^\s*(?:(?:public|private|protected|static|final|abstract
1320
1321
 
1321
1322
  //#endregion
1322
1323
  //#region src/engines/ai-slop/non-production-paths.ts
1323
- const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
1324
+ const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|docs?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
1324
1325
  const BASENAME_PATTERN = /(?:^|\/)(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]*\.[mc]?[jt]sx?$|(?:^|\/)[^/]+[-_](?:benchmark|bench|demo|example)\.[mc]?[jt]sx?$/i;
1325
1326
  const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) || BASENAME_PATTERN.test(relativePath);
1326
1327
 
@@ -1329,7 +1330,7 @@ const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) ||
1329
1330
  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";
1330
1331
  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")];
1331
1332
  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")];
1332
- const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?)\b/i;
1333
+ const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?|if|when|unless|until|only|except|otherwise|needs?|must|should|ensure|avoid|prevent|requires?)\b/i;
1333
1334
  const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
1334
1335
  const MAX_TRIVIAL_COMMENT_LENGTH = 60;
1335
1336
  const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
@@ -1478,6 +1479,7 @@ const TODO_PATTERN = new RegExp(`\\b(?:${[
1478
1479
  "PLACEHOLDER",
1479
1480
  "STUB"
1480
1481
  ].join("|")})[:\\s]`);
1482
+ const TODO_TRACKING_RE = /https?:\/\/|#\d+|\bgh-\d+\b|\b[A-Z][A-Z0-9]+-\d+\b|\b(?:issue|ticket|jira)\b/i;
1481
1483
  const isBlockCloserAfterReturn = (line) => line.startsWith("}") || line.startsWith("};") || line.startsWith("),") || line.startsWith(");") || line.startsWith("],") || line.startsWith("]);");
1482
1484
  const isGuardedSingleLineExit = (lines, lineIndex) => {
1483
1485
  const contextLines = [];
@@ -1497,7 +1499,10 @@ const detectTodoStubs = (content, relativePath) => {
1497
1499
  for (let i = 0; i < lines.length; i++) {
1498
1500
  const trimmed = lines[i].trim();
1499
1501
  if (!trimmed.startsWith("//") && !trimmed.startsWith("#") && !trimmed.startsWith("*") && !trimmed.startsWith("/*")) continue;
1500
- if (TODO_PATTERN.test(trimmed)) diagnostics.push(slop(relativePath, i + 1, "ai-slop/todo-stub", "info", "Unresolved TODO/FIXME/HACK comment indicates incomplete code", "Resolve the TODO or create a tracked issue for it", false));
1502
+ if (TODO_PATTERN.test(trimmed)) {
1503
+ if (TODO_TRACKING_RE.test(trimmed)) continue;
1504
+ diagnostics.push(slop(relativePath, i + 1, "ai-slop/todo-stub", "info", "Unresolved TODO/FIXME/HACK comment indicates incomplete code", "Resolve the TODO or create a tracked issue for it", false));
1505
+ }
1501
1506
  }
1502
1507
  return diagnostics;
1503
1508
  };
@@ -2011,11 +2016,42 @@ const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|li
2011
2016
  const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
2012
2017
  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;
2013
2018
  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;
2019
+ const MIGRATION_PATH_RE$1 = /(?:^|[\\/])(?:migrations?|db[\\/]migrate)[\\/]/i;
2014
2020
  const PLACEHOLDER_HOSTS = new Set([
2015
2021
  "example.com",
2016
2022
  "example.org",
2017
2023
  "example.net"
2018
2024
  ]);
2025
+ const LOOPBACK_HOSTS = new Set([
2026
+ "localhost",
2027
+ "127.0.0.1",
2028
+ "0.0.0.0",
2029
+ "::1"
2030
+ ]);
2031
+ const VENDOR_API_DOMAINS = [
2032
+ "github.com",
2033
+ "githubusercontent.com",
2034
+ "googleapis.com",
2035
+ "accounts.google.com",
2036
+ "stripe.com",
2037
+ "openai.com",
2038
+ "anthropic.com",
2039
+ "slack.com",
2040
+ "twilio.com",
2041
+ "sendgrid.com",
2042
+ "mailgun.net",
2043
+ "cloudflare.com",
2044
+ "discord.com",
2045
+ "telegram.org",
2046
+ "login.microsoftonline.com",
2047
+ "graph.microsoft.com",
2048
+ "twitter.com",
2049
+ "x.com",
2050
+ "twimg.com",
2051
+ "t.co",
2052
+ "api.telegram.org"
2053
+ ];
2054
+ const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
2019
2055
  const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
2020
2056
  const HARDCODED_URL_FINDING = {
2021
2057
  rule: "ai-slop/hardcoded-url",
@@ -2059,14 +2095,18 @@ const shouldFlagUrlLiteral = (line, urlText) => {
2059
2095
  const host = safeUrlHost(urlText);
2060
2096
  if (!host) return false;
2061
2097
  if (PLACEHOLDER_HOSTS.has(host)) return false;
2098
+ if (LOOPBACK_HOSTS.has(host)) return false;
2099
+ if (isVendorApiHost(host)) return false;
2062
2100
  if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
2063
2101
  return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
2064
2102
  };
2103
+ const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
2065
2104
  const hasUsefulIdShape = (value) => {
2066
2105
  if (PLACEHOLDER_ID_RE.test(value)) return false;
2106
+ if (ENV_VAR_NAME_RE.test(value)) return false;
2067
2107
  if (/^https?:\/\//i.test(value)) return false;
2068
2108
  if (/^[A-Za-z]+$/.test(value)) return false;
2069
- return /[0-9_-]/.test(value);
2109
+ return /[0-9]/.test(value);
2070
2110
  };
2071
2111
  const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
2072
2112
  const diagnostics = [];
@@ -2093,6 +2133,7 @@ const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
2093
2133
  const scanFileForConfigLiterals = (content, relativePath, ext) => {
2094
2134
  if (!SOURCE_EXTENSIONS.has(ext)) return [];
2095
2135
  if (isNonProductionPath(relativePath)) return [];
2136
+ if (MIGRATION_PATH_RE$1.test(relativePath)) return [];
2096
2137
  return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
2097
2138
  };
2098
2139
  const detectHardcodedConfigLiterals = async (context) => {
@@ -2240,7 +2281,9 @@ const PYTHON_STDLIB = new Set([
2240
2281
  "builtins",
2241
2282
  "bz2",
2242
2283
  "calendar",
2284
+ "code",
2243
2285
  "codecs",
2286
+ "codeop",
2244
2287
  "collections",
2245
2288
  "concurrent",
2246
2289
  "configparser",
@@ -2313,6 +2356,7 @@ const PYTHON_STDLIB = new Set([
2313
2356
  "readline",
2314
2357
  "reprlib",
2315
2358
  "resource",
2359
+ "rlcompleter",
2316
2360
  "secrets",
2317
2361
  "select",
2318
2362
  "selectors",
@@ -2411,6 +2455,7 @@ const PYTHON_IMPORT_TO_PIP = {
2411
2455
  pptx: ["python-pptx"],
2412
2456
  git: ["gitpython"],
2413
2457
  socks: ["pysocks"],
2458
+ psycopg2: ["psycopg2-binary", "psycopg2"],
2414
2459
  redis: ["redis"],
2415
2460
  cairo: ["pycairo"],
2416
2461
  serial: ["pyserial"],
@@ -2500,6 +2545,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
2500
2545
  }
2501
2546
  const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
2502
2547
  if (extras) for (const m of extras[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
2548
+ const groups = content.match(/\[dependency-groups\]([\s\S]*?)(?=\n\[[^[]|$)/);
2549
+ if (groups) for (const m of groups[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
2503
2550
  const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
2504
2551
  let match = poetryRe.exec(content);
2505
2552
  while (match !== null) {
@@ -2732,9 +2779,28 @@ const extractJsImports = (content) => {
2732
2779
  const extractPyImports = (content) => {
2733
2780
  const lines = content.split("\n");
2734
2781
  const results = [];
2782
+ let inDoc = null;
2783
+ let typeCheckIndent = -1;
2735
2784
  for (let i = 0; i < lines.length; i++) {
2736
- const line = lines[i].trim();
2737
- if (line.startsWith("#")) continue;
2785
+ const raw = lines[i];
2786
+ const line = raw.trim();
2787
+ if (inDoc) {
2788
+ if (line.includes(inDoc)) inDoc = null;
2789
+ continue;
2790
+ }
2791
+ if (line === "" || line.startsWith("#")) continue;
2792
+ const triples = line.match(/"""|'''/g);
2793
+ if (triples) {
2794
+ if (triples.length % 2 === 1) inDoc = triples[triples.length - 1];
2795
+ continue;
2796
+ }
2797
+ const indent = raw.length - raw.trimStart().length;
2798
+ if (typeCheckIndent >= 0 && indent <= typeCheckIndent) typeCheckIndent = -1;
2799
+ if (/^if\s+(?:[\w.]+\.)?TYPE_CHECKING\b/.test(line)) {
2800
+ typeCheckIndent = indent;
2801
+ continue;
2802
+ }
2803
+ if (typeCheckIndent >= 0) continue;
2738
2804
  const fromMatch = line.match(/^from\s+([\w.]+)\s+import\b/);
2739
2805
  if (fromMatch && !fromMatch[1].startsWith(".")) {
2740
2806
  results.push({
@@ -2744,8 +2810,8 @@ const extractPyImports = (content) => {
2744
2810
  continue;
2745
2811
  }
2746
2812
  const importMatch = line.match(/^import\s+([\w.,\s]+?)(?:\s+as\s+\w+)?\s*$/);
2747
- if (importMatch) for (const raw of importMatch[1].split(",")) {
2748
- const cleaned = raw.trim().split(/\s+as\s+/)[0];
2813
+ if (importMatch) for (const part of importMatch[1].split(",")) {
2814
+ const cleaned = part.trim().split(/\s+as\s+/)[0];
2749
2815
  if (cleaned && !cleaned.startsWith(".")) results.push({
2750
2816
  spec: cleaned,
2751
2817
  line: i + 1
@@ -2802,6 +2868,7 @@ const detectHallucinatedImports = async (context) => {
2802
2868
  continue;
2803
2869
  }
2804
2870
  const relPath = path.relative(context.rootDirectory, filePath);
2871
+ if (isNonProductionPath(relPath)) continue;
2805
2872
  const imports = isJs ? extractJsImports(content) : extractPyImports(content);
2806
2873
  for (const { spec, line } of imports) {
2807
2874
  const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
@@ -2929,7 +2996,7 @@ const collectBlocks = (sourceLines, syntax) => {
2929
2996
  //#endregion
2930
2997
  //#region src/engines/ai-slop/meta-comment.ts
2931
2998
  const PLAN_REFERENCE_RES = [
2932
- /\b(?:stage|step|phase)\s+\d+\b(?!\s*[:.]?\s*(?:bytes|ms|seconds|of\s+\d))/i,
2999
+ /^(?:stage|step|phase)\s+\d+\s*[:.\-–—]/i,
2933
3000
  /\bstep\s+\d+\s+of\s+the\s+plan\b/i,
2934
3001
  /\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
2935
3002
  /\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
@@ -3031,24 +3098,6 @@ const looksLikeLicenseHeader = (block) => {
3031
3098
  const text = block.rawLines.join(" ").toLowerCase();
3032
3099
  return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
3033
3100
  };
3034
- const BARE_LABEL_RE = /^[A-Z][A-Za-z0-9 ]{1,28}$/;
3035
- const isBareSectionLabel = (prose) => {
3036
- if (!BARE_LABEL_RE.test(prose)) return false;
3037
- if (prose.endsWith(".")) return false;
3038
- if (prose.split(/\s+/).length > 3) return false;
3039
- if (STEP_COMMENT_VERB_RE.test(prose)) return false;
3040
- return true;
3041
- };
3042
- const DATA_ENTRY_START = /^\s*(?:\{|\[|["'`]|\d|\w+:\s|case\s)/;
3043
- const nextLineLooksLikeDataEntry = (nextLine) => {
3044
- if (nextLine === null) return false;
3045
- if (!DATA_ENTRY_START.test(nextLine)) return false;
3046
- const trimmed = nextLine.trim();
3047
- if (trimmed.startsWith("case ")) return true;
3048
- if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("\"") || trimmed.startsWith("'") || trimmed.startsWith("`")) return true;
3049
- if (/^\w+\s*:/.test(trimmed)) return true;
3050
- return false;
3051
- };
3052
3101
  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));
3053
3102
  const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
3054
3103
  const GO_FIELD_LEAD_RE = /^(\w+)\s+/;
@@ -3137,10 +3186,6 @@ const detectNarrativeInBlock = (block, ext) => {
3137
3186
  matched: true,
3138
3187
  reason: "phase/section header"
3139
3188
  };
3140
- if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine) && !looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
3141
- matched: true,
3142
- reason: "bare section label"
3143
- };
3144
3189
  const joined = block.prose.join(" ");
3145
3190
  const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
3146
3191
  if (hasWhyMarker || hasDocIndicator(block)) return {
@@ -3171,17 +3216,11 @@ const detectNarrativeInBlock = (block, ext) => {
3171
3216
  };
3172
3217
  const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
3173
3218
  const isAboveDeclaration = looksLikeDeclarationPreamble(block.nextNonBlankLine, ext);
3174
- if (nonEmptyProseCount >= 5) {
3175
- if (isAboveDeclaration) return {
3176
- matched: false,
3177
- reason: ""
3178
- };
3179
- return {
3180
- matched: true,
3181
- reason: "long narrative block"
3182
- };
3183
- }
3184
- if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration) return {
3219
+ if (nonEmptyProseCount >= 5 && !isAboveDeclaration && hasPreambleSlopSignal(block)) return {
3220
+ matched: true,
3221
+ reason: "long narrative block"
3222
+ };
3223
+ if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration && hasPreambleSlopSignal(block)) return {
3185
3224
  matched: true,
3186
3225
  reason: "multi-line narrative prose"
3187
3226
  };
@@ -3623,7 +3662,14 @@ const JS_EXTS$1 = new Set([
3623
3662
  ".mjs",
3624
3663
  ".cjs"
3625
3664
  ]);
3626
- const CATCH_HEAD_RE = /\bcatch\s*(?:\([^)]*\))?\s*\{/g;
3665
+ const CATCH_HEAD_RE = /\bcatch\s*(?:\(\s*([^)]*?)\s*\))?\s*\{/g;
3666
+ const isIdentifier = (s) => /^[A-Za-z_$][\w$]*$/.test(s);
3667
+ const recoveryDropsError = (binding, body) => {
3668
+ const name = binding?.trim() ?? "";
3669
+ if (name === "") return true;
3670
+ if (!isIdentifier(name)) return false;
3671
+ return !new RegExp(`\\b${name}\\b`).test(body);
3672
+ };
3627
3673
  const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
3628
3674
  const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
3629
3675
  const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
@@ -3673,14 +3719,15 @@ const detectJsSilentRecovery = (content, relPath) => {
3673
3719
  const body = extractCatchBody(content, match.index + match[0].length - 1);
3674
3720
  if (body === null) continue;
3675
3721
  if (!isLogOnlyBody(body)) continue;
3722
+ if (!recoveryDropsError(match[1], body)) continue;
3676
3723
  const line = content.slice(0, match.index).split("\n").length;
3677
3724
  out.push({
3678
3725
  filePath: relPath,
3679
3726
  engine: "ai-slop",
3680
3727
  rule: "ai-slop/silent-recovery",
3681
3728
  severity: "warning",
3682
- message: "Catch only logs then continues, leaving execution in a possibly broken state",
3683
- help: "Handle the error: rethrow, return an error value, or recover explicitly. Logging alone lets the program proceed as if nothing failed.",
3729
+ message: "Catch logs without the caught error then continues; the failure cause is lost",
3730
+ help: "Include the caught error in the log, or rethrow / recover explicitly, so the failure stays diagnosable.",
3684
3731
  line,
3685
3732
  column: 0,
3686
3733
  category: "AI Slop",
@@ -3690,6 +3737,7 @@ const detectJsSilentRecovery = (content, relPath) => {
3690
3737
  return out;
3691
3738
  };
3692
3739
  const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
3740
+ const PY_EXCEPT_BINDING_RE = /\bas\s+(\w+)\s*:/;
3693
3741
  const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
3694
3742
  const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
3695
3743
  const detectPySilentRecovery = (content, relPath) => {
@@ -3713,13 +3761,14 @@ const detectPySilentRecovery = (content, relPath) => {
3713
3761
  const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
3714
3762
  const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
3715
3763
  if (!allLogs || !sawLog) continue;
3764
+ if (!recoveryDropsError(PY_EXCEPT_BINDING_RE.exec(lines[i])?.[1], bodyLines.join(" "))) continue;
3716
3765
  out.push({
3717
3766
  filePath: relPath,
3718
3767
  engine: "ai-slop",
3719
3768
  rule: "ai-slop/silent-recovery",
3720
3769
  severity: "warning",
3721
- message: "except only logs then continues, leaving execution in a possibly broken state",
3722
- help: "Handle the error: re-raise, return an error value, or recover explicitly. Logging alone lets the program proceed as if nothing failed.",
3770
+ message: "except logs without the caught error then continues; the failure cause is lost",
3771
+ help: "Include the caught error in the log, or re-raise / recover explicitly, so the failure stays diagnosable.",
3723
3772
  line: i + 1,
3724
3773
  column: 0,
3725
3774
  category: "AI Slop",
@@ -3822,10 +3871,11 @@ const extractPyImportedSymbols = (lines) => {
3822
3871
  const importLines = /* @__PURE__ */ new Set();
3823
3872
  for (let i = 0; i < lines.length; i++) {
3824
3873
  const trimmed = lines[i].trim();
3825
- const fromMatch = trimmed.match(/^from\s+[\w.]+\s+import\s+(.+)/);
3874
+ const fromMatch = trimmed.match(/^from\s+([\w.]+)\s+import\s+(.+)/);
3826
3875
  if (fromMatch) {
3827
3876
  importLines.add(i);
3828
- const importPart = fromMatch[1].replace(/#.*$/, "").trim();
3877
+ if (fromMatch[1] === "__future__") continue;
3878
+ const importPart = fromMatch[2].replace(/#.*$/, "").trim();
3829
3879
  if (importPart === "*") continue;
3830
3880
  const cleaned = importPart.replace(/[()]/g, "");
3831
3881
  for (const item of cleaned.split(",")) {
@@ -7147,6 +7197,13 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
7147
7197
  //#endregion
7148
7198
  //#region src/scoring/index.ts
7149
7199
  const PERFECT_SCORE = 100;
7200
+ const STYLE_RULES = new Set([
7201
+ "ai-slop/trivial-comment",
7202
+ "ai-slop/narrative-comment",
7203
+ "complexity/file-too-large",
7204
+ "complexity/function-too-long"
7205
+ ]);
7206
+ const STYLE_WEIGHT = .5;
7150
7207
  const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
7151
7208
  if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
7152
7209
  const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
@@ -7161,7 +7218,8 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
7161
7218
  for (const d of diagnostics) {
7162
7219
  const engineWeight = weights[d.engine] ?? 1;
7163
7220
  const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
7164
- deductions += severityPenalty * engineWeight;
7221
+ const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
7222
+ deductions += severityPenalty * engineWeight * styleFactor;
7165
7223
  }
7166
7224
  const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
7167
7225
  const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
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 { t as APP_VERSION } from "./version-ls3wZmOU.js";
3
+ import { t as APP_VERSION } from "./version-DYg_ShBx.js";
4
4
  import { r as runGenericLinter, t as fixRubyLint } from "./generic-D_T4cUaC.js";
5
5
  import { n as runExpoDoctor } from "./expo-doctor-BcIkOte5.js";
6
6
  import { createRequire, isBuiltin } from "node:module";
@@ -1280,14 +1280,16 @@ const THIN_WRAPPER_PATTERNS = [
1280
1280
  const AI_NAMING_PATTERNS = [/(?:helper|util|handler|process|do|handle|execute|perform)_?\d+/i, /(?:data|temp|result|value|item|obj|arr|str|num|val)\d+/];
1281
1281
  const FRAMEWORK_METHOD_NAMES = /^(?:setUp|tearDown|setUpClass|tearDownClass|setUpModule|tearDownModule)$/;
1282
1282
  const DUNDER_PATTERN = /^__\w+__$/;
1283
- const hasHardcodedArgs = (matchText) => {
1284
- const innerCallMatch = matchText.match(/=>\s*\w+\(([^)]*)\)\s*;?\s*$/);
1285
- if (!innerCallMatch) {
1286
- const returnCallMatch = matchText.match(/return\s+\w+\(([^)]*)\)\s*;?\s*\}/);
1287
- if (!returnCallMatch) return false;
1288
- return /['"`]\w+['"`]|(?<!\w)\d+(?!\w)/.test(returnCallMatch[1]);
1289
- }
1290
- return /['"`]\w+['"`]|(?<!\w)\d+(?!\w)/.test(innerCallMatch[1]);
1283
+ const stripParam = (p) => p.trim().split(/[:=]/)[0].trim().replace(/^[*&]+/, "");
1284
+ const paramNames = (paramsText) => new Set(paramsText.split(",").map(stripParam).filter((p) => p && p !== "self" && p !== "cls"));
1285
+ const isIdentityForward = (matchText) => {
1286
+ const paramsMatch = matchText.match(/\(([^)]*)\)/);
1287
+ const innerMatch = matchText.match(/(?:return\s+\w+|=>\s*\w+)\s*\(([^)]*)\)/);
1288
+ if (!paramsMatch || !innerMatch) return false;
1289
+ const params = paramNames(paramsMatch[1]);
1290
+ const args = innerMatch[1].split(",").map((a) => a.trim()).filter((a) => a.length > 0);
1291
+ if (args.length === 0) return false;
1292
+ return args.every((a) => /^[A-Za-z_$][\w$]*$/.test(a) && params.has(a));
1291
1293
  };
1292
1294
  const isUseContextWrapper = (matchText) => /\buse\w+/.test(matchText) && /useContext\s*\(/.test(matchText);
1293
1295
  const detectThinWrappers = (content, relativePath, ext) => {
@@ -1307,7 +1309,7 @@ const detectThinWrappers = (content, relativePath, ext) => {
1307
1309
  const prevLine = lines[lineNumber - 2]?.trim();
1308
1310
  if (prevLine && prevLine.startsWith("@")) continue;
1309
1311
  }
1310
- if (hasHardcodedArgs(matchText)) continue;
1312
+ if (!isIdentityForward(matchText)) continue;
1311
1313
  if (isUseContextWrapper(matchText)) continue;
1312
1314
  diagnostics.push({
1313
1315
  filePath: relativePath,
@@ -1392,8 +1394,7 @@ const JUSTIFICATION_OPENERS = [
1392
1394
  /^(?:First|Then|Finally|Next|Lastly|Subsequently),?\s+(?:it|we|the\s+(?:function|method|class))\b/i
1393
1395
  ];
1394
1396
  const EXPLANATORY_OPENERS = /^(Matches|Detects|Represents|Holds|Stores|Tracks|Handles|Manages|Controls|Contains|Captures|Encapsulates|Wraps|Describes)\s+[A-Za-z`'"]/;
1395
- const STEP_COMMENT_VERB_RE = /^(?:Render|Enable|Disable|Initialize|Init|Setup|Set|Get|Fetch|Load|Save|Build|Create|Delete|Remove|Add|Update|Process|Execute|Run|Start|Stop|Clean|Cleanup|Configure|Validate|Check|Verify|Parse|Extract|Apply|Wait|Sleep|Skip|Allow|Deny|Lock|Unlock|Refresh|Reload|Reset|Clear|Send|Receive|Read|Write|Print|Log|Emit|Dispatch|Fire|Open|Close|Bind|Connect|Disconnect|Register|Unregister|Push|Pop|Insert|Append|Prepend|Sort|Filter|Find|Search|Replace|Encode|Decode|Convert|Transform|Map|Reduce|Iterate|Loop|Walk|Visit|Mark|Unmark|Toggle|Switch|Restart|Resume|Pause|Abort|Cancel|Compute|Calculate|Resolve|Reject|Ignore|Handle|Track|Trace|Increment|Decrement|Round|Truncate|Resize|Move|Copy|Clone|Merge|Split|Join|Wrap|Unwrap|Bump|Drain|Flush|Sync|Persist|Commit|Rollback|Yield|Return|Discard|Defer|Pin|Unpin|Mount|Unmount|Spawn|Kill|Restore)(?:\s|$)/;
1396
- const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:|to\s+avoid|to\s+ensure|to\s+prevent|in\s+order\s+to|necessary|guarantee[sd]?|prevents?|regardless\s+of|required\s+(?:for|to|by)|for\s+example|e\.g\.|i\.e\.|useful\s+(?:for|when)|intended\s+to|on\s+purpose|by\s+design)\b/i;
1397
+ const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:|to\s+avoid|to\s+ensure|to\s+prevent|in\s+order\s+to|necessary|guarantee[sd]?|prevents?|regardless\s+of|required\s+(?:for|to|by)|for\s+example|e\.g\.|i\.e\.|useful\s+(?:for|when)|intended\s+to|on\s+purpose|by\s+design|ideally|however|although|even\s+though|despite|whereas|unfortunately|trade-?off|first\s+need)\b/i;
1397
1398
  const MEANINGFUL_JSDOC_TAGS = new Set([
1398
1399
  "deprecated",
1399
1400
  "see",
@@ -1489,7 +1490,7 @@ const PHP_DECL_START = /^\s*(?:(?:public|private|protected|static|final|abstract
1489
1490
 
1490
1491
  //#endregion
1491
1492
  //#region src/engines/ai-slop/non-production-paths.ts
1492
- const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
1493
+ const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|docs?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
1493
1494
  const BASENAME_PATTERN = /(?:^|\/)(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]*\.[mc]?[jt]sx?$|(?:^|\/)[^/]+[-_](?:benchmark|bench|demo|example)\.[mc]?[jt]sx?$/i;
1494
1495
  const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) || BASENAME_PATTERN.test(relativePath);
1495
1496
 
@@ -1498,7 +1499,7 @@ const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) ||
1498
1499
  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";
1499
1500
  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")];
1500
1501
  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")];
1501
- const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?)\b/i;
1502
+ const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?|if|when|unless|until|only|except|otherwise|needs?|must|should|ensure|avoid|prevent|requires?)\b/i;
1502
1503
  const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
1503
1504
  const MAX_TRIVIAL_COMMENT_LENGTH = 60;
1504
1505
  const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
@@ -1647,6 +1648,7 @@ const TODO_PATTERN = new RegExp(`\\b(?:${[
1647
1648
  "PLACEHOLDER",
1648
1649
  "STUB"
1649
1650
  ].join("|")})[:\\s]`);
1651
+ const TODO_TRACKING_RE = /https?:\/\/|#\d+|\bgh-\d+\b|\b[A-Z][A-Z0-9]+-\d+\b|\b(?:issue|ticket|jira)\b/i;
1650
1652
  const isBlockCloserAfterReturn = (line) => line.startsWith("}") || line.startsWith("};") || line.startsWith("),") || line.startsWith(");") || line.startsWith("],") || line.startsWith("]);");
1651
1653
  const isGuardedSingleLineExit = (lines, lineIndex) => {
1652
1654
  const contextLines = [];
@@ -1666,7 +1668,10 @@ const detectTodoStubs = (content, relativePath) => {
1666
1668
  for (let i = 0; i < lines.length; i++) {
1667
1669
  const trimmed = lines[i].trim();
1668
1670
  if (!trimmed.startsWith("//") && !trimmed.startsWith("#") && !trimmed.startsWith("*") && !trimmed.startsWith("/*")) continue;
1669
- if (TODO_PATTERN.test(trimmed)) diagnostics.push(slop(relativePath, i + 1, "ai-slop/todo-stub", "info", "Unresolved TODO/FIXME/HACK comment indicates incomplete code", "Resolve the TODO or create a tracked issue for it", false));
1671
+ if (TODO_PATTERN.test(trimmed)) {
1672
+ if (TODO_TRACKING_RE.test(trimmed)) continue;
1673
+ diagnostics.push(slop(relativePath, i + 1, "ai-slop/todo-stub", "info", "Unresolved TODO/FIXME/HACK comment indicates incomplete code", "Resolve the TODO or create a tracked issue for it", false));
1674
+ }
1670
1675
  }
1671
1676
  return diagnostics;
1672
1677
  };
@@ -2180,11 +2185,42 @@ const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|li
2180
2185
  const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
2181
2186
  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
2187
  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;
2188
+ const MIGRATION_PATH_RE$1 = /(?:^|[\\/])(?:migrations?|db[\\/]migrate)[\\/]/i;
2183
2189
  const PLACEHOLDER_HOSTS = new Set([
2184
2190
  "example.com",
2185
2191
  "example.org",
2186
2192
  "example.net"
2187
2193
  ]);
2194
+ const LOOPBACK_HOSTS = new Set([
2195
+ "localhost",
2196
+ "127.0.0.1",
2197
+ "0.0.0.0",
2198
+ "::1"
2199
+ ]);
2200
+ const VENDOR_API_DOMAINS = [
2201
+ "github.com",
2202
+ "githubusercontent.com",
2203
+ "googleapis.com",
2204
+ "accounts.google.com",
2205
+ "stripe.com",
2206
+ "openai.com",
2207
+ "anthropic.com",
2208
+ "slack.com",
2209
+ "twilio.com",
2210
+ "sendgrid.com",
2211
+ "mailgun.net",
2212
+ "cloudflare.com",
2213
+ "discord.com",
2214
+ "telegram.org",
2215
+ "login.microsoftonline.com",
2216
+ "graph.microsoft.com",
2217
+ "twitter.com",
2218
+ "x.com",
2219
+ "twimg.com",
2220
+ "t.co",
2221
+ "api.telegram.org"
2222
+ ];
2223
+ const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
2188
2224
  const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
2189
2225
  const HARDCODED_URL_FINDING = {
2190
2226
  rule: "ai-slop/hardcoded-url",
@@ -2228,14 +2264,18 @@ const shouldFlagUrlLiteral = (line, urlText) => {
2228
2264
  const host = safeUrlHost(urlText);
2229
2265
  if (!host) return false;
2230
2266
  if (PLACEHOLDER_HOSTS.has(host)) return false;
2267
+ if (LOOPBACK_HOSTS.has(host)) return false;
2268
+ if (isVendorApiHost(host)) return false;
2231
2269
  if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
2232
2270
  return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
2233
2271
  };
2272
+ const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
2234
2273
  const hasUsefulIdShape = (value) => {
2235
2274
  if (PLACEHOLDER_ID_RE.test(value)) return false;
2275
+ if (ENV_VAR_NAME_RE.test(value)) return false;
2236
2276
  if (/^https?:\/\//i.test(value)) return false;
2237
2277
  if (/^[A-Za-z]+$/.test(value)) return false;
2238
- return /[0-9_-]/.test(value);
2278
+ return /[0-9]/.test(value);
2239
2279
  };
2240
2280
  const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
2241
2281
  const diagnostics = [];
@@ -2262,6 +2302,7 @@ const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
2262
2302
  const scanFileForConfigLiterals = (content, relativePath, ext) => {
2263
2303
  if (!SOURCE_EXTENSIONS.has(ext)) return [];
2264
2304
  if (isNonProductionPath(relativePath)) return [];
2305
+ if (MIGRATION_PATH_RE$1.test(relativePath)) return [];
2265
2306
  return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
2266
2307
  };
2267
2308
  const detectHardcodedConfigLiterals = async (context) => {
@@ -2409,7 +2450,9 @@ const PYTHON_STDLIB = new Set([
2409
2450
  "builtins",
2410
2451
  "bz2",
2411
2452
  "calendar",
2453
+ "code",
2412
2454
  "codecs",
2455
+ "codeop",
2413
2456
  "collections",
2414
2457
  "concurrent",
2415
2458
  "configparser",
@@ -2482,6 +2525,7 @@ const PYTHON_STDLIB = new Set([
2482
2525
  "readline",
2483
2526
  "reprlib",
2484
2527
  "resource",
2528
+ "rlcompleter",
2485
2529
  "secrets",
2486
2530
  "select",
2487
2531
  "selectors",
@@ -2580,6 +2624,7 @@ const PYTHON_IMPORT_TO_PIP = {
2580
2624
  pptx: ["python-pptx"],
2581
2625
  git: ["gitpython"],
2582
2626
  socks: ["pysocks"],
2627
+ psycopg2: ["psycopg2-binary", "psycopg2"],
2583
2628
  redis: ["redis"],
2584
2629
  cairo: ["pycairo"],
2585
2630
  serial: ["pyserial"],
@@ -2669,6 +2714,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
2669
2714
  }
2670
2715
  const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
2671
2716
  if (extras) for (const m of extras[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
2717
+ const groups = content.match(/\[dependency-groups\]([\s\S]*?)(?=\n\[[^[]|$)/);
2718
+ if (groups) for (const m of groups[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
2672
2719
  const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
2673
2720
  let match = poetryRe.exec(content);
2674
2721
  while (match !== null) {
@@ -2901,9 +2948,28 @@ const extractJsImports = (content) => {
2901
2948
  const extractPyImports = (content) => {
2902
2949
  const lines = content.split("\n");
2903
2950
  const results = [];
2951
+ let inDoc = null;
2952
+ let typeCheckIndent = -1;
2904
2953
  for (let i = 0; i < lines.length; i++) {
2905
- const line = lines[i].trim();
2906
- if (line.startsWith("#")) continue;
2954
+ const raw = lines[i];
2955
+ const line = raw.trim();
2956
+ if (inDoc) {
2957
+ if (line.includes(inDoc)) inDoc = null;
2958
+ continue;
2959
+ }
2960
+ if (line === "" || line.startsWith("#")) continue;
2961
+ const triples = line.match(/"""|'''/g);
2962
+ if (triples) {
2963
+ if (triples.length % 2 === 1) inDoc = triples[triples.length - 1];
2964
+ continue;
2965
+ }
2966
+ const indent = raw.length - raw.trimStart().length;
2967
+ if (typeCheckIndent >= 0 && indent <= typeCheckIndent) typeCheckIndent = -1;
2968
+ if (/^if\s+(?:[\w.]+\.)?TYPE_CHECKING\b/.test(line)) {
2969
+ typeCheckIndent = indent;
2970
+ continue;
2971
+ }
2972
+ if (typeCheckIndent >= 0) continue;
2907
2973
  const fromMatch = line.match(/^from\s+([\w.]+)\s+import\b/);
2908
2974
  if (fromMatch && !fromMatch[1].startsWith(".")) {
2909
2975
  results.push({
@@ -2913,8 +2979,8 @@ const extractPyImports = (content) => {
2913
2979
  continue;
2914
2980
  }
2915
2981
  const importMatch = line.match(/^import\s+([\w.,\s]+?)(?:\s+as\s+\w+)?\s*$/);
2916
- if (importMatch) for (const raw of importMatch[1].split(",")) {
2917
- const cleaned = raw.trim().split(/\s+as\s+/)[0];
2982
+ if (importMatch) for (const part of importMatch[1].split(",")) {
2983
+ const cleaned = part.trim().split(/\s+as\s+/)[0];
2918
2984
  if (cleaned && !cleaned.startsWith(".")) results.push({
2919
2985
  spec: cleaned,
2920
2986
  line: i + 1
@@ -2971,6 +3037,7 @@ const detectHallucinatedImports = async (context) => {
2971
3037
  continue;
2972
3038
  }
2973
3039
  const relPath = path.relative(context.rootDirectory, filePath);
3040
+ if (isNonProductionPath(relPath)) continue;
2974
3041
  const imports = isJs ? extractJsImports(content) : extractPyImports(content);
2975
3042
  for (const { spec, line } of imports) {
2976
3043
  const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
@@ -3098,7 +3165,7 @@ const collectBlocks = (sourceLines, syntax) => {
3098
3165
  //#endregion
3099
3166
  //#region src/engines/ai-slop/meta-comment.ts
3100
3167
  const PLAN_REFERENCE_RES = [
3101
- /\b(?:stage|step|phase)\s+\d+\b(?!\s*[:.]?\s*(?:bytes|ms|seconds|of\s+\d))/i,
3168
+ /^(?:stage|step|phase)\s+\d+\s*[:.\-–—]/i,
3102
3169
  /\bstep\s+\d+\s+of\s+the\s+plan\b/i,
3103
3170
  /\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
3104
3171
  /\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
@@ -3200,24 +3267,6 @@ const looksLikeLicenseHeader = (block) => {
3200
3267
  const text = block.rawLines.join(" ").toLowerCase();
3201
3268
  return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
3202
3269
  };
3203
- const BARE_LABEL_RE = /^[A-Z][A-Za-z0-9 ]{1,28}$/;
3204
- const isBareSectionLabel = (prose) => {
3205
- if (!BARE_LABEL_RE.test(prose)) return false;
3206
- if (prose.endsWith(".")) return false;
3207
- if (prose.split(/\s+/).length > 3) return false;
3208
- if (STEP_COMMENT_VERB_RE.test(prose)) return false;
3209
- return true;
3210
- };
3211
- const DATA_ENTRY_START = /^\s*(?:\{|\[|["'`]|\d|\w+:\s|case\s)/;
3212
- const nextLineLooksLikeDataEntry = (nextLine) => {
3213
- if (nextLine === null) return false;
3214
- if (!DATA_ENTRY_START.test(nextLine)) return false;
3215
- const trimmed = nextLine.trim();
3216
- if (trimmed.startsWith("case ")) return true;
3217
- if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("\"") || trimmed.startsWith("'") || trimmed.startsWith("`")) return true;
3218
- if (/^\w+\s*:/.test(trimmed)) return true;
3219
- return false;
3220
- };
3221
3270
  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));
3222
3271
  const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
3223
3272
  const GO_FIELD_LEAD_RE = /^(\w+)\s+/;
@@ -3306,10 +3355,6 @@ const detectNarrativeInBlock = (block, ext) => {
3306
3355
  matched: true,
3307
3356
  reason: "phase/section header"
3308
3357
  };
3309
- if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine) && !looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
3310
- matched: true,
3311
- reason: "bare section label"
3312
- };
3313
3358
  const joined = block.prose.join(" ");
3314
3359
  const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
3315
3360
  if (hasWhyMarker || hasDocIndicator(block)) return {
@@ -3340,17 +3385,11 @@ const detectNarrativeInBlock = (block, ext) => {
3340
3385
  };
3341
3386
  const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
3342
3387
  const isAboveDeclaration = looksLikeDeclarationPreamble(block.nextNonBlankLine, ext);
3343
- if (nonEmptyProseCount >= 5) {
3344
- if (isAboveDeclaration) return {
3345
- matched: false,
3346
- reason: ""
3347
- };
3348
- return {
3349
- matched: true,
3350
- reason: "long narrative block"
3351
- };
3352
- }
3353
- if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration) return {
3388
+ if (nonEmptyProseCount >= 5 && !isAboveDeclaration && hasPreambleSlopSignal(block)) return {
3389
+ matched: true,
3390
+ reason: "long narrative block"
3391
+ };
3392
+ if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration && hasPreambleSlopSignal(block)) return {
3354
3393
  matched: true,
3355
3394
  reason: "multi-line narrative prose"
3356
3395
  };
@@ -3792,7 +3831,14 @@ const JS_EXTS$1 = new Set([
3792
3831
  ".mjs",
3793
3832
  ".cjs"
3794
3833
  ]);
3795
- const CATCH_HEAD_RE = /\bcatch\s*(?:\([^)]*\))?\s*\{/g;
3834
+ const CATCH_HEAD_RE = /\bcatch\s*(?:\(\s*([^)]*?)\s*\))?\s*\{/g;
3835
+ const isIdentifier = (s) => /^[A-Za-z_$][\w$]*$/.test(s);
3836
+ const recoveryDropsError = (binding, body) => {
3837
+ const name = binding?.trim() ?? "";
3838
+ if (name === "") return true;
3839
+ if (!isIdentifier(name)) return false;
3840
+ return !new RegExp(`\\b${name}\\b`).test(body);
3841
+ };
3796
3842
  const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
3797
3843
  const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
3798
3844
  const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
@@ -3842,14 +3888,15 @@ const detectJsSilentRecovery = (content, relPath) => {
3842
3888
  const body = extractCatchBody(content, match.index + match[0].length - 1);
3843
3889
  if (body === null) continue;
3844
3890
  if (!isLogOnlyBody(body)) continue;
3891
+ if (!recoveryDropsError(match[1], body)) continue;
3845
3892
  const line = content.slice(0, match.index).split("\n").length;
3846
3893
  out.push({
3847
3894
  filePath: relPath,
3848
3895
  engine: "ai-slop",
3849
3896
  rule: "ai-slop/silent-recovery",
3850
3897
  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.",
3898
+ message: "Catch logs without the caught error then continues; the failure cause is lost",
3899
+ help: "Include the caught error in the log, or rethrow / recover explicitly, so the failure stays diagnosable.",
3853
3900
  line,
3854
3901
  column: 0,
3855
3902
  category: "AI Slop",
@@ -3859,6 +3906,7 @@ const detectJsSilentRecovery = (content, relPath) => {
3859
3906
  return out;
3860
3907
  };
3861
3908
  const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
3909
+ const PY_EXCEPT_BINDING_RE = /\bas\s+(\w+)\s*:/;
3862
3910
  const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
3863
3911
  const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
3864
3912
  const detectPySilentRecovery = (content, relPath) => {
@@ -3882,13 +3930,14 @@ const detectPySilentRecovery = (content, relPath) => {
3882
3930
  const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
3883
3931
  const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
3884
3932
  if (!allLogs || !sawLog) continue;
3933
+ if (!recoveryDropsError(PY_EXCEPT_BINDING_RE.exec(lines[i])?.[1], bodyLines.join(" "))) continue;
3885
3934
  out.push({
3886
3935
  filePath: relPath,
3887
3936
  engine: "ai-slop",
3888
3937
  rule: "ai-slop/silent-recovery",
3889
3938
  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.",
3939
+ message: "except logs without the caught error then continues; the failure cause is lost",
3940
+ help: "Include the caught error in the log, or re-raise / recover explicitly, so the failure stays diagnosable.",
3892
3941
  line: i + 1,
3893
3942
  column: 0,
3894
3943
  category: "AI Slop",
@@ -3991,10 +4040,11 @@ const extractPyImportedSymbols = (lines) => {
3991
4040
  const importLines = /* @__PURE__ */ new Set();
3992
4041
  for (let i = 0; i < lines.length; i++) {
3993
4042
  const trimmed = lines[i].trim();
3994
- const fromMatch = trimmed.match(/^from\s+[\w.]+\s+import\s+(.+)/);
4043
+ const fromMatch = trimmed.match(/^from\s+([\w.]+)\s+import\s+(.+)/);
3995
4044
  if (fromMatch) {
3996
4045
  importLines.add(i);
3997
- const importPart = fromMatch[1].replace(/#.*$/, "").trim();
4046
+ if (fromMatch[1] === "__future__") continue;
4047
+ const importPart = fromMatch[2].replace(/#.*$/, "").trim();
3998
4048
  if (importPart === "*") continue;
3999
4049
  const cleaned = importPart.replace(/[()]/g, "");
4000
4050
  for (const item of cleaned.split(",")) {
@@ -7106,6 +7156,13 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
7106
7156
  //#endregion
7107
7157
  //#region src/scoring/index.ts
7108
7158
  const PERFECT_SCORE = 100;
7159
+ const STYLE_RULES = new Set([
7160
+ "ai-slop/trivial-comment",
7161
+ "ai-slop/narrative-comment",
7162
+ "complexity/file-too-large",
7163
+ "complexity/function-too-long"
7164
+ ]);
7165
+ const STYLE_WEIGHT = .5;
7109
7166
  const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
7110
7167
  if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
7111
7168
  const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
@@ -7120,7 +7177,8 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
7120
7177
  for (const d of diagnostics) {
7121
7178
  const engineWeight = weights[d.engine] ?? 1;
7122
7179
  const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
7123
- deductions += severityPenalty * engineWeight;
7180
+ const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
7181
+ deductions += severityPenalty * engineWeight * styleFactor;
7124
7182
  }
7125
7183
  const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
7126
7184
  const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
@@ -8301,12 +8359,12 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
8301
8359
  engineTimings
8302
8360
  };
8303
8361
  if (options.sarif) {
8304
- const { buildSarifLog } = await import("./sarif-Cneulb6L.js");
8362
+ const { buildSarifLog } = await import("./sarif-BtSQ92c6.js");
8305
8363
  console.log(JSON.stringify(buildSarifLog(results), null, 2));
8306
8364
  return completion;
8307
8365
  }
8308
8366
  if (options.json) {
8309
- const { buildJsonOutput } = await import("./json-CZU3lEfE.js");
8367
+ const { buildJsonOutput } = await import("./json-DaFOYHcf.js");
8310
8368
  const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
8311
8369
  console.log(JSON.stringify(jsonOut, null, 2));
8312
8370
  return completion;
@@ -1,5 +1,5 @@
1
1
  import { t as ENGINE_INFO } from "./engine-info-DCvIfZ0f.js";
2
- import { t as APP_VERSION } from "./version-ls3wZmOU.js";
2
+ import { t as APP_VERSION } from "./version-DYg_ShBx.js";
3
3
 
4
4
  //#region src/output/json.ts
5
5
  const buildJsonOutput = (results, scoreResult, fileCount, elapsedMs) => {
package/dist/mcp.js CHANGED
@@ -538,14 +538,16 @@ const THIN_WRAPPER_PATTERNS = [
538
538
  const AI_NAMING_PATTERNS = [/(?:helper|util|handler|process|do|handle|execute|perform)_?\d+/i, /(?:data|temp|result|value|item|obj|arr|str|num|val)\d+/];
539
539
  const FRAMEWORK_METHOD_NAMES = /^(?:setUp|tearDown|setUpClass|tearDownClass|setUpModule|tearDownModule)$/;
540
540
  const DUNDER_PATTERN = /^__\w+__$/;
541
- const hasHardcodedArgs = (matchText) => {
542
- const innerCallMatch = matchText.match(/=>\s*\w+\(([^)]*)\)\s*;?\s*$/);
543
- if (!innerCallMatch) {
544
- const returnCallMatch = matchText.match(/return\s+\w+\(([^)]*)\)\s*;?\s*\}/);
545
- if (!returnCallMatch) return false;
546
- return /['"`]\w+['"`]|(?<!\w)\d+(?!\w)/.test(returnCallMatch[1]);
547
- }
548
- return /['"`]\w+['"`]|(?<!\w)\d+(?!\w)/.test(innerCallMatch[1]);
541
+ const stripParam = (p) => p.trim().split(/[:=]/)[0].trim().replace(/^[*&]+/, "");
542
+ const paramNames = (paramsText) => new Set(paramsText.split(",").map(stripParam).filter((p) => p && p !== "self" && p !== "cls"));
543
+ const isIdentityForward = (matchText) => {
544
+ const paramsMatch = matchText.match(/\(([^)]*)\)/);
545
+ const innerMatch = matchText.match(/(?:return\s+\w+|=>\s*\w+)\s*\(([^)]*)\)/);
546
+ if (!paramsMatch || !innerMatch) return false;
547
+ const params = paramNames(paramsMatch[1]);
548
+ const args = innerMatch[1].split(",").map((a) => a.trim()).filter((a) => a.length > 0);
549
+ if (args.length === 0) return false;
550
+ return args.every((a) => /^[A-Za-z_$][\w$]*$/.test(a) && params.has(a));
549
551
  };
550
552
  const isUseContextWrapper = (matchText) => /\buse\w+/.test(matchText) && /useContext\s*\(/.test(matchText);
551
553
  const detectThinWrappers = (content, relativePath, ext) => {
@@ -565,7 +567,7 @@ const detectThinWrappers = (content, relativePath, ext) => {
565
567
  const prevLine = lines[lineNumber - 2]?.trim();
566
568
  if (prevLine && prevLine.startsWith("@")) continue;
567
569
  }
568
- if (hasHardcodedArgs(matchText)) continue;
570
+ if (!isIdentityForward(matchText)) continue;
569
571
  if (isUseContextWrapper(matchText)) continue;
570
572
  diagnostics.push({
571
573
  filePath: relativePath,
@@ -650,8 +652,7 @@ const JUSTIFICATION_OPENERS = [
650
652
  /^(?:First|Then|Finally|Next|Lastly|Subsequently),?\s+(?:it|we|the\s+(?:function|method|class))\b/i
651
653
  ];
652
654
  const EXPLANATORY_OPENERS = /^(Matches|Detects|Represents|Holds|Stores|Tracks|Handles|Manages|Controls|Contains|Captures|Encapsulates|Wraps|Describes)\s+[A-Za-z`'"]/;
653
- const STEP_COMMENT_VERB_RE = /^(?:Render|Enable|Disable|Initialize|Init|Setup|Set|Get|Fetch|Load|Save|Build|Create|Delete|Remove|Add|Update|Process|Execute|Run|Start|Stop|Clean|Cleanup|Configure|Validate|Check|Verify|Parse|Extract|Apply|Wait|Sleep|Skip|Allow|Deny|Lock|Unlock|Refresh|Reload|Reset|Clear|Send|Receive|Read|Write|Print|Log|Emit|Dispatch|Fire|Open|Close|Bind|Connect|Disconnect|Register|Unregister|Push|Pop|Insert|Append|Prepend|Sort|Filter|Find|Search|Replace|Encode|Decode|Convert|Transform|Map|Reduce|Iterate|Loop|Walk|Visit|Mark|Unmark|Toggle|Switch|Restart|Resume|Pause|Abort|Cancel|Compute|Calculate|Resolve|Reject|Ignore|Handle|Track|Trace|Increment|Decrement|Round|Truncate|Resize|Move|Copy|Clone|Merge|Split|Join|Wrap|Unwrap|Bump|Drain|Flush|Sync|Persist|Commit|Rollback|Yield|Return|Discard|Defer|Pin|Unpin|Mount|Unmount|Spawn|Kill|Restore)(?:\s|$)/;
654
- const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:|to\s+avoid|to\s+ensure|to\s+prevent|in\s+order\s+to|necessary|guarantee[sd]?|prevents?|regardless\s+of|required\s+(?:for|to|by)|for\s+example|e\.g\.|i\.e\.|useful\s+(?:for|when)|intended\s+to|on\s+purpose|by\s+design)\b/i;
655
+ const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:|to\s+avoid|to\s+ensure|to\s+prevent|in\s+order\s+to|necessary|guarantee[sd]?|prevents?|regardless\s+of|required\s+(?:for|to|by)|for\s+example|e\.g\.|i\.e\.|useful\s+(?:for|when)|intended\s+to|on\s+purpose|by\s+design|ideally|however|although|even\s+though|despite|whereas|unfortunately|trade-?off|first\s+need)\b/i;
655
656
  const MEANINGFUL_JSDOC_TAGS = new Set([
656
657
  "deprecated",
657
658
  "see",
@@ -747,7 +748,7 @@ const PHP_DECL_START = /^\s*(?:(?:public|private|protected|static|final|abstract
747
748
 
748
749
  //#endregion
749
750
  //#region src/engines/ai-slop/non-production-paths.ts
750
- const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
751
+ const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|docs?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
751
752
  const BASENAME_PATTERN = /(?:^|\/)(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]*\.[mc]?[jt]sx?$|(?:^|\/)[^/]+[-_](?:benchmark|bench|demo|example)\.[mc]?[jt]sx?$/i;
752
753
  const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) || BASENAME_PATTERN.test(relativePath);
753
754
 
@@ -756,7 +757,7 @@ const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) ||
756
757
  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";
757
758
  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")];
758
759
  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")];
759
- const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?)\b/i;
760
+ const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?|if|when|unless|until|only|except|otherwise|needs?|must|should|ensure|avoid|prevent|requires?)\b/i;
760
761
  const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
761
762
  const MAX_TRIVIAL_COMMENT_LENGTH = 60;
762
763
  const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
@@ -905,6 +906,7 @@ const TODO_PATTERN = new RegExp(`\\b(?:${[
905
906
  "PLACEHOLDER",
906
907
  "STUB"
907
908
  ].join("|")})[:\\s]`);
909
+ const TODO_TRACKING_RE = /https?:\/\/|#\d+|\bgh-\d+\b|\b[A-Z][A-Z0-9]+-\d+\b|\b(?:issue|ticket|jira)\b/i;
908
910
  const isBlockCloserAfterReturn = (line) => line.startsWith("}") || line.startsWith("};") || line.startsWith("),") || line.startsWith(");") || line.startsWith("],") || line.startsWith("]);");
909
911
  const isGuardedSingleLineExit = (lines, lineIndex) => {
910
912
  const contextLines = [];
@@ -924,7 +926,10 @@ const detectTodoStubs = (content, relativePath) => {
924
926
  for (let i = 0; i < lines.length; i++) {
925
927
  const trimmed = lines[i].trim();
926
928
  if (!trimmed.startsWith("//") && !trimmed.startsWith("#") && !trimmed.startsWith("*") && !trimmed.startsWith("/*")) continue;
927
- if (TODO_PATTERN.test(trimmed)) diagnostics.push(slop(relativePath, i + 1, "ai-slop/todo-stub", "info", "Unresolved TODO/FIXME/HACK comment indicates incomplete code", "Resolve the TODO or create a tracked issue for it", false));
929
+ if (TODO_PATTERN.test(trimmed)) {
930
+ if (TODO_TRACKING_RE.test(trimmed)) continue;
931
+ diagnostics.push(slop(relativePath, i + 1, "ai-slop/todo-stub", "info", "Unresolved TODO/FIXME/HACK comment indicates incomplete code", "Resolve the TODO or create a tracked issue for it", false));
932
+ }
928
933
  }
929
934
  return diagnostics;
930
935
  };
@@ -1438,11 +1443,42 @@ const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|li
1438
1443
  const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
1439
1444
  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;
1440
1445
  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;
1446
+ const MIGRATION_PATH_RE$1 = /(?:^|[\\/])(?:migrations?|db[\\/]migrate)[\\/]/i;
1441
1447
  const PLACEHOLDER_HOSTS = new Set([
1442
1448
  "example.com",
1443
1449
  "example.org",
1444
1450
  "example.net"
1445
1451
  ]);
1452
+ const LOOPBACK_HOSTS = new Set([
1453
+ "localhost",
1454
+ "127.0.0.1",
1455
+ "0.0.0.0",
1456
+ "::1"
1457
+ ]);
1458
+ const VENDOR_API_DOMAINS = [
1459
+ "github.com",
1460
+ "githubusercontent.com",
1461
+ "googleapis.com",
1462
+ "accounts.google.com",
1463
+ "stripe.com",
1464
+ "openai.com",
1465
+ "anthropic.com",
1466
+ "slack.com",
1467
+ "twilio.com",
1468
+ "sendgrid.com",
1469
+ "mailgun.net",
1470
+ "cloudflare.com",
1471
+ "discord.com",
1472
+ "telegram.org",
1473
+ "login.microsoftonline.com",
1474
+ "graph.microsoft.com",
1475
+ "twitter.com",
1476
+ "x.com",
1477
+ "twimg.com",
1478
+ "t.co",
1479
+ "api.telegram.org"
1480
+ ];
1481
+ const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
1446
1482
  const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
1447
1483
  const HARDCODED_URL_FINDING = {
1448
1484
  rule: "ai-slop/hardcoded-url",
@@ -1486,14 +1522,18 @@ const shouldFlagUrlLiteral = (line, urlText) => {
1486
1522
  const host = safeUrlHost(urlText);
1487
1523
  if (!host) return false;
1488
1524
  if (PLACEHOLDER_HOSTS.has(host)) return false;
1525
+ if (LOOPBACK_HOSTS.has(host)) return false;
1526
+ if (isVendorApiHost(host)) return false;
1489
1527
  if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
1490
1528
  return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
1491
1529
  };
1530
+ const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
1492
1531
  const hasUsefulIdShape = (value) => {
1493
1532
  if (PLACEHOLDER_ID_RE.test(value)) return false;
1533
+ if (ENV_VAR_NAME_RE.test(value)) return false;
1494
1534
  if (/^https?:\/\//i.test(value)) return false;
1495
1535
  if (/^[A-Za-z]+$/.test(value)) return false;
1496
- return /[0-9_-]/.test(value);
1536
+ return /[0-9]/.test(value);
1497
1537
  };
1498
1538
  const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
1499
1539
  const diagnostics = [];
@@ -1520,6 +1560,7 @@ const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
1520
1560
  const scanFileForConfigLiterals = (content, relativePath, ext) => {
1521
1561
  if (!SOURCE_EXTENSIONS.has(ext)) return [];
1522
1562
  if (isNonProductionPath(relativePath)) return [];
1563
+ if (MIGRATION_PATH_RE$1.test(relativePath)) return [];
1523
1564
  return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
1524
1565
  };
1525
1566
  const detectHardcodedConfigLiterals = async (context) => {
@@ -1667,7 +1708,9 @@ const PYTHON_STDLIB = new Set([
1667
1708
  "builtins",
1668
1709
  "bz2",
1669
1710
  "calendar",
1711
+ "code",
1670
1712
  "codecs",
1713
+ "codeop",
1671
1714
  "collections",
1672
1715
  "concurrent",
1673
1716
  "configparser",
@@ -1740,6 +1783,7 @@ const PYTHON_STDLIB = new Set([
1740
1783
  "readline",
1741
1784
  "reprlib",
1742
1785
  "resource",
1786
+ "rlcompleter",
1743
1787
  "secrets",
1744
1788
  "select",
1745
1789
  "selectors",
@@ -1838,6 +1882,7 @@ const PYTHON_IMPORT_TO_PIP = {
1838
1882
  pptx: ["python-pptx"],
1839
1883
  git: ["gitpython"],
1840
1884
  socks: ["pysocks"],
1885
+ psycopg2: ["psycopg2-binary", "psycopg2"],
1841
1886
  redis: ["redis"],
1842
1887
  cairo: ["pycairo"],
1843
1888
  serial: ["pyserial"],
@@ -1927,6 +1972,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
1927
1972
  }
1928
1973
  const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
1929
1974
  if (extras) for (const m of extras[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
1975
+ const groups = content.match(/\[dependency-groups\]([\s\S]*?)(?=\n\[[^[]|$)/);
1976
+ if (groups) for (const m of groups[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
1930
1977
  const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
1931
1978
  let match = poetryRe.exec(content);
1932
1979
  while (match !== null) {
@@ -2159,9 +2206,28 @@ const extractJsImports = (content) => {
2159
2206
  const extractPyImports = (content) => {
2160
2207
  const lines = content.split("\n");
2161
2208
  const results = [];
2209
+ let inDoc = null;
2210
+ let typeCheckIndent = -1;
2162
2211
  for (let i = 0; i < lines.length; i++) {
2163
- const line = lines[i].trim();
2164
- if (line.startsWith("#")) continue;
2212
+ const raw = lines[i];
2213
+ const line = raw.trim();
2214
+ if (inDoc) {
2215
+ if (line.includes(inDoc)) inDoc = null;
2216
+ continue;
2217
+ }
2218
+ if (line === "" || line.startsWith("#")) continue;
2219
+ const triples = line.match(/"""|'''/g);
2220
+ if (triples) {
2221
+ if (triples.length % 2 === 1) inDoc = triples[triples.length - 1];
2222
+ continue;
2223
+ }
2224
+ const indent = raw.length - raw.trimStart().length;
2225
+ if (typeCheckIndent >= 0 && indent <= typeCheckIndent) typeCheckIndent = -1;
2226
+ if (/^if\s+(?:[\w.]+\.)?TYPE_CHECKING\b/.test(line)) {
2227
+ typeCheckIndent = indent;
2228
+ continue;
2229
+ }
2230
+ if (typeCheckIndent >= 0) continue;
2165
2231
  const fromMatch = line.match(/^from\s+([\w.]+)\s+import\b/);
2166
2232
  if (fromMatch && !fromMatch[1].startsWith(".")) {
2167
2233
  results.push({
@@ -2171,8 +2237,8 @@ const extractPyImports = (content) => {
2171
2237
  continue;
2172
2238
  }
2173
2239
  const importMatch = line.match(/^import\s+([\w.,\s]+?)(?:\s+as\s+\w+)?\s*$/);
2174
- if (importMatch) for (const raw of importMatch[1].split(",")) {
2175
- const cleaned = raw.trim().split(/\s+as\s+/)[0];
2240
+ if (importMatch) for (const part of importMatch[1].split(",")) {
2241
+ const cleaned = part.trim().split(/\s+as\s+/)[0];
2176
2242
  if (cleaned && !cleaned.startsWith(".")) results.push({
2177
2243
  spec: cleaned,
2178
2244
  line: i + 1
@@ -2229,6 +2295,7 @@ const detectHallucinatedImports = async (context) => {
2229
2295
  continue;
2230
2296
  }
2231
2297
  const relPath = path.relative(context.rootDirectory, filePath);
2298
+ if (isNonProductionPath(relPath)) continue;
2232
2299
  const imports = isJs ? extractJsImports(content) : extractPyImports(content);
2233
2300
  for (const { spec, line } of imports) {
2234
2301
  const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
@@ -2356,7 +2423,7 @@ const collectBlocks = (sourceLines, syntax) => {
2356
2423
  //#endregion
2357
2424
  //#region src/engines/ai-slop/meta-comment.ts
2358
2425
  const PLAN_REFERENCE_RES = [
2359
- /\b(?:stage|step|phase)\s+\d+\b(?!\s*[:.]?\s*(?:bytes|ms|seconds|of\s+\d))/i,
2426
+ /^(?:stage|step|phase)\s+\d+\s*[:.\-–—]/i,
2360
2427
  /\bstep\s+\d+\s+of\s+the\s+plan\b/i,
2361
2428
  /\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
2362
2429
  /\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
@@ -2458,24 +2525,6 @@ const looksLikeLicenseHeader = (block) => {
2458
2525
  const text = block.rawLines.join(" ").toLowerCase();
2459
2526
  return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
2460
2527
  };
2461
- const BARE_LABEL_RE = /^[A-Z][A-Za-z0-9 ]{1,28}$/;
2462
- const isBareSectionLabel = (prose) => {
2463
- if (!BARE_LABEL_RE.test(prose)) return false;
2464
- if (prose.endsWith(".")) return false;
2465
- if (prose.split(/\s+/).length > 3) return false;
2466
- if (STEP_COMMENT_VERB_RE.test(prose)) return false;
2467
- return true;
2468
- };
2469
- const DATA_ENTRY_START = /^\s*(?:\{|\[|["'`]|\d|\w+:\s|case\s)/;
2470
- const nextLineLooksLikeDataEntry = (nextLine) => {
2471
- if (nextLine === null) return false;
2472
- if (!DATA_ENTRY_START.test(nextLine)) return false;
2473
- const trimmed = nextLine.trim();
2474
- if (trimmed.startsWith("case ")) return true;
2475
- if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("\"") || trimmed.startsWith("'") || trimmed.startsWith("`")) return true;
2476
- if (/^\w+\s*:/.test(trimmed)) return true;
2477
- return false;
2478
- };
2479
2528
  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));
2480
2529
  const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
2481
2530
  const GO_FIELD_LEAD_RE = /^(\w+)\s+/;
@@ -2564,10 +2613,6 @@ const detectNarrativeInBlock = (block, ext) => {
2564
2613
  matched: true,
2565
2614
  reason: "phase/section header"
2566
2615
  };
2567
- if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine) && !looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
2568
- matched: true,
2569
- reason: "bare section label"
2570
- };
2571
2616
  const joined = block.prose.join(" ");
2572
2617
  const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
2573
2618
  if (hasWhyMarker || hasDocIndicator(block)) return {
@@ -2598,17 +2643,11 @@ const detectNarrativeInBlock = (block, ext) => {
2598
2643
  };
2599
2644
  const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
2600
2645
  const isAboveDeclaration = looksLikeDeclarationPreamble(block.nextNonBlankLine, ext);
2601
- if (nonEmptyProseCount >= 5) {
2602
- if (isAboveDeclaration) return {
2603
- matched: false,
2604
- reason: ""
2605
- };
2606
- return {
2607
- matched: true,
2608
- reason: "long narrative block"
2609
- };
2610
- }
2611
- if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration) return {
2646
+ if (nonEmptyProseCount >= 5 && !isAboveDeclaration && hasPreambleSlopSignal(block)) return {
2647
+ matched: true,
2648
+ reason: "long narrative block"
2649
+ };
2650
+ if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration && hasPreambleSlopSignal(block)) return {
2612
2651
  matched: true,
2613
2652
  reason: "multi-line narrative prose"
2614
2653
  };
@@ -3050,7 +3089,14 @@ const JS_EXTS$1 = new Set([
3050
3089
  ".mjs",
3051
3090
  ".cjs"
3052
3091
  ]);
3053
- const CATCH_HEAD_RE = /\bcatch\s*(?:\([^)]*\))?\s*\{/g;
3092
+ const CATCH_HEAD_RE = /\bcatch\s*(?:\(\s*([^)]*?)\s*\))?\s*\{/g;
3093
+ const isIdentifier = (s) => /^[A-Za-z_$][\w$]*$/.test(s);
3094
+ const recoveryDropsError = (binding, body) => {
3095
+ const name = binding?.trim() ?? "";
3096
+ if (name === "") return true;
3097
+ if (!isIdentifier(name)) return false;
3098
+ return !new RegExp(`\\b${name}\\b`).test(body);
3099
+ };
3054
3100
  const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
3055
3101
  const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
3056
3102
  const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
@@ -3100,14 +3146,15 @@ const detectJsSilentRecovery = (content, relPath) => {
3100
3146
  const body = extractCatchBody(content, match.index + match[0].length - 1);
3101
3147
  if (body === null) continue;
3102
3148
  if (!isLogOnlyBody(body)) continue;
3149
+ if (!recoveryDropsError(match[1], body)) continue;
3103
3150
  const line = content.slice(0, match.index).split("\n").length;
3104
3151
  out.push({
3105
3152
  filePath: relPath,
3106
3153
  engine: "ai-slop",
3107
3154
  rule: "ai-slop/silent-recovery",
3108
3155
  severity: "warning",
3109
- message: "Catch only logs then continues, leaving execution in a possibly broken state",
3110
- help: "Handle the error: rethrow, return an error value, or recover explicitly. Logging alone lets the program proceed as if nothing failed.",
3156
+ message: "Catch logs without the caught error then continues; the failure cause is lost",
3157
+ help: "Include the caught error in the log, or rethrow / recover explicitly, so the failure stays diagnosable.",
3111
3158
  line,
3112
3159
  column: 0,
3113
3160
  category: "AI Slop",
@@ -3117,6 +3164,7 @@ const detectJsSilentRecovery = (content, relPath) => {
3117
3164
  return out;
3118
3165
  };
3119
3166
  const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
3167
+ const PY_EXCEPT_BINDING_RE = /\bas\s+(\w+)\s*:/;
3120
3168
  const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
3121
3169
  const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
3122
3170
  const detectPySilentRecovery = (content, relPath) => {
@@ -3140,13 +3188,14 @@ const detectPySilentRecovery = (content, relPath) => {
3140
3188
  const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
3141
3189
  const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
3142
3190
  if (!allLogs || !sawLog) continue;
3191
+ if (!recoveryDropsError(PY_EXCEPT_BINDING_RE.exec(lines[i])?.[1], bodyLines.join(" "))) continue;
3143
3192
  out.push({
3144
3193
  filePath: relPath,
3145
3194
  engine: "ai-slop",
3146
3195
  rule: "ai-slop/silent-recovery",
3147
3196
  severity: "warning",
3148
- message: "except only logs then continues, leaving execution in a possibly broken state",
3149
- help: "Handle the error: re-raise, return an error value, or recover explicitly. Logging alone lets the program proceed as if nothing failed.",
3197
+ message: "except logs without the caught error then continues; the failure cause is lost",
3198
+ help: "Include the caught error in the log, or re-raise / recover explicitly, so the failure stays diagnosable.",
3150
3199
  line: i + 1,
3151
3200
  column: 0,
3152
3201
  category: "AI Slop",
@@ -3248,10 +3297,11 @@ const extractPyImportedSymbols = (lines) => {
3248
3297
  const importLines = /* @__PURE__ */ new Set();
3249
3298
  for (let i = 0; i < lines.length; i++) {
3250
3299
  const trimmed = lines[i].trim();
3251
- const fromMatch = trimmed.match(/^from\s+[\w.]+\s+import\s+(.+)/);
3300
+ const fromMatch = trimmed.match(/^from\s+([\w.]+)\s+import\s+(.+)/);
3252
3301
  if (fromMatch) {
3253
3302
  importLines.add(i);
3254
- const importPart = fromMatch[1].replace(/#.*$/, "").trim();
3303
+ if (fromMatch[1] === "__future__") continue;
3304
+ const importPart = fromMatch[2].replace(/#.*$/, "").trim();
3255
3305
  if (importPart === "*") continue;
3256
3306
  const cleaned = importPart.replace(/[()]/g, "");
3257
3307
  for (const item of cleaned.split(",")) {
@@ -6075,6 +6125,13 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
6075
6125
  //#endregion
6076
6126
  //#region src/scoring/index.ts
6077
6127
  const PERFECT_SCORE = 100;
6128
+ const STYLE_RULES = new Set([
6129
+ "ai-slop/trivial-comment",
6130
+ "ai-slop/narrative-comment",
6131
+ "complexity/file-too-large",
6132
+ "complexity/function-too-long"
6133
+ ]);
6134
+ const STYLE_WEIGHT = .5;
6078
6135
  const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
6079
6136
  if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
6080
6137
  const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
@@ -6089,7 +6146,8 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
6089
6146
  for (const d of diagnostics) {
6090
6147
  const engineWeight = weights[d.engine] ?? 1;
6091
6148
  const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
6092
- deductions += severityPenalty * engineWeight;
6149
+ const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
6150
+ deductions += severityPenalty * engineWeight * styleFactor;
6093
6151
  }
6094
6152
  const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
6095
6153
  const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
@@ -6456,7 +6514,7 @@ const handleAislopBaseline = (input) => {
6456
6514
 
6457
6515
  //#endregion
6458
6516
  //#region src/version.ts
6459
- const APP_VERSION = "0.9.5";
6517
+ const APP_VERSION = "0.10.0";
6460
6518
 
6461
6519
  //#endregion
6462
6520
  //#region src/telemetry/env.ts
@@ -1,4 +1,4 @@
1
- import { t as APP_VERSION } from "./version-ls3wZmOU.js";
1
+ import { t as APP_VERSION } from "./version-DYg_ShBx.js";
2
2
  import path from "node:path";
3
3
 
4
4
  //#region src/output/sarif.ts
@@ -0,0 +1,5 @@
1
+ //#region src/version.ts
2
+ const APP_VERSION = "0.10.0";
3
+
4
+ //#endregion
5
+ export { APP_VERSION as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aislop",
3
- "version": "0.9.5",
3
+ "version": "0.10.0",
4
4
  "description": "Catch the slop AI coding agents leave in your code: narrative comments, swallowed exceptions, as-any casts, dead code, oversized functions. 40+ rules across 7 languages (TS/JS, Python, Go, Rust, Ruby, PHP, Java). Sub-second, deterministic, no LLM at runtime. MIT-licensed.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +0,0 @@
1
- //#region src/version.ts
2
- const APP_VERSION = "0.9.5";
3
-
4
- //#endregion
5
- export { APP_VERSION as t };