AI SLOP Detector v3.1.1: Three Formula Refinements and the Adversarial Tester That Found Them #39
flamehaven01
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
We shipped v2.9.0 with a scoring engine we trusted. We ran tests. Everything passed.
Then we built a tool specifically designed to find cases where the score was less precise than it could be — and it found three.
This is the story of v3.1.0. And the patch that followed six hours later.
Glossary — internal terminology used throughout this post
100 × (1 - GQG).if,for,while,exceptadds 1.ifstatement, areturn, a function call each become typed nodes.Quick context
AI SLOP Detector is a static analyzer that measures structural code quality — not style, not formatting. It scores each file across four dimensions and assigns a
deficitbetween 0 (clean) and 100 (critical):These four numbers feed a single formula — a weighted geometric mean — called the GQG. The output is the deficit score:
100 × (1 - GQG).The calibrator's job is to find the best weights for that formula by searching over thousands of known cases.
Before v3.1.0: the self-scan
We don't ship a version without running the detector against itself. Before cutting v3.1.0, we ran v3.0.3 — a structural debt reduction pass on the three highest-deficit files in the codebase.
analysis/cross_file.pydropped from 70.3 to 28.7 (critical → clean).ci_gate.pyfrom 69.3 to 22.3.cli.pyfrom 68.4 to 20.9. The fixes were mechanical: extracted nested closures to private methods, replacedif/elif/elsedispatch chains with dict dispatch, removed re-declared constants.The point is not that these numbers are good. It's that the tool had to earn its own PASS before we shipped the version that refines the formula. Shipping a scoring engine while your own codebase sits at
suspiciouswould have been its own kind of slop.The adversarial tester: fhval SPAR
In a previous post we described
fhval— flamehaven-validator. The core concern: when every tool in an ecosystem is built by the same person against the same baseline, internal consistency can masquerade as correctness. Passing your own tests proves nothing about whether your tests are asking the right questions.For v3.1.0 we added a
sparsubcommand — an adversarial regression loop that interrogates the scorer from the outside. Running SPAR against the v3.0.x scorer:Three gaps. Two documented scope limits. Score: 55 FAIL.
Each gap pointed at a specific detection weakness. The SPAR methodology itself — how Layer A/B/C work, why adversarial ground truth is hard to author from inside the codebase — is a separate topic covered in tomorrow's post. Here we focus on what the gaps told us and what we changed.
Refinement 1: The calibrator and scorer were using different formulas
The scorer computes a weighted geometric mean. The calibrator — which finds optimal weights — was computing a weighted arithmetic mean as its optimization target.
Those are not the same thing, and for a quality gate, the difference is structural.
Consider a file with three dimension scores: LDR=0.9 (good), inflation_quality=0.1 (very bad), DDC=0.8 (good).
The arithmetic mean gives deficit=40. The geometric mean gives deficit=58. The gap is 18 points — not rounding, but structural. The geometric mean amplifies weak dimensions because one bad score pulls the entire product down. The arithmetic mean averages over them.
The scorer uses the geometric mean for good reason: a file with excellent LDR but zero actual logic (all docstrings) should not score deficit=30. It should score much higher. The formula enforces that.
The first-generation calibrator used an arithmetic mean as a simpler starting approximation. So it was finding weights that minimize error against a different objective than the scorer actually computes. The result: roughly 5–7 point underestimation on files with uneven dimension profiles — which are precisely the target of this tool.
The AM ≥ GM inequality means the calibrator's scores were always optimistic. For balanced files (all dimensions similar) the gap is small and harmless. For uneven files, it was systematic — and those are the cases that matter most.
Refinement:
This is why SPAR anomaly A3 (
stub_class_8_methods) jumped from deficit 20.0 to 40.0: the stub class had heavily uneven dimensions, and the geometric mean scored it correctly once the calibrator was trained against the right target.Refinement 2: The complexity modifier had a dead zone at the common end
The inflation metric applies a complexity modifier to penalize functions that are simultaneously simple and jargon-heavy — a common pattern in AI-generated code: a two-line function surrounded by an elaborate docstring.
The first-generation modifier formula:
For CC=1:
1.0 + (1-3)/10 = 0.8→max(1.0, 0.8)= 1.0For CC=2:
1.0 + (2-3)/10 = 0.9→max(1.0, 0.9)= 1.0For CC=3:
1.0 + (3-3)/10 = 1.0→max(1.0, 1.0)= 1.0CC=1, 2, and 3 all received the same modifier: 1.0. This meant simple functions — the three most common complexity levels — paid no complexity premium on inflation, regardless of how jargon-heavy they were. The modifier only activated from CC=4 upward.
Simple jargon-heavy functions are the most common AI code signature. The formula was least sensitive precisely where it needed to be most sensitive.
Now CC=2 gets a 1.10× modifier, CC=3 gets 1.20×. The penalty scales from the simplest meaningful function upward.
Refinement 3: Purity weight was documented but not connected
The GQG formula includes a purity dimension:
.slopconfig.yamlhad aweights.purityfield. The calibrator's weight search had a purity parameter. Neither was connected to this constant — users could configureweights.purity: 0.20and nothing would change.One line. The config surface now matches the implementation.
Two new detection patterns
Stub evasion: empty container returns
The existing
return_constant_stubpattern caughtreturn True,return 0,return "string"— but notreturn {},return [],return (),return set(). These are equally common stub patterns in class skeletons:Both are now caught by
return_constant_stubandinterface_only_class.Fragmented god function: AST clone detection
SPAR anomaly A4 was a file with 12 one-liner helper functions:
Each function individually looks clean: low complexity, no nesting, short. No single function exceeds any per-function threshold. But collectively, this is a decomposed god function — a large computation split into structurally identical fragments that evade per-function gates.
The new pattern:
function_clone_cluster.How it works. For each file, build a 30-dimensional histogram of AST node types for every function: how many
Ifnodes,Returnnodes,Callnodes,BinOpnodes, and so on. The histogram is normalized to a probability distribution. Then compute pairwise Jensen-Shannon Divergence between all function pairs. JSD is bounded between 0 and 1. Two functions with near-identical AST structure produce JSD close to 0.Functions with JSD < 0.05 get an edge in a graph. BFS finds connected components. The largest component is the clone cluster.
Why JSD and not simpler metrics. Cosine similarity or Euclidean distance on raw histograms don't handle sparse distributions well — short functions have mostly empty histograms, and small absolute differences dominate. JSD compares distributions rather than raw vectors, stable when most histogram dimensions are near zero. It also has an upper bound of 1, which makes the 0.05 threshold interpretable rather than dataset-dependent.
The JSD threshold (0.05) was calibrated against the internal test corpus. It will produce false positives on files with many similar utility functions — for example, a large set of
_validate_field_X()validators that are structurally identical by design. Adjust via--configif needed.Placeholder variable naming (v1.0)
SPAR anomaly A5 was vocabulary-clean code with zero semantic content:
No buzzwords. No docstring bloat. Every traditional linter passes this. The new
placeholder_variable_namingpattern applies two checks:self,cls,_) → HIGH.This is v1.0: it detects naming style, not semantic quality. Known false positive zone: scientific and math libraries legitimately use single-letter conventions (
x,y,z,mu,sigma). Suppress withdomain_overridesin.slopconfig.yaml.SPAR result after v3.1.0
55 → 85 PASS.
The two remaining blind spots are not gaps to close — they're the documented scope limits of static analysis: a tool that reads AST cannot determine whether arithmetic is semantically meaningful, or whether annotation-heavy imports serve a real runtime purpose. Those require a different class of model. Documenting the ceiling is part of the job.
The full SPAR methodology — how Layer A/B/C work, why Layer A ground truth is hard to author from inside the codebase, and what "validating the validator" means in practice — is covered in tomorrow's post.
v3.1.1: the self-inspection patch
v3.1.0 and v3.1.1 shipped on the same day. The clone detection pattern introduced in v3.1.0 had a visibility gap:
function_clone_clusterfired in the Issues section but produced no signal in the Core Metrics table. A community issue caught it within hours.But before cutting v3.1.1, we ran the tool against itself — and the new patterns found something:
Both files are part of the detection engine itself. Root cause:
check_nodemethods with cyclomatic complexity 20–31, caused by compound boolean logic that had accumulated across releases. The tool was flagging its own pattern implementations as having the exact complexity problems it was designed to detect.We extracted four module-level helpers in
placeholder.py(_strip_docstring,_has_abstractmethod,_empty_container_repr,_is_placeholder_stmt) and added_make_god_issue()and_collect_numbered_vars()topython_advanced.py. Eachcheck_nodemethod went from 20–70 lines to 8–15. The detector earned its own PASS before shipping the patch.Additional v3.1.1 refinements:
box.ROUNDEDacross all project output (was mixing three styles).extractJson()strips[INFO]log lines beforeJSON.parse— previously caused silent parse failures when CLI log output appeared alongside JSON. Workspace analysis replaced with a QuickPick list of deficit files sorted by score; clicking opens the file in the editor.If you installed 3.1.0, upgrade to 3.1.1 before using clone detection in CI.
How this fits alongside existing tools
The key gap: a file can be fully SonarQube-clean while containing zero actual logic — all stubs, all docstrings, all type annotations. Cognitive complexity doesn't measure whether the complexity is real. LDR does. Inflation does.
The complementary tool here is mutation testing. SPAR tests whether the scorer measures what it claims. Mutation testing tests whether your tests catch what they claim to catch. Both are adversarial approaches to the meta-problem: how do you validate the validator?
Score evolution
If you're running AI SLOP Detector on an existing project, upgrading to 3.1.x will change your scores. The formula alignment in Refinement 1 increases deficit on files with uneven dimension profiles, typically by 3–8 points. This is not drift — it's the scorer becoming more precise in the region where it matters most. Files that were borderline
suspiciousmay move intoinflated_signal. Check your CI threshold after upgrading.Previous scores were valid estimates produced by the first-generation model. v3.1.x scores are tighter estimates with better sensitivity where dimensions are uneven — which is precisely the profile of AI-generated code.
Honest limitations
function_clone_clusterthreshold (JSD < 0.05) was calibrated against the internal test corpus. It will fire false positives on legitimate utility function clusters. Adjust via--config.placeholder_variable_namingv1.0 has no semantic context.def distance(x, y, z)is legitimate; the pattern doesn't know that.Install / upgrade
pip install ai-slop-detector==3.1.1 # or pip install --upgrade ai-slop-detectorVS Code extension: search "AI SLOP Detector" in Extensions, or install from VSIX:
GitHub: flamehaven01/AI-SLOP-Detector
Previous posts in this series:
All reactions