aislop 0.9.3 → 0.9.4

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # aislop
2
2
 
3
- **The engineering standards layer and quality gate for AI-written code.**
3
+ **Catch the slop AI coding agents leave in your code.**
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/aislop.svg)](https://www.npmjs.com/package/aislop)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/aislop.svg)](https://www.npmjs.com/package/aislop)
@@ -9,7 +9,9 @@
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
10
10
  [![Node >= 20](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](https://nodejs.org)
11
11
 
12
- Catches the slop AI agents leave behind: dead code, oversized functions and files, unused imports, `as any` casts, swallowed errors, hallucinated imports, todo stubs, narrative comments. Scores 0–100. Deterministic (regex + AST, no LLMs). 8+ languages.
12
+ The patterns Claude Code, Cursor, Codex, and OpenCode leave behind: narrative comments above self-explanatory code, swallowed exceptions, `as any` casts, hallucinated imports, duplicated helpers, dead code, todo stubs, oversized functions. Tests pass. Lint passes. The code rots anyway.
13
+
14
+ aislop catches them. 40+ rules across 7 languages (TS/JS, Python, Go, Rust, Ruby, PHP, Java). Scores every change 0–100. Sub-second. Deterministic — no LLM in the runtime path, same code in, same score out. MIT-licensed, free CLI.
13
15
 
14
16
  ## Quick start
15
17
 
@@ -265,6 +267,10 @@ See the full [rules reference](docs/rules.md).
265
267
 
266
268
  [Installation](docs/installation.md) · [Commands](docs/commands.md) · [Rules](docs/rules.md) · [Config](docs/configuration.md) · [Scoring](docs/scoring.md) · [CI/CD](docs/ci.md) · [Telemetry](docs/telemetry.md)
267
269
 
270
+ ## Community
271
+
272
+ [Discussions](https://github.com/scanaislop/aislop/discussions) for questions, rule requests, and false-positive triage · [Issues](https://github.com/scanaislop/aislop/issues) for bugs
273
+
268
274
  ## Contributing
269
275
 
270
276
  See [CONTRIBUTING.md](CONTRIBUTING.md). AI assistants: [AGENTS.md](AGENTS.md).
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.3";
37
+ const APP_VERSION = "0.9.4";
38
38
 
39
39
  //#endregion
40
40
  //#region src/telemetry/env.ts
@@ -2763,6 +2763,11 @@ const BROAD_EXCEPT_RE = /^\s*except\s+(Exception|BaseException)\s*(?:as\s+\w+)?\
2763
2763
  const PRINT_RE = /^\s*print\s*\(/;
2764
2764
  const DEF_RE = /^\s*(?:async\s+)?def\s+\w+\s*\(/;
2765
2765
  const MUTABLE_DEFAULT_RE = /(\w+)\s*(?::\s*[^,)=]+)?\s*=\s*(\[\s*\]|\{\s*\}|set\(\s*\))/;
2766
+ const RANGE_LEN_LOOP_RE = /^\s*for\s+([A-Za-z_]\w*)\s+in\s+range\s*\(\s*len\s*\(\s*([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*\)\s*\)\s*:\s*(?:#.*)?$/;
2767
+ const CHAINED_DICT_GET_RE = /\.get\s*\([^)]*,\s*\{\s*\}\s*\)\s*\.get\s*\(/;
2768
+ const SAME_VALUE_BRANCH_RE = /^(\s*)(?:if|elif)\s+([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*==\s*["'][^"']+["']\s*:/;
2769
+ const INSTANCE_BRANCH_RE = /^(\s*)(?:if|elif)\s+isinstance\s*\(\s*([A-Za-z_]\w*)\s*,\s*[^)]+\)\s*:/;
2770
+ const BRANCH_LADDER_THRESHOLD = 4;
2766
2771
  const isTestFile$1 = (relPath, basename) => basename.startsWith("test_") || basename.endsWith("_test.py") || basename === "conftest.py" || relPath.split(path.sep).some((seg) => seg === "tests" || seg === "test");
2767
2772
  const isScriptOrEntrypoint = (basename) => basename === "__main__.py" || basename === "manage.py" || basename === "setup.py";
2768
2773
  const SCRIPT_DIR_NAMES = new Set([
@@ -2815,6 +2820,13 @@ const pushFinding = (out, a) => {
2815
2820
  fixable: false
2816
2821
  });
2817
2822
  };
2823
+ const pushLineFinding = (out, relPath, line, finding) => {
2824
+ pushFinding(out, {
2825
+ relPath,
2826
+ line,
2827
+ ...finding
2828
+ });
2829
+ };
2818
2830
  const flagBareExcept = (lines, relPath, out) => {
2819
2831
  for (let i = 0; i < lines.length; i++) {
2820
2832
  if (!BARE_EXCEPT_RE.test(lines[i])) continue;
@@ -2896,6 +2908,76 @@ const flagPrintInProduction = (lines, relPath, basename, out) => {
2896
2908
  });
2897
2909
  }
2898
2910
  };
2911
+ const flagRangeLenLoops = (lines, relPath, out) => {
2912
+ for (let i = 0; i < lines.length; i++) {
2913
+ const match = RANGE_LEN_LOOP_RE.exec(lines[i]);
2914
+ if (!match) continue;
2915
+ pushLineFinding(out, relPath, i + 1, {
2916
+ rule: "ai-slop/python-range-len-loop",
2917
+ severity: "info",
2918
+ message: `\`range(len(${match[2]}))\` loop — usually a hand-rolled iteration pattern.`,
2919
+ help: "Prefer direct iteration (`for item in items`) or `enumerate(items)` when the index is needed. Keeping index plumbing out of the loop reduces checkpoint-to-checkpoint bloat."
2920
+ });
2921
+ }
2922
+ };
2923
+ const flagChainedDictGets = (lines, relPath, out) => {
2924
+ for (let i = 0; i < lines.length; i++) {
2925
+ if (!CHAINED_DICT_GET_RE.test(lines[i])) continue;
2926
+ pushLineFinding(out, relPath, i + 1, {
2927
+ rule: "ai-slop/python-chained-dict-get",
2928
+ severity: "warning",
2929
+ message: "Chained `.get(..., {})` defaults hide missing-data cases.",
2930
+ help: "Normalize the input at the boundary, use a typed object, or split the lookup into explicit steps. Empty-dict fallback chains are a common agent shortcut that becomes brittle as schemas evolve."
2931
+ });
2932
+ }
2933
+ };
2934
+ const countBranchLadder = (lines, start, pattern, selector, indent) => {
2935
+ let count = 1;
2936
+ for (let i = start + 1; i < lines.length; i++) {
2937
+ const line = lines[i];
2938
+ const trimmed = line.trim();
2939
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
2940
+ const match = pattern.exec(line);
2941
+ if (match?.[1] === indent && match[2] === selector) {
2942
+ count++;
2943
+ continue;
2944
+ }
2945
+ if (line.startsWith(`${indent}elif `)) break;
2946
+ if (line.length - line.trimStart().length <= indent.length && !line.startsWith(`${indent}else`)) break;
2947
+ }
2948
+ return count;
2949
+ };
2950
+ const flagBranchLadders = (lines, relPath, out) => {
2951
+ const reported = /* @__PURE__ */ new Set();
2952
+ for (let i = 0; i < lines.length; i++) {
2953
+ if (reported.has(i)) continue;
2954
+ const valueMatch = SAME_VALUE_BRANCH_RE.exec(lines[i]);
2955
+ if (valueMatch) {
2956
+ const count = countBranchLadder(lines, i, SAME_VALUE_BRANCH_RE, valueMatch[2], valueMatch[1]);
2957
+ if (count >= BRANCH_LADDER_THRESHOLD) {
2958
+ reported.add(i);
2959
+ pushLineFinding(out, relPath, i + 1, {
2960
+ rule: "ai-slop/python-repetitive-dispatch",
2961
+ severity: "warning",
2962
+ message: `${count} repeated branches dispatch on \`${valueMatch[2]}\`.`,
2963
+ help: "Use a table, set membership, or handler map when branches share the same shape. SlopCodeBench highlights these selector ladders as code that keeps growing instead of absorbing new cases cleanly."
2964
+ });
2965
+ }
2966
+ continue;
2967
+ }
2968
+ const instanceMatch = INSTANCE_BRANCH_RE.exec(lines[i]);
2969
+ if (!instanceMatch) continue;
2970
+ const count = countBranchLadder(lines, i, INSTANCE_BRANCH_RE, instanceMatch[2], instanceMatch[1]);
2971
+ if (count < BRANCH_LADDER_THRESHOLD) continue;
2972
+ reported.add(i);
2973
+ pushLineFinding(out, relPath, i + 1, {
2974
+ rule: "ai-slop/python-isinstance-ladder",
2975
+ severity: "warning",
2976
+ message: `${count} repeated \`isinstance(${instanceMatch[2]}, ...)\` branches.`,
2977
+ help: "Prefer a handler map, protocol, or normalized intermediate representation when each type branch has the same role. Repeated type ladders are one of the maintainability smells SCBench-style checks look for."
2978
+ });
2979
+ }
2980
+ };
2899
2981
  const detectPythonPatterns = async (context) => {
2900
2982
  const diagnostics = [];
2901
2983
  const files = getSourceFiles(context);
@@ -2915,6 +2997,9 @@ const detectPythonPatterns = async (context) => {
2915
2997
  flagBroadExceptWithSilentBody(lines, relPath, diagnostics);
2916
2998
  flagMutableDefaults(lines, relPath, diagnostics);
2917
2999
  flagPrintInProduction(lines, relPath, basename, diagnostics);
3000
+ flagRangeLenLoops(lines, relPath, diagnostics);
3001
+ flagChainedDictGets(lines, relPath, diagnostics);
3002
+ flagBranchLadders(lines, relPath, diagnostics);
2918
3003
  }
2919
3004
  return diagnostics;
2920
3005
  };
@@ -8717,6 +8802,10 @@ const RULE_LABELS = {
8717
8802
  "ai-slop/python-broad-except": "Broad except",
8718
8803
  "ai-slop/python-mutable-default": "Mutable default argument",
8719
8804
  "ai-slop/python-print-debug": "print() left in code",
8805
+ "ai-slop/python-range-len-loop": "range(len(...)) loop",
8806
+ "ai-slop/python-chained-dict-get": "Chained dict get",
8807
+ "ai-slop/python-repetitive-dispatch": "Repetitive dispatch ladder",
8808
+ "ai-slop/python-isinstance-ladder": "isinstance ladder",
8720
8809
  "ai-slop/go-library-panic": "panic() in Go library code",
8721
8810
  "ai-slop/rust-non-test-unwrap": "Rust .unwrap() in production code",
8722
8811
  "ai-slop/rust-todo-stub": "Rust todo!() stub",
@@ -8820,6 +8909,9 @@ const renderSummary = (input, deps = {}) => {
8820
8909
  }
8821
8910
  return lines.join("\n");
8822
8911
  };
8912
+ const renderStarCta = (deps = {}) => {
8913
+ return `\n ${style(deps.theme ?? theme, "muted", "★ Found this useful? Star us at github.com/scanaislop/aislop")}\n`;
8914
+ };
8823
8915
  const renderCleanRun = (input, deps = {}) => {
8824
8916
  const t = deps.theme ?? theme;
8825
8917
  const s = deps.symbols ?? symbols;
@@ -8889,11 +8981,12 @@ const buildScanRender = (input) => {
8889
8981
  const warnings = input.diagnostics.filter((d) => d.severity === "warning").length;
8890
8982
  const fixable = input.diagnostics.filter((d) => d.fixable).length;
8891
8983
  const hasVulnerableDeps = input.diagnostics.some((d) => d.rule === "security/vulnerable-dependency");
8984
+ const starCta = input.printBrand !== false ? renderStarCta(deps) : "";
8892
8985
  if (input.diagnostics.length === 0 && input.score.score === 100) return `${header}${renderCleanRun({
8893
8986
  score: input.score.score,
8894
8987
  label: input.score.label,
8895
8988
  elapsedMs: input.elapsedMs
8896
- }, deps)}`;
8989
+ }, deps)}${starCta}`;
8897
8990
  const diagBlock = input.diagnostics.length === 0 ? "" : renderDiagnostics(input.diagnostics, input.verbose);
8898
8991
  const nextSteps = [];
8899
8992
  if (fixable > 0) nextSteps.push({
@@ -8920,7 +9013,7 @@ const buildScanRender = (input) => {
8920
9013
  nextSteps,
8921
9014
  breakdown: computeBreakdown(input.diagnostics),
8922
9015
  thresholds: input.thresholds
8923
- }, deps)}`;
9016
+ }, deps)}${starCta}`;
8924
9017
  };
8925
9018
  const scanCommand = async (directory, config, options) => {
8926
9019
  const resolvedDir = path.resolve(directory);
@@ -11459,6 +11552,10 @@ const BUILTIN_RULES = [
11459
11552
  "ai-slop/python-broad-except",
11460
11553
  "ai-slop/python-mutable-default",
11461
11554
  "ai-slop/python-print-debug",
11555
+ "ai-slop/python-range-len-loop",
11556
+ "ai-slop/python-chained-dict-get",
11557
+ "ai-slop/python-repetitive-dispatch",
11558
+ "ai-slop/python-isinstance-ladder",
11462
11559
  "ai-slop/go-library-panic",
11463
11560
  "ai-slop/rust-non-test-unwrap",
11464
11561
  "ai-slop/rust-todo-stub",
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-BNO_Lw7E.js";
1
+ import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-C45P3Q1N.js";
2
2
  import { n as runSubprocess, t as isToolInstalled } from "./subprocess-CQUJDGgn.js";
3
3
  import { r as runGenericLinter, t as fixRubyLint } from "./generic-BrcWMW7E.js";
4
4
  import { n as runExpoDoctor } from "./expo-doctor-Bz0LZhQ6.js";
@@ -2931,6 +2931,11 @@ const BROAD_EXCEPT_RE = /^\s*except\s+(Exception|BaseException)\s*(?:as\s+\w+)?\
2931
2931
  const PRINT_RE = /^\s*print\s*\(/;
2932
2932
  const DEF_RE = /^\s*(?:async\s+)?def\s+\w+\s*\(/;
2933
2933
  const MUTABLE_DEFAULT_RE = /(\w+)\s*(?::\s*[^,)=]+)?\s*=\s*(\[\s*\]|\{\s*\}|set\(\s*\))/;
2934
+ const RANGE_LEN_LOOP_RE = /^\s*for\s+([A-Za-z_]\w*)\s+in\s+range\s*\(\s*len\s*\(\s*([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*\)\s*\)\s*:\s*(?:#.*)?$/;
2935
+ const CHAINED_DICT_GET_RE = /\.get\s*\([^)]*,\s*\{\s*\}\s*\)\s*\.get\s*\(/;
2936
+ const SAME_VALUE_BRANCH_RE = /^(\s*)(?:if|elif)\s+([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*==\s*["'][^"']+["']\s*:/;
2937
+ const INSTANCE_BRANCH_RE = /^(\s*)(?:if|elif)\s+isinstance\s*\(\s*([A-Za-z_]\w*)\s*,\s*[^)]+\)\s*:/;
2938
+ const BRANCH_LADDER_THRESHOLD = 4;
2934
2939
  const isTestFile$1 = (relPath, basename) => basename.startsWith("test_") || basename.endsWith("_test.py") || basename === "conftest.py" || relPath.split(path.sep).some((seg) => seg === "tests" || seg === "test");
2935
2940
  const isScriptOrEntrypoint = (basename) => basename === "__main__.py" || basename === "manage.py" || basename === "setup.py";
2936
2941
  const SCRIPT_DIR_NAMES = new Set([
@@ -2983,6 +2988,13 @@ const pushFinding = (out, a) => {
2983
2988
  fixable: false
2984
2989
  });
2985
2990
  };
2991
+ const pushLineFinding = (out, relPath, line, finding) => {
2992
+ pushFinding(out, {
2993
+ relPath,
2994
+ line,
2995
+ ...finding
2996
+ });
2997
+ };
2986
2998
  const flagBareExcept = (lines, relPath, out) => {
2987
2999
  for (let i = 0; i < lines.length; i++) {
2988
3000
  if (!BARE_EXCEPT_RE.test(lines[i])) continue;
@@ -3064,6 +3076,76 @@ const flagPrintInProduction = (lines, relPath, basename, out) => {
3064
3076
  });
3065
3077
  }
3066
3078
  };
3079
+ const flagRangeLenLoops = (lines, relPath, out) => {
3080
+ for (let i = 0; i < lines.length; i++) {
3081
+ const match = RANGE_LEN_LOOP_RE.exec(lines[i]);
3082
+ if (!match) continue;
3083
+ pushLineFinding(out, relPath, i + 1, {
3084
+ rule: "ai-slop/python-range-len-loop",
3085
+ severity: "info",
3086
+ message: `\`range(len(${match[2]}))\` loop — usually a hand-rolled iteration pattern.`,
3087
+ help: "Prefer direct iteration (`for item in items`) or `enumerate(items)` when the index is needed. Keeping index plumbing out of the loop reduces checkpoint-to-checkpoint bloat."
3088
+ });
3089
+ }
3090
+ };
3091
+ const flagChainedDictGets = (lines, relPath, out) => {
3092
+ for (let i = 0; i < lines.length; i++) {
3093
+ if (!CHAINED_DICT_GET_RE.test(lines[i])) continue;
3094
+ pushLineFinding(out, relPath, i + 1, {
3095
+ rule: "ai-slop/python-chained-dict-get",
3096
+ severity: "warning",
3097
+ message: "Chained `.get(..., {})` defaults hide missing-data cases.",
3098
+ help: "Normalize the input at the boundary, use a typed object, or split the lookup into explicit steps. Empty-dict fallback chains are a common agent shortcut that becomes brittle as schemas evolve."
3099
+ });
3100
+ }
3101
+ };
3102
+ const countBranchLadder = (lines, start, pattern, selector, indent) => {
3103
+ let count = 1;
3104
+ for (let i = start + 1; i < lines.length; i++) {
3105
+ const line = lines[i];
3106
+ const trimmed = line.trim();
3107
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
3108
+ const match = pattern.exec(line);
3109
+ if (match?.[1] === indent && match[2] === selector) {
3110
+ count++;
3111
+ continue;
3112
+ }
3113
+ if (line.startsWith(`${indent}elif `)) break;
3114
+ if (line.length - line.trimStart().length <= indent.length && !line.startsWith(`${indent}else`)) break;
3115
+ }
3116
+ return count;
3117
+ };
3118
+ const flagBranchLadders = (lines, relPath, out) => {
3119
+ const reported = /* @__PURE__ */ new Set();
3120
+ for (let i = 0; i < lines.length; i++) {
3121
+ if (reported.has(i)) continue;
3122
+ const valueMatch = SAME_VALUE_BRANCH_RE.exec(lines[i]);
3123
+ if (valueMatch) {
3124
+ const count = countBranchLadder(lines, i, SAME_VALUE_BRANCH_RE, valueMatch[2], valueMatch[1]);
3125
+ if (count >= BRANCH_LADDER_THRESHOLD) {
3126
+ reported.add(i);
3127
+ pushLineFinding(out, relPath, i + 1, {
3128
+ rule: "ai-slop/python-repetitive-dispatch",
3129
+ severity: "warning",
3130
+ message: `${count} repeated branches dispatch on \`${valueMatch[2]}\`.`,
3131
+ help: "Use a table, set membership, or handler map when branches share the same shape. SlopCodeBench highlights these selector ladders as code that keeps growing instead of absorbing new cases cleanly."
3132
+ });
3133
+ }
3134
+ continue;
3135
+ }
3136
+ const instanceMatch = INSTANCE_BRANCH_RE.exec(lines[i]);
3137
+ if (!instanceMatch) continue;
3138
+ const count = countBranchLadder(lines, i, INSTANCE_BRANCH_RE, instanceMatch[2], instanceMatch[1]);
3139
+ if (count < BRANCH_LADDER_THRESHOLD) continue;
3140
+ reported.add(i);
3141
+ pushLineFinding(out, relPath, i + 1, {
3142
+ rule: "ai-slop/python-isinstance-ladder",
3143
+ severity: "warning",
3144
+ message: `${count} repeated \`isinstance(${instanceMatch[2]}, ...)\` branches.`,
3145
+ help: "Prefer a handler map, protocol, or normalized intermediate representation when each type branch has the same role. Repeated type ladders are one of the maintainability smells SCBench-style checks look for."
3146
+ });
3147
+ }
3148
+ };
3067
3149
  const detectPythonPatterns = async (context) => {
3068
3150
  const diagnostics = [];
3069
3151
  const files = getSourceFiles(context);
@@ -3083,6 +3165,9 @@ const detectPythonPatterns = async (context) => {
3083
3165
  flagBroadExceptWithSilentBody(lines, relPath, diagnostics);
3084
3166
  flagMutableDefaults(lines, relPath, diagnostics);
3085
3167
  flagPrintInProduction(lines, relPath, basename, diagnostics);
3168
+ flagRangeLenLoops(lines, relPath, diagnostics);
3169
+ flagChainedDictGets(lines, relPath, diagnostics);
3170
+ flagBranchLadders(lines, relPath, diagnostics);
3086
3171
  }
3087
3172
  return diagnostics;
3088
3173
  };
@@ -7170,6 +7255,10 @@ const RULE_LABELS = {
7170
7255
  "ai-slop/python-broad-except": "Broad except",
7171
7256
  "ai-slop/python-mutable-default": "Mutable default argument",
7172
7257
  "ai-slop/python-print-debug": "print() left in code",
7258
+ "ai-slop/python-range-len-loop": "range(len(...)) loop",
7259
+ "ai-slop/python-chained-dict-get": "Chained dict get",
7260
+ "ai-slop/python-repetitive-dispatch": "Repetitive dispatch ladder",
7261
+ "ai-slop/python-isinstance-ladder": "isinstance ladder",
7173
7262
  "ai-slop/go-library-panic": "panic() in Go library code",
7174
7263
  "ai-slop/rust-non-test-unwrap": "Rust .unwrap() in production code",
7175
7264
  "ai-slop/rust-todo-stub": "Rust todo!() stub",
@@ -7273,6 +7362,9 @@ const renderSummary = (input, deps = {}) => {
7273
7362
  }
7274
7363
  return lines.join("\n");
7275
7364
  };
7365
+ const renderStarCta = (deps = {}) => {
7366
+ return `\n ${style(deps.theme ?? theme, "muted", "★ Found this useful? Star us at github.com/scanaislop/aislop")}\n`;
7367
+ };
7276
7368
  const renderCleanRun = (input, deps = {}) => {
7277
7369
  const t = deps.theme ?? theme;
7278
7370
  const s = deps.symbols ?? symbols;
@@ -7388,11 +7480,12 @@ const buildScanRender = (input) => {
7388
7480
  const warnings = input.diagnostics.filter((d) => d.severity === "warning").length;
7389
7481
  const fixable = input.diagnostics.filter((d) => d.fixable).length;
7390
7482
  const hasVulnerableDeps = input.diagnostics.some((d) => d.rule === "security/vulnerable-dependency");
7483
+ const starCta = input.printBrand !== false ? renderStarCta(deps) : "";
7391
7484
  if (input.diagnostics.length === 0 && input.score.score === 100) return `${header}${renderCleanRun({
7392
7485
  score: input.score.score,
7393
7486
  label: input.score.label,
7394
7487
  elapsedMs: input.elapsedMs
7395
- }, deps)}`;
7488
+ }, deps)}${starCta}`;
7396
7489
  const diagBlock = input.diagnostics.length === 0 ? "" : renderDiagnostics(input.diagnostics, input.verbose);
7397
7490
  const nextSteps = [];
7398
7491
  if (fixable > 0) nextSteps.push({
@@ -7419,7 +7512,7 @@ const buildScanRender = (input) => {
7419
7512
  nextSteps,
7420
7513
  breakdown: computeBreakdown(input.diagnostics),
7421
7514
  thresholds: input.thresholds
7422
- }, deps)}`;
7515
+ }, deps)}${starCta}`;
7423
7516
  };
7424
7517
  const scanCommand = async (directory, config, options) => {
7425
7518
  const resolvedDir = path.resolve(directory);
@@ -7530,7 +7623,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
7530
7623
  engineTimings
7531
7624
  };
7532
7625
  if (options.json) {
7533
- const { buildJsonOutput } = await import("./json-BhO1Ufj3.js");
7626
+ const { buildJsonOutput } = await import("./json-CXiEvR_M.js");
7534
7627
  const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
7535
7628
  console.log(JSON.stringify(jsonOut, null, 2));
7536
7629
  return completion;
@@ -9355,6 +9448,10 @@ const BUILTIN_RULES = [
9355
9448
  "ai-slop/python-broad-except",
9356
9449
  "ai-slop/python-mutable-default",
9357
9450
  "ai-slop/python-print-debug",
9451
+ "ai-slop/python-range-len-loop",
9452
+ "ai-slop/python-chained-dict-get",
9453
+ "ai-slop/python-repetitive-dispatch",
9454
+ "ai-slop/python-isinstance-ladder",
9358
9455
  "ai-slop/go-library-panic",
9359
9456
  "ai-slop/rust-non-test-unwrap",
9360
9457
  "ai-slop/rust-todo-stub",
@@ -1,4 +1,4 @@
1
- import { n as ENGINE_INFO, t as APP_VERSION } from "./version-BNO_Lw7E.js";
1
+ import { n as ENGINE_INFO, t as APP_VERSION } from "./version-C45P3Q1N.js";
2
2
 
3
3
  //#region src/output/json.ts
4
4
  const buildJsonOutput = (results, scoreResult, fileCount, elapsedMs) => {
package/dist/mcp.js CHANGED
@@ -2190,6 +2190,11 @@ const BROAD_EXCEPT_RE = /^\s*except\s+(Exception|BaseException)\s*(?:as\s+\w+)?\
2190
2190
  const PRINT_RE = /^\s*print\s*\(/;
2191
2191
  const DEF_RE = /^\s*(?:async\s+)?def\s+\w+\s*\(/;
2192
2192
  const MUTABLE_DEFAULT_RE = /(\w+)\s*(?::\s*[^,)=]+)?\s*=\s*(\[\s*\]|\{\s*\}|set\(\s*\))/;
2193
+ const RANGE_LEN_LOOP_RE = /^\s*for\s+([A-Za-z_]\w*)\s+in\s+range\s*\(\s*len\s*\(\s*([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*\)\s*\)\s*:\s*(?:#.*)?$/;
2194
+ const CHAINED_DICT_GET_RE = /\.get\s*\([^)]*,\s*\{\s*\}\s*\)\s*\.get\s*\(/;
2195
+ const SAME_VALUE_BRANCH_RE = /^(\s*)(?:if|elif)\s+([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*==\s*["'][^"']+["']\s*:/;
2196
+ const INSTANCE_BRANCH_RE = /^(\s*)(?:if|elif)\s+isinstance\s*\(\s*([A-Za-z_]\w*)\s*,\s*[^)]+\)\s*:/;
2197
+ const BRANCH_LADDER_THRESHOLD = 4;
2193
2198
  const isTestFile$1 = (relPath, basename) => basename.startsWith("test_") || basename.endsWith("_test.py") || basename === "conftest.py" || relPath.split(path.sep).some((seg) => seg === "tests" || seg === "test");
2194
2199
  const isScriptOrEntrypoint = (basename) => basename === "__main__.py" || basename === "manage.py" || basename === "setup.py";
2195
2200
  const SCRIPT_DIR_NAMES = new Set([
@@ -2242,6 +2247,13 @@ const pushFinding = (out, a) => {
2242
2247
  fixable: false
2243
2248
  });
2244
2249
  };
2250
+ const pushLineFinding = (out, relPath, line, finding) => {
2251
+ pushFinding(out, {
2252
+ relPath,
2253
+ line,
2254
+ ...finding
2255
+ });
2256
+ };
2245
2257
  const flagBareExcept = (lines, relPath, out) => {
2246
2258
  for (let i = 0; i < lines.length; i++) {
2247
2259
  if (!BARE_EXCEPT_RE.test(lines[i])) continue;
@@ -2323,6 +2335,76 @@ const flagPrintInProduction = (lines, relPath, basename, out) => {
2323
2335
  });
2324
2336
  }
2325
2337
  };
2338
+ const flagRangeLenLoops = (lines, relPath, out) => {
2339
+ for (let i = 0; i < lines.length; i++) {
2340
+ const match = RANGE_LEN_LOOP_RE.exec(lines[i]);
2341
+ if (!match) continue;
2342
+ pushLineFinding(out, relPath, i + 1, {
2343
+ rule: "ai-slop/python-range-len-loop",
2344
+ severity: "info",
2345
+ message: `\`range(len(${match[2]}))\` loop — usually a hand-rolled iteration pattern.`,
2346
+ help: "Prefer direct iteration (`for item in items`) or `enumerate(items)` when the index is needed. Keeping index plumbing out of the loop reduces checkpoint-to-checkpoint bloat."
2347
+ });
2348
+ }
2349
+ };
2350
+ const flagChainedDictGets = (lines, relPath, out) => {
2351
+ for (let i = 0; i < lines.length; i++) {
2352
+ if (!CHAINED_DICT_GET_RE.test(lines[i])) continue;
2353
+ pushLineFinding(out, relPath, i + 1, {
2354
+ rule: "ai-slop/python-chained-dict-get",
2355
+ severity: "warning",
2356
+ message: "Chained `.get(..., {})` defaults hide missing-data cases.",
2357
+ help: "Normalize the input at the boundary, use a typed object, or split the lookup into explicit steps. Empty-dict fallback chains are a common agent shortcut that becomes brittle as schemas evolve."
2358
+ });
2359
+ }
2360
+ };
2361
+ const countBranchLadder = (lines, start, pattern, selector, indent) => {
2362
+ let count = 1;
2363
+ for (let i = start + 1; i < lines.length; i++) {
2364
+ const line = lines[i];
2365
+ const trimmed = line.trim();
2366
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
2367
+ const match = pattern.exec(line);
2368
+ if (match?.[1] === indent && match[2] === selector) {
2369
+ count++;
2370
+ continue;
2371
+ }
2372
+ if (line.startsWith(`${indent}elif `)) break;
2373
+ if (line.length - line.trimStart().length <= indent.length && !line.startsWith(`${indent}else`)) break;
2374
+ }
2375
+ return count;
2376
+ };
2377
+ const flagBranchLadders = (lines, relPath, out) => {
2378
+ const reported = /* @__PURE__ */ new Set();
2379
+ for (let i = 0; i < lines.length; i++) {
2380
+ if (reported.has(i)) continue;
2381
+ const valueMatch = SAME_VALUE_BRANCH_RE.exec(lines[i]);
2382
+ if (valueMatch) {
2383
+ const count = countBranchLadder(lines, i, SAME_VALUE_BRANCH_RE, valueMatch[2], valueMatch[1]);
2384
+ if (count >= BRANCH_LADDER_THRESHOLD) {
2385
+ reported.add(i);
2386
+ pushLineFinding(out, relPath, i + 1, {
2387
+ rule: "ai-slop/python-repetitive-dispatch",
2388
+ severity: "warning",
2389
+ message: `${count} repeated branches dispatch on \`${valueMatch[2]}\`.`,
2390
+ help: "Use a table, set membership, or handler map when branches share the same shape. SlopCodeBench highlights these selector ladders as code that keeps growing instead of absorbing new cases cleanly."
2391
+ });
2392
+ }
2393
+ continue;
2394
+ }
2395
+ const instanceMatch = INSTANCE_BRANCH_RE.exec(lines[i]);
2396
+ if (!instanceMatch) continue;
2397
+ const count = countBranchLadder(lines, i, INSTANCE_BRANCH_RE, instanceMatch[2], instanceMatch[1]);
2398
+ if (count < BRANCH_LADDER_THRESHOLD) continue;
2399
+ reported.add(i);
2400
+ pushLineFinding(out, relPath, i + 1, {
2401
+ rule: "ai-slop/python-isinstance-ladder",
2402
+ severity: "warning",
2403
+ message: `${count} repeated \`isinstance(${instanceMatch[2]}, ...)\` branches.`,
2404
+ help: "Prefer a handler map, protocol, or normalized intermediate representation when each type branch has the same role. Repeated type ladders are one of the maintainability smells SCBench-style checks look for."
2405
+ });
2406
+ }
2407
+ };
2326
2408
  const detectPythonPatterns = async (context) => {
2327
2409
  const diagnostics = [];
2328
2410
  const files = getSourceFiles(context);
@@ -2342,6 +2424,9 @@ const detectPythonPatterns = async (context) => {
2342
2424
  flagBroadExceptWithSilentBody(lines, relPath, diagnostics);
2343
2425
  flagMutableDefaults(lines, relPath, diagnostics);
2344
2426
  flagPrintInProduction(lines, relPath, basename, diagnostics);
2427
+ flagRangeLenLoops(lines, relPath, diagnostics);
2428
+ flagChainedDictGets(lines, relPath, diagnostics);
2429
+ flagBranchLadders(lines, relPath, diagnostics);
2345
2430
  }
2346
2431
  return diagnostics;
2347
2432
  };
@@ -5757,7 +5842,7 @@ const handleAislopBaseline = (input) => {
5757
5842
 
5758
5843
  //#endregion
5759
5844
  //#region src/version.ts
5760
- const APP_VERSION = "0.9.3";
5845
+ const APP_VERSION = "0.9.4";
5761
5846
 
5762
5847
  //#endregion
5763
5848
  //#region src/telemetry/env.ts
@@ -29,7 +29,7 @@ const getEngineLabel = (engine) => ENGINE_INFO[engine].label;
29
29
 
30
30
  //#endregion
31
31
  //#region src/version.ts
32
- const APP_VERSION = "0.9.3";
32
+ const APP_VERSION = "0.9.4";
33
33
 
34
34
  //#endregion
35
35
  export { ENGINE_INFO as n, getEngineLabel as r, APP_VERSION as t };
package/package.json CHANGED
@@ -1,93 +1,94 @@
1
1
  {
2
- "name": "aislop",
3
- "version": "0.9.3",
4
- "description": "The engineering standards layer and quality gate for AI-written code. Define your standard once. Every agent — Claude Code, Cursor, Codex — is held to it automatically, on every edit and every PR. Catches the slop they leave behind, enforces the rules your team sets. 8+ languages. Deterministic.",
5
- "type": "module",
6
- "bin": {
7
- "aislop": "./dist/cli.js",
8
- "aislop-mcp": "./dist/mcp.js"
9
- },
10
- "files": [
11
- "dist",
12
- "scripts"
13
- ],
14
- "exports": {
15
- ".": {
16
- "types": "./dist/index.d.ts",
17
- "default": "./dist/index.js"
18
- }
19
- },
20
- "scripts": {
21
- "dev": "tsdown --watch",
22
- "build": "rm -rf dist && NODE_ENV=production tsdown",
23
- "postinstall": "node scripts/postinstall-tools.mjs",
24
- "typecheck": "tsc --noEmit",
25
- "test": "pnpm build && vitest run",
26
- "scan": "pnpm build && node dist/cli.js scan .",
27
- "scan:exclude": "pnpm build && node dist/cli.js scan . --exclude .idea --exclude .gitnore --exclude node_modules",
28
- "scan:include": "pnpm build && node dist/cli.js scan . --include src --include tests",
29
- "scan:json": "pnpm build && node dist/cli.js scan . --json",
30
- "quality": "pnpm typecheck && pnpm test && node dist/cli.js scan . --json"
31
- },
32
- "keywords": [
33
- "aislop",
34
- "ai-slop",
35
- "code-quality",
36
- "linter",
37
- "formatter",
38
- "cli",
39
- "ai",
40
- "copilot",
41
- "code-review",
42
- "static-analysis",
43
- "typescript",
44
- "javascript",
45
- "python",
46
- "go",
47
- "rust",
48
- "ruby",
49
- "php"
50
- ],
51
- "author": "heavykenny",
52
- "license": "MIT",
53
- "homepage": "https://github.com/scanaislop/aislop#readme",
54
- "repository": {
55
- "type": "git",
56
- "url": "git+https://github.com/scanaislop/aislop.git"
57
- },
58
- "bugs": {
59
- "url": "https://github.com/scanaislop/aislop/issues"
60
- },
61
- "engines": {
62
- "node": ">=20"
63
- },
64
- "packageManager": "pnpm@10.28.0",
65
- "dependencies": {
66
- "@biomejs/biome": "^2.4.5",
67
- "@clack/prompts": "^1.2.0",
68
- "@modelcontextprotocol/sdk": "^1.29.0",
69
- "adm-zip": "^0.5.16",
70
- "commander": "^14.0.3",
71
- "expo-doctor": "^1.18.10",
72
- "knip": "^5.85.0",
73
- "micromatch": "^4.0.8",
74
- "oxlint": "^1.51.0",
75
- "picocolors": "^1.1.1",
76
- "tar": "^7.5.11",
77
- "typescript": "^5.9.3",
78
- "wcwidth": "^1.0.1",
79
- "yaml": "^2.8.2",
80
- "zod": "^4.3.6"
81
- },
82
- "devDependencies": {
83
- "@types/micromatch": "^4.0.10",
84
- "@types/node": "^25.6.0",
85
- "tsdown": "^0.20.3",
86
- "vitest": "^4.0.18"
87
- },
88
- "pnpm": {
89
- "overrides": {
90
- "postcss@<8.5.10": "^8.5.10"
91
- }
92
- }
2
+ "name": "aislop",
3
+ "version": "0.9.4",
4
+ "description": "The engineering standards layer and quality gate for AI-written code. Define your standard once. Every agent — Claude Code, Cursor, Codex — is held to it automatically, on every edit and every PR. Catches the slop they leave behind, enforces the rules your team sets. 8+ languages. Deterministic.",
5
+ "type": "module",
6
+ "bin": {
7
+ "aislop": "./dist/cli.js",
8
+ "aislop-mcp": "./dist/mcp.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "scripts"
13
+ ],
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
20
+ "scripts": {
21
+ "dev": "tsdown --watch",
22
+ "build": "rm -rf dist && NODE_ENV=production tsdown",
23
+ "postinstall": "node scripts/postinstall-tools.mjs",
24
+ "typecheck": "tsc --noEmit",
25
+ "test": "pnpm build && vitest run",
26
+ "scan": "pnpm build && node dist/cli.js scan .",
27
+ "scan:exclude": "pnpm build && node dist/cli.js scan . --exclude .idea --exclude .gitnore --exclude node_modules",
28
+ "scan:include": "pnpm build && node dist/cli.js scan . --include src --include tests",
29
+ "scan:json": "pnpm build && node dist/cli.js scan . --json",
30
+ "quality": "pnpm typecheck && pnpm test && node dist/cli.js scan . --json"
31
+ },
32
+ "keywords": [
33
+ "aislop",
34
+ "ai-slop",
35
+ "code-quality",
36
+ "linter",
37
+ "formatter",
38
+ "cli",
39
+ "ai",
40
+ "copilot",
41
+ "code-review",
42
+ "static-analysis",
43
+ "typescript",
44
+ "javascript",
45
+ "python",
46
+ "go",
47
+ "rust",
48
+ "ruby",
49
+ "php"
50
+ ],
51
+ "author": "heavykenny",
52
+ "license": "MIT",
53
+ "homepage": "https://github.com/scanaislop/aislop#readme",
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/scanaislop/aislop.git"
57
+ },
58
+ "bugs": {
59
+ "url": "https://github.com/scanaislop/aislop/issues"
60
+ },
61
+ "engines": {
62
+ "node": ">=20"
63
+ },
64
+ "packageManager": "pnpm@10.28.0",
65
+ "dependencies": {
66
+ "@biomejs/biome": "^2.4.5",
67
+ "@clack/prompts": "^1.2.0",
68
+ "@modelcontextprotocol/sdk": "^1.29.0",
69
+ "adm-zip": "^0.5.16",
70
+ "commander": "^14.0.3",
71
+ "expo-doctor": "^1.18.10",
72
+ "knip": "^5.85.0",
73
+ "micromatch": "^4.0.8",
74
+ "oxlint": "^1.51.0",
75
+ "picocolors": "^1.1.1",
76
+ "tar": "^7.5.11",
77
+ "typescript": "^5.9.3",
78
+ "wcwidth": "^1.0.1",
79
+ "yaml": "^2.8.2",
80
+ "zod": "^4.3.6"
81
+ },
82
+ "devDependencies": {
83
+ "@types/micromatch": "^4.0.10",
84
+ "@types/node": "^25.6.0",
85
+ "tsdown": "^0.20.3",
86
+ "vitest": "^4.0.18"
87
+ },
88
+ "pnpm": {
89
+ "overrides": {
90
+ "postcss@<8.5.10": "^8.5.10",
91
+ "qs@>=6.11.1 <=6.15.1": "^6.15.2"
92
+ }
93
+ }
93
94
  }