aislop 0.9.6 → 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.6";
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
  };
@@ -2023,6 +2028,30 @@ const LOOPBACK_HOSTS = new Set([
2023
2028
  "0.0.0.0",
2024
2029
  "::1"
2025
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}`));
2026
2055
  const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
2027
2056
  const HARDCODED_URL_FINDING = {
2028
2057
  rule: "ai-slop/hardcoded-url",
@@ -2067,6 +2096,7 @@ const shouldFlagUrlLiteral = (line, urlText) => {
2067
2096
  if (!host) return false;
2068
2097
  if (PLACEHOLDER_HOSTS.has(host)) return false;
2069
2098
  if (LOOPBACK_HOSTS.has(host)) return false;
2099
+ if (isVendorApiHost(host)) return false;
2070
2100
  if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
2071
2101
  return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
2072
2102
  };
@@ -2251,7 +2281,9 @@ const PYTHON_STDLIB = new Set([
2251
2281
  "builtins",
2252
2282
  "bz2",
2253
2283
  "calendar",
2284
+ "code",
2254
2285
  "codecs",
2286
+ "codeop",
2255
2287
  "collections",
2256
2288
  "concurrent",
2257
2289
  "configparser",
@@ -2324,6 +2356,7 @@ const PYTHON_STDLIB = new Set([
2324
2356
  "readline",
2325
2357
  "reprlib",
2326
2358
  "resource",
2359
+ "rlcompleter",
2327
2360
  "secrets",
2328
2361
  "select",
2329
2362
  "selectors",
@@ -2512,6 +2545,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
2512
2545
  }
2513
2546
  const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
2514
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]);
2515
2550
  const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
2516
2551
  let match = poetryRe.exec(content);
2517
2552
  while (match !== null) {
@@ -2744,9 +2779,28 @@ const extractJsImports = (content) => {
2744
2779
  const extractPyImports = (content) => {
2745
2780
  const lines = content.split("\n");
2746
2781
  const results = [];
2782
+ let inDoc = null;
2783
+ let typeCheckIndent = -1;
2747
2784
  for (let i = 0; i < lines.length; i++) {
2748
- const line = lines[i].trim();
2749
- 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;
2750
2804
  const fromMatch = line.match(/^from\s+([\w.]+)\s+import\b/);
2751
2805
  if (fromMatch && !fromMatch[1].startsWith(".")) {
2752
2806
  results.push({
@@ -2756,8 +2810,8 @@ const extractPyImports = (content) => {
2756
2810
  continue;
2757
2811
  }
2758
2812
  const importMatch = line.match(/^import\s+([\w.,\s]+?)(?:\s+as\s+\w+)?\s*$/);
2759
- if (importMatch) for (const raw of importMatch[1].split(",")) {
2760
- 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];
2761
2815
  if (cleaned && !cleaned.startsWith(".")) results.push({
2762
2816
  spec: cleaned,
2763
2817
  line: i + 1
@@ -2814,6 +2868,7 @@ const detectHallucinatedImports = async (context) => {
2814
2868
  continue;
2815
2869
  }
2816
2870
  const relPath = path.relative(context.rootDirectory, filePath);
2871
+ if (isNonProductionPath(relPath)) continue;
2817
2872
  const imports = isJs ? extractJsImports(content) : extractPyImports(content);
2818
2873
  for (const { spec, line } of imports) {
2819
2874
  const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
@@ -2941,7 +2996,7 @@ const collectBlocks = (sourceLines, syntax) => {
2941
2996
  //#endregion
2942
2997
  //#region src/engines/ai-slop/meta-comment.ts
2943
2998
  const PLAN_REFERENCE_RES = [
2944
- /\b(?:stage|step|phase)\s+\d+\b(?!\s*[:.]?\s*(?:bytes|ms|seconds|of\s+\d))/i,
2999
+ /^(?:stage|step|phase)\s+\d+\s*[:.\-–—]/i,
2945
3000
  /\bstep\s+\d+\s+of\s+the\s+plan\b/i,
2946
3001
  /\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
2947
3002
  /\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
@@ -3043,24 +3098,6 @@ const looksLikeLicenseHeader = (block) => {
3043
3098
  const text = block.rawLines.join(" ").toLowerCase();
3044
3099
  return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
3045
3100
  };
3046
- const BARE_LABEL_RE = /^[A-Z][A-Za-z0-9 ]{1,28}$/;
3047
- const isBareSectionLabel = (prose) => {
3048
- if (!BARE_LABEL_RE.test(prose)) return false;
3049
- if (prose.endsWith(".")) return false;
3050
- if (prose.split(/\s+/).length > 3) return false;
3051
- if (STEP_COMMENT_VERB_RE.test(prose)) return false;
3052
- return true;
3053
- };
3054
- const DATA_ENTRY_START = /^\s*(?:\{|\[|["'`]|\d|\w+:\s|case\s)/;
3055
- const nextLineLooksLikeDataEntry = (nextLine) => {
3056
- if (nextLine === null) return false;
3057
- if (!DATA_ENTRY_START.test(nextLine)) return false;
3058
- const trimmed = nextLine.trim();
3059
- if (trimmed.startsWith("case ")) return true;
3060
- if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("\"") || trimmed.startsWith("'") || trimmed.startsWith("`")) return true;
3061
- if (/^\w+\s*:/.test(trimmed)) return true;
3062
- return false;
3063
- };
3064
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));
3065
3102
  const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
3066
3103
  const GO_FIELD_LEAD_RE = /^(\w+)\s+/;
@@ -3149,10 +3186,6 @@ const detectNarrativeInBlock = (block, ext) => {
3149
3186
  matched: true,
3150
3187
  reason: "phase/section header"
3151
3188
  };
3152
- if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine) && !looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
3153
- matched: true,
3154
- reason: "bare section label"
3155
- };
3156
3189
  const joined = block.prose.join(" ");
3157
3190
  const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
3158
3191
  if (hasWhyMarker || hasDocIndicator(block)) return {
@@ -3183,17 +3216,11 @@ const detectNarrativeInBlock = (block, ext) => {
3183
3216
  };
3184
3217
  const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
3185
3218
  const isAboveDeclaration = looksLikeDeclarationPreamble(block.nextNonBlankLine, ext);
3186
- if (nonEmptyProseCount >= 5) {
3187
- if (isAboveDeclaration) return {
3188
- matched: false,
3189
- reason: ""
3190
- };
3191
- return {
3192
- matched: true,
3193
- reason: "long narrative block"
3194
- };
3195
- }
3196
- 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 {
3197
3224
  matched: true,
3198
3225
  reason: "multi-line narrative prose"
3199
3226
  };
@@ -3635,7 +3662,14 @@ const JS_EXTS$1 = new Set([
3635
3662
  ".mjs",
3636
3663
  ".cjs"
3637
3664
  ]);
3638
- 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
+ };
3639
3673
  const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
3640
3674
  const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
3641
3675
  const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
@@ -3685,14 +3719,15 @@ const detectJsSilentRecovery = (content, relPath) => {
3685
3719
  const body = extractCatchBody(content, match.index + match[0].length - 1);
3686
3720
  if (body === null) continue;
3687
3721
  if (!isLogOnlyBody(body)) continue;
3722
+ if (!recoveryDropsError(match[1], body)) continue;
3688
3723
  const line = content.slice(0, match.index).split("\n").length;
3689
3724
  out.push({
3690
3725
  filePath: relPath,
3691
3726
  engine: "ai-slop",
3692
3727
  rule: "ai-slop/silent-recovery",
3693
3728
  severity: "warning",
3694
- message: "Catch only logs then continues, leaving execution in a possibly broken state",
3695
- 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.",
3696
3731
  line,
3697
3732
  column: 0,
3698
3733
  category: "AI Slop",
@@ -3702,6 +3737,7 @@ const detectJsSilentRecovery = (content, relPath) => {
3702
3737
  return out;
3703
3738
  };
3704
3739
  const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
3740
+ const PY_EXCEPT_BINDING_RE = /\bas\s+(\w+)\s*:/;
3705
3741
  const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
3706
3742
  const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
3707
3743
  const detectPySilentRecovery = (content, relPath) => {
@@ -3725,13 +3761,14 @@ const detectPySilentRecovery = (content, relPath) => {
3725
3761
  const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
3726
3762
  const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
3727
3763
  if (!allLogs || !sawLog) continue;
3764
+ if (!recoveryDropsError(PY_EXCEPT_BINDING_RE.exec(lines[i])?.[1], bodyLines.join(" "))) continue;
3728
3765
  out.push({
3729
3766
  filePath: relPath,
3730
3767
  engine: "ai-slop",
3731
3768
  rule: "ai-slop/silent-recovery",
3732
3769
  severity: "warning",
3733
- message: "except only logs then continues, leaving execution in a possibly broken state",
3734
- 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.",
3735
3772
  line: i + 1,
3736
3773
  column: 0,
3737
3774
  category: "AI Slop",
@@ -3834,10 +3871,11 @@ const extractPyImportedSymbols = (lines) => {
3834
3871
  const importLines = /* @__PURE__ */ new Set();
3835
3872
  for (let i = 0; i < lines.length; i++) {
3836
3873
  const trimmed = lines[i].trim();
3837
- const fromMatch = trimmed.match(/^from\s+[\w.]+\s+import\s+(.+)/);
3874
+ const fromMatch = trimmed.match(/^from\s+([\w.]+)\s+import\s+(.+)/);
3838
3875
  if (fromMatch) {
3839
3876
  importLines.add(i);
3840
- const importPart = fromMatch[1].replace(/#.*$/, "").trim();
3877
+ if (fromMatch[1] === "__future__") continue;
3878
+ const importPart = fromMatch[2].replace(/#.*$/, "").trim();
3841
3879
  if (importPart === "*") continue;
3842
3880
  const cleaned = importPart.replace(/[()]/g, "");
3843
3881
  for (const item of cleaned.split(",")) {
@@ -7159,6 +7197,13 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
7159
7197
  //#endregion
7160
7198
  //#region src/scoring/index.ts
7161
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;
7162
7207
  const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
7163
7208
  if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
7164
7209
  const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
@@ -7173,7 +7218,8 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
7173
7218
  for (const d of diagnostics) {
7174
7219
  const engineWeight = weights[d.engine] ?? 1;
7175
7220
  const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
7176
- deductions += severityPenalty * engineWeight;
7221
+ const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
7222
+ deductions += severityPenalty * engineWeight * styleFactor;
7177
7223
  }
7178
7224
  const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
7179
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-CPpO6jbj.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
  };
@@ -2192,6 +2197,30 @@ const LOOPBACK_HOSTS = new Set([
2192
2197
  "0.0.0.0",
2193
2198
  "::1"
2194
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}`));
2195
2224
  const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
2196
2225
  const HARDCODED_URL_FINDING = {
2197
2226
  rule: "ai-slop/hardcoded-url",
@@ -2236,6 +2265,7 @@ const shouldFlagUrlLiteral = (line, urlText) => {
2236
2265
  if (!host) return false;
2237
2266
  if (PLACEHOLDER_HOSTS.has(host)) return false;
2238
2267
  if (LOOPBACK_HOSTS.has(host)) return false;
2268
+ if (isVendorApiHost(host)) return false;
2239
2269
  if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
2240
2270
  return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
2241
2271
  };
@@ -2420,7 +2450,9 @@ const PYTHON_STDLIB = new Set([
2420
2450
  "builtins",
2421
2451
  "bz2",
2422
2452
  "calendar",
2453
+ "code",
2423
2454
  "codecs",
2455
+ "codeop",
2424
2456
  "collections",
2425
2457
  "concurrent",
2426
2458
  "configparser",
@@ -2493,6 +2525,7 @@ const PYTHON_STDLIB = new Set([
2493
2525
  "readline",
2494
2526
  "reprlib",
2495
2527
  "resource",
2528
+ "rlcompleter",
2496
2529
  "secrets",
2497
2530
  "select",
2498
2531
  "selectors",
@@ -2681,6 +2714,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
2681
2714
  }
2682
2715
  const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
2683
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]);
2684
2719
  const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
2685
2720
  let match = poetryRe.exec(content);
2686
2721
  while (match !== null) {
@@ -2913,9 +2948,28 @@ const extractJsImports = (content) => {
2913
2948
  const extractPyImports = (content) => {
2914
2949
  const lines = content.split("\n");
2915
2950
  const results = [];
2951
+ let inDoc = null;
2952
+ let typeCheckIndent = -1;
2916
2953
  for (let i = 0; i < lines.length; i++) {
2917
- const line = lines[i].trim();
2918
- 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;
2919
2973
  const fromMatch = line.match(/^from\s+([\w.]+)\s+import\b/);
2920
2974
  if (fromMatch && !fromMatch[1].startsWith(".")) {
2921
2975
  results.push({
@@ -2925,8 +2979,8 @@ const extractPyImports = (content) => {
2925
2979
  continue;
2926
2980
  }
2927
2981
  const importMatch = line.match(/^import\s+([\w.,\s]+?)(?:\s+as\s+\w+)?\s*$/);
2928
- if (importMatch) for (const raw of importMatch[1].split(",")) {
2929
- 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];
2930
2984
  if (cleaned && !cleaned.startsWith(".")) results.push({
2931
2985
  spec: cleaned,
2932
2986
  line: i + 1
@@ -2983,6 +3037,7 @@ const detectHallucinatedImports = async (context) => {
2983
3037
  continue;
2984
3038
  }
2985
3039
  const relPath = path.relative(context.rootDirectory, filePath);
3040
+ if (isNonProductionPath(relPath)) continue;
2986
3041
  const imports = isJs ? extractJsImports(content) : extractPyImports(content);
2987
3042
  for (const { spec, line } of imports) {
2988
3043
  const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
@@ -3110,7 +3165,7 @@ const collectBlocks = (sourceLines, syntax) => {
3110
3165
  //#endregion
3111
3166
  //#region src/engines/ai-slop/meta-comment.ts
3112
3167
  const PLAN_REFERENCE_RES = [
3113
- /\b(?:stage|step|phase)\s+\d+\b(?!\s*[:.]?\s*(?:bytes|ms|seconds|of\s+\d))/i,
3168
+ /^(?:stage|step|phase)\s+\d+\s*[:.\-–—]/i,
3114
3169
  /\bstep\s+\d+\s+of\s+the\s+plan\b/i,
3115
3170
  /\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
3116
3171
  /\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
@@ -3212,24 +3267,6 @@ const looksLikeLicenseHeader = (block) => {
3212
3267
  const text = block.rawLines.join(" ").toLowerCase();
3213
3268
  return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
3214
3269
  };
3215
- const BARE_LABEL_RE = /^[A-Z][A-Za-z0-9 ]{1,28}$/;
3216
- const isBareSectionLabel = (prose) => {
3217
- if (!BARE_LABEL_RE.test(prose)) return false;
3218
- if (prose.endsWith(".")) return false;
3219
- if (prose.split(/\s+/).length > 3) return false;
3220
- if (STEP_COMMENT_VERB_RE.test(prose)) return false;
3221
- return true;
3222
- };
3223
- const DATA_ENTRY_START = /^\s*(?:\{|\[|["'`]|\d|\w+:\s|case\s)/;
3224
- const nextLineLooksLikeDataEntry = (nextLine) => {
3225
- if (nextLine === null) return false;
3226
- if (!DATA_ENTRY_START.test(nextLine)) return false;
3227
- const trimmed = nextLine.trim();
3228
- if (trimmed.startsWith("case ")) return true;
3229
- if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("\"") || trimmed.startsWith("'") || trimmed.startsWith("`")) return true;
3230
- if (/^\w+\s*:/.test(trimmed)) return true;
3231
- return false;
3232
- };
3233
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));
3234
3271
  const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
3235
3272
  const GO_FIELD_LEAD_RE = /^(\w+)\s+/;
@@ -3318,10 +3355,6 @@ const detectNarrativeInBlock = (block, ext) => {
3318
3355
  matched: true,
3319
3356
  reason: "phase/section header"
3320
3357
  };
3321
- if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine) && !looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
3322
- matched: true,
3323
- reason: "bare section label"
3324
- };
3325
3358
  const joined = block.prose.join(" ");
3326
3359
  const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
3327
3360
  if (hasWhyMarker || hasDocIndicator(block)) return {
@@ -3352,17 +3385,11 @@ const detectNarrativeInBlock = (block, ext) => {
3352
3385
  };
3353
3386
  const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
3354
3387
  const isAboveDeclaration = looksLikeDeclarationPreamble(block.nextNonBlankLine, ext);
3355
- if (nonEmptyProseCount >= 5) {
3356
- if (isAboveDeclaration) return {
3357
- matched: false,
3358
- reason: ""
3359
- };
3360
- return {
3361
- matched: true,
3362
- reason: "long narrative block"
3363
- };
3364
- }
3365
- 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 {
3366
3393
  matched: true,
3367
3394
  reason: "multi-line narrative prose"
3368
3395
  };
@@ -3804,7 +3831,14 @@ const JS_EXTS$1 = new Set([
3804
3831
  ".mjs",
3805
3832
  ".cjs"
3806
3833
  ]);
3807
- 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
+ };
3808
3842
  const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
3809
3843
  const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
3810
3844
  const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
@@ -3854,14 +3888,15 @@ const detectJsSilentRecovery = (content, relPath) => {
3854
3888
  const body = extractCatchBody(content, match.index + match[0].length - 1);
3855
3889
  if (body === null) continue;
3856
3890
  if (!isLogOnlyBody(body)) continue;
3891
+ if (!recoveryDropsError(match[1], body)) continue;
3857
3892
  const line = content.slice(0, match.index).split("\n").length;
3858
3893
  out.push({
3859
3894
  filePath: relPath,
3860
3895
  engine: "ai-slop",
3861
3896
  rule: "ai-slop/silent-recovery",
3862
3897
  severity: "warning",
3863
- message: "Catch only logs then continues, leaving execution in a possibly broken state",
3864
- 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.",
3865
3900
  line,
3866
3901
  column: 0,
3867
3902
  category: "AI Slop",
@@ -3871,6 +3906,7 @@ const detectJsSilentRecovery = (content, relPath) => {
3871
3906
  return out;
3872
3907
  };
3873
3908
  const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
3909
+ const PY_EXCEPT_BINDING_RE = /\bas\s+(\w+)\s*:/;
3874
3910
  const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
3875
3911
  const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
3876
3912
  const detectPySilentRecovery = (content, relPath) => {
@@ -3894,13 +3930,14 @@ const detectPySilentRecovery = (content, relPath) => {
3894
3930
  const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
3895
3931
  const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
3896
3932
  if (!allLogs || !sawLog) continue;
3933
+ if (!recoveryDropsError(PY_EXCEPT_BINDING_RE.exec(lines[i])?.[1], bodyLines.join(" "))) continue;
3897
3934
  out.push({
3898
3935
  filePath: relPath,
3899
3936
  engine: "ai-slop",
3900
3937
  rule: "ai-slop/silent-recovery",
3901
3938
  severity: "warning",
3902
- message: "except only logs then continues, leaving execution in a possibly broken state",
3903
- 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.",
3904
3941
  line: i + 1,
3905
3942
  column: 0,
3906
3943
  category: "AI Slop",
@@ -4003,10 +4040,11 @@ const extractPyImportedSymbols = (lines) => {
4003
4040
  const importLines = /* @__PURE__ */ new Set();
4004
4041
  for (let i = 0; i < lines.length; i++) {
4005
4042
  const trimmed = lines[i].trim();
4006
- const fromMatch = trimmed.match(/^from\s+[\w.]+\s+import\s+(.+)/);
4043
+ const fromMatch = trimmed.match(/^from\s+([\w.]+)\s+import\s+(.+)/);
4007
4044
  if (fromMatch) {
4008
4045
  importLines.add(i);
4009
- const importPart = fromMatch[1].replace(/#.*$/, "").trim();
4046
+ if (fromMatch[1] === "__future__") continue;
4047
+ const importPart = fromMatch[2].replace(/#.*$/, "").trim();
4010
4048
  if (importPart === "*") continue;
4011
4049
  const cleaned = importPart.replace(/[()]/g, "");
4012
4050
  for (const item of cleaned.split(",")) {
@@ -7118,6 +7156,13 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
7118
7156
  //#endregion
7119
7157
  //#region src/scoring/index.ts
7120
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;
7121
7166
  const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
7122
7167
  if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
7123
7168
  const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
@@ -7132,7 +7177,8 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
7132
7177
  for (const d of diagnostics) {
7133
7178
  const engineWeight = weights[d.engine] ?? 1;
7134
7179
  const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
7135
- deductions += severityPenalty * engineWeight;
7180
+ const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
7181
+ deductions += severityPenalty * engineWeight * styleFactor;
7136
7182
  }
7137
7183
  const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
7138
7184
  const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
@@ -8313,12 +8359,12 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
8313
8359
  engineTimings
8314
8360
  };
8315
8361
  if (options.sarif) {
8316
- const { buildSarifLog } = await import("./sarif-CLVijBAO.js");
8362
+ const { buildSarifLog } = await import("./sarif-BtSQ92c6.js");
8317
8363
  console.log(JSON.stringify(buildSarifLog(results), null, 2));
8318
8364
  return completion;
8319
8365
  }
8320
8366
  if (options.json) {
8321
- const { buildJsonOutput } = await import("./json-CxiErSgX.js");
8367
+ const { buildJsonOutput } = await import("./json-DaFOYHcf.js");
8322
8368
  const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
8323
8369
  console.log(JSON.stringify(jsonOut, null, 2));
8324
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-CPpO6jbj.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
  };
@@ -1450,6 +1455,30 @@ const LOOPBACK_HOSTS = new Set([
1450
1455
  "0.0.0.0",
1451
1456
  "::1"
1452
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}`));
1453
1482
  const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
1454
1483
  const HARDCODED_URL_FINDING = {
1455
1484
  rule: "ai-slop/hardcoded-url",
@@ -1494,6 +1523,7 @@ const shouldFlagUrlLiteral = (line, urlText) => {
1494
1523
  if (!host) return false;
1495
1524
  if (PLACEHOLDER_HOSTS.has(host)) return false;
1496
1525
  if (LOOPBACK_HOSTS.has(host)) return false;
1526
+ if (isVendorApiHost(host)) return false;
1497
1527
  if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
1498
1528
  return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
1499
1529
  };
@@ -1678,7 +1708,9 @@ const PYTHON_STDLIB = new Set([
1678
1708
  "builtins",
1679
1709
  "bz2",
1680
1710
  "calendar",
1711
+ "code",
1681
1712
  "codecs",
1713
+ "codeop",
1682
1714
  "collections",
1683
1715
  "concurrent",
1684
1716
  "configparser",
@@ -1751,6 +1783,7 @@ const PYTHON_STDLIB = new Set([
1751
1783
  "readline",
1752
1784
  "reprlib",
1753
1785
  "resource",
1786
+ "rlcompleter",
1754
1787
  "secrets",
1755
1788
  "select",
1756
1789
  "selectors",
@@ -1939,6 +1972,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
1939
1972
  }
1940
1973
  const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
1941
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]);
1942
1977
  const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
1943
1978
  let match = poetryRe.exec(content);
1944
1979
  while (match !== null) {
@@ -2171,9 +2206,28 @@ const extractJsImports = (content) => {
2171
2206
  const extractPyImports = (content) => {
2172
2207
  const lines = content.split("\n");
2173
2208
  const results = [];
2209
+ let inDoc = null;
2210
+ let typeCheckIndent = -1;
2174
2211
  for (let i = 0; i < lines.length; i++) {
2175
- const line = lines[i].trim();
2176
- 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;
2177
2231
  const fromMatch = line.match(/^from\s+([\w.]+)\s+import\b/);
2178
2232
  if (fromMatch && !fromMatch[1].startsWith(".")) {
2179
2233
  results.push({
@@ -2183,8 +2237,8 @@ const extractPyImports = (content) => {
2183
2237
  continue;
2184
2238
  }
2185
2239
  const importMatch = line.match(/^import\s+([\w.,\s]+?)(?:\s+as\s+\w+)?\s*$/);
2186
- if (importMatch) for (const raw of importMatch[1].split(",")) {
2187
- 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];
2188
2242
  if (cleaned && !cleaned.startsWith(".")) results.push({
2189
2243
  spec: cleaned,
2190
2244
  line: i + 1
@@ -2241,6 +2295,7 @@ const detectHallucinatedImports = async (context) => {
2241
2295
  continue;
2242
2296
  }
2243
2297
  const relPath = path.relative(context.rootDirectory, filePath);
2298
+ if (isNonProductionPath(relPath)) continue;
2244
2299
  const imports = isJs ? extractJsImports(content) : extractPyImports(content);
2245
2300
  for (const { spec, line } of imports) {
2246
2301
  const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
@@ -2368,7 +2423,7 @@ const collectBlocks = (sourceLines, syntax) => {
2368
2423
  //#endregion
2369
2424
  //#region src/engines/ai-slop/meta-comment.ts
2370
2425
  const PLAN_REFERENCE_RES = [
2371
- /\b(?:stage|step|phase)\s+\d+\b(?!\s*[:.]?\s*(?:bytes|ms|seconds|of\s+\d))/i,
2426
+ /^(?:stage|step|phase)\s+\d+\s*[:.\-–—]/i,
2372
2427
  /\bstep\s+\d+\s+of\s+the\s+plan\b/i,
2373
2428
  /\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
2374
2429
  /\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
@@ -2470,24 +2525,6 @@ const looksLikeLicenseHeader = (block) => {
2470
2525
  const text = block.rawLines.join(" ").toLowerCase();
2471
2526
  return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
2472
2527
  };
2473
- const BARE_LABEL_RE = /^[A-Z][A-Za-z0-9 ]{1,28}$/;
2474
- const isBareSectionLabel = (prose) => {
2475
- if (!BARE_LABEL_RE.test(prose)) return false;
2476
- if (prose.endsWith(".")) return false;
2477
- if (prose.split(/\s+/).length > 3) return false;
2478
- if (STEP_COMMENT_VERB_RE.test(prose)) return false;
2479
- return true;
2480
- };
2481
- const DATA_ENTRY_START = /^\s*(?:\{|\[|["'`]|\d|\w+:\s|case\s)/;
2482
- const nextLineLooksLikeDataEntry = (nextLine) => {
2483
- if (nextLine === null) return false;
2484
- if (!DATA_ENTRY_START.test(nextLine)) return false;
2485
- const trimmed = nextLine.trim();
2486
- if (trimmed.startsWith("case ")) return true;
2487
- if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("\"") || trimmed.startsWith("'") || trimmed.startsWith("`")) return true;
2488
- if (/^\w+\s*:/.test(trimmed)) return true;
2489
- return false;
2490
- };
2491
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));
2492
2529
  const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
2493
2530
  const GO_FIELD_LEAD_RE = /^(\w+)\s+/;
@@ -2576,10 +2613,6 @@ const detectNarrativeInBlock = (block, ext) => {
2576
2613
  matched: true,
2577
2614
  reason: "phase/section header"
2578
2615
  };
2579
- if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine) && !looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
2580
- matched: true,
2581
- reason: "bare section label"
2582
- };
2583
2616
  const joined = block.prose.join(" ");
2584
2617
  const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
2585
2618
  if (hasWhyMarker || hasDocIndicator(block)) return {
@@ -2610,17 +2643,11 @@ const detectNarrativeInBlock = (block, ext) => {
2610
2643
  };
2611
2644
  const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
2612
2645
  const isAboveDeclaration = looksLikeDeclarationPreamble(block.nextNonBlankLine, ext);
2613
- if (nonEmptyProseCount >= 5) {
2614
- if (isAboveDeclaration) return {
2615
- matched: false,
2616
- reason: ""
2617
- };
2618
- return {
2619
- matched: true,
2620
- reason: "long narrative block"
2621
- };
2622
- }
2623
- 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 {
2624
2651
  matched: true,
2625
2652
  reason: "multi-line narrative prose"
2626
2653
  };
@@ -3062,7 +3089,14 @@ const JS_EXTS$1 = new Set([
3062
3089
  ".mjs",
3063
3090
  ".cjs"
3064
3091
  ]);
3065
- 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
+ };
3066
3100
  const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
3067
3101
  const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
3068
3102
  const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
@@ -3112,14 +3146,15 @@ const detectJsSilentRecovery = (content, relPath) => {
3112
3146
  const body = extractCatchBody(content, match.index + match[0].length - 1);
3113
3147
  if (body === null) continue;
3114
3148
  if (!isLogOnlyBody(body)) continue;
3149
+ if (!recoveryDropsError(match[1], body)) continue;
3115
3150
  const line = content.slice(0, match.index).split("\n").length;
3116
3151
  out.push({
3117
3152
  filePath: relPath,
3118
3153
  engine: "ai-slop",
3119
3154
  rule: "ai-slop/silent-recovery",
3120
3155
  severity: "warning",
3121
- message: "Catch only logs then continues, leaving execution in a possibly broken state",
3122
- 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.",
3123
3158
  line,
3124
3159
  column: 0,
3125
3160
  category: "AI Slop",
@@ -3129,6 +3164,7 @@ const detectJsSilentRecovery = (content, relPath) => {
3129
3164
  return out;
3130
3165
  };
3131
3166
  const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
3167
+ const PY_EXCEPT_BINDING_RE = /\bas\s+(\w+)\s*:/;
3132
3168
  const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
3133
3169
  const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
3134
3170
  const detectPySilentRecovery = (content, relPath) => {
@@ -3152,13 +3188,14 @@ const detectPySilentRecovery = (content, relPath) => {
3152
3188
  const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
3153
3189
  const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
3154
3190
  if (!allLogs || !sawLog) continue;
3191
+ if (!recoveryDropsError(PY_EXCEPT_BINDING_RE.exec(lines[i])?.[1], bodyLines.join(" "))) continue;
3155
3192
  out.push({
3156
3193
  filePath: relPath,
3157
3194
  engine: "ai-slop",
3158
3195
  rule: "ai-slop/silent-recovery",
3159
3196
  severity: "warning",
3160
- message: "except only logs then continues, leaving execution in a possibly broken state",
3161
- 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.",
3162
3199
  line: i + 1,
3163
3200
  column: 0,
3164
3201
  category: "AI Slop",
@@ -3260,10 +3297,11 @@ const extractPyImportedSymbols = (lines) => {
3260
3297
  const importLines = /* @__PURE__ */ new Set();
3261
3298
  for (let i = 0; i < lines.length; i++) {
3262
3299
  const trimmed = lines[i].trim();
3263
- const fromMatch = trimmed.match(/^from\s+[\w.]+\s+import\s+(.+)/);
3300
+ const fromMatch = trimmed.match(/^from\s+([\w.]+)\s+import\s+(.+)/);
3264
3301
  if (fromMatch) {
3265
3302
  importLines.add(i);
3266
- const importPart = fromMatch[1].replace(/#.*$/, "").trim();
3303
+ if (fromMatch[1] === "__future__") continue;
3304
+ const importPart = fromMatch[2].replace(/#.*$/, "").trim();
3267
3305
  if (importPart === "*") continue;
3268
3306
  const cleaned = importPart.replace(/[()]/g, "");
3269
3307
  for (const item of cleaned.split(",")) {
@@ -6087,6 +6125,13 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
6087
6125
  //#endregion
6088
6126
  //#region src/scoring/index.ts
6089
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;
6090
6135
  const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
6091
6136
  if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
6092
6137
  const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
@@ -6101,7 +6146,8 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
6101
6146
  for (const d of diagnostics) {
6102
6147
  const engineWeight = weights[d.engine] ?? 1;
6103
6148
  const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
6104
- deductions += severityPenalty * engineWeight;
6149
+ const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
6150
+ deductions += severityPenalty * engineWeight * styleFactor;
6105
6151
  }
6106
6152
  const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
6107
6153
  const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
@@ -6468,7 +6514,7 @@ const handleAislopBaseline = (input) => {
6468
6514
 
6469
6515
  //#endregion
6470
6516
  //#region src/version.ts
6471
- const APP_VERSION = "0.9.6";
6517
+ const APP_VERSION = "0.10.0";
6472
6518
 
6473
6519
  //#endregion
6474
6520
  //#region src/telemetry/env.ts
@@ -1,4 +1,4 @@
1
- import { t as APP_VERSION } from "./version-CPpO6jbj.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.6",
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.6";
3
-
4
- //#endregion
5
- export { APP_VERSION as t };