Audience: Contributors, researchers, and integrators who need to understand the precise scoring formulas and algorithmic decisions behind each metric.
- Logic Density Ratio (LDR)
- Inflation-to-Code Ratio (ICR) — v2.8.0 Redesign
- Deep Dependency Check (DDC)
- Deficit Score Composition
- Status Determination — Monotonic Axis
- Project-Level Aggregation (SR9)
- Python Advanced Patterns — AST Models
- Function-Scoped Justification
- ML Feature Vector
- ML Secondary Signal (MLScore)
- Snapshot Audit Boundary
Purpose: Measures how much of a file is actual executable logic versus blank lines, comments, and structural boilerplate.
ldr_score = logic_lines / total_lines (if total_lines > 0, else 0.0)
Where:
total_lines= all non-empty lines (blank lines counted separately)logic_lines= lines that are not blank, not pure comments, not docstrings, not structural tokens (class/def/pass/return alone, etc.)
| Grade | Condition | Interpretation |
|---|---|---|
| A | ldr >= 0.60 | High density — substantial logic |
| B | ldr >= 0.45 | Adequate density |
| C | ldr >= 0.30 | Low density — likely padded |
| D | ldr < 0.30 | Very low density — probable slop |
Files flagged as is_abc_interface = True (ABC base classes with abstract-only
methods) or is_type_stub = True (.pyi files) receive reduced penalty weight,
since structural boilerplate is expected.
v2.8.0 major redesign. Prior to v2.8.0, complexity divided the penalty (rewarding overly complex code). The new model treats complexity as an amplifier — a jargon-heavy complex function is penalized more than a jargon-heavy simple one.
density = unjustified_jargon_count / max(logic_lines, 1)
complexity_modifier = max(1.0, 1.0 + (avg_complexity - 3.0) / 10.0)
inflation_score = min(density * complexity_modifier * 10.0, 10.0)
| avg_complexity | modifier | Effect |
|---|---|---|
| <= 3 | 1.0x | No amplification (simple code baseline) |
| 6 | 1.3x | Moderate amplification |
| 10 | 1.7x | Significant amplification |
| 13 | 2.0x | Double penalty |
| 23 | 3.0x | Triple penalty |
Key property: complexity_modifier is clamped to [1.0, +inf). Complexity
can only increase the penalty — never reduce it. This prevents a complex god
function from hiding behind its own algorithmic weight.
| Range | Status |
|---|---|
| 0.0–1.0 | Low |
| 1.0–3.0 | Moderate |
| 3.0–6.0 | High |
| 6.0–10.0 | Extreme |
# OLD — complexity wrongly divided the penalty:
inflation_score = jargon_count * weight / (avg_complexity + 1)
Purpose: Measures what fraction of imported modules are actually used.
usage_ratio = len(actually_used) / len(imported) (if imported else 1.0)
Where actually_used is the intersection of imported names with names found
in the AST body (function calls, attribute accesses, type annotations).
| Grade | Condition | Interpretation |
|---|---|---|
| A | ratio >= 0.80 | Imports well-used |
| B | ratio >= 0.60 | Some unused, acceptable |
| C | ratio >= 0.40 | Significant dead imports |
| D | ratio < 0.40 | Likely hallucinated imports |
Supplementary signal: fake_imports — imports that are in the AST but
resolve to no known purpose (ML, HTTP, DB libs imported then never referenced
in any call expression). These are surfaced separately as hallucination_deps.
Purpose: Single composite score [0, 100] summarising all signals. Higher = more deficit (worse quality).
base_deficit = 100 × (1 − GQG_4D)
GQG_4D = exp(
(w_ldr × log(max(1e-4, ldr))
+ w_inf × log(max(1e-4, 1 − icr_norm))
+ w_ddc × log(max(1e-4, ddc))
+ w_purity × log(max(1e-4, purity_score)))
/ total_w
)
# max(1e-4, ...) prevents log(0) = -inf collapsing the entire score.
# icr_norm = min(inflation_score, 2.0) / 2.0
# purity_score = exp(−0.5 × n_critical_patterns)
total_w = w_ldr + w_inflation + w_ddc + w_purity
pattern_penalty = Sigma(severity_weight[sev] * count[sev])
deficit_score = base_deficit + pattern_penalty
| Signal | Weight (w) | Source |
|---|---|---|
| ldr | 0.40 | .slopconfig.yaml weights.ldr |
| inflation | 0.30 | .slopconfig.yaml weights.inflation |
| ddc | 0.20 | .slopconfig.yaml weights.ddc |
| purity | 0.10 | .slopconfig.yaml weights.purity |
w_ldr + w_inflation + w_ddc + w_purity = 1.00.
total_w normalises the geometric mean, so weights need not sum to any fixed value.
These are the DEFAULT_CONFIG canonical fallback values. The LEDA engine's
dogfooding-calibrated weights live in DOMAIN_PROFILES["general"] and differ
significantly (ddc ≈ 0.62, ldr ≈ 0.15) — see docs/LEDA_CALIBRATION.md §4.3.
| Severity | Penalty per occurrence |
|---|---|
| critical | 0.10 |
| high | 0.05 |
| medium | 0.02 |
| low | 0.01 |
The sum is capped internally so that extreme pattern counts cannot push the score above 100.
v2.8.0 redesign. Prior versions had multi-axis branching (separate LDR, DDC, pattern-count branches) that could produce inconsistent status for identical deficit scores. v2.8.0 uses a single monotonic axis on
deficit_score, with two explicit supplementary overrides.
deficit_score >= 70 --> CRITICAL_DEFICIT
deficit_score >= 50 --> INFLATED_SIGNAL
deficit_score >= 30 --> SUSPICIOUS
else --> CLEAN
IF critical_pattern_count >= 5 AND status == CLEAN:
status = SUSPICIOUS
IF ddc_usage_ratio < 0.20 AND status in {CLEAN, SUSPICIOUS}:
status = DEPENDENCY_NOISE
Key property: Overrides can only raise status, never lower it.
A file already at INFLATED_SIGNAL or CRITICAL_DEFICIT is unaffected.
| Status | Meaning |
|---|---|
| CLEAN | No significant quality issues detected |
| SUSPICIOUS | Marginal quality — review recommended |
| INFLATED_SIGNAL | High jargon or low logic density — likely AI pad |
| CRITICAL_DEFICIT | Severe multi-dimensional deficit |
| DEPENDENCY_NOISE | Import graph dominated by unused dependencies |
Purpose: Aggregate per-file LDR scores into a single project-level metric without allowing a majority of clean files to mask a critically degraded one.
project_ldr = 0.6 * min(file_ldr_scores) + 0.4 * mean(file_ldr_scores)
This is derived from the SR9 conservative aggregation principle (TOE Sovereign Resonance protocol), where:
mincaptures the worst-case file (weighted 60%)meancaptures the central tendency (weighted 40%)
Contrast with naive mean: a project with 99 clean files (LDR=0.8) and one empty wrapper file (LDR=0.05) would score 0.795 under mean — appearing healthy. Under SR9, it scores 0.6 * 0.05 + 0.4 * 0.795 = 0.348 — correctly flagging the dragging file.
avg_deficit_score = mean(file.deficit_score for file in project)
weighted_deficit_score = sum(file.deficit_score * file.total_lines)
/ sum(file.total_lines) [line-weighted mean]
avg_inflation = mean(file.inflation.inflation_score)
avg_ddc = mean(file.ddc.usage_ratio)
New in v2.8.0. Uses Python
astmodule for precise scope-aware detection.
complexity(fn) = 1 + count(
If, IfExp,
For, While, ExceptHandler,
With,
BoolOp[op=And], BoolOp[op=Or]
)
This is a simplified McCabe metric implemented over the AST. Each branching node adds 1 to the base complexity of 1.
Triggered when: logic_lines > 50 OR complexity > 10
is_god_function = (
count_logic_lines(fn) > 50
OR cyclomatic_complexity(fn) > 10
)
Logic lines within a function are counted the same way as file-level LDR: executable statements excluding blank lines and pure comments.
Severity: HIGH
depth(node):
if node has no control-flow children: return 0
return 1 + max(depth(child) for child in control_flow_children(node))
control_flow_children = bodies of: If, For, While, With, Try
(orelse branches counted at same depth as main body)
Alert: depth(fn) > 4
Severity: HIGH
Example — depth 5 (triggers):
def f():
for x in items: # depth 1
if x > 0: # depth 2
while True: # depth 3
try: # depth 4
if cond: # depth 5 <-- triggers
passis_terminal(stmt) = isinstance(stmt, (Return, Raise, Break, Continue))
dead_code_in_block(stmts):
terminal_seen = False
for stmt in stmts:
if terminal_seen: yield stmt # unreachable
if is_terminal(stmt): terminal_seen = True
recurse into: orelse, finalbody, handlers
Scope: Applied recursively to function bodies, orelse blocks, finally blocks, and exception handler bodies. Dead code inside nested functions is attributed to the innermost function.
Severity: MEDIUM
v2.8.0 redesign. Previously, a single
import torchat the top of a file would justify all AI jargon anywhere in the file. Now each function must contain (or be decorated by) its own justifier.
scope_start(fn) = min(decorator.lineno for decorator in fn.decorator_list,
default fn.lineno)
scope_end(fn) = fn.end_lineno
The decorator's lineno is included so that @torch.jit.script above a
function is correctly counted as part of that function's justification scope.
justified(jargon_at_line L, fn) =
any(
justifier_line in [scope_start(fn), scope_end(fn)]
for justifier_line in justifier_lines_in_file
)
Where justifier_lines includes:
- Import statements that import a known domain library
- Decorator lines referencing domain-specific decorators
- Function-body usage of domain APIs
import torch # line 1 — file-scope; does NOT justify line 20
def encode(data): # scope [10, 18]
return base64(data) # line 12: "encode" jargon here IS NOT justified
# (torch import is outside scope)
@torch.jit.script # line 20 — inside scope [20, 28]
def transform(tensor): # line 21
return tensor * 2.0 # line 23: "transform" jargon IS justified
# (decorator at line 20 is in scope)ML scoring is an optional secondary signal. No model file = no scoring. The rule-based
deficit_scoreremains the authoritative primary signal.
| # | Feature Name | Source | Range |
|---|---|---|---|
| 1 | ldr_score |
LDR result | [0, 1] |
| 2 | inflation_score |
ICR result (normalized: /10 internally) | [0, 10] |
| 3 | ddc_score |
DDC usage_ratio | [0, 1] |
| 4 | pattern_count_critical |
pattern_issues count by severity | [0, +inf) |
| 5 | pattern_count_high |
pattern_issues count by severity | [0, +inf) |
| 6 | pattern_count_medium |
pattern_issues count by severity | [0, +inf) |
| 7 | pattern_count_low |
pattern_issues count by severity | [0, +inf) |
| 8 | god_function_count |
v2.8.0 pattern (Section 7.2) | [0, +inf) |
| 9 | dead_code_count |
v2.8.0 pattern (Section 7.4) | [0, +inf) |
| 10 | deep_nesting_count |
v2.8.0 pattern (Section 7.3) | [0, +inf) |
| 11 | avg_complexity |
radon cyclomatic mean over file | [1, +inf) |
| 12 | cross_language_patterns |
patterns from wrong-language idioms | [0, +inf) |
| 13 | hallucination_count |
pattern_id contains "hallucin" | [0, +inf) |
| 14 | total_lines |
LDR result | [0, +inf) |
| 15 | logic_lines |
LDR result | [0, +inf) |
| 16 | empty_lines |
LDR result | [0, +inf) |
label = 1 (slop) if deficit_score >= 30.0
label = 0 (clean) if deficit_score < 30.0
The threshold of 30 corresponds to the SUSPICIOUS boundary on the primary status axis (Section 5).
| Type | Identifier | Backend |
|---|---|---|
| Random Forest | random_forest |
sklearn.ensemble |
| XGBoost | xgboost |
xgboost.XGBClassifier |
| Ensemble (soft) | ensemble |
RF + XGB probability mean |
@dataclass
class MLScore:
slop_probability: float # [0, 1] — probability this file is slop
confidence: float # [0, 1] — max class probability (model certainty)
model_type: str # "random_forest" | "xgboost" | "ensemble"
agreement: bool # True if rule-based and ML agree (see below)
features_used: int # always 16 in v2.8.0label = "slop" if slop_probability >= 0.70
label = "uncertain" if slop_probability >= 0.40
label = "clean" if slop_probability < 0.40
rule_is_slop = (deficit_score >= 30.0)
ml_is_slop = (slop_probability >= 0.40)
agreement = (rule_is_slop == ml_is_slop)
Agreement is informational only — it does not affect the primary
deficit_score or status. When the two signals disagree, the rule-based
score takes precedence.
| Confidence | Interpretation |
|---|---|
| >= 0.90 | High certainty — model strongly separates this sample |
| 0.70–0.90 | Moderate certainty |
| < 0.70 | Low certainty — borderline case, rule-based is primary |
The built-in synthetic_generator.py produces labelled samples for bootstrap
training without requiring a real codebase:
Slop sample construction:
- 5–15 unused imports (hallucination pattern)
- 3–8 empty functions with jargon names and jargon docstrings
- 1 god function template (150+ lines, complexity > 12)
bare_exceptand dead-code-after-return blocks- mutable default argument
Clean sample construction:
- 12 diverse utility functions (clamp, deduplicate, truncate, chunk, retry, etc.)
- All imports used, meaningful docstrings only where warranted
- Complexity <= 5, nesting depth <= 3, no dead code
Reported accuracy on synthetic set (600 samples, 70/30 split):
- Random Forest: 1.000 train / ~0.98 test
- Top discriminating features:
deep_nesting_count(#1),ldr_score(#2),dead_code_count(#5)
Warning: Models trained exclusively on synthetic data will overfit to synthetic patterns. For production use, supplement with real codebase samples via the manual label pipeline (
pipeline.py).
| Version | Signal | Change |
|---|---|---|
| v3.7.3 | Config | Pydantic import made optional (try/except); package always loads |
| v3.7.2 | Config | _validate_yaml_config() — Layer 1 guard at YAML load time |
| v3.7.2 | Models | __post_init__ clamps on LDR/Inflation/DDC results (Layer 2) |
| v3.7.2 | History | HistoryEntry.__post_init__ field clamps + fired_rules JSON (Layer 3) |
| v3.7.2 | VS Code | parseSlopReport() typed boundary, ISlopReport interface |
| v3.7.0 | Weights | DEFAULT_CONFIG["weights"].ddc corrected 0.30 → 0.20 |
| v3.7.0 | ML | ThresholdClassifier (pure-Python) replaces sklearn RandomForest |
| v3.5.0 | Calibr. | P1: project_id scoping in history.db (Schema v5) |
| v3.5.0 | Calibr. | P2: milestone trigger = count_files_with_multiple_runs |
| v3.5.0 | Calibr. | P3: domain-anchored ±0.15 grid search bounds |
| v3.5.0 | Calibr. | P4: DOMAIN_DRIFT_LIMIT=0.25 divergence warning |
| v3.4.0 | Pattern | JS/TS 4 patterns + Go 4 patterns added |
| v3.1.0 | Pattern | Clone/naming/stub patterns added |
| v3.0.0 | Scoring | GQG weighted geometric mean replaces arithmetic mean |
| v3.0.0 | Formula | max(1e-4, x) clamp guards added to all log() arguments |
| v2.9.0 | Pattern | phantom_import (hallucinated package detection) |
| v2.8.0 | ICR | Complexity now amplifies penalty (was: divided penalty) |
| v2.8.0 | Status | Single monotonic axis replaces multi-axis branching |
| v2.8.0 | LDR (proj) | SR9 conservative aggregation: 0.6min + 0.4mean |
| v2.8.0 | Justif. | Function-scoped (was: file-scoped) |
| v2.6.3 | Pattern | @slop.ignore decorator support |
All weights and thresholds are overridable via .slopconfig.yaml:
weights:
ldr: 0.40
inflation: 0.30
ddc: 0.30
purity: 0.10
thresholds:
deficit:
suspicious: 30
inflated: 50
critical: 70
ldr:
grade_a: 0.60
grade_b: 0.45
grade_c: 0.30
pattern_penalties:
critical: 0.10
high: 0.05
medium: 0.02
low: 0.01This document reflects the implementation in src/slop_detector/ as of v3.7.9.
For source-level detail see metrics/inflation.py, core.py,
patterns/python_advanced.py, ml/scorer.py, and docs/SCHEMA_VALIDATION.md
(runtime range guards for all metric result dataclasses).
The repository keeps the scoring model and governance enforcement separate. This section records the math that is safe to treat as part of the scoring surface and the boundary around it.
Temporal drift is read-only summary data. It describes how a file's score moves over time but does not alter the score itself.
drift_direction = sign(recent_average - historical_average)
drift_delta = recent_average - historical_average
This is used for reporting and trend inspection only.
The governance record is a reconstructable artifact, not a scoring input. The canonical hash is computed over the record with volatile timestamps removed:
canonical_record = JSON(record without generated_at_utc and record_hash)
record_hash = SHA-256(canonical_record)
- Scoring math may include deficit composition, deterministic topology, and hotspot prioritization.
- Enforcement math may verify the governance record hash and policy fields.
- Enforcement may fail closed, but it does not feed back into the deficit score.