From c8fec9148a1ebd9333fa76ba079368fc82b39250 Mon Sep 17 00:00:00 2001 From: Mike-7777777 <41225783+Mike-7777777@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:13:21 +0800 Subject: [PATCH 1/5] Pin scb-check version and record it with checkpoint metrics The composite quality scores come from `uvx scb-check`, which had no version constraint, so verbosity was computed by whatever release uvx resolved at run time. On a fixed input, scb-check 0.1.3 reports 2.42x the verbosity of 0.1.0/0.1.1/0.1.2/0.2.0; erosion is unaffected. Pin the version at the call site and store it under scb_check_version on every checkpoint that scb-check successfully measured, so results stay comparable and past runs stay interpretable. Checkpoints where scb-check fails keep their existing behaviour and record no version. --- src/slop_code/metrics/checkpoint/driver.py | 13 +++++++++++-- tests/metrics/checkpoint_results_test.py | 6 +++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/slop_code/metrics/checkpoint/driver.py b/src/slop_code/metrics/checkpoint/driver.py index 39c1e76d..939efeff 100644 --- a/src/slop_code/metrics/checkpoint/driver.py +++ b/src/slop_code/metrics/checkpoint/driver.py @@ -21,6 +21,13 @@ logger = get_logger(__name__) +# `scb-check` owns the composite quality scores, and its rule set changes +# between releases, so an unpinned `uvx` invocation makes verbosity +# incomparable across runs. Bump this deliberately; the value is recorded +# with every checkpoint so past results stay interpretable. +SCB_CHECK_VERSION = "0.2.0" +SCB_CHECK_VERSION_KEY = "scb_check_version" + def _number(value: Any) -> float | None: if isinstance(value, bool): @@ -70,7 +77,7 @@ def _get_scb_check_metrics(checkpoint_dir: Path) -> dict[str, Any]: command = [ "uvx", - "scb-check", + f"scb-check=={SCB_CHECK_VERSION}", "check", "--report", "--include-all", @@ -102,7 +109,9 @@ def _get_scb_check_metrics(checkpoint_dir: Path) -> dict[str, Any]: ) return {} - return _scb_check_metrics_from_report(report) + metrics = _scb_check_metrics_from_report(report) + metrics[SCB_CHECK_VERSION_KEY] = SCB_CHECK_VERSION + return metrics def get_checkpoint_metrics( diff --git a/tests/metrics/checkpoint_results_test.py b/tests/metrics/checkpoint_results_test.py index c4a28e45..d9606700 100644 --- a/tests/metrics/checkpoint_results_test.py +++ b/tests/metrics/checkpoint_results_test.py @@ -875,13 +875,16 @@ def fake_run(command, **kwargs): assert commands == [ [ "uvx", - "scb-check", + f"scb-check=={checkpoint_driver.SCB_CHECK_VERSION}", "check", "--report", "--include-all", str(tmp_path / "snapshot"), ] ] + assert ( + result["scb_check_version"] == checkpoint_driver.SCB_CHECK_VERSION + ) assert result["verbosity"] == pytest.approx(0.5) assert result["erosion"] == pytest.approx(0.2) assert result["cloned_sloc_lines"] == 10 @@ -929,6 +932,7 @@ def fake_run(command, **kwargs): assert "verbosity" not in result assert "erosion" not in result assert "cloned_pct" not in result + assert "scb_check_version" not in result class TestComputeCheckpointDelta: From e68e81d802172344a23a43d8d7eb18999a75b64a Mon Sep 17 00:00:00 2001 From: gabeorlanski Date: Tue, 28 Jul 2026 12:26:37 -0500 Subject: [PATCH 2/5] Remove legacy slop rules --- CLAUDE.md | 3 +- configs/slop_rules.yaml | 3790 ----------------- configs/slop_rules_candidates.yaml | 249 -- docs/README.md | 5 +- docs/commands/metrics.md | 6 +- docs/commands/utils.md | 3 +- docs/metrics-reference.md | 45 +- docs/metrics/README.md | 15 +- docs/metrics/checkpoint-results.md | 47 +- docs/metrics/configuration.md | 117 +- docs/metrics/interpreting-results.md | 58 +- docs/metrics/output-files.md | 33 +- docs/metrics/run-results.md | 17 +- src/slop_code/common/__init__.py | 2 - src/slop_code/common/constants.py | 1 - src/slop_code/dashboard/data.py | 6 +- src/slop_code/dashboard/graphs/boxplot.py | 4 +- src/slop_code/dashboard/graphs/comparison.py | 16 +- .../dashboard/graphs/quality_deltas.py | 2 +- src/slop_code/dashboard/graphs/scatter.py | 49 +- .../dashboard/pages/head_to_head_evolution.py | 22 +- .../dashboard/pages/head_to_head_quality.py | 16 +- .../dashboard/pages/run_analysis_quality.py | 10 +- .../entrypoints/commands/backfill_reports.py | 239 -- .../entrypoints/commands/consolidate_runs.py | 32 +- src/slop_code/entrypoints/commands/static.py | 104 +- .../entrypoints/commands/variance.py | 14 +- src/slop_code/entrypoints/utils.py | 5 - src/slop_code/metrics/__init__.py | 4 - src/slop_code/metrics/checkpoint/__init__.py | 4 - .../metrics/checkpoint/composites.py | 41 - src/slop_code/metrics/checkpoint/delta.py | 2 +- src/slop_code/metrics/checkpoint/driver.py | 2 +- .../metrics/checkpoint/extractors.py | 27 - src/slop_code/metrics/driver.py | 39 - .../metrics/languages/python/__init__.py | 16 - .../metrics/languages/python/ast_grep.py | 259 -- src/slop_code/metrics/models.py | 85 +- src/slop_code/metrics/quality_io.py | 13 - src/slop_code/metrics/summary/aggregators.py | 2 - src/slop_code/visualization/diff_viewer.py | 2 - tests/dashboard/graphs/scatter_test.py | 2 - .../commands/test_backfill_reports.py | 78 - tests/entrypoints/commands/test_variance.py | 33 +- tests/metrics/checkpoint/composites_test.py | 38 - tests/metrics/checkpoint_results_test.py | 100 +- tests/metrics/driver_test.py | 14 +- tests/metrics/language/py_ast_grep_test.py | 713 ---- tests/metrics/models_test.py | 92 - tests/metrics/summary_test.py | 3 +- 50 files changed, 122 insertions(+), 6357 deletions(-) delete mode 100644 configs/slop_rules.yaml delete mode 100644 configs/slop_rules_candidates.yaml delete mode 100644 src/slop_code/metrics/checkpoint/composites.py delete mode 100644 src/slop_code/metrics/languages/python/ast_grep.py delete mode 100644 tests/metrics/checkpoint/composites_test.py delete mode 100644 tests/metrics/language/py_ast_grep_test.py diff --git a/CLAUDE.md b/CLAUDE.md index 4e0144a5..9e452f47 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -247,7 +247,7 @@ markers: # Custom pytest markers beyond built-ins ### Quality Metrics - Language-specific parsers extract AST information - Metrics: cyclomatic complexity, duplication, code churn, maintainability index -- ast-grep rules in `configs/ast-grep-rules/` for pattern-based analysis +- Pinned `scb-check` reports the composite verbosity and erosion scores - LLM judge for subjective quality assessment via rubric templates ### Logging and Debugging @@ -344,4 +344,3 @@ Skills are invoked with `/skill-name `. See `.claude/skills/` for full doc - `docs/evaluation-tests/stateful-testing.md` - State across checkpoints and modules - `docs/evaluation-tests/complex-parametrization.md` - Loading cases from JSON, YAML, directories - `docs/evaluation-tests/debugging-workflows.md` - Debugging test failures and workflows - diff --git a/configs/slop_rules.yaml b/configs/slop_rules.yaml deleted file mode 100644 index 011b4f2d..00000000 --- a/configs/slop_rules.yaml +++ /dev/null @@ -1,3790 +0,0 @@ ---- -id: chained-comparison-opportunity -language: python -severity: warning -message: Use chained comparison (e.g., a < b < c) instead of 'and' -metadata: - weight: 3 - category: slop -rule: - kind: boolean_operator - any: - - pattern: $A < $B and $B < $C - - pattern: $A > $B and $B > $C - - pattern: $A <= $B and $B <= $C - - pattern: $A >= $B and $B >= $C - - pattern: $A < $B and $B <= $C - - pattern: $A <= $B and $B < $C - - pattern: $A > $B and $B >= $C - - pattern: $A >= $B and $B > $C ---- -id: chained-dict-get -language: python -severity: warning -message: Chained .get().get() - overly defensive, extract to helper -metadata: - weight: 3 - category: slop -rule: - kind: call - has: - kind: attribute - has: - kind: call - has: - kind: attribute - regex: \.get$ - regex: \.get$ ---- -id: comprehension-used-but-ignored-result -language: python -severity: warning -message: Comprehension result is never used - this looks like a side-effect-only comprehension -metadata: - weight: 3 - category: slop -rule: - kind: expression_statement - has: - any: - - kind: list_comprehension - - kind: set_comprehension - - kind: dictionary_comprehension - - kind: generator_expression ---- -id: duplicated-if-condition -language: python -severity: warning -message: Duplicated if condition in elif chain -metadata: - weight: 3 - category: slop -rule: - kind: if_statement - pattern: "if $COND:\n $$$\nelif $COND:\n $$$\n" ---- -id: isinstance-return-ladder -language: python -severity: warning -message: Long isinstance/elif ladder returning simple values; prefer a dispatch table - or polymorphism. -metadata: - weight: 3 - category: slop -rule: - pattern: "if isinstance($X, $T1):\n return $V1\nelif isinstance($X, $T2):\n return\ - \ $V2\n" ---- -id: manual-min-max -language: python -severity: warning -message: Manual min/max logic - use built-in min() or max() -metadata: - weight: 3 - category: slop -rule: - kind: if_statement - any: - - pattern: "if $A > $B:\n $M = $A\nelse:\n $M = $B\n" - - pattern: "if $A < $B:\n $M = $A\nelse:\n $M = $B\n" - - pattern: "if $A >= $B:\n $M = $A\nelse:\n $M = $B\n" - - pattern: "if $A <= $B:\n $M = $A\nelse:\n $M = $B\n" ---- -id: manual-str-join -language: python -severity: warning -message: Augmented assignment in loop - if building a string, use ''.join() -metadata: - weight: 3 - category: slop -rule: - kind: for_statement - has: - kind: block - has: - kind: expression_statement - has: - kind: augmented_assignment - pattern: $S += $X ---- -id: nested-if-no-else -language: python -severity: warning -message: Nested if statements without else - consider flattening or combining conditions -metadata: - weight: 3 - category: slop -rule: - kind: if_statement - has: - kind: block - has: - kind: if_statement - not: - has: - kind: else_clause ---- -id: pointless-lambda-call -language: python -severity: warning -message: Immediately called lambda - just execute the code -metadata: - weight: 3 - category: slop -rule: - kind: call - has: - kind: parenthesized_expression - has: - kind: lambda ---- -id: repeated-dict-key-assignment -language: python -severity: warning -message: Repeated dict[key] = func(...) pattern - consider loop or dict comprehension -metadata: - weight: 3 - category: slop -rule: - kind: expression_statement - has: - kind: assignment - pattern: $DICT[$KEY] = $FUNC($$$) - follows: - kind: expression_statement - has: - kind: assignment - pattern: $DICT[$KEY2] = $FUNC($$$) ---- -id: repeated-if-continue -language: python -severity: warning -message: Multiple if-continue guards - consider list comprehension or filter function -metadata: - weight: 3 - category: slop -rule: - kind: for_statement - has: - kind: block - has: - kind: if_statement - has: - kind: block - has: - kind: continue_statement - follows: - kind: if_statement - has: - kind: block - has: - kind: continue_statement ---- -id: repeated-if-return-error -language: python -severity: warning -message: Multiple if-return error checks - consolidate validation logic -metadata: - weight: 3 - category: slop -rule: - kind: if_statement - has: - kind: block - has: - kind: return_statement - regex: error|Error|ERROR - follows: - kind: if_statement - has: - kind: block - has: - kind: return_statement - regex: error|Error|ERROR ---- -id: repeated-validation-calls -language: python -severity: warning -message: Repeated validation function calls - consider data-driven validation -metadata: - weight: 3 - category: slop -rule: - kind: expression_statement - has: - kind: call - regex: ^validate_ - follows: - kind: expression_statement - has: - kind: call - regex: ^validate_ ---- -id: ternary-same-value -language: python -severity: warning -message: Ternary returns same value for both branches - remove condition -metadata: - weight: 3 - category: slop -rule: - kind: conditional_expression - pattern: $VALUE if $COND else $VALUE ---- -id: repeated-function-or-chain -language: python -severity: warning -message: Same function called 4+ times with 'or' - use any(func(x) for x in [...]) -metadata: - weight: 3 - category: slop -rule: - kind: boolean_operator - pattern: $F($$$A) or $F($$$B) or $F($$$C) or $F($$$D) ---- -id: value-compare-return-ladder -language: python -severity: warning -message: Long value comparison ladder returning literals; prefer a dispatch map or - dict lookup with a default. -metadata: - weight: 3 - category: slop -rule: - kind: if_statement - pattern: "if $X == $V1:\n return $R1\nelif $X == $V2:\n return $R2\nelif $X ==\ - \ $V3:\n return $R3\n" - not: - has: - kind: else_clause ---- -id: repetitive-list-append-elif -language: python -severity: warning -message: Appending to the same list across elifs reads like copy/paste - refactor -metadata: - weight: 3 - category: slop -rule: - pattern: "if $C1:\n $L.append($A)\nelif $C2:\n $L.append($B)\nelif $C3:\n $L.append($C)\n" ---- -id: for-range-len -language: python -severity: warning -message: range(len(seq)) loop suggests index juggling; prefer enumerate -metadata: - weight: 3 - category: slop -rule: - pattern: "for $I in range(len($SEQ)):\n $$$\n" ---- -id: boolean-return-if-else -language: python -severity: warning -message: if/else returning True/False - simplify to return the condition directly -metadata: - weight: 3 - category: slop -rule: - kind: if_statement - any: - - pattern: "if $COND:\n return True\nelse:\n return False\n" - - pattern: "if $COND:\n return False\nelse:\n return True\n" ---- -id: json-dumps-then-loads -language: python -severity: warning -message: json.loads(json.dumps(x)) is noisy; copy the structure directly -metadata: - weight: 3 - category: slop -rule: - pattern: json.loads(json.dumps($X)) ---- -id: pointless-bool-cast -language: python -severity: warning -message: Wrapping a condition in bool() before if is redundant ceremony -metadata: - weight: 3 - category: slop -rule: - kind: if_statement - pattern: "if bool($COND):\n $$$\n" ---- -id: consecutive-if-same-variable -language: python -severity: warning -message: Consecutive if statements checking same variable - should use elif for efficiency -metadata: - weight: 3 - category: slop -rule: - kind: if_statement - has: - kind: comparison_operator - has: - kind: identifier - pattern: $VAR - follows: - kind: if_statement - all: - - has: - kind: comparison_operator - has: - kind: identifier - pattern: $VAR - - has: - kind: block - has: - kind: return_statement - stopBy: neighbor ---- -id: nested-attribute-guard-chain -language: python -severity: warning -message: 'Nested attribute guard chain (if x: if x.y: if x.y.z:) - use walrus operator - or getattr' -metadata: - weight: 3 - category: slop -rule: - kind: if_statement - all: - - has: - kind: identifier - pattern: $OBJ - - has: - kind: block - has: - kind: if_statement - all: - - has: - kind: attribute - has: - kind: identifier - pattern: $OBJ - - has: - kind: block - has: - kind: if_statement ---- -id: repeated-isinstance-validation -language: python -severity: warning -message: Repeated isinstance validation pattern - extract to validation helper or - use schema -metadata: - weight: 3 - category: slop -rule: - kind: if_statement - all: - - has: - kind: not_operator - has: - kind: call - pattern: isinstance($VAR, $TYPE) - - has: - kind: block - has: - kind: raise_statement - follows: - kind: if_statement - all: - - has: - kind: not_operator - has: - kind: call - regex: isinstance - - has: - kind: block - has: - kind: raise_statement - stopBy: neighbor ---- -id: split-magic-index -language: python -severity: warning -message: String split with magic index - extract to named variable or use unpacking -metadata: - weight: 3 - category: slop -rule: - kind: subscript - all: - - has: - kind: call - has: - kind: attribute - regex: \.split$ - - has: - kind: integer - regex: ^[2-9]|[1-9][0-9]+$ ---- -id: ternary-none-comparison -language: python -severity: warning -message: Ternary with None comparison - consider using 'or' operator or walrus -metadata: - weight: 3 - category: slop -rule: - kind: conditional_expression - pattern: $VAL if $VAR is None else $OTHER ---- -id: redundant-bool-in-condition -language: python -severity: warning -message: Redundant bool() cast in condition - if already checks truthiness -metadata: - weight: 3 - category: slop -rule: - kind: if_statement - has: - kind: boolean_operator - has: - kind: call - pattern: bool($X) ---- -id: deep-dict-access -language: python -severity: warning -message: Deep nested dict access (4+ levels) - extract to helper or use dataclass -metadata: - weight: 3 - category: slop -rule: - kind: subscript - has: - kind: subscript - has: - kind: subscript - has: - kind: subscript ---- -id: long-tuple-unpacking -language: python -severity: warning -message: Unpacking 5+ values from tuple - use named tuple or dataclass -metadata: - weight: 3 - category: slop -rule: - kind: assignment - has: - kind: pattern_list - has: - nthChild: - position: 5 ---- -id: fetchone-none-check -language: python -severity: hint -message: cursor.fetchone() followed by None check - common pattern, consider helper -metadata: - weight: 3 - category: slop -rule: - kind: if_statement - has: - kind: comparison_operator - regex: is None - follows: - kind: expression_statement - has: - kind: assignment - has: - kind: call - has: - kind: attribute - regex: \.fetchone$ - stopBy: neighbor ---- -id: dict-get-zero-default -language: python -severity: warning -message: dict.get() with 0 default - consider if missing key should really be 0 -metadata: - weight: 3 - category: slop -rule: - kind: call - all: - - has: - kind: attribute - regex: \.get$ - - has: - kind: argument_list - has: - kind: integer - regex: ^0$ - nthChild: - position: 2 ---- -id: dict-get-empty-string-default -language: python -severity: warning -message: dict.get() with '' default - consider if missing key should really be empty - string -metadata: - weight: 3 - category: slop -rule: - kind: call - all: - - has: - kind: attribute - regex: \.get$ - - has: - kind: argument_list - has: - kind: string - regex: ^['"]['"]$ - nthChild: - position: 2 ---- -id: dict-get-empty-list-default -language: python -severity: warning -message: dict.get() with [] default - mutable default, use .setdefault() or defaultdict -metadata: - weight: 3 - category: slop -rule: - kind: call - all: - - has: - kind: attribute - regex: \.get$ - - has: - kind: argument_list - has: - kind: list - not: - has: - any: - - kind: identifier - - kind: integer - - kind: string - nthChild: - position: 2 ---- -id: dict-get-empty-dict-default -language: python -severity: warning -message: dict.get() with {} default - mutable default, use .setdefault() or defaultdict -metadata: - weight: 3 - category: slop -rule: - kind: call - all: - - has: - kind: attribute - regex: \.get$ - - has: - kind: argument_list - has: - kind: dictionary - not: - has: - kind: pair - nthChild: - position: 2 ---- -id: check-key-in-dict-keys -language: python -severity: warning -message: Checking 'key in dict.keys()' - inefficient, use 'key in dict' -metadata: - weight: 3 - category: slop -rule: - kind: comparison_operator - regex: \sin\s - has: - kind: call - has: - kind: attribute - regex: \.keys$ ---- -id: json-loads-read -language: python -severity: warning -message: json.loads(f.read()) - use json.load(f) -metadata: - weight: 3 - category: slop -rule: - kind: call - has: - kind: argument_list - has: - kind: call - has: - kind: attribute - regex: \.read$ ---- -id: json-roundtrip-dumps-loads -language: python -severity: warning -message: json.loads(json.dumps(obj)) round-trip without mutation is pointless; avoid - double serialization. -metadata: - weight: 3 - category: slop -rule: - kind: call - pattern: json.loads(json.dumps($OBJ)) ---- -id: list-dict-keys -language: python -severity: warning -message: list(d.keys()) is verbose - use list(d) or iterate directly -metadata: - weight: 3 - category: slop -rule: - kind: call - all: - - has: - kind: identifier - regex: ^list$ - - has: - kind: argument_list - has: - kind: call - has: - kind: attribute - regex: \.keys$ ---- -id: listcomp-in-builtin-call -language: python -severity: warning -message: List comprehension in all/any/sum/set/dict call - use generator expression - or comprehension for memory efficiency -metadata: - weight: 3 - category: slop -rule: - kind: call - all: - - has: - kind: identifier - regex: ^(all|any|sum|set|dict)$ - - has: - kind: argument_list - has: - kind: list_comprehension ---- -id: unnecessary-list-call -language: python -severity: warning -message: Unnecessary list() around iterable in for loop -metadata: - weight: 3 - category: slop -rule: - kind: for_statement - has: - kind: call - has: - kind: identifier - regex: ^list$ ---- -id: len-as-condition -language: python -severity: warning -message: Comparing len() to 0 is verbose - use truthiness 'if x:' or 'if not x:' -metadata: - weight: 3 - category: slop -rule: - kind: comparison_operator - all: - - has: - kind: call - has: - kind: identifier - regex: ^len$ - - has: - kind: integer - pattern: '0' ---- -id: list-map-filter -language: python -severity: warning -message: list(map(...)) or list(filter(...)) - consider list comprehension instead -metadata: - weight: 3 - category: slop -rule: - kind: call - all: - - has: - kind: identifier - regex: ^list$ - - has: - kind: argument_list - has: - kind: call - has: - kind: identifier - regex: ^(map|filter)$ ---- -id: range-len-antipattern -language: python -severity: warning -message: for i in range(len(x)) - use 'enumerate(x)' or iterate directly -metadata: - weight: 3 - category: slop -rule: - kind: for_statement - has: - kind: call - all: - - has: - kind: identifier - regex: ^range$ - - has: - kind: argument_list - has: - kind: call - has: - kind: identifier - regex: ^len$ ---- -id: membership-test-list-literal -language: python -severity: warning -message: Membership test on list literal with 4+ items - use set literal {...} for - O(1) lookup -metadata: - weight: 3 - category: slop -rule: - kind: comparison_operator - pattern: $X in [$A, $B, $C, $D, $$$REST] ---- -id: bool-comparison -language: python -severity: warning -message: Comparing to boolean literal - use 'if x' instead of 'if x == True' -metadata: - weight: 2 - category: slop -rule: - kind: comparison_operator - regex: ==\s*(True|False) ---- -id: chained-none-check -language: python -severity: warning -message: Chained 'is not None' checks - use try/except or walrus operator -metadata: - weight: 2 - category: slop -rule: - kind: boolean_operator - regex: is not None\s+(and|or)\s+\w+\s+is not None ---- -id: dict-get-default-none -language: python -severity: warning -message: dict.get(k, None) - None is the default return value, so the second argument - is redundant -metadata: - weight: 2 - category: slop -rule: - kind: call - all: - - has: - kind: attribute - regex: \.get$ - - has: - kind: argument_list - has: - kind: none ---- -id: empty-init -language: python -severity: warning -message: Empty __init__ method - redundant -metadata: - weight: 2 - category: slop -rule: - kind: function_definition - all: - - has: - kind: identifier - regex: ^__init__$ - - has: - kind: block - has: - kind: pass_statement ---- -id: empty-string-or -language: python -severity: warning -message: Checking empty string with 'or' - defensive pattern -metadata: - weight: 2 - category: slop -rule: - kind: boolean_operator - regex: ==\s*(""|'')\s+(or|and) ---- -id: explicit-bool-cast -language: python -severity: warning -message: bool() cast - usually unnecessary, use truthiness directly -metadata: - weight: 2 - category: slop -rule: - kind: call - has: - kind: identifier - regex: ^bool$ ---- -id: get-then-none-check -language: python -severity: warning -message: .get() followed by None check - use 'in' check or EAFP -metadata: - weight: 2 - category: slop -rule: - kind: if_statement - has: - kind: comparison_operator - regex: is None|is not None - follows: - kind: expression_statement - has: - kind: assignment - has: - kind: call - has: - kind: attribute - regex: \.get$ ---- -id: guard-return-none -language: python -severity: warning -message: 'if x is None: return None - overly defensive early guard' -metadata: - weight: 2 - category: slop -rule: - kind: if_statement - pattern: 'if $X is None: return None' ---- -id: if-none-raise -language: python -severity: warning -message: 'if x is None: raise - defensive None guard before operation' -metadata: - weight: 2 - category: slop -rule: - kind: if_statement - all: - - has: - kind: comparison_operator - regex: is None - - has: - kind: block - has: - kind: raise_statement ---- -id: if-not-guard -language: python -severity: warning -message: 'if not x: raise/return - defensive guard pattern' -metadata: - weight: 2 - category: slop -rule: - kind: if_statement - all: - - has: - kind: not_operator - - has: - kind: block - any: - - has: - kind: raise_statement - - has: - kind: return_statement ---- -id: if-return-bool-else -language: python -severity: warning -message: if/else returning True/False - simplify to 'return bool(condition)' -metadata: - weight: 2 - category: slop -rule: - kind: if_statement - all: - - has: - kind: block - has: - kind: return_statement - regex: return True - - has: - kind: else_clause - has: - kind: block - has: - kind: return_statement - regex: return False ---- -id: int-float-coerce -language: python -severity: warning -message: Casting via int(float(x)) is sloppy and can mis-handle strings; parse once - or validate explicitly. -metadata: - weight: 2 - category: slop -rule: - kind: call - pattern: int(float($X)) ---- -id: len-comparison -language: python -severity: warning -message: len() > 0 or len() != 0 - use truthiness instead -metadata: - weight: 2 - category: slop -rule: - kind: comparison_operator - has: - kind: call - regex: ^len\( - regex: (>\s*0|!=\s*0|>=\s*1) ---- -id: manual-dict-setdefault -language: python -severity: warning -message: 'Manual dict setdefault pattern (if k not in d: d[k] = ...) - use d.setdefault()' -metadata: - weight: 2 - category: slop -rule: - kind: if_statement - all: - - has: - kind: comparison_operator - regex: not in - - has: - kind: block - has: - kind: expression_statement - has: - kind: assignment - has: - kind: subscript ---- -id: multiple-isinstance-or -language: python -severity: warning -message: Multiple isinstance() with or - use isinstance(x, (A, B)) tuple form -metadata: - weight: 2 - category: slop -rule: - kind: boolean_operator - regex: isinstance.*or.*isinstance ---- -id: range-len-pattern -language: python -severity: warning -message: range(len(x)) - use enumerate() instead -metadata: - weight: 2 - category: slop -rule: - kind: call - pattern: range(len($X)) ---- -id: redundant-bool-ternary -language: python -severity: warning -message: Redundant boolean ternary 'True if x else False' - use 'bool(x)' -metadata: - weight: 2 - category: slop -rule: - kind: conditional_expression - regex: True\s+if.*else\s+False|False\s+if.*else\s+True ---- -id: redundant-continue -language: python -severity: warning -message: Redundant continue at end of loop -metadata: - weight: 2 - category: slop -rule: - kind: block - regex: continue\s*$ - inside: - any: - - kind: for_statement - - kind: while_statement ---- -id: redundant-list-comprehension -language: python -severity: warning -message: Redundant list comprehension - usage of [x for x in iterable] is unnecessary, - use list(iterable) -metadata: - weight: 2 - category: slop -rule: - kind: list_comprehension - pattern: '[$X for $X in $ITER]' ---- -id: redundant-none-empty-check -language: python -severity: warning -message: Redundant None and empty check - use truthiness -metadata: - weight: 2 - category: slop -rule: - kind: boolean_operator - regex: is not None.*(!=\s*""|len\(|==\s*""|!=\s*''|==\s*'') ---- -id: redundant-return-none -language: python -severity: warning -message: Explicit 'return None' at end of function - implicit None is cleaner -metadata: - weight: 2 - category: slop -rule: - kind: return_statement - regex: ^return None$ ---- -id: set-literal-list -language: python -severity: warning -message: set([list]) - use set literal {x, y} instead -metadata: - weight: 2 - category: slop -rule: - kind: call - has: - kind: argument_list - has: - kind: list ---- -id: unnecessary-cast-str -language: python -severity: warning -message: Unnecessary str() cast on string literal or f-string -metadata: - weight: 2 - category: slop -rule: - kind: call - all: - - has: - kind: identifier - regex: ^str$ - - has: - kind: argument_list - has: - kind: string ---- -id: unnecessary-elif -language: python -severity: warning -message: Unnecessary elif after return/raise - use if instead -metadata: - weight: 2 - category: slop -rule: - kind: if_statement - all: - - has: - kind: block - any: - - has: - kind: return_statement - - has: - kind: raise_statement - - has: - kind: elif_clause ---- -id: unnecessary-else-raise -language: python -severity: warning -message: else after raise - the else block is redundant -metadata: - weight: 2 - category: slop -rule: - kind: if_statement - all: - - has: - kind: block - has: - kind: raise_statement - - has: - kind: else_clause ---- -id: unnecessary-lambda -language: python -severity: warning -message: 'Unnecessary lambda - lambda x: func(x) can be replaced by func' -metadata: - weight: 2 - category: slop -rule: - kind: lambda - pattern: 'lambda $ARG: $FN($ARG)' ---- -id: verbose-and-return -language: python -severity: warning -message: 'Early ''if not x: return False'' pattern - may be simplifiable' -metadata: - weight: 2 - category: slop -rule: - kind: if_statement - all: - - has: - kind: not_operator - - has: - kind: block - has: - kind: return_statement - regex: return False - not: - has: - kind: else_clause ---- -id: verbose-none-default -language: python -severity: warning -message: Verbose None default pattern - use 'x = x or default' -metadata: - weight: 2 - category: slop -rule: - pattern: "if $VAR is None:\n $VAR = $DEFAULT\n" ---- -id: verbose-or-return -language: python -severity: warning -message: 'Early ''if x: return True'' pattern - may be simplifiable' -metadata: - weight: 2 - category: slop -rule: - kind: if_statement - has: - kind: block - has: - kind: return_statement - regex: return True - not: - has: - kind: else_clause ---- -id: verbose-dict-key-access -language: python -severity: warning -message: Verbose dict access - use dict.get() or walrus operator (var := dict.get(key)) -metadata: - weight: 2 - category: slop -rule: - kind: if_statement - pattern: "if \"$KEY\" in $DICT:\n $VAR = $DICT[\"$KEY\"]\n" ---- -id: defensive-isinstance-return -language: python -severity: warning -message: Defensive isinstance check - consider type hints, schema validation, or EAFP -metadata: - weight: 2 - category: slop -rule: - kind: if_statement - pattern: "if not isinstance($VAR, $TYPE):\n return $ERR\n" ---- -id: defensive-if-return-same -language: python -severity: warning -message: Defensive if-return-same - simplify to 'return x or None' or use walrus operator -metadata: - weight: 2 - category: slop -rule: - kind: if_statement - pattern: "if $X:\n return $X\n" ---- -id: redundant-str-in-fstring -language: python -severity: warning -message: Redundant str() call inside f-string - f-strings automatically convert to - string -metadata: - weight: 2 - category: slop -rule: - kind: interpolation - has: - kind: call - pattern: str($X) ---- -id: return-ternary-or-none -language: python -severity: warning -message: Return ternary 'x if x else None' is redundant - just return x -metadata: - weight: 2 - category: slop -rule: - kind: return_statement - has: - kind: conditional_expression - pattern: $X if $X else None ---- -id: if-pass-else-action -language: python -severity: warning -message: if-pass-else pattern - invert the condition and remove pass branch -metadata: - weight: 2 - category: slop -rule: - kind: if_statement - all: - - has: - kind: block - has: - kind: pass_statement - not: - has: - kind: expression_statement - - has: - kind: else_clause ---- -id: dict-comprehension-from-keys -language: python -severity: warning -message: dict.fromkeys() pattern - consider dict comprehension for clarity -metadata: - weight: 2 - category: slop -rule: - kind: call - pattern: dict.fromkeys($KEYS, $VAL) ---- -id: defensive-or-empty -language: python -severity: warning -message: '''x if x else []'' - use ''x or []'' instead' -metadata: - weight: 2 - category: slop -rule: - kind: conditional_expression - any: - - pattern: $X if $X else [] - - pattern: $X if $X else {} ---- -id: isinstance-bool-exclusion -language: python -severity: warning -message: isinstance(x, bool) check - over-defensive type exclusion -metadata: - weight: 2 - category: slop -rule: - kind: call - regex: isinstance\([^,]+,\s*bool\) ---- -id: isinstance-guard-raise -language: python -severity: warning -message: Defensive isinstance check that raises on failure - excessive type validation -metadata: - weight: 2 - category: slop -rule: - kind: if_statement - all: - - has: - kind: not_operator - has: - kind: call - regex: isinstance\( - - has: - kind: block - has: - any: - - has: - kind: raise_statement - - has: - kind: call - regex: exit ---- -id: type-equality -language: python -severity: warning -message: type(x) == Type comparison - use isinstance() instead -metadata: - weight: 2 - category: slop -rule: - kind: comparison_operator - has: - kind: call - regex: ^type\( ---- -id: dict-str-any -language: python -severity: warning -message: Dict[str, Any] return/field - prefer precise value types instead of Any -metadata: - weight: 2 - category: slop -rule: - kind: type - regex: ^Dict\[\s*str\s*,\s*Any\s*\]$ ---- -id: list-any -language: python -severity: warning -message: List[Any] annotation - tighten element typing instead of using Any -metadata: - weight: 2 - category: slop -rule: - kind: type - regex: ^List\[\s*Any\s*\]$ ---- -id: union-with-any -language: python -severity: warning -message: Union containing Any defeats type checking - drop Any or use a narrower union -metadata: - weight: 2 - category: slop -rule: - kind: type - regex: Union\[[^\]]*Any[^\]]*\] ---- -id: object-type-annotation -language: python -severity: warning -message: Using 'object' as type annotation is too broad - use a specific type, Protocol, - or Any -metadata: - weight: 2 - category: slop -rule: - kind: type - any: - - regex: ^object$ - - regex: ^"object"$ - - regex: ^'object'$ ---- -id: verbose-dict-update -language: python -severity: warning -message: Verbose dict assignment in loop - consider dict comprehension -metadata: - weight: 4 - category: slop -rule: - kind: for_statement - has: - kind: block - has: - kind: expression_statement - has: - kind: assignment - has: - kind: subscript ---- -id: verbose-list-append-loop -language: python -severity: warning -message: Loop with list.append() - consider list comprehension -metadata: - weight: 4 - category: slop -rule: - kind: for_statement - has: - kind: block - has: - kind: expression_statement - has: - kind: call - has: - kind: attribute - regex: \.append$ ---- -id: verbose-dict-elif-updates -language: python -severity: warning -message: Repeated dict[...] assignments across if/elif branches; prefer a dispatch - map or helper to centralize the updates. -metadata: - weight: 4 - category: slop -rule: - pattern: "if $COND1:\n $$$PRE1\n $D[$KEY1] = $VAL1\n $$$POST1\nelif $COND2:\n\ - \ $$$PRE2\n $D[$KEY2] = $VAL2\n $$$POST2\n" ---- -id: verbose-missing-remove-loop -language: python -severity: warning -message: Finding missing items by copying a list and removing matches is verbose; - use set difference instead. -metadata: - weight: 4 - category: slop -rule: - kind: for_statement - pattern: "for $ITEM in $REQ:\n if $ITEM in $TARGET:\n $MISSING.remove($ITEM)\n" - follows: - kind: expression_statement - has: - pattern: $MISSING = list($REQ) - stopBy: end ---- -id: duplicate-regex-return -language: python -severity: warning -message: Repeating re.search checks returning the same code; consolidate into one - pattern or helper. -metadata: - weight: 4 - category: slop -rule: - kind: if_statement - pattern: "if re.search($PAT_A, $TEXT):\n return $CODE\n" - follows: - pattern: "if re.search($PAT_B, $TEXT):\n return $CODE\n" ---- -id: tokenizer-char-append-branch -language: python -severity: warning -message: Tokenizer branches appending tokens one character at a time; use a lookup - table to map chars to tokens instead of repetitive append/pos++ blocks. -metadata: - weight: 4 - category: slop -rule: - kind: expression_statement - has: - pattern: $OBJ.tokens.append($NEW_TOKEN) - precedes: - kind: expression_statement - has: - pattern: $OBJ.pos += 1 ---- -id: manual-quote-strip -language: python -severity: warning -message: Manually stripping quotes with startswith/endswith is verbose; use a helper - or literal parser. -metadata: - weight: 4 - category: slop -rule: - pattern: "if $VAL.startswith($Q1) and $VAL.endswith($Q1):\n $OUT = $VAL[1:-1]\n" ---- -id: manual-bool-str-parse -language: python -severity: warning -message: Parsing 'true'/'false' string literals manually; prefer literal_eval/json - parsing or centralized helpers. -metadata: - weight: 4 - category: slop -rule: - pattern: "if $VAL == 'true':\n $OUT = True\nelif $VAL == 'false':\n $OUT =\ - \ False\n" ---- -id: manual-float-dot-check -language: python -severity: warning -message: Using '.' in string to guess float default is brittle; parse with a real - literal parser. -metadata: - weight: 4 - category: slop -rule: - pattern: "if '.' in $VAL:\n $OUT = float($VAL)\n" ---- -id: manual-line-continuation-join -language: python -severity: warning -message: Building continued lines via current_line += ' ' + stripped is verbose; join - once or use a dedicated helper. -metadata: - weight: 4 - category: slop -rule: - pattern: $LINE += " " + $PART ---- -id: init-populate-iterate-dict -language: python -severity: hint -message: 'Initialize-populate-iterate pattern detected on ''$DICT''. - - Empty dict is created, populated in a loop, then iterated over. - - ' -note: 'Consider refactoring to: - - 1. Dict comprehension: {key: val for item in items} - - 2. Helper function: def build_lookup(items) -> dict - - 3. Inline the logic if the intermediate dict isn''t needed elsewhere - - ' -metadata: - weight: 4 - category: slop -rule: - pattern: "for $VARS in $DICT.$METHOD():\n $$$BODY\n" - follows: - all: - - kind: for_statement - - has: - kind: subscript - all: - - has: - kind: identifier - field: value - pattern: $DICT - - inside: - kind: assignment - field: left - stopBy: end - - follows: - kind: expression_statement - has: - kind: assignment - all: - - has: - kind: identifier - field: left - pattern: $DICT - - has: - kind: dictionary - field: right - not: - has: - kind: pair - stopBy: - kind: for_statement - stopBy: end -constraints: - METHOD: - regex: ^(items|keys|values)$ ---- -id: init-populate-iterate-list -language: python -severity: hint -message: 'Initialize-populate-iterate pattern detected on ''$LIST''. - - Empty list is created, populated via .append() in a loop, then iterated over. - - ' -note: 'Consider refactoring to: - - 1. List comprehension: [val for item in items if cond] - - 2. Helper function: def collect_items(source) -> list - - 3. Generator expression if only iterating once: (val for item in items) - - ' -metadata: - weight: 4 - category: slop -rule: - pattern: "for $VAR in $LIST:\n $$$BODY\n" - not: - has: - kind: call - has: - kind: attribute - regex: \.(items|keys|values)$ - follows: - all: - - kind: for_statement - - has: - kind: call - has: - kind: attribute - all: - - has: - kind: identifier - field: object - pattern: $LIST - - has: - field: attribute - regex: ^append$ - stopBy: end - - follows: - kind: expression_statement - has: - kind: assignment - all: - - has: - kind: identifier - field: left - pattern: $LIST - - has: - kind: list - field: right - not: - has: - any: - - kind: identifier - - kind: string - - kind: integer - stopBy: - kind: for_statement - stopBy: end ---- -id: verbose-type-conversion-chain -language: python -severity: warning -message: Chained type conversions (str(int(...)) or similar) - simplify or validate - input -metadata: - weight: 4 - category: slop -rule: - kind: call - any: - - pattern: str(int($X)) - - pattern: int(str($X)) - - pattern: str(float($X)) - - pattern: float(str($X)) - - pattern: list(tuple($X)) - - pattern: tuple(list($X)) ---- -id: double-conversion-int-float -language: python -severity: warning -message: Double conversion int(float(x)) - parse directly or validate input format -metadata: - weight: 4 - category: slop -rule: - kind: call - any: - - pattern: int(float($X)) - - pattern: float(int($X)) ---- -id: list-comprehension-identity -language: python -severity: warning -message: List comprehension with identity transform - use list() directly -metadata: - weight: 4 - category: slop -rule: - kind: list_comprehension - pattern: '[$ITEM for $ITEM in $ITER if $COND]' - not: - has: - kind: if_clause - has: - kind: identifier - pattern: $ITEM ---- -id: consecutive-append-calls -language: python -severity: warning -message: Consecutive list.append() calls - use extend() with a list literal -metadata: - weight: 4 - category: slop -rule: - kind: expression_statement - has: - kind: call - has: - kind: attribute - regex: \.append$ - follows: - kind: expression_statement - has: - kind: call - has: - kind: attribute - regex: \.append$ - stopBy: neighbor ---- -id: verbose-dict-iteration -language: python -severity: warning -message: Iterating dict.keys() explicitly - dict iteration yields keys by default -metadata: - weight: 4 - category: slop -rule: - kind: for_statement - pattern: "for $KEY in $DICT.keys():\n $$$BODY\n" ---- -id: verbose-string-concat-str -language: python -severity: warning -message: str() + str() concatenation - use f-string instead -metadata: - weight: 4 - category: slop -rule: - pattern: str($X) + str($Y) ---- -id: verbose-range-zero-start -language: python -severity: warning -message: range(0, n) - the 0 is the default start, use range(n) -metadata: - weight: 4 - category: slop -rule: - pattern: range(0, $END) ---- -id: verbose-string-literal-plus-str -language: python -severity: warning -message: String literal + str() - use f-string instead -metadata: - weight: 4 - category: slop -rule: - kind: binary_operator - regex: ^['"].*['"]\s*\+\s*str\( ---- -id: verbose-sorted-list -language: python -severity: warning -message: sorted(list(x)) - sorted() already creates a new list -metadata: - weight: 4 - category: slop -rule: - pattern: sorted(list($X)) ---- -id: items-unused-value -language: python -severity: warning -message: Iterating dict.items() but only using key - use 'for k in d:' instead -metadata: - weight: 4 - category: slop -rule: - kind: for_statement - pattern: "for $KEY, _ in $DICT.items():\n $$$BODY\n" ---- -id: items-unused-key -language: python -severity: warning -message: Iterating dict.items() but only using value - use 'for v in d.values():' - instead -metadata: - weight: 4 - category: slop -rule: - kind: for_statement - pattern: "for _, $VAL in $DICT.items():\n $$$BODY\n" ---- -id: list-extend-from-loop -language: python -severity: warning -message: Loop appending items from iterable - use list.extend() instead -metadata: - weight: 4 - category: slop -rule: - kind: for_statement - pattern: "for $ITEM in $ITER:\n $LIST.append($ITEM)\n" ---- -id: join-list-literal -language: python -severity: warning -message: join([...]) materializes a list before join - pass an iterable directly -metadata: - weight: 4 - category: slop -rule: - kind: call - all: - - has: - kind: attribute - regex: \.join$ - - has: - kind: argument_list - has: - kind: list ---- -id: join-list-comprehension -language: python -severity: warning -message: join([expr for ...]) builds a throwaway list - use a generator expression -metadata: - weight: 4 - category: slop -rule: - kind: call - all: - - has: - kind: attribute - regex: \.join$ - - has: - kind: argument_list - has: - kind: list_comprehension ---- -id: manual-sum-loop -language: python -severity: warning -message: Manual accumulation loop - use sum(...) instead of a throwaway counter variable -metadata: - weight: 4 - category: slop -rule: - kind: for_statement - pattern: "for $ITEM in $ITER:\n $TOTAL += $EXPR\n" - follows: - kind: expression_statement - has: - kind: assignment - pattern: $TOTAL = 0 - stopBy: neighbor ---- -id: manual-count-loop -language: python -severity: warning -message: Manual counting loop - use sum(1 for ...) or len(...) instead of a counter - variable -metadata: - weight: 4 - category: slop -rule: - kind: for_statement - pattern: "for $ITEM in $ITER:\n $COUNT += 1\n" - follows: - kind: expression_statement - has: - kind: assignment - pattern: $COUNT = 0 - stopBy: neighbor ---- -id: set-add-loop -language: python -severity: warning -message: Loop building a set with add() - use a set comprehension -metadata: - weight: 4 - category: slop -rule: - kind: for_statement - pattern: "for $ITEM in $ITER:\n $SET.add($VAL)\n" ---- -id: manual-dict-get-assign -language: python -severity: warning -message: 'if key in dict: ... else: ... assignment ladder - use dict.get(...)' -metadata: - weight: 4 - category: slop -rule: - pattern: "if $KEY in $DICT:\n $OUT = $DICT[$KEY]\nelse:\n $OUT = $DEFAULT\n" ---- -id: frozenset-list-wrap -language: python -severity: warning -message: frozenset(...) does not need a prebuilt list - avoid the extra materialization -metadata: - weight: 4 - category: slop -rule: - kind: call - all: - - has: - kind: identifier - regex: ^frozenset$ - - has: - kind: argument_list - has: - any: - - kind: list - - kind: list_comprehension ---- -id: list-self-concat -language: python -severity: warning -message: list = list + [item] is noisier than append() and copies the list each time -metadata: - weight: 4 - category: slop -rule: - pattern: $LIST = $LIST + [$ITEM] ---- -id: manual-dict-counter-if-else -language: python -severity: warning -message: Manual dict counter ladder - use dict.get(..., 0) + 1, setdefault(), or Counter -metadata: - weight: 4 - category: slop -rule: - pattern: "if $KEY in $DICT:\n $DICT[$KEY] += 1\nelse:\n $DICT[$KEY] = 1\n" ---- -id: set-from-list-literal -language: python -severity: warning -message: Wrapping a list literal in set() adds needless ceremony -metadata: - weight: 4 - category: slop -rule: - kind: call - has: - kind: argument_list - has: - kind: list ---- -id: sorted-dict-keys-wrap -language: python -severity: warning -message: sorted(d.keys()) is noisier than sorted(d) -metadata: - weight: 4 - category: slop -rule: - pattern: sorted($DICT.keys()) ---- -id: predicate-count-via-sum-one -language: python -severity: warning -message: sum(1 for ...) is noisier than summing booleans directly -metadata: - weight: 3 - category: slop -rule: - pattern: sum(1 for $ITEM in $ITER if $COND) ---- -id: manual-index-unpack -language: python -severity: warning -message: Manual index unpacking is noisier than tuple unpacking -metadata: - weight: 3 - category: slop -rule: - pattern: '$A = $PAIR[0] $B = $PAIR[1] ' ---- -id: redundant-guard-same-return -language: python -severity: warning -message: Guard returning the same expression in both paths adds dead ceremony -metadata: - weight: 4 - category: slop -rule: - pattern: 'if $COND: return $RET return $RET ' ---- -id: duplicate-wrapper-return-branches -language: python -severity: warning -message: Duplicating the same wrapper call across return branches is verbose -metadata: - weight: 4 - category: slop -rule: - pattern: 'if $COND: return $F($A) else: return $F($B) ' ---- -id: materialize-then-sort-return -language: python -severity: warning -message: Building a throwaway list before returning sorted(...) is verbose -metadata: - weight: 4 - category: slop -rule: - kind: return_statement - pattern: return sorted($TMP) - follows: - kind: expression_statement - has: - kind: assignment - any: - - pattern: $TMP = [$X for $Y in $ITER] - - pattern: $TMP = [$X for $Y in $ITER if $COND] - stopBy: neighbor ---- -id: sorted-list-comprehension-wrap -language: python -severity: warning -message: sorted([...]) materializes a throwaway list before sorting -metadata: - weight: 4 - category: slop -rule: - any: - - pattern: sorted([$X for $Y in $ITER]) - - pattern: sorted([$X for $Y in $ITER if $COND]) ---- -id: set-generator-wrap -language: python -severity: warning -message: set(generator) is noisier than a set comprehension -metadata: - weight: 3 - category: slop -rule: - any: - - pattern: set($X for $Y in $ITER) - - pattern: set($X for $Y in $ITER if $COND) ---- -id: manual-find-return-none -language: python -severity: warning -message: First-match search loop returning None is verbose; use next(..., None) -metadata: - weight: 4 - category: slop -rule: - pattern: 'for $ITEM in $ITER: if $COND: return $RET return None ' ---- -id: conditional-same-target-append -language: python -severity: warning -message: Branches appending to the same list are verbose; move the branch into the - value -metadata: - weight: 4 - category: slop -rule: - pattern: 'if $COND: $OUT.append($A) else: $OUT.append($B) ' ---- -id: conditional-same-target-extend -language: python -severity: warning -message: Branches extending the same list are verbose; move the branch into the value -metadata: - weight: 4 - category: slop -rule: - pattern: 'if $COND: $OUT.extend($A) else: $OUT.extend($B) ' ---- -id: conditional-same-target-subscript-assign -language: python -severity: warning -message: Branches assigning the same container slot are verbose; branch on the value - instead -metadata: - weight: 4 - category: slop -rule: - pattern: 'if $COND: $OBJ[$KEY] = $A else: $OBJ[$KEY] = $B ' ---- -id: duplicate-wrapper-assign-branches -language: python -severity: warning -message: Duplicating the same wrapper call across assignment branches is verbose -metadata: - weight: 4 - category: slop -rule: - pattern: 'if $COND: $VAR = $F($A) else: $VAR = $F($B) ' ---- -id: sorted-key-indexed-dict-comprehension -language: python -severity: warning -message: sorted(d.keys()) plus indexed lookup is noisier than iterating items directly -metadata: - weight: 4 - category: slop -rule: - any: - - pattern: '{$K: $DICT[$K] for $K in sorted($DICT.keys())}' - - pattern: '{$K: $F($DICT[$K]) for $K in sorted($DICT.keys())}' ---- -id: duplicate-tuple-return-branches -language: python -severity: warning -message: Duplicating tuple literals across branches is verbose when only one slot - changes -metadata: - weight: 3 - category: slop -rule: - any: - - pattern: 'if $COND: return ($A, $B) else: return ($C, $B) ' - - pattern: 'if $COND: return ($A, $B) else: return ($A, $C) ' - -# Generated from ../scrit/scratch/*.yaml and ../scrit/scratch/saved/*.yaml. ---- -id: guard-helper-graveyard-module -language: python -severity: warning -message: Module contains a long run of adjacent guard-heavy helper functions; this is a defensive utility graveyard rather than focused code -metadata: - weight: 11 - category: slop -rule: - kind: module - has: - stopBy: neighbor - kind: function_definition - has: - stopBy: end - kind: block - all: - - has: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - - has: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - follows: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - follows: - stopBy: neighbor - kind: function_definition - has: - stopBy: end - kind: block - all: - - has: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - - has: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - follows: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - follows: - stopBy: neighbor - kind: function_definition - has: - stopBy: end - kind: block - all: - - has: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - - has: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - follows: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - follows: - stopBy: neighbor - kind: function_definition - has: - stopBy: end - kind: block - all: - - has: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - - has: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - follows: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement ---- -id: defensive-none-passthrough -language: python -severity: warning -message: 'Repeated `if x is None: return None` sentinel plumbing is defensive glue code; normalize Optional values at the boundary instead of hand-threading `None` through helpers' -metadata: - weight: 5 - category: slop - min_file_count: 4 -rule: - kind: if_statement - pattern: "if $VALUE is None:\n return None\n" ---- -id: defensive-falsy-passthrough -language: python -severity: warning -message: 'Repeated `if not x: return None` guards are defensive sentinel plumbing; normalize empties once instead of scattering nullable escape hatches' -metadata: - weight: 5 - category: slop - min_file_count: 4 -rule: - kind: if_statement - pattern: "if not $VALUE:\n return None\n" ---- -id: defensive-type-probe-default-return -language: python -severity: warning -message: Inline `isinstance` probes that quietly return a sentinel are defensive boundary code; centralize coercion/validation instead of hand-rolling per-helper fallbacks -metadata: - weight: 5 - category: slop - min_file_count: 2 -rule: - kind: if_statement - any: - - pattern: "if not isinstance($VALUE, $TYPE):\n return None\n" - - pattern: "if not isinstance($VALUE, $TYPE):\n return False\n" - - pattern: "if not isinstance($VALUE, $TYPE):\n return []\n" - - pattern: "if not isinstance($VALUE, $TYPE):\n return {}\n" ---- -id: paranoid-inline-type-validation -language: python -severity: warning -message: 'Repeated `if not isinstance(...): raise ...` ladders are defensive schema policing in business logic; push validation into one parser/schema/helper instead of re-checking every field inline' -metadata: - weight: 5 - category: slop - min_file_count: 3 -rule: - kind: if_statement - any: - - pattern: "if not isinstance($VALUE, $TYPE):\n raise ValueError($$$ARGS)\n" - - pattern: "if not isinstance($VALUE, $TYPE):\n raise TypeError($$$ARGS)\n" ---- -id: nullable-forwarding-wrapper -language: python -severity: warning -message: '`if x is None: return None; return transform(x)` is a hand-written maybe-wrapper; fold the nullability into one shared coercion path instead of repeating the sentinel dance' -metadata: - weight: 5 - category: slop - min_file_count: 2 -rule: - kind: if_statement - all: - - pattern: "if $VALUE is None:\n return None\n" - - not: - inside: - kind: if_statement - - precedes: - stopBy: neighbor - kind: return_statement ---- -id: exception-sentinel-helper-function -language: python -severity: warning -message: Whole-function `try/except Exception` returning a sentinel is defensive wrapper code; use targeted exceptions or a real result object -metadata: - weight: 8 - category: slop - min_file_count: 2 -rule: - kind: function_definition - has: - stopBy: end - kind: try_statement - all: - - has: - stopBy: neighbor - kind: except_clause - regex: Exception - - has: - stopBy: neighbor - kind: except_clause - has: - stopBy: end - kind: return_statement - regex: (None|False|\[\]|\{\}) ---- -id: silent-type-probe-helper-function -language: python -severity: warning -message: Helper quietly probes input types and returns sentinels instead of validating at the boundary; that is defensive slop -metadata: - weight: 8 - category: slop - min_file_count: 2 -rule: - kind: function_definition - has: - stopBy: end - kind: block - all: - - has: - stopBy: neighbor - kind: if_statement - pattern: "if not isinstance($VALUE, $TYPE):\n return $SENTINEL\n" - - has: - stopBy: neighbor - kind: return_statement ---- -id: none-error-pair-propagation -language: python -severity: warning -message: '`value, err = f(...); if err: return None, err` is Go-style error plumbing; raise exceptions or use a real result type' -metadata: - weight: 8 - category: slop - min_file_count: 2 -rule: - kind: if_statement - all: - - pattern: "if $ERR:\n return None, $ERR\n" - - follows: - stopBy: neighbor - kind: expression_statement - has: - kind: assignment - all: - - has: - field: left - kind: pattern_list - - has: - field: right - kind: call ---- -id: inline-error-list-pair-return -language: python -severity: warning -message: '`return None, [error(...)]` open-codes an ad-hoc result type; raise typed exceptions or use a proper result object' -metadata: - weight: 8 - category: slop - min_file_count: 2 -rule: - kind: return_statement - regex: ^return None\s*,\s*\[(error|err|problem|issue)\( ---- -id: ad-hoc-result-function -language: python -severity: warning -message: Function returns a success tuple and multiple `(None, error)` style failures; this is ad-hoc result plumbing rather than idiomatic Python exceptions -metadata: - weight: 9 - category: slop - min_file_count: 2 -rule: - kind: function_definition - all: - - regex: (?s)return None\s*,\s*(err|error|errors|failure|problem)s?\b|return None\s*,\s*(?:["'][A-Z][A-Z0-9_]{4,}["']|\(["'][A-Z][A-Z0-9_]{4,}["']) - - regex: (?s)return .+\s*,\s*(None|\[\])\s*$ ---- -id: none-error-code-return -language: python -severity: warning -message: '`return None, ''INVALID_*''` is ad-hoc error-code plumbing; raise exceptions or use a typed result object' -metadata: - weight: 8 - category: slop - min_file_count: 5 -rule: - kind: return_statement - regex: ^return None\s*,\s*['"][A-Z][A-Z0-9_]{4,}['"]$ ---- -id: none-error-tuple-return -language: python -severity: warning -message: '`return None, (''INVALID_*'', ...)` is ad-hoc result-tuple error plumbing; raise exceptions or use a typed result object' -metadata: - weight: 8 - category: slop - min_file_count: 5 -rule: - kind: return_statement - regex: ^return None\s*,\s*\(['"][A-Z][A-Z0-9_]{4,}['"]\s*, ---- -id: ad-hoc-error-code-parser-function -language: python -severity: warning -message: Function encodes multiple `return None, 'INVALID_*'` branches and a success tuple; this is ad-hoc result plumbing instead of proper exceptions or result types -metadata: - weight: 9 - category: slop - min_file_count: 2 -rule: - kind: function_definition - all: - - regex: (?s)return None\s*,\s*(?:['"][A-Z][A-Z0-9_]{4,}['"]|\(['"][A-Z][A-Z0-9_]{4,}['"]).*return None\s*,\s*(?:['"][A-Z][A-Z0-9_]{4,}['"]|\(['"][A-Z][A-Z0-9_]{4,}['"]) - - regex: (?s)return .+\s*,\s*(?:None|\[\])\s*$ ---- -id: go-style-error-propagating-function -language: python -severity: warning -message: Function unpacks `(value, err)` from helpers and immediately propagates `err`; this is Go-style error plumbing rather than idiomatic Python control flow -metadata: - weight: 8 - category: slop - min_file_count: 2 -rule: - kind: function_definition - has: - stopBy: end - kind: block - all: - - has: - stopBy: neighbor - kind: expression_statement - has: - kind: assignment - all: - - has: - field: left - kind: pattern_list - - has: - field: right - kind: call - - has: - stopBy: neighbor - kind: if_statement - any: - - pattern: "if $ERR:\n return $ERR\n" - - pattern: "if $ERR:\n return None, $ERR\n" - follows: - stopBy: neighbor - kind: expression_statement - has: - kind: assignment - all: - - has: - field: left - kind: pattern_list - - has: - field: right - kind: call ---- -id: multi-branch-result-parser-function -language: python -severity: warning -message: Function has many explicit `(None, error)` failure branches and a success tuple return; this is ad-hoc result-parser plumbing better expressed with exceptions or schemas -metadata: - weight: 10 - category: slop - min_file_count: 2 -rule: - kind: function_definition - all: - - regex: (?s)return None\s*,\s*(?:(?:err|error|errors|failure|problem)s?\b|(?:["'][A-Z][A-Z0-9_]{4,}["']|\(["'][A-Z][A-Z0-9_]{4,}["'])).*return None\s*,\s*(?:(?:err|error|errors|failure|problem)s?\b|(?:["'][A-Z][A-Z0-9_]{4,}["']|\(["'][A-Z][A-Z0-9_]{4,}["'])).*return None\s*,\s*(?:(?:err|error|errors|failure|problem)s?\b|(?:["'][A-Z][A-Z0-9_]{4,}["']|\(["'][A-Z][A-Z0-9_]{4,}["'])) - - regex: (?s)return .+\s*,\s*(None|\[\])\s*$ ---- -id: nullable-scalar-cast-wrapper -language: python -severity: warning -message: '`if x is None: return None` before a primitive cast is hand-rolled nullable coercion glue; normalize once instead of scattering wrappers' -metadata: - weight: 7 - category: slop - min_file_count: 2 -rule: - any: - - pattern: "def $NAME($VALUE):\n if $VALUE is None:\n return None\n return int($VALUE)\n" - - pattern: "def $NAME($VALUE):\n if $VALUE is None:\n return None\n return float($VALUE)\n" - - pattern: "def $NAME($VALUE):\n if $VALUE is None:\n return None\n return str($VALUE)\n" - - pattern: "def $NAME($VALUE):\n if $VALUE is None:\n return None\n return bool($VALUE)\n" - - pattern: "def $NAME($VALUE):\n if $VALUE is None:\n return None\n return round(float($VALUE), $PREC)\n" ---- -id: result-parser-function -language: python -severity: warning -message: Function has many validation branches plus success/error result tuples; this is ad-hoc parser plumbing better expressed with exceptions or schemas -metadata: - weight: 9 - category: slop - min_file_count: 2 -rule: - kind: function_definition - has: - stopBy: end - kind: block - all: - - has: - stopBy: end - kind: if_statement - - has: - stopBy: end - kind: if_statement - follows: - stopBy: end - kind: if_statement - - has: - stopBy: end - kind: if_statement - follows: - stopBy: end - kind: if_statement - follows: - stopBy: end - kind: if_statement - - has: - stopBy: end - kind: if_statement - follows: - stopBy: end - kind: if_statement - follows: - stopBy: end - kind: if_statement - follows: - stopBy: end - kind: if_statement - - has: - stopBy: end - kind: return_statement - regex: return None\s*,\s*(err|error|errors|failure|problem)|return None\s*,\s*(?:["'][A-Z][A-Z0-9_]{4,}["']|\(["'][A-Z][A-Z0-9_]{4,}["']) - - has: - stopBy: end - kind: return_statement - regex: return .+\s*,\s*(None|\[\])\s*$ ---- -id: ad-hoc-result-parser-function -language: python -severity: warning -message: Function mixes many validation branches with success/error result tuples; this is defensive parser plumbing better modeled with exceptions or a real result type -metadata: - weight: 9 - category: slop - min_file_count: 2 -rule: - kind: function_definition - all: - - regex: (?s)(?:\n\s*if [^\n]+:){4,} - - regex: (?s)return .+\s*,\s*(None|\[\])\s*$ - - regex: (?s)return None\s*,\s*(?:err|error|errors|failure|problem)s?\b|return None\s*,\s*(?:['"][A-Z][A-Z0-9_]{4,}['"]|\(['"][A-Z][A-Z0-9_]{4,}['"]) ---- -id: go-style-result-function -language: python -severity: warning -message: Function returns `(value, None)` on success and `(None, err)` on failure; this is Go-style error plumbing rather than idiomatic Python -metadata: - weight: 9 - category: slop - min_file_count: 2 -rule: - kind: function_definition - has: - stopBy: end - kind: block - all: - - has: - stopBy: end - kind: return_statement - regex: return None\s*,\s*(err|error|errors|failure|problem)s?\b - - has: - stopBy: end - kind: return_statement - regex: return .+\s*,\s*(None|\[\])\s*$ ---- -id: ad-hoc-result-tuple-parser -language: python -severity: warning -message: Function open-codes multiple `return None, 'INVALID_*'` branches plus a success tuple; use exceptions or a real result type instead of Go-style result plumbing -metadata: - weight: 9 - category: slop - min_file_count: 2 -rule: - kind: function_definition - has: - stopBy: end - kind: block - all: - - has: - stopBy: end - kind: return_statement - regex: return None\s*,\s*(["\'][A-Z][A-Z0-9_]{4,}["\']|\(["\'][A-Z][A-Z0-9_]{4,}["\']) - - has: - stopBy: end - kind: return_statement - regex: return None\s*,\s*(["\'][A-Z][A-Z0-9_]{4,}["\']|\(["\'][A-Z][A-Z0-9_]{4,}["\']) - follows: - stopBy: end - kind: return_statement - regex: return None\s*,\s*(["\'][A-Z][A-Z0-9_]{4,}["\']|\(["\'][A-Z][A-Z0-9_]{4,}["\']) - - has: - stopBy: end - kind: return_statement - regex: return .+\s*,\s*(None|\[\])\s*$ ---- -id: typed-result-parser-function -language: python -severity: warning -message: Function mixes repeated `isinstance` validation with success/error result tuples; this is defensive parser plumbing better expressed with exceptions or schemas -metadata: - weight: 9 - category: slop - min_file_count: 2 -rule: - kind: function_definition - all: - - regex: '(?s)if not isinstance\([^\n]+\):.*if not isinstance\([^\n]+\):' - - regex: (?s)return .+\s*,\s*(None|\[\])\s*$ - - regex: (?s)return None\s*,\s*(err|error|errors|failure|problem)s?\b|return None\s*,\s*(?:["'][A-Z][A-Z0-9_]{4,}["']|\(["'][A-Z][A-Z0-9_]{4,}["']) ---- -id: paranoid-validator-helper-function -language: python -severity: warning -message: Function is a ladder of inline validation raises; centralize schema/validation instead of re-policing every field in helper code -metadata: - weight: 9 - category: slop - min_file_count: 3 -rule: - kind: function_definition - has: - stopBy: end - kind: block - all: - - has: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: raise_statement - regex: (ValueError|TypeError) - - has: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: raise_statement - regex: (ValueError|TypeError) - follows: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: raise_statement - regex: (ValueError|TypeError) ---- -id: guard-rail-helper-function -language: python -severity: warning -message: Function is mostly stacked guard-clause exits; this is defensive helper plumbing rather than cohesive behavior -metadata: - weight: 10 - category: slop - min_file_count: 6 -rule: - kind: function_definition - has: - stopBy: end - kind: block - all: - - has: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - - has: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - follows: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement ---- -id: flat-guard-ladder-helper -language: python -severity: warning -message: Flat helper with three or more guard-clause returns is branchy defensive glue; push normalization into a table, validator, or parser -metadata: - weight: 9 - category: slop - min_file_count: 4 -rule: - kind: function_definition - regex: '^def [^\n]*:\n(?: [^\n]*\n| [^\n]*\n){0,80} if [^\n]*:\n return [^\n]*\n(?: [^\n]*\n| [^\n]*\n){0,80} if [^\n]*:\n return [^\n]*\n(?: [^\n]*\n| [^\n]*\n){0,80} if [^\n]*:\n return [^\n]*(?:\n(?: [^\n]*| [^\n]*))*\n?$' ---- -id: three-guard-rail-helper-function -language: python -severity: warning -message: Function has three or more top-level guard-clause returns; this is over-defensive helper glue rather than focused logic -metadata: - weight: 10 - category: slop - min_file_count: 4 -rule: - kind: function_definition - has: - stopBy: end - kind: block - all: - - has: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - - has: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - follows: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - - has: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - follows: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - follows: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement ---- -id: guard-clause-wrapper-function -language: python -severity: warning -message: Function is mostly a stack of guard-clause returns before a final return; this is defensive wrapper glue rather than real logic -metadata: - weight: 8 - category: slop -rule: - any: - - pattern: "def $NAME($$$ARGS):\n if $COND1:\n return $RET1\n if $COND2:\n return $RET2\n return $RESULT\n" - - pattern: "def $NAME($$$ARGS):\n if $COND1:\n return $RET1\n if $COND2:\n return $RET2\n if $COND3:\n return $RET3\n return $RESULT\n" - - pattern: "def $NAME($$$ARGS):\n if $COND1:\n return $RET1\n if $COND2:\n return $RET2\n if $COND3:\n return $RET3\n if $COND4:\n return $RET4\n return $RESULT\n" ---- -id: defensive-guard-rail-function -language: python -severity: warning -message: Function is mostly defensive guard rails and sentinel exits instead of cohesive logic; this is overengineered boundary plumbing -metadata: - weight: 10 - category: slop - min_file_count: 3 -rule: - kind: function_definition - regex: '(?s)^def .*:\n(?: .*\n){12,40} if .*:\n return .*\n(?: .*\n){0,40} if .*:\n return .*' ---- -id: long-guarded-workflow-function -language: python -severity: warning -message: Long function with defensive early-return guard rails is hand-rolled workflow glue; split validation/coercion from core behavior -metadata: - weight: 10 - category: slop - min_file_count: 3 -rule: - kind: function_definition - regex: '(?s)^def .*:\n(?: .*\n){20,80} if .*:\n return .*' ---- -id: try-except-sentinel-wrapper -language: python -severity: warning -message: Whole-function `try/except Exception` returning a sentinel is defensive wrapper code; use targeted exceptions or a real result type -metadata: - weight: 8 - category: slop - min_file_count: 2 -rule: - kind: function_definition - regex: '(?s)^def .*:\n(?: .*\n){4,40} try:\n(?: .*\n)+? except Exception(?: as .*?)?:\n return (None|False|\[\]|\{\})' ---- -id: frozen-micro-dataclass -language: python -severity: warning -message: Tiny frozen dataclass used as a local value wrapper is over-modeled ceremony; prefer a tuple, typed dict, or the raw values -metadata: - weight: 6 - category: slop - min_file_count: 10 -rule: - kind: decorated_definition - all: - - regex: '@dataclass\([^\n]*frozen=True' - - regex: 'class .*:\n(?: [A-Za-z_][A-Za-z0-9_]*: .*\n){0,2} [A-Za-z_][A-Za-z0-9_]*: .*\n?$' ---- -id: repeated-structured-dict-return -language: python -severity: warning -message: Repeated structured dict returns in one file are ad-hoc result-object plumbing; use one real result type instead of open-coded payload dicts -metadata: - weight: 6 - category: slop - min_file_count: 8 -rule: - kind: return_statement - regex: '(?s)^return \{\n\s*["\''][^"\'']+["\'']:\s*.*\n\s*["\''][^"\'']+["\'']:\s*.*\n(?:\s*["\''][^"\'']+["\'']:\s*.*\n)*\}$ - - ' ---- -id: flat-guard-return-helper -language: python -severity: warning -message: 'Flat helper with repeated `if ...: return ...` guards is defensive branchy glue code' -metadata: - weight: 9 - category: slop - min_file_count: 6 -rule: - kind: function_definition - regex: '^def [^\n]*:\n(?: [^\n]*\n| [^\n]*\n){0,80} if [^\n]*:\n return [^\n]*\n(?: [^\n]*\n| [^\n]*\n){0,80} if [^\n]*:\n return [^\n]*(?:\n(?: [^\n]*| [^\n]*))*\n?$' ---- -id: flat-try-except-sentinel-helper -language: python -severity: warning -message: Flat helper wrapped in `try/except Exception` with a sentinel fallback is defensive slop -metadata: - weight: 8 - category: slop - min_file_count: 2 -rule: - kind: function_definition - regex: '^def [^\n]*:\n(?: [^\n]*\n| [^\n]*\n){0,80} try:\n(?: [^\n]*\n)+ except Exception(?: as [^:]+)?:\n return (None|False|\[\]|\{\})(?:\n(?: [^\n]*| [^\n]*))*\n?$' ---- -id: flat-isinstance-return-helper -language: python -severity: warning -message: Flat helper that type-probes and quietly returns a sentinel is defensive normalization glue -metadata: - weight: 8 - category: slop - min_file_count: 2 -rule: - kind: function_definition - regex: '^def [^\n]*:\n(?: [^\n]*\n| [^\n]*\n){0,80} if not isinstance\([^\n]*\):\n return (None|False|\[\]|\{\}|\(\)|0|""|'''')(?:\n(?: [^\n]*| [^\n]*))*\n?$' ---- -id: flat-error-return-helper -language: python -severity: warning -message: Flat helper that returns named error sentinels from multiple guard clauses is defensive error plumbing -metadata: - weight: 8 - category: slop - min_file_count: 2 -rule: - kind: function_definition - regex: '^def [^\n]*:\n(?: [^\n]*\n| [^\n]*\n){0,80} if [^\n]*:\n return [^\n]*(error|err|invalid|failed)[^\n]*\n(?: [^\n]*\n| [^\n]*\n){0,80} if [^\n]*:\n return [^\n]*(error|err|invalid|failed)[^\n]*(?:\n(?: [^\n]*| [^\n]*))*\n?$' ---- -id: guard-railed-utility-module -language: python -severity: warning -message: 'Module is dominated by defensive `if ...: return ...` guard rails; this is branchy utility slop rather than focused code' -metadata: - weight: 12 - category: slop -rule: - kind: module - regex: (?ms)(?:.*^ if [^\n]*:\n return [^\n]*\n){20,}.*$ ---- -id: swallow-exception-pass-multi-stmt -language: python -severity: warning -message: '`except ...: pass` after multi-step work hides real failures; catch specific errors or log' -rule: - any: - - pattern: "try:\n $FIRST\n $SECOND\n $$$REST\nexcept Exception:\n pass\n" - - pattern: "try:\n $FIRST\n $SECOND\n $$$REST\nexcept:\n pass\n" ---- -id: defensive-fstring-raise-heavy -language: python -severity: warning -message: Function body has 3+ `raise ...(f'...{var}...')` statements; dense per-field validation belongs at the boundary, not in a helper. -metadata: - weight: 4 - category: slop -rule: - kind: function_definition - regex: raise\s+\w+[^)]*f"[^"]*\{[^}]*\}[\s\S]*raise\s+\w+[^)]*f"[^"]*\{[^}]*\}[\s\S]*raise\s+\w+[^)]*f"[^"]*\{[^}]*\} ---- -id: defensive-isinstance-raise-heavy -language: python -severity: warning -message: 'Function body runs 3+ `if not isinstance(...): raise` checks; these are boundary validations leaking into core logic.' -metadata: - weight: 4 - category: slop -rule: - kind: function_definition - regex: if\s+not\s+isinstance\([^)]+\):[\s\S]*?raise[\s\S]*if\s+not\s+isinstance\([^)]+\):[\s\S]*?raise[\s\S]*if\s+not\s+isinstance\([^)]+\):[\s\S]*?raise ---- -id: defensive-none-return-heavy -language: python -severity: warning -message: "Function body has 3+ `if \u2026 is None: return None` / `if not \u2026: return None` guards; plumbing `None` through helpers is defensive slop." -metadata: - weight: 4 - category: slop -rule: - kind: function_definition - regex: (if\s+\w[\w.]*\s+is\s+None:\s*\n\s+return\s+None[\s\S]*){3,} ---- -id: defensive-try-soup-function -language: python -severity: warning -message: "Function body has 3+ `try/except` blocks \u2014 that is error-handling-as-control-flow. Rethink the contract or split the function." -metadata: - weight: 4 - category: slop - min_file_count: 3 -rule: - kind: function_definition - regex: (try:[\s\S]*?except[\s\S]*?){3,} ---- -id: defensive-except-exception-heavy -language: python -severity: warning -message: Function body has 2+ `except Exception:` / bare `except:` clauses; catching-everything multiple times in one function is a paranoia smell. -metadata: - weight: 4 - category: slop - min_file_count: 3 -rule: - kind: function_definition - regex: (except\s+Exception[\s:][\s\S]*){2,} ---- -id: defensive-long-dispatch-ladder -language: python -severity: warning -message: "Function contains 4+ `if/elif == \"STRING\":` branches \u2014 open-coded command/type routing; use a `{name: handler}` dispatch table." -metadata: - weight: 5 - category: slop - min_file_count: 2 -rule: - kind: function_definition - regex: ((?:if|elif)\s+\w+\s*==\s*"[^"]+"\s*:[\s\S]*?){4,} ---- -id: defensive-error-message-repeated-fields -language: python -severity: info -message: Function raises the same error type 5+ times; rewrite as a small validator at the boundary or a single `raise` with an accumulated message. -metadata: - weight: 3 - category: slop -rule: - kind: function_definition - regex: (raise\s+\w+Error\([\s\S]*?){5,} ---- -id: defensive-validator-function -language: python -severity: warning -message: "Function body runs `if not isinstance(...): raise \u2026` plus returns; this is a boundary validator living inside core logic. Validate once at the entrypoint, not per helper." -metadata: - weight: 4 - category: slop -rule: - kind: function_definition - all: - - has: - field: body - kind: block - has: - stopBy: end - kind: if_statement - any: - - pattern: "if not isinstance($V, $T):\n raise $$$A\n" - - pattern: "if not isinstance($V, ($$$T)):\n raise $$$A\n" - - has: - field: body - kind: block - has: - stopBy: end - kind: raise_statement - has: - stopBy: end - kind: string - regex: \{[^}]*\} ---- -id: defensive-validator-returnmix -language: python -severity: warning -message: "Function combines `isinstance` guards with `is None` guards and returns; stop per-field validation \u2014 assert the contract at the boundary." -metadata: - weight: 4 - category: slop -rule: - kind: function_definition - all: - - has: - field: body - kind: block - has: - stopBy: end - kind: if_statement - any: - - pattern: "if not isinstance($V, $T):\n return $$$A\n" - - pattern: "if not isinstance($V, ($$$T)):\n return $$$A\n" - - has: - field: body - kind: block - has: - stopBy: end - kind: if_statement - any: - - pattern: "if $V2 is None:\n return $$$A\n" - - pattern: "if not $V2:\n return $$$A\n" ---- -id: defensive-function-isinstance-heavy -language: python -severity: warning -message: Function has 2+ `isinstance` checks in its body; inline type-policing is defensive slop. Validate at the boundary, then trust the type. -metadata: - weight: 5 - category: slop -rule: - kind: function_definition - all: - - has: - field: body - kind: block - has: - stopBy: end - kind: call - regex: ^isinstance\( - - has: - field: body - kind: block - has: - stopBy: end - kind: if_statement - pattern: "if not isinstance($V, $T):\n $$$A\n" - - has: - field: body - kind: block - has: - stopBy: end - kind: if_statement - any: - - pattern: "if isinstance($V, $T):\n $$$A\n" - - pattern: "if not isinstance($V, $T):\n $$$A\n" - precedes: - stopBy: end - kind: if_statement - any: - - pattern: "if isinstance($V2, $T2):\n $$$B\n" - - pattern: "if not isinstance($V2, $T2):\n $$$B\n" ---- -id: defensive-function-nullable-heavy -language: python -severity: warning -message: "Function has 2+ `if \u2026 is None: return/raise` guards threaded through its body; normalize nullability at the boundary, don't plumb `None`." -metadata: - weight: 5 - category: slop -rule: - kind: function_definition - has: - field: body - kind: block - all: - - has: - stopBy: end - kind: if_statement - any: - - pattern: "if $V is None:\n return $$$A\n" - - pattern: "if $V is None:\n raise $$$A\n" - - has: - stopBy: end - kind: if_statement - any: - - pattern: "if $V2 is None:\n return $$$A\n" - - pattern: "if $V2 is None:\n raise $$$A\n" - precedes: - stopBy: end - kind: if_statement - any: - - pattern: "if $V3 is None:\n return $$$A\n" - - pattern: "if $V3 is None:\n raise $$$A\n" ---- -id: type-probe-ladder -language: python -severity: warning -message: "Two `if isinstance(same_var, T): \u2026` branches back-to-back are a type-probe ladder; use a dispatch table or polymorphism." -metadata: - weight: 4 - category: slop -rule: - kind: if_statement - all: - - pattern: "if isinstance($V, $T1):\n $$$A\n" - - precedes: - stopBy: neighbor - kind: if_statement - pattern: "if isinstance($V, $T2):\n $$$B\n" ---- -id: multi-none-or-chain -language: python -severity: warning -message: '`if a is None or b is None [or c is None]: return/raise/continue` is defensive multi-arg null policing; normalize nullability at the boundary.' -metadata: - weight: 4 - category: slop -rule: - kind: if_statement - all: - - regex: is None\s+or\s+\w[\w.]*\s+is None - - has: - field: consequence - kind: block - has: - stopBy: neighbor - any: - - kind: return_statement - - kind: raise_statement - - kind: continue_statement ---- -id: isinstance-return-constructor -language: python -severity: warning -message: '`if isinstance(x, T): return T(x)` re-wraps a value already of the target type; drop the guard and the wrap.' -metadata: - weight: 4 - category: slop -rule: - kind: if_statement - any: - - pattern: "if isinstance($VALUE, $TYPE):\n return $TYPE($VALUE)\n" - - pattern: "if isinstance($VALUE, $TYPE):\n return $TYPE($$$ARGS)\n" ---- -id: wrap-raise-fstring -language: python -severity: warning -message: "`except X as e: raise MyError(f'\u2026: {e}') from e` renames exceptions for cosmetics; let the original propagate or catch something specific." -metadata: - weight: 4 - category: slop -rule: - kind: except_clause - all: - - regex: '^except\s+(Exception|BaseException|OSError|ValueError|TypeError|RuntimeError|KeyError|IndexError|AttributeError|ArithmeticError)(\s+as\s+\w+)?\s*:' - - has: - stopBy: neighbor - kind: block - all: - - has: - stopBy: neighbor - kind: raise_statement - has: - stopBy: end - kind: string - regex: \{[^}]*\} - - not: - has: - stopBy: neighbor - not: - any: - - kind: raise_statement - - kind: comment ---- -id: except-return-static-sentinel -language: python -severity: warning -message: '`except Exception: return None/False/[]/{}` is bare-exception-to-sentinel; catch something specific or let it propagate.' -metadata: - weight: 5 - category: slop -rule: - kind: except_clause - all: - - any: - - regex: '^except\s*:' - - regex: '^except\s+(Exception|BaseException)(\s+as\s+\w+)?\s*:' - - has: - stopBy: neighbor - kind: block - all: - - has: - stopBy: neighbor - kind: return_statement - regex: ^return\s+(None|False|True|\[\]|\{\}|""|0|\(\)|set\(\))\s*$ - - not: - has: - stopBy: neighbor - not: - any: - - kind: return_statement - - kind: comment ---- -id: triple-attr-none-default-chain -language: python -severity: info -message: '`x = data.get(k, d1) or d2` double-guards a `.get` miss; pick one default path.' -metadata: - weight: 3 - category: slop -rule: - kind: assignment - any: - - pattern: '$X = $DATA.get($KEY, $D1) or $D2 - - ' - - pattern: '$X = $DATA.get($KEY) or $DATA.get($KEY2) or $FALLBACK - - ' ---- -id: redundant-coerce-to-str -language: python -severity: info -message: '`str(x) if x is not None else ''''` coerces a typed value; trust the annotation.' -metadata: - weight: 3 - category: slop -rule: - kind: conditional_expression - any: - - pattern: str($X) if $X is not None else "" - - pattern: str($X) if $X is not None else '' - - pattern: str($X) if isinstance($X, str) else "" - - pattern: str($X) if isinstance($X, str) else '' ---- -id: none-continue-in-loop -language: python -severity: warning -message: '`if x is None: continue` inside a loop is defensive null-skipping; filter at the source or raise on contract violation.' -metadata: - weight: 3 - category: slop -rule: - kind: if_statement - pattern: "if $X is None:\n continue\n" - inside: - stopBy: end - any: - - kind: for_statement - - kind: while_statement ---- -id: parse-and-fallback-exception -language: python -severity: warning -message: '`try: return int/float/json.loads(x) except Exception: return ` parse-with-silent-fallback swallows format errors.' -metadata: - weight: 5 - category: slop -rule: - kind: try_statement - all: - - has: - stopBy: neighbor - kind: block - has: - stopBy: neighbor - kind: return_statement - any: - - regex: ^return\s+int\( - - regex: ^return\s+float\( - - regex: ^return\s+bool\( - - regex: ^return\s+json\.loads?\( - - regex: ^return\s+ast\.literal_eval\( - - has: - stopBy: neighbor - kind: except_clause - regex: '^except\s+(Exception|BaseException|ValueError|TypeError)(\s+as\s+\w+)?\s*:' ---- -id: return-none-guard-ladder -language: python -severity: warning -message: "Back-to-back `if \u2026: return None` guard clauses treat Optional as a pervasive escape hatch; normalize nullability at the boundary." -metadata: - weight: 4 - category: slop -rule: - kind: if_statement - all: - - has: - field: consequence - kind: block - all: - - has: - stopBy: neighbor - kind: return_statement - regex: ^return\s+None\s*$ - - not: - has: - stopBy: neighbor - not: - any: - - kind: return_statement - - kind: comment - - precedes: - stopBy: neighbor - kind: if_statement - has: - field: consequence - kind: block - all: - - has: - stopBy: neighbor - kind: return_statement - regex: ^return\s+None\s*$ - - not: - has: - stopBy: neighbor - not: - any: - - kind: return_statement - - kind: comment ---- -id: raise-guard-ladder -language: python -severity: warning -message: 'Back-to-back `if : raise ...` guard clauses that raise different error messages are defensive input policing; push validation to the boundary.' -metadata: - weight: 4 - category: slop - min_file_count: 3 -rule: - kind: if_statement - all: - - has: - field: consequence - kind: block - all: - - has: - stopBy: neighbor - kind: raise_statement - - not: - has: - stopBy: neighbor - not: - any: - - kind: raise_statement - - kind: comment - - precedes: - stopBy: neighbor - kind: if_statement - has: - field: consequence - kind: block - all: - - has: - stopBy: neighbor - kind: raise_statement - - not: - has: - stopBy: neighbor - not: - any: - - kind: raise_statement - - kind: comment ---- -id: function-with-many-type-guards -language: python -severity: warning -message: 'Function has 3+ `if not isinstance / if x is None: raise` guard clauses before doing work; validate at the boundary and trust the type downstream.' -metadata: - weight: 5 - category: slop -rule: - kind: function_definition - has: - field: body - kind: block - all: - - has: - stopBy: neighbor - kind: if_statement - any: - - pattern: "if not isinstance($V1, $T1):\n raise $$$A1\n" - - pattern: "if not isinstance($V1, ($$$T1)):\n raise $$$A1\n" - - has: - stopBy: neighbor - kind: if_statement - any: - - pattern: "if not isinstance($V2, $T2):\n raise $$$A2\n" - - pattern: "if not isinstance($V2, ($$$T2)):\n raise $$$A2\n" ---- -id: sentinel-fallback-function -language: python -severity: warning -message: 'Whole-function `try: ; except Exception: return None/False/[]/{}` swallows every error and returns a sentinel; catch specifically or let it propagate.' -metadata: - weight: 6 - category: slop -rule: - kind: function_definition - has: - field: body - kind: block - all: - - has: - stopBy: neighbor - kind: try_statement - all: - - has: - stopBy: neighbor - kind: except_clause - all: - - any: - - regex: '^except\s*:' - - regex: '^except\s+(Exception|BaseException)(\s+as\s+\w+)?\s*:' - - has: - stopBy: neighbor - kind: block - all: - - has: - stopBy: neighbor - kind: return_statement - regex: ^return\s+(None|False|True|\[\]|\{\}|""|0|\(\)|set\(\))\s*$ - - not: - has: - stopBy: neighbor - not: - any: - - kind: return_statement - - kind: comment - - not: - has: - stopBy: neighbor - any: - - kind: for_statement - - kind: while_statement - - kind: match_statement - - kind: with_statement ---- -id: dispatch-by-string-equality -language: python -severity: warning -message: 'Long `if op == "X":` / `elif op == "Y":` string-equality dispatch is open-coded command routing; use a `{name: handler}` table.' -metadata: - weight: 4 - category: slop -rule: - kind: if_statement - all: - - any: - - regex: ^if\s+\w+\s*==\s*"[A-Z_][A-Z0-9_]*" - - regex: ^if\s+\w+\s*==\s*'[A-Z_][A-Z0-9_]*' - - precedes: - stopBy: neighbor - kind: if_statement - any: - - regex: ^if\s+\w+\s*==\s*"[A-Z_][A-Z0-9_]*" - - regex: ^if\s+\w+\s*==\s*'[A-Z_][A-Z0-9_]*' ---- -id: go-style-err-tuple-return -language: python -severity: warning -message: '`return None, error_msg` / `return None, None` Go-style dual-return open-codes an ad-hoc result type; raise exceptions or use a real result object.' -metadata: - weight: 5 - category: slop -rule: - kind: return_statement - any: - - pattern: return None, None - - pattern: return None, $MSG - - pattern: return False, $MSG - - pattern: return True, $MSG - not: - any: - - has: - stopBy: end - kind: call ---- -id: len-arity-raise -language: python -severity: info -message: "`if len(args) != N: raise \u2026` open-codes arity checks; bind via parameters or `*args` unpacking so the error is structural." -metadata: - weight: 3 - category: slop -rule: - kind: if_statement - any: - - pattern: "if len($ARGS) != $N:\n raise $$$A\n" - - pattern: "if len($ARGS) < $N:\n raise $$$A\n" - - pattern: "if len($ARGS) > $N:\n raise $$$A\n" ---- -id: raw-guard-module -language: python -severity: warning -message: test -rule: - kind: module - regex: (?ms)(?:.*^ if [^\n]*:\n return [^\n]*\n){20,}.*$ ---- -id: guard-rail-utility-module -language: python -severity: warning -message: Utility module is dominated by defensive guard-rail helpers and sentinel exits -rule: - kind: module - regex: '(?m)(?:^def [^\n]*:\n(?: [^\n]*\n| [^\n]*\n){0,80} if [^\n]*:\n return [^\n]*\n(?: [^\n]*\n| [^\n]*\n){0,80} if [^\n]*:\n return [^\n]*(?:\n(?: [^\n]*| [^\n]*))*\n?){6,}' ---- -id: exp-parserish -language: python -severity: warning -message: test -rule: - kind: function_definition - all: - - regex: (?s)(?:\n if [^\n]+:){4,} - - regex: (?s)return .+,\s*(None|\[\])\s*$ - - regex: (?s)return None,\s*(err|error|errors|failure|problem)s?\b|return None,\s*(?:["'][A-Z][A-Z0-9_]{4,}["']|\(["'][A-Z][A-Z0-9_]{4,}["']) ---- -id: exp-result-parser -language: python -severity: warning -message: test -rule: - kind: function_definition - has: - stopBy: end - kind: block - all: - - has: - stopBy: end - kind: if_statement - - has: - stopBy: end - kind: if_statement - follows: - stopBy: end - kind: if_statement - - has: - stopBy: end - kind: if_statement - follows: - stopBy: end - kind: if_statement - follows: - stopBy: end - kind: if_statement - - has: - stopBy: end - kind: if_statement - follows: - stopBy: end - kind: if_statement - follows: - stopBy: end - kind: if_statement - follows: - stopBy: end - kind: if_statement - - has: - stopBy: end - kind: return_statement - regex: return None\s*,\s*(err|error|errors|failure|problem)|return None\s*,\s*(?:["'][A-Z][A-Z0-9_]{4,}["']|\(["'][A-Z][A-Z0-9_]{4,}["']) - - has: - stopBy: end - kind: return_statement - regex: return .+\s*,\s*(None|\[\])\s*$ ---- -id: exp-guard-func -language: python -severity: warning -message: test -rule: - kind: function_definition - regex: '(?s)^def .*:\n\s+if .*:\n\s+return .*\n\s+if .*:\n\s+return .*\n\s+return ' ---- -id: exp-guardy-func -language: python -severity: warning -message: test -rule: - kind: function_definition - has: - stopBy: end - kind: block - all: - - has: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - - has: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - follows: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement ---- -id: exp-size-only -language: python -severity: warning -message: test -rule: - kind: function_definition - regex: '(?s)^def .*:\n(?: .*\n){1,120} .*\n?$' ---- -id: exp-flat-guardy-func -language: python -severity: warning -message: test -rule: - kind: function_definition - all: - - regex: '^def [^\n]*:\n(?: [^\n]*\n){4,40}$' - - has: - stopBy: end - kind: block - all: - - has: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - - has: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement - follows: - stopBy: neighbor - kind: if_statement - has: - stopBy: end - kind: return_statement ---- -id: exp-flat-guard-regex -language: python -severity: warning -message: test -rule: - kind: function_definition - regex: '^def [^\n]*:\n(?: [^\n]*\n| [^\n]*\n){0,40} if [^\n]*:\n return [^\n]*\n(?: [^\n]*\n| [^\n]*\n){0,40} if [^\n]*:\n return [^\n]*(?:\n(?: [^\n]*| [^\n]*))*\n?$' ---- -id: exp-three-error-returns -language: python -severity: warning -message: test -rule: - kind: function_definition - has: - stopBy: end - kind: block - all: - - has: - stopBy: end - kind: return_statement - regex: return None\s*,\s*(?:err|error|errors|failure|problem|["'][A-Z][A-Z0-9_]{4,}|\(["'][A-Z][A-Z0-9_]{4,}) - - has: - stopBy: end - kind: return_statement - regex: return None\s*,\s*(?:err|error|errors|failure|problem|["'][A-Z][A-Z0-9_]{4,}|\(["'][A-Z][A-Z0-9_]{4,}) - follows: - stopBy: end - kind: return_statement - regex: return None\s*,\s*(?:err|error|errors|failure|problem|["'][A-Z][A-Z0-9_]{4,}|\(["'][A-Z][A-Z0-9_]{4,}) - - has: - stopBy: end - kind: return_statement - regex: return None\s*,\s*(?:err|error|errors|failure|problem|["'][A-Z][A-Z0-9_]{4,}|\(["'][A-Z][A-Z0-9_]{4,}) - follows: - stopBy: end - kind: return_statement - regex: return None\s*,\s*(?:err|error|errors|failure|problem|["'][A-Z][A-Z0-9_]{4,}|\(["'][A-Z][A-Z0-9_]{4,}) - follows: - stopBy: end - kind: return_statement - regex: return None\s*,\s*(?:err|error|errors|failure|problem|["'][A-Z][A-Z0-9_]{4,}|\(["'][A-Z][A-Z0-9_]{4,}) - - has: - stopBy: end - kind: return_statement - regex: return .+\s*,\s*(None|\[\])\s*$ ---- -id: nullable-subscript-wrapper -language: python -severity: warning -message: '`if x is None: return None; return x[...]` is a tiny nullable projection wrapper; inline it at the boundary or use one normalizer' -metadata: - weight: 7 - category: slop - min_file_count: 2 -rule: - kind: function_definition - regex: (?s)^def .*:\n\s*if [A-Za-z_][A-Za-z0-9_]* is None:\n\s+return None\n\s+return [A-Za-z_][A-Za-z0-9_]*\[[^\n]+\] ---- -id: nullable-method-wrapper -language: python -severity: warning -message: '`if x is None: return None; return x.method(...)` is a tiny nullable wrapper helper; centralize normalization instead of proliferating wrappers' -metadata: - weight: 7 - category: slop - min_file_count: 2 -rule: - kind: function_definition - regex: (?s)^def .*:\n\s*if [A-Za-z_][A-Za-z0-9_]* is None:\n\s+return None\n\s+return [A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*\( diff --git a/configs/slop_rules_candidates.yaml b/configs/slop_rules_candidates.yaml deleted file mode 100644 index 37039406..00000000 --- a/configs/slop_rules_candidates.yaml +++ /dev/null @@ -1,249 +0,0 @@ -# Candidate slop rules distilled from the 2026-04 verbose-pattern audit across -# 7 agent runs. Kept separate from slop_rules.yaml until they've been reviewed -# and calibrated against the benchmark corpus. Merge into slop_rules.yaml when -# ready. ---- -id: except-bare-reraise -language: python -severity: warning -message: Catching an exception only to re-raise it adds noise; drop the try/except -metadata: - weight: 3 - category: slop -rule: - kind: except_clause - has: - kind: block - regex: "^\\s*raise\\s*$" ---- -id: except-pass-silence -language: python -severity: warning -message: except clause with bare pass silently swallows errors -metadata: - weight: 4 - category: slop -rule: - kind: except_clause - has: - kind: block - has: - kind: pass_statement ---- -id: except-oserror-ioerror-pair -language: python -severity: warning -message: IOError is an alias for OSError since Py3.3; drop the pair -metadata: - weight: 2 - category: slop -rule: - kind: except_clause - regex: "\\(OSError,\\s*IOError\\)|\\(IOError,\\s*OSError\\)" ---- -id: pragma-no-cover-defensive -language: python -severity: warning -message: pragma no cover suppresses coverage on dead branches; remove the branch instead -metadata: - weight: 3 - category: slop -rule: - kind: comment - regex: "pragma:\\s*no\\s+cover" ---- -id: legacy-typing-capitalized-imports -language: python -severity: warning -message: Use built-in generics (list/dict/tuple, X | None) instead of typing.Dict/List/Optional/Tuple -metadata: - weight: 3 - category: slop -rule: - kind: import_from_statement - all: - - has: - kind: dotted_name - regex: "^typing$" - - has: - kind: dotted_name - regex: "^(Dict|List|Optional|Tuple)$" ---- -id: sorted-items-default-key -language: python -severity: warning -message: "sorted(d.items(), key=lambda i: i[0]) - default sort already sorts by key" -metadata: - weight: 3 - category: slop -rule: - pattern: "sorted($D.items(), key=lambda $I: $I[0])" ---- -id: identity-dict-comprehension -language: python -severity: warning -message: "{k: v for k, v in d.items()} is just dict(d)" -metadata: - weight: 3 - category: slop -rule: - pattern: "{$K: $V for $K, $V in $D.items()}" ---- -id: is-true-false-strict -language: python -severity: warning -message: is True / is False identity check - use truthiness -metadata: - weight: 2 - category: slop -rule: - kind: comparison_operator - any: - - pattern: $X is True - - pattern: $X is False - - pattern: $X is not True - - pattern: $X is not False ---- -id: lbyl-isfile-before-open -language: python -severity: warning -message: os.path.isfile guard before open is redundant and race-prone; use try/except OSError -metadata: - weight: 3 - category: slop -rule: - kind: if_statement - all: - - has: - kind: call - regex: "os\\.path\\.(isfile|exists)\\(" - - has: - kind: block - has: - kind: with_statement - regex: "open\\(" ---- -id: bool-numeric-triple-guard -language: python -severity: warning -message: isinstance bool guard combined with numeric isinstance - wrap in helper rather than inlining -metadata: - weight: 3 - category: slop -rule: - kind: boolean_operator - regex: "isinstance\\([^,]+,\\s*bool\\).*isinstance" ---- -id: per-method-assert-not-none -language: python -severity: hint -message: assert self.X is not None at method entry - enforce in __init__ once -metadata: - weight: 2 - category: slop -rule: - kind: assert_statement - pattern: "assert self.$F is not None" ---- -id: del-unused-param -language: python -severity: warning -message: del on a named parameter inside a function silences unused-param lint - drop from signature -metadata: - weight: 3 - category: slop -rule: - kind: delete_statement - not: - has: - any: - - kind: subscript - - kind: attribute - - kind: call - inside: - kind: function_definition - stopBy: end ---- -id: wrapper-isvalid-oneliner -language: python -severity: hint -message: One-line is_valid_X wrapping a regex match/search/fullmatch adds no value -metadata: - weight: 2 - category: slop -rule: - kind: function_definition - all: - - has: - kind: identifier - regex: "^is_valid" - - has: - kind: block - has: - kind: return_statement - regex: "bool\\(.*\\.(match|fullmatch|search)\\(" ---- -id: manual-removeprefix-replace -language: python -severity: warning -message: s.replace(p, '') if s.startswith(p) else s - use str.removeprefix -metadata: - weight: 3 - category: slop -rule: - pattern: "$S.replace($P, $EMPTY) if $S.startswith($P) else $S" ---- -id: returncode-int-cast -language: python -severity: warning -message: int(proc.returncode) - returncode is already int -metadata: - weight: 2 - category: slop -rule: - pattern: int($X.returncode) ---- -id: stdout-or-if-stdout -language: python -severity: warning -message: (x or '') if x else '' - the 'or' branch already handles None -metadata: - weight: 3 - category: slop -rule: - pattern: "($X or $EMPTY) if $X else $EMPTY" ---- -id: section-banner-comment -language: python -severity: warning -message: Decorative section banner comment - delete, let structure speak for itself -metadata: - weight: 2 - category: slop -rule: - kind: comment - regex: "^#\\s*[-=\u2500]{10,}\\s*$" ---- -id: validate-function-family -language: python -severity: hint -message: validate_X function family - 4+ siblings in one file suggests schema-driven refactor target -metadata: - weight: 3 - category: slop -rule: - kind: function_definition - has: - kind: identifier - regex: "^validate_[a-z_]+$" ---- -id: dataclass-count-explosion -language: python -severity: hint -message: "@dataclass decorator - 10+ in one file suggests TypedDict or consolidation target" -metadata: - weight: 2 - category: slop -rule: - kind: decorator - regex: "^@dataclass" diff --git a/docs/README.md b/docs/README.md index 4935f423..53756df9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -86,12 +86,13 @@ Understand how SlopCodeBench manages workspaces, executes code in different envi ### [metrics/](metrics/) **Code quality measurement and analysis** -Learn about SlopCodeBench's 7-category metric system for measuring code quality, including interpretation, configuration, and output formats. +Learn about SlopCodeBench's code quality metrics, including interpretation, +configuration, and output formats. **Key files:** - [metrics/README.md](metrics/README.md) - Overview and metric categories - [metrics/interpreting-results.md](metrics/interpreting-results.md) - Explanation of each metric -- [metrics/configuration.md](metrics/configuration.md) - Thresholds and AST-grep rules +- [metrics/configuration.md](metrics/configuration.md) - Metric configuration ## Support Files diff --git a/docs/commands/metrics.md b/docs/commands/metrics.md index ce2d4b85..913d943f 100644 --- a/docs/commands/metrics.md +++ b/docs/commands/metrics.md @@ -59,7 +59,7 @@ Calculates for each checkpoint: - Lines of code (LOC), total lines - Cyclomatic complexity (mean, max, high count) - Maintainability index -- AST-grep "slop" violations +- `scb-check` verbosity and erosion scores - Lint errors Results saved to: @@ -253,9 +253,9 @@ slop-code metrics variance [OPTIONS] PRESET RUNS_DIR | Preset | Metrics Included | |--------|------------------| -| `base` | Pass rate, cost, duration, LOC, lint, slop, CC | +| `base` | Pass rate, cost, duration, LOC, lint, verbosity, CC | | `tests` | Pass rates by test bucket (core, error, functionality) | -| `quality` | Quality metrics on the reduced surface: slop, rubric, CC, graph metrics, `mass.cc`, `mass.high_cc_pct`, and the surviving deltas (`delta.loc`, `delta.ast_grep_violations`, `delta.churn_ratio`) | +| `quality` | Quality metrics on the reduced surface: verbosity, rubric, CC, graph metrics, `mass.cc`, `mass.high_cc_pct`, and the surviving deltas (`delta.loc`, `delta.verbosity`, `delta.churn_ratio`) | ### Behavior diff --git a/docs/commands/utils.md b/docs/commands/utils.md index 4dd97b09..f29a28fb 100644 --- a/docs/commands/utils.md +++ b/docs/commands/utils.md @@ -108,8 +108,7 @@ slop-code utils backfill-reports [OPTIONS] RESULTS_DIR 2. Loads problem configuration for each 3. Generates report entries for each checkpoint 4. Updates `checkpoint_results.jsonl` -5. Backfills AST-grep category data -6. Generates `result.json` summary +5. Generates `result.json` summary ### Examples diff --git a/docs/metrics-reference.md b/docs/metrics-reference.md index 7d6643b7..980e2c70 100644 --- a/docs/metrics-reference.md +++ b/docs/metrics-reference.md @@ -37,8 +37,8 @@ P1 = sum(c1_p1, c2_p1), P2 = sum(c1_p2, c2_p2, c3_p2) → MetricStats | Solves more? | `pct_problems_solved`, `pct_checkpoints_solved` | | Meets core spec? | `pct_checkpoints_core_solved`, `pass_rates.checkpoint.core` | | Breaks prior work? | `pass_rates.checkpoint.regression` | -| Cleaner code? | `ratios.lint.mean`, `ratios.ast_grep.mean`, `verbosity.mean` | -| Quality degrades? | `delta.loc`, `delta.ast_grep_violations`, `delta.churn_ratio`, `erosion.mean` | +| Cleaner code? | `ratios.lint.mean`, `verbosity.mean` | +| Quality degrades? | `delta.loc`, `delta.verbosity`, `delta.churn_ratio`, `erosion.mean` | | Cheaper end-to-end? | `costs.problem.mean`, `costs.total` | | Fewer steps? | `steps.checkpoint.mean` | @@ -142,25 +142,16 @@ Per function/method, radon scale. | `lint_fixable` | Auto-fixable violations | | `lint_per_loc` | `lint_errors / loc` | -### AST-Grep Violations - -Rules in `configs/slop_rules.yaml`, weighted 1-4 per rule. - -| Key | Description | -|-----|-------------| -| `ast_grep_violations` | Total violations | -| `violation_pct` | AST-grep flagged lines / `loc` | - ### Redundancy & Waste | Key | Description | |-----|-------------| | `clone_lines` | Total duplicated lines | -| `cloned_sloc_lines` | Duplicated SLOC lines after filtering comments/docstrings | -| `cloned_pct` | `cloned_sloc_lines / loc` | +| `cloned_sloc_lines` | Duplicated SLOC lines reported by `scb-check` | +| `cloned_pct` | `scb-check` clone LOC / total LOC | | `clone_ratio` | Cloned SLOC / file SLOC (per-file metric; aggregated separately) | -| `verbosity_flagged_sloc_lines` | SLOC lines flagged by clone or AST-grep coverage union | -| `verbosity_flagged_pct` | `verbosity_flagged_sloc_lines / loc` | +| `verbosity_flagged_sloc_lines` | Verbosity-flagged SLOC reported by `scb-check` | +| `verbosity_flagged_pct` | `scb-check` flagged LOC / total LOC | | `single_use_functions` | Functions called only once | | `trivial_wrappers` | Functions that just delegate to another | | `unused_variables` | Variables assigned but never read | @@ -206,7 +197,7 @@ Present for all checkpoints after the first. | Key | Description | |-----|-------------| | `delta.loc` | % change in LOC | -| `delta.ast_grep_violations` | % change in AST-grep violations | +| `delta.verbosity` | % change in the `scb-check` verbosity score | | `delta.churn_ratio` | `(lines_added + lines_removed) / prior_total_lines` | --- @@ -268,23 +259,11 @@ Checkpoints with zero tests for a type are excluded from that type's average. | Key | Type | Description | |-----|------|-------------| | `cc.high_count`, `cc.high_mean`, `cc.max` | MetricStats | CC stats across checkpoints | -| `ratios.rubric`, `ratios.lint`, `ratios.ast_grep` | MetricStats | `metric / loc` per checkpoint | +| `ratios.rubric`, `ratios.lint` | MetricStats | `metric / loc` per checkpoint | ### Composite Scores -**Verbosity** (code bloat): -``` -verbosity_flagged_pct -``` - -**Erosion** (structural degradation): -``` -mass.high_cc_pct -``` - -Where: -- `loc` is the repo's non-blank, non-comment LOC metric -- `verbosity_flagged_pct` is the SLOC coverage of - `duplicate_lines ∪ ast_grep_flagged_lines` - -Both are `MetricStats` over per-checkpoint values. +`verbosity` and `erosion` come directly from the report emitted by the pinned +`scb-check` release. `scb_check_version` records the release used for each +successful checkpoint measurement. Both scores are aggregated as `MetricStats` +over per-checkpoint values. diff --git a/docs/metrics/README.md b/docs/metrics/README.md index 9b7c2597..db7e28db 100644 --- a/docs/metrics/README.md +++ b/docs/metrics/README.md @@ -13,7 +13,7 @@ When an agent completes a checkpoint, the metrics system analyzes the submitted - **Line metrics**: LOC, comments, total lines - **Lint metrics**: Ruff errors and violations - **Complexity metrics**: Cyclomatic complexity (A-F ratings), nesting depth -- **Pattern violations**: AST-grep rule violations across 7 categories +- **Composite quality**: Verbosity and erosion from pinned `scb-check` - **Code quality**: Waste detection (trivial wrappers, single-use functions), code clones - **Dependencies**: Graph metrics for import relationships @@ -38,7 +38,7 @@ Results are saved to JSON/JSONL files in each checkpoint's `quality_analysis/` d - **Looking at output files?** See [Output Files Reference](output-files.md) - file locations and formats ### Configuration -- **Adjusting thresholds?** Read [Configuration Guide](configuration.md) - thresholds and AST-grep rules +- **Adjusting thresholds?** Read [Configuration Guide](configuration.md) ## Core Concepts @@ -49,7 +49,7 @@ Results are saved to JSON/JSONL files in each checkpoint's `quality_analysis/` d | **LOC** | Lines of code (source lines, excluding blanks) | | **Cyclomatic Complexity (CC)** | Number of independent paths through code (A=1-5, F=41+) | | **Maintainability Index (MI)** | Composite score of code maintainability (A >= 19) | -| **AST-grep Violations** | Pattern-based slop-rule violations from `configs/slop_rules.yaml` | +| **Verbosity** | Code-bloat score produced by pinned `scb-check` | | **Waste** | Abstraction inefficiencies (trivial wrappers, single-use functions) | | **Clones** | Duplicate code blocks detected via AST hashing | | **Delta Metrics** | Percentage changes between checkpoints | @@ -61,7 +61,7 @@ Results are saved to JSON/JSONL files in each checkpoint's `quality_analysis/` d ### What metrics indicate good code quality? - **CC ratings**: More A/B ratings, fewer D/E/F - **Lint errors**: Lower is better -- **AST-grep violations**: Lower is better (especially safety/complexity categories) +- **Verbosity**: Lower is better - **Waste metrics**: Fewer trivial wrappers and single-use functions - **Clone ratio**: Lower percentage means less duplication @@ -76,8 +76,7 @@ outputs/run_name/problem_name/checkpoint_N/ ├── quality_analysis/ │ ├── overall_quality.json # Aggregated snapshot metrics │ ├── files.jsonl # Per-file metrics -│ ├── symbols.jsonl # Per-function/class metrics -│ └── ast_grep.jsonl # Pattern violations +│ └── symbols.jsonl # Per-function/class metrics └── evaluation/ ├── stdout.txt, stderr.txt, report.json # Test artifacts ``` @@ -94,8 +93,7 @@ Use checkpoint-level files for detailed analysis of a specific checkpoint. Use r ### How do I compare checkpoints? Delta metrics (prefixed with `delta.`) show percentage changes: - `delta.loc`: Lines of code change -- `delta.lint_errors`: Lint error change -- `delta.ast_grep_violations`: Violation change +- `delta.verbosity`: Verbosity score change - `delta.churn_ratio`: Code churn (lines added + removed / prior total) ## Code Location @@ -109,7 +107,6 @@ Delta metrics (prefixed with `delta.`) show percentage changes: - `CorrectnessResults`: Test result model - `GroupType`: Test categorization (CORE, FUNCTIONALITY, REGRESSION, ERROR) - `PassPolicy`: Success criteria -- **AST-grep rules**: `configs/slop_rules.yaml` - **Main entry points**: - Snapshot quality: `slop_code.metrics.driver.measure_snapshot_quality()` - Checkpoint metrics: `slop_code.metrics.checkpoint.driver.get_checkpoint_metrics()` diff --git a/docs/metrics/checkpoint-results.md b/docs/metrics/checkpoint-results.md index 18553ae4..a9d6307a 100644 --- a/docs/metrics/checkpoint-results.md +++ b/docs/metrics/checkpoint-results.md @@ -158,8 +158,6 @@ This file contains aggregated metrics across the entire checkpoint. It's organiz | **complexity** | cc_ratings, mi_ratings, cc_max, cc_mean | Complexity distribution | | **waste** | single_use_functions, trivial_wrappers, single_method_classes | Abstraction efficiency | | **redundancy** | clone_instances, clone_lines, clone_ratio_sum, cloned_sloc_lines | Code duplication | -| **ast_grep** | violations, category_counts, category_weighted | Slop-rule violations | -| **root** | verbosity_flagged_sloc_lines | Union coverage used by verbosity | | **graph** | node_count, edge_count, cyclic_dependency_mass | Dependency analysis | Example excerpt: @@ -225,7 +223,6 @@ Per-file metrics, one JSON object per line: "depth": 1, "is_entry_language": true, "symbol_count": 42, - "ast_grep_violation_count": 103, "clone_instances": 8, "clone_lines": 30, "single_use_count": 5, @@ -274,38 +271,6 @@ Per-function/class/method metrics, one JSON object per line: - Understand function dependencies (`variables_defined`, `variables_used`) - Find functions with high `raise_count` (error-prone) -### ast_grep.jsonl - -AST-grep pattern violations, one per line: - -```json -{ - "rule_id": "complex-tuple-type", - "severity": "warning", - "category": "types", - "subcategory": "types", - "weight": 2, - "line": 256, - "column": 15, - "end_line": 256, - "end_column": 39, - "file_path": "industry.py" -} -``` - -| Field | Meaning | -|-------|---------| -| `rule_id` | Name of the pattern that was violated | -| `category` | Always `slop` for the built-in ruleset | -| `weight` | Severity: 1 (minor) to 4 (critical) | -| `line` / `column` | Location in source code | - -**Use this to:** -- Filter violations by category (e.g., safety violations) -- Prioritize fixes by weight (focus on weight 3-4) -- Identify specific patterns to improve -- See [Interpreting Results](interpreting-results.md) for category details - ## Delta Metrics Delta metrics measure how quality changes between consecutive checkpoints. They're included in the checkpoint-level metrics. @@ -315,9 +280,7 @@ Delta metrics measure how quality changes between consecutive checkpoints. They' | Metric | Formula | Interpretation | |--------|---------|-----------------| | `delta.loc` | `(curr_loc - prev_loc) / prev_loc * 100` | % change in code size | -| `delta.lint_errors` | Same formula | % change in lint errors | -| `delta.ast_grep_violations` | Same formula | % change in violations | -| `delta.cc_high_count` | Same formula | % change in complex functions | +| `delta.verbosity` | Same formula | % change in the `scb-check` verbosity score | | `delta.churn_ratio` | `(added + removed) / prev_total` | Code churn as % of prior size | **Special cases:** @@ -435,8 +398,7 @@ checkpoint_N/ ├── quality_analysis/ # Code quality metrics │ ├── overall_quality.json # Aggregate quality metrics │ ├── files.jsonl # Per-file metrics -│ ├── symbols.jsonl # Per-function/class metrics -│ └── ast_grep.jsonl # Pattern violations +│ └── symbols.jsonl # Per-function/class metrics ├── evaluation/ # Test artifacts │ ├── stdout.txt # Pytest output │ ├── stderr.txt # Pytest errors @@ -523,12 +485,10 @@ def checkpoint_health(checkpoint_dir): # Check requirements core_passes = eval_results['pass_counts'].get('Core', 0) == eval_results['total_counts'].get('Core', 0) high_complexity = quality['complexity']['cc_max'] > 20 - too_many_violations = quality['ast_grep']['violations'] > 50 return { 'requirements_met': core_passes, 'high_complexity': high_complexity, - 'too_many_violations': too_many_violations, 'loc': quality['lines']['loc'], 'lint_errors': quality['lint']['errors'] } @@ -549,10 +509,7 @@ def quality_delta(checkpoint_dir, prior_dir): # Calculate deltas loc_delta = (curr['lines']['loc'] - prev['lines']['loc']) / prev['lines']['loc'] * 100 - violations_delta = (curr['ast_grep']['violations'] - prev['ast_grep']['violations']) / prev['ast_grep']['violations'] * 100 - print(f"LOC change: {loc_delta:.1f}%") - print(f"Violations change: {violations_delta:.1f}%") print(f"Complexity max: {prev['complexity']['cc_max']} → {curr['complexity']['cc_max']}") ``` diff --git a/docs/metrics/configuration.md b/docs/metrics/configuration.md index 515e7dbe..e9f9b635 100644 --- a/docs/metrics/configuration.md +++ b/docs/metrics/configuration.md @@ -5,7 +5,7 @@ last_updated: 2025-12-17 # Metrics Configuration -This guide explains how to configure metrics thresholds and AST-grep rules. +This guide explains how to configure metric thresholds. ## Metrics Thresholds @@ -49,121 +49,6 @@ MI ratings are also fixed: | B | 10-19 | Moderately maintainable | | C | < 10 | Difficult to maintain | -## AST-grep Rules - -AST-grep rules detect code patterns using structural matching on the AST. - -### Rule Location - -Rules are stored in `configs/slop_rules.yaml`. - -### Rule Format - -Each rule is a YAML document with this structure: - -```yaml ---- -id: rule-identifier -language: python -severity: warning # warning, error, info, hint -message: "Human-readable description of the issue" -metadata: - weight: 2 # Severity weight (1-4) - category: slop # Slop rule family -rule: - kind: identifier # AST node type to match - regex: "_list$" # Pattern to match -``` - -### Rule Weights - -Weights indicate severity for weighted scoring: - -| Weight | Meaning | Examples | -|--------|---------|----------| -| 1 | Style preference | Verbose identifiers, redundant expressions | -| 2 | Best practice | Generic variable names, missing type hints | -| 3 | Likely problem | Deep nesting, complex conditionals | -| 4 | Bug or security risk | Bare except, dangerous patterns | - -### Example Rules - -**Manual sum loop** (`slop_rules.yaml`): -```yaml ---- -id: manual-sum-loop -language: python -severity: warning -message: Manual accumulation loop - use sum(...) instead of a throwaway counter variable -metadata: - weight: 4 - category: slop -rule: - kind: for_statement - pattern: "for $ITEM in $ITER:\n $TOTAL += $EXPR\n" -``` - -**Redundant guard with same return** (`slop_rules.yaml`): -```yaml ---- -id: redundant-guard-same-return -language: python -severity: warning -message: Guard returning the same expression in both paths adds dead ceremony -metadata: - weight: 4 - category: slop -rule: - pattern: "if $COND: return $RET return $RET " -``` - -### Overriding Rules File - -Set the `AST_GREP_RULES_PATH` environment variable to use a different rules file: - -```bash -export AST_GREP_RULES_PATH=/path/to/custom/slop_rules.yaml -slop-code run ... -``` - -### Writing Custom Rules - -1. Edit `configs/slop_rules.yaml` or point `AST_GREP_RULES_PATH` at a custom file -2. Use ast-grep pattern syntax for matching -3. Assign an appropriate weight - -**Pattern reference:** - -| Pattern | Matches | -|---------|---------| -| `$VAR` | Any single node | -| `$$$` | Zero or more nodes | -| `kind: function_definition` | Specific AST node type | -| `regex: "pattern"` | Node text matching regex | -| `has:` | Node contains child matching pattern | -| `inside:` | Node is inside parent matching pattern | - -For full pattern documentation, see [ast-grep docs](https://ast-grep.github.io/). - -### Testing Rules - -Test a rule against your code: - -```bash -# Using ast-grep directly -sg -p 'def $FUNC($$$): pass' -l python path/to/code/ - -# Scan with rules -sg scan --rule /path/to/rule.yaml path/to/code/ -``` - -## Disabling Metrics - -Currently, all metrics run by default. To exclude specific categories from analysis: - -1. **Edit `configs/slop_rules.yaml`**: Remove rules you do not want to count -2. **Filter in analysis**: Post-process results to exclude unwanted metrics - ## Metric Computation Options ### Entry Language diff --git a/docs/metrics/interpreting-results.md b/docs/metrics/interpreting-results.md index 3007a456..05a14678 100644 --- a/docs/metrics/interpreting-results.md +++ b/docs/metrics/interpreting-results.md @@ -140,34 +140,6 @@ Detects duplicate code using AST hashing. - High `clone_ratio` (> 10%) suggests refactoring opportunities - Duplicates often indicate missing abstractions -## AST-grep Violations - -Pattern-based detection of code quality issues across 7 categories. - -### Categories - -| Category | Description | Example Rules | -|----------|-------------|---------------| -| **slop** | Unnecessary code surface | Verbose loops, redundant guards, needless materialization, broad `Any`/`object` typing | - -### Metrics - -| Metric | Description | -|--------|-------------| -| `ast_grep_violations` | Total violations found | -| `sg_slop_violations` | Slop-rule violation count | -| `ast_grep_per_loc` | Violations per line of code | - -### Rule Weights - -Each rule has a weight (1-4) indicating severity: -- **Weight 1**: Minor issues, style preferences -- **Weight 2**: Moderate issues, best practice violations -- **Weight 3**: Significant issues, likely problems -- **Weight 4**: Critical issues, bugs or security risks - -The `weighted` metric sums (violations * weight) for prioritized scoring. - ## Graph Metrics Dependency analysis based on import relationships (Python only). @@ -192,11 +164,8 @@ Percentage changes between consecutive checkpoints. | Metric | Description | Formula | |--------|-------------|---------| | `delta.loc` | LOC change | `(curr - prev) / prev * 100` | -| `delta.lint_errors` | Lint error change | Same formula | -| `delta.ast_grep_violations` | Violation change | Same formula | -| `delta.cc_high_count` | Complex function change | Same formula | +| `delta.verbosity` | `scb-check` verbosity change | Same formula | | `delta.churn_ratio` | Code churn | `(added + removed) / prev_total` | -| `delta.new_violations_per_loc` | New violations rate | `(total - carried_over) / loc` | **Interpretation:** - Positive delta: Metric increased (often worse) @@ -206,30 +175,17 @@ Percentage changes between consecutive checkpoints. ## Composite Summary Scores -High-level scores that combine multiple metrics for quick comparison across runs. +High-level scores reported by the pinned `scb-check` release and aggregated +across checkpoints. ### Verbosity Score -Measures code bloat and over-abstraction. - -**Formula:** -```python -verbosity = mean(verbosity_flagged_pct) -``` - -Lower is better. High values indicate that a large share of SLOC is covered by -duplicate code or AST-grep verbosity flags, with overlapping lines counted once. +Measures code bloat and over-abstraction. Lower is better. ### Erosion Score -Measures structural degradation (high-complexity mass concentration). - -**Formula:** -```python -erosion = mean(mass.high_cc_pct) -``` - -Lower is better. High values indicate structural decay and accumulating technical debt. +Measures structural degradation. Lower is better. `scb_check_version` records +the exact checker release used for the checkpoint scores. ## Summary: What Good Code Looks Like @@ -240,7 +196,7 @@ Lower is better. High values indicate structural decay and accumulating technica | CC ratings | Mostly A/B | Multiple D/E/F | | `cc_max` | < 15 | > 30 | | `lint_errors` | 0 | > 10 | -| `ast_grep_violations` | < 5 | > 20 | +| `verbosity` | Lower relative to comparable runs | Higher relative to comparable runs | | `clone_ratio` | < 5% | > 15% | | `trivial_wrappers` | 0 | > 3 | | `cyclic_dependency_mass` | 0 | > 0.1 | diff --git a/docs/metrics/output-files.md b/docs/metrics/output-files.md index 76c40814..5de3a173 100644 --- a/docs/metrics/output-files.md +++ b/docs/metrics/output-files.md @@ -22,8 +22,7 @@ outputs/run_name/problem_name/checkpoint_N/ ├── quality_analysis/ │ ├── overall_quality.json # Snapshot-level aggregates │ ├── files.jsonl # Per-file metrics (one JSON per line) -│ ├── symbols.jsonl # Per-symbol metrics (one JSON per line) -│ └── ast_grep.jsonl # Pattern violations (one per line) +│ └── symbols.jsonl # Per-symbol metrics (one JSON per line) ├── diff.json # File change statistics ├── rubric.jsonl # Rubric grading results └── ... @@ -112,15 +111,6 @@ Aggregated metrics for the entire snapshot. This is the primary file for checkpo "clone_ratio_sum": 0.03, "files_with_clones": 2 }, - "ast_grep": { - "violations": 8, - "rules_checked": 137, - "counts": {"manual-sum-loop": 2, "guard-return-none": 3}, - "weighted": 12, - "category_counts": {"slop": 8}, - "category_weighted": {"slop": 12} - }, - "verbosity_flagged_sloc_lines": 29, "graph": { "node_count": 8, "edge_count": 15, @@ -144,8 +134,6 @@ Aggregated metrics for the entire snapshot. This is the primary file for checkpo | `complexity` | CC and MI distributions | | `waste` | Abstraction waste counts | | `redundancy` | Code clone aggregates, including filtered cloned SLOC | -| `ast_grep` | Slop-rule violation aggregates | -| `verbosity_flagged_sloc_lines` | Union of cloned SLOC and AST-grep-flagged SLOC | | `graph` | Dependency graph metrics (optional) | ## files.jsonl @@ -188,20 +176,7 @@ Per-file metrics in JSON Lines format (one JSON object per line). "single_use_count": 1, "trivial_wrapper_count": 0, "single_method_class_count": 0 - }, - "ast_grep_violations": [ - { - "rule_id": "manual-sum-loop", - "severity": "warning", - "category": "slop", - "weight": 4, - "line": 45, - "column": 4, - "end_line": 45, - "end_column": 15 - } - ], - "ast_grep_rules_checked": 21 + } } ``` @@ -342,7 +317,8 @@ metrics = get_checkpoint_metrics( # Access specific values print(f"LOC: {metrics['loc']}") print(f"CC max: {metrics['cc_max']}") -print(f"AST-grep violations: {metrics['ast_grep_violations']}") +print(f"Verbosity: {metrics['verbosity']}") +print(f"scb-check version: {metrics['scb_check_version']}") print(f"Pass rate: {metrics['pass_rate']:.1%}") ``` @@ -360,7 +336,6 @@ snapshot = measure_snapshot_quality( print(f"Files: {snapshot.file_count}") print(f"Total LOC: {snapshot.lines.loc}") print(f"CC max: {snapshot.functions.cc_max}") -print(f"AST-grep violations: {snapshot.ast_grep.violations}") ``` ### Run-Level Metrics diff --git a/docs/metrics/run-results.md b/docs/metrics/run-results.md index cbc0a6ad..8f1fae25 100644 --- a/docs/metrics/run-results.md +++ b/docs/metrics/run-results.md @@ -86,9 +86,9 @@ One JSON object per line, one line per checkpoint across all problems. "cc_extreme_count": 0, "clone_instances": 0, "clone_lines": 0, - "ast_grep_violations": 35, - "sg_slop_violations": 35, - "ast_grep_per_loc": 0.08139534883720931, + "verbosity": 0.26, + "erosion": 0.11, + "scb_check_version": "0.1.3", "lint_per_loc": 0.1883720930232558 } ``` @@ -137,9 +137,9 @@ One JSON object per line, one line per checkpoint across all problems. - `cc_high_count`: Functions with CC > 10 - `cc_extreme_count`: Functions with CC > 30 - `clone_instances`: Duplicate code blocks -- `ast_grep_violations`: Total pattern violations -- `sg_slop_violations`: Slop-rule violation count -- `ast_grep_per_loc`: Violations per line of code +- `verbosity`: Code-bloat score from `scb-check` +- `erosion`: Structural-degradation score from `scb-check` +- `scb_check_version`: Pinned `scb-check` release that produced the scores - `lint_per_loc`: Lint errors per line of code **Use cases:** @@ -246,15 +246,12 @@ Complete aggregated statistics across all checkpoints and problems. "mi_ratings": {"A": 8, "B": 1, "C": 0} }, "ratios": { - "ast_grep_per_loc": {"mean": 0.092, "stddev": 0.045, ...}, "lint_per_loc": {"mean": 0.156, "stddev": 0.034, ...}, "rubric_per_loc": {"mean": 0.034, "stddev": 0.019, ...} }, "delta": { "loc": {"mean": 23.5, "stddev": 15.2, ...}, - "lint_errors": {"mean": -12.3, "stddev": 24.5, ...}, - "ast_grep_violations": {"mean": 18.4, "stddev": 31.2, ...}, - "cc_high_count": {"mean": 5.2, "stddev": 8.1, ...}, + "verbosity": {"mean": 18.4, "stddev": 31.2, ...}, "churn_ratio": {"mean": 0.38, "stddev": 0.21, ...} }, "composite_scores": { diff --git a/src/slop_code/common/__init__.py b/src/slop_code/common/__init__.py index 6d3bdb8f..3fd0f10d 100644 --- a/src/slop_code/common/__init__.py +++ b/src/slop_code/common/__init__.py @@ -11,7 +11,6 @@ from slop_code.common.common import mask_sensitive_values from slop_code.common.constants import AGENT_DIR_NAME from slop_code.common.constants import AGENT_TAR_FILENAME -from slop_code.common.constants import AST_GREP_QUALITY_SAVENAME from slop_code.common.constants import CHECKPOINT_CONFIG_NAME from slop_code.common.constants import CHECKPOINT_RESULTS_FILENAME from slop_code.common.constants import CONFIG_FILENAME @@ -70,7 +69,6 @@ "QUALITY_DIR", "QUALITY_METRIC_SAVENAME", "SYMBOLS_QUALITY_SAVENAME", - "AST_GREP_QUALITY_SAVENAME", "EVALUATION_FILENAME", "VERIFIER_REPORT_FILENAME", "SUMMARY_FILENAME", diff --git a/src/slop_code/common/constants.py b/src/slop_code/common/constants.py index ac92a076..f0c27f5a 100644 --- a/src/slop_code/common/constants.py +++ b/src/slop_code/common/constants.py @@ -19,7 +19,6 @@ QUALITY_DIR = "quality_analysis" FILES_QUALITY_SAVENAME = "files.jsonl" SYMBOLS_QUALITY_SAVENAME = "symbols.jsonl" -AST_GREP_QUALITY_SAVENAME = "ast_grep.jsonl" CONFIG_FILENAME = "config.yaml" SUMMARY_FILENAME = "result.json" CHECKPOINT_RESULTS_FILENAME = "checkpoint_results.jsonl" diff --git a/src/slop_code/dashboard/data.py b/src/slop_code/dashboard/data.py index 27d4df62..cc2df738 100644 --- a/src/slop_code/dashboard/data.py +++ b/src/slop_code/dashboard/data.py @@ -848,9 +848,9 @@ def compute_problem_deltas(df: pd.DataFrame) -> pd.DataFrame: prev_row.get("lint_errors", 0), curr_row.get("lint_errors", 0), ), - "ast_grep_delta_pct": _safe_delta_pct( - prev_row.get("ast_grep_violations", 0), - curr_row.get("ast_grep_violations", 0), + "verbosity_delta_pct": _safe_delta_pct( + prev_row.get("verbosity", 0), + curr_row.get("verbosity", 0), ), "complex_delta_pct": _safe_delta_pct( prev_row["cc_high_count"], diff --git a/src/slop_code/dashboard/graphs/boxplot.py b/src/slop_code/dashboard/graphs/boxplot.py index 5cd0f431..c282020d 100644 --- a/src/slop_code/dashboard/graphs/boxplot.py +++ b/src/slop_code/dashboard/graphs/boxplot.py @@ -31,7 +31,7 @@ def build_checkpoint_delta_boxplot(context: ChartContext) -> go.Figure: subplot_titles=[ "LOC", "Lint", - "AST Grep Rules", + "Verbosity", "Rubric", "Novel Flags", "High Complexity Symbols", @@ -41,7 +41,7 @@ def build_checkpoint_delta_boxplot(context: ChartContext) -> go.Figure: metrics = [ ("lines_delta_pct", 1, 1), ("lint_delta_pct", 1, 2), - ("ast_grep_delta_pct", 1, 3), + ("verbosity_delta_pct", 1, 3), ("rubric_delta_pct", 2, 1), ("rubric_non_carryover", 2, 2), ("complex_delta_pct", 2, 3), diff --git a/src/slop_code/dashboard/graphs/comparison.py b/src/slop_code/dashboard/graphs/comparison.py index e079411c..e01e96e7 100644 --- a/src/slop_code/dashboard/graphs/comparison.py +++ b/src/slop_code/dashboard/graphs/comparison.py @@ -35,7 +35,7 @@ def build_problem_comparison_chart( subplot_titles=( "LOC", "Lint Errors", - "AST-grep Violations", + "Verbosity", "High Complexity", "Total Rubric Flags", "New Rubric Flags", @@ -48,7 +48,7 @@ def build_problem_comparison_chart( "Pass Rate: Error (%)", "Mass: CC", "Δ LOC (%)", - "Δ AST-grep (%)", + "Δ Verbosity (%)", "Δ Churn Ratio", "CC Concentration", "Mass: CC", @@ -153,9 +153,7 @@ def add_pass_rate_col(prefix): zeroes = pd.Series([0.0] * len(run_df), index=run_df.index) run_df["_mass_cc"] = run_df.get("mass.cc", zeroes) run_df["_delta_loc"] = run_df.get("delta.loc", zeroes) - run_df["_delta_ast_grep"] = run_df.get( - "delta.ast_grep_violations", zeroes - ) + run_df["_delta_verbosity"] = run_df.get("delta.verbosity", zeroes) run_df["_delta_churn_ratio"] = run_df.get("delta.churn_ratio", zeroes) run_df["_cc_concentration"] = run_df.get("cc_concentration", zeroes) @@ -200,7 +198,7 @@ def add_trace(col, row, col_idx, show_legend=False): # Row 1 add_trace("loc", 1, 1, show_legend=True) add_trace("lint_errors", 1, 2) - add_trace("ast_grep_violations", 1, 3) + add_trace("verbosity", 1, 3) # Row 2 add_trace("cc_high_count", 2, 1) @@ -223,16 +221,14 @@ def add_trace(col, row, col_idx, show_legend=False): add_trace("_delta_loc", 5, 3) # Row 6 - add_trace("_delta_ast_grep", 6, 1) + add_trace("_delta_verbosity", 6, 1) add_trace("_delta_churn_ratio", 6, 2) add_trace("_cc_concentration", 6, 3) # Update y-axes titles fig.update_yaxes(title_text="Lines", row=1, col=1, gridcolor="lightgray") fig.update_yaxes(title_text="Errors", row=1, col=2, gridcolor="lightgray") - fig.update_yaxes( - title_text="Violations", row=1, col=3, gridcolor="lightgray" - ) + fig.update_yaxes(title_text="Score", row=1, col=3, gridcolor="lightgray") fig.update_yaxes(title_text="Count", row=2, col=1, gridcolor="lightgray") fig.update_yaxes(title_text="Flags", row=2, col=2, gridcolor="lightgray") diff --git a/src/slop_code/dashboard/graphs/quality_deltas.py b/src/slop_code/dashboard/graphs/quality_deltas.py index 86ceabfc..072cb15a 100644 --- a/src/slop_code/dashboard/graphs/quality_deltas.py +++ b/src/slop_code/dashboard/graphs/quality_deltas.py @@ -9,7 +9,7 @@ METRICS = [ ("lint_delta_pct", "Lint Δ%"), - ("ast_grep_delta_pct", "AST-grep Δ%"), + ("verbosity_delta_pct", "Verbosity Δ%"), ("rubric_delta_pct", "Rubric Δ%"), ("complex_delta_pct", "Complexity Δ%"), ("rubric_non_carryover", "Novel Flags (count)"), diff --git a/src/slop_code/dashboard/graphs/scatter.py b/src/slop_code/dashboard/graphs/scatter.py index 7065737d..b6847b21 100644 --- a/src/slop_code/dashboard/graphs/scatter.py +++ b/src/slop_code/dashboard/graphs/scatter.py @@ -250,9 +250,6 @@ def _aggregate_scatter_metrics( def aggregate_erosion_vs_solve( context: ChartContext, solve_type: Literal["all", "iso", "core"] = "all", - include_lint: bool = True, - include_rubric: bool = True, - include_ast_grep: bool = True, ) -> list[ScatterMetrics]: return _aggregate_scatter_metrics( context, @@ -261,25 +258,6 @@ def aggregate_erosion_vs_solve( ) -def aggregate_lint_ast_grep_vs_solve( - context: ChartContext, -) -> list[ScatterMetrics]: - return _aggregate_scatter_metrics( - context, - lambda df: ( - df[ - [ - "ratios.violation_pct.mean", - "ratios.lint.mean", - ] - ] - .fillna(0) - .sum(axis=1) - .mean() - ), - ) - - def aggregate_high_complexity_vs_solve( context: ChartContext, ) -> list[ScatterMetrics]: @@ -321,13 +299,6 @@ def compute_cost_per_problem(df: pd.DataFrame) -> float: return _aggregate_scatter_metrics(context, compute_cost_per_problem) -def aggregate_ast_grep_vs_solve(context: ChartContext) -> list[ScatterMetrics]: - return _aggregate_scatter_metrics( - context, - lambda df: df["ratios.violation_pct.mean"].fillna(0).mean(), - ) - - def aggregate_lint_vs_solve(context: ChartContext) -> list[ScatterMetrics]: return _aggregate_scatter_metrics( context, lambda df: df["ratios.lint.mean"].fillna(0).mean() @@ -468,26 +439,17 @@ class ScatterChartConfig: SCATTER_CHARTS: dict[str, ScatterChartConfig] = { "verbosity_vs_solve": ScatterChartConfig( - aggregator=lambda context: aggregate_erosion_vs_solve( - context, - include_lint=False, - include_ast_grep=True, - include_rubric=True, - ), + aggregator=aggregate_erosion_vs_solve, x_axis=AxisConfig("Verbosity Score", "log"), y_axis=AxisConfig("% Checkpoints Solved"), ), "verbosity_vs_core_solve": ScatterChartConfig( - aggregator=lambda context: aggregate_erosion_vs_solve( - context, "core", include_lint=False - ), + aggregator=lambda context: aggregate_erosion_vs_solve(context, "core"), x_axis=AxisConfig("Verbosity Score", "log"), y_axis=AxisConfig("% Checkpoints Core Solved"), ), "verbosity_vs_iso_solve": ScatterChartConfig( - aggregator=lambda context: aggregate_erosion_vs_solve( - context, "iso", include_lint=False - ), + aggregator=lambda context: aggregate_erosion_vs_solve(context, "iso"), x_axis=AxisConfig("Verbosity Score", "log"), y_axis=AxisConfig("% Checkpoints ISO Solved"), ), @@ -526,11 +488,6 @@ class ScatterChartConfig: x_axis=AxisConfig("Lint Errors", "log"), y_axis=AxisConfig("% Checkpoints Solved"), ), - "ast_grep_vs_solve": ScatterChartConfig( - aggregator=aggregate_ast_grep_vs_solve, - x_axis=AxisConfig("AST Grep Errors", "log"), - y_axis=AxisConfig("% Checkpoints Solved"), - ), "rubric_vs_solve": ScatterChartConfig( aggregator=aggregate_rubric_vs_solve, x_axis=AxisConfig("Rubric Flags", "log"), diff --git a/src/slop_code/dashboard/pages/head_to_head_evolution.py b/src/slop_code/dashboard/pages/head_to_head_evolution.py index 5c4dad44..028d9429 100644 --- a/src/slop_code/dashboard/pages/head_to_head_evolution.py +++ b/src/slop_code/dashboard/pages/head_to_head_evolution.py @@ -84,12 +84,12 @@ dbc.Card( [ dbc.CardHeader( - "AST-grep Violations Trajectory", + "Verbosity Trajectory", className="fw-bold", ), dbc.CardBody( loading_graph( - "h2h-ast-grep-trajectory", height="400px" + "h2h-verbosity-trajectory", height="400px" ) ), ], @@ -110,7 +110,7 @@ Output("h2h-loc-trajectory", "figure"), Output("h2h-complexity-trajectory", "figure"), Output("h2h-lint-trajectory", "figure"), - Output("h2h-ast-grep-trajectory", "figure"), + Output("h2h-verbosity-trajectory", "figure"), ], [Input("h2h-run-a", "value"), Input("h2h-run-b", "value")], ) @@ -148,8 +148,8 @@ def update_evolution(run_a_path, run_b_path): state_cols = ["loc", "cc_high_count"] if "lint_errors" in df_a_raw.columns: state_cols.append("lint_errors") - if "ast_grep_violations" in df_a_raw.columns: - state_cols.append("ast_grep_violations") + if "verbosity" in df_a_raw.columns: + state_cols.append("verbosity") # 1. Add Progress (Create Scatter Data) def add_progress(df): @@ -283,11 +283,11 @@ def make_trajectory(col, title, y_axis): "lint_errors", "Avg Lint Errors per Progress Step", "Errors" ) - # 4. AST-grep Violations - ast_grep_traj = make_trajectory( - "ast_grep_violations", - "Avg AST-grep Violations per Progress Step", - "Violations", + # 4. Verbosity + verbosity_traj = make_trajectory( + "verbosity", + "Avg Verbosity per Progress Step", + "Verbosity", ) - return loc_traj, comp_traj, lint_traj, ast_grep_traj + return loc_traj, comp_traj, lint_traj, verbosity_traj diff --git a/src/slop_code/dashboard/pages/head_to_head_quality.py b/src/slop_code/dashboard/pages/head_to_head_quality.py index 93ada563..2b6944f6 100644 --- a/src/slop_code/dashboard/pages/head_to_head_quality.py +++ b/src/slop_code/dashboard/pages/head_to_head_quality.py @@ -122,12 +122,12 @@ def make_quality_scatter_checkpoint_level( dbc.Card( [ dbc.CardHeader( - "AST-grep Violations Comparison", + "Verbosity Comparison", className="fw-bold", ), dbc.CardBody( loading_graph( - "h2h-ast-grep-scatter", height="400px" + "h2h-verbosity-scatter", height="400px" ) ), ], @@ -162,7 +162,7 @@ def make_quality_scatter_checkpoint_level( @callback( [ Output("h2h-lint-scatter", "figure"), - Output("h2h-ast-grep-scatter", "figure"), + Output("h2h-verbosity-scatter", "figure"), Output("h2h-complexity-scatter", "figure"), ], [Input("h2h-run-a", "value"), Input("h2h-run-b", "value")], @@ -214,11 +214,11 @@ def short_label(name): run_a_path, run_b_path, ) - ast_grep_fig = make_quality_scatter_checkpoint_level( + verbosity_fig = make_quality_scatter_checkpoint_level( df_chk, - "ast_grep_violations", - "AST-grep Violations per Checkpoint", - "Violations", + "verbosity", + "Verbosity per Checkpoint", + "Verbosity", name_a, name_b, run_a_path, @@ -235,4 +235,4 @@ def short_label(name): run_b_path, ) - return lint_fig, ast_grep_fig, comp_fig + return lint_fig, verbosity_fig, comp_fig diff --git a/src/slop_code/dashboard/pages/run_analysis_quality.py b/src/slop_code/dashboard/pages/run_analysis_quality.py index 2a20cb62..d3febac7 100644 --- a/src/slop_code/dashboard/pages/run_analysis_quality.py +++ b/src/slop_code/dashboard/pages/run_analysis_quality.py @@ -26,7 +26,7 @@ [ dbc.Col(loading_graph("ra-lint-dist", height="350px"), md=4), dbc.Col( - loading_graph("ra-ast-grep-dist", height="350px"), md=4 + loading_graph("ra-verbosity-dist", height="350px"), md=4 ), dbc.Col(loading_graph("ra-complex-dist", height="350px"), md=4), ], @@ -74,7 +74,7 @@ def build_histogram(df, col, title, color): @callback( [ Output("ra-lint-dist", "figure"), - Output("ra-ast-grep-dist", "figure"), + Output("ra-verbosity-dist", "figure"), Output("ra-complex-dist", "figure"), Output("ra-func-loc-dist", "figure"), Output("ra-cmp-loc-dist", "figure"), @@ -97,8 +97,8 @@ def update_quality(run_path): lint_hist = build_histogram( df, "lint_errors", "Lint Errors Distribution", "#d62728" ) - ast_grep_hist = build_histogram( - df, "ast_grep_violations", "AST-grep Violations Distribution", "#ff7f0e" + verbosity_hist = build_histogram( + df, "verbosity", "Verbosity Distribution", "#ff7f0e" ) comp_hist = build_histogram( df, "cc_high_count", "Complexity Distribution", "#9467bd" @@ -147,7 +147,7 @@ def update_quality(run_path): return ( lint_hist, - ast_grep_hist, + verbosity_hist, comp_hist, func_loc_hist, cmp_loc_hist, diff --git a/src/slop_code/entrypoints/commands/backfill_reports.py b/src/slop_code/entrypoints/commands/backfill_reports.py index 28ddbf15..87b3e4a5 100644 --- a/src/slop_code/entrypoints/commands/backfill_reports.py +++ b/src/slop_code/entrypoints/commands/backfill_reports.py @@ -24,10 +24,6 @@ from slop_code.entrypoints.utils import display_and_save_summary from slop_code.evaluation import ProblemConfig from slop_code.logging import setup_logging -from slop_code.metrics.languages.python.ast_grep import RuleLookup -from slop_code.metrics.languages.python.ast_grep import ( - build_ast_grep_rules_lookup, -) def _backfill_top20_share( @@ -216,209 +212,6 @@ def _process_single_run_backfill( return all_reports, all_errors, problems_processed -def _update_ast_grep_jsonl( - jsonl_path: Path, - rules_lookup: RuleLookup, - logger: Any, -) -> tuple[int, int]: - """Update ast_grep.jsonl with category/subcategory/weight from rules. - - Also updates the overall_quality.json with recalculated category_counts - and category_weighted aggregates. - - Args: - jsonl_path: Path to the ast_grep.jsonl file. - rules_lookup: Mapping of rule_id to category/subcategory/weight. - logger: Logger instance. - - Returns: - Tuple of (total_violations, violations_updated) - """ - if not jsonl_path.exists(): - return 0, 0 - - violations: list[dict] = [] - updated_count = 0 - - try: - with jsonl_path.open("r", encoding="utf-8") as f: - for line in f: - line = line.strip() - if not line: - continue - try: - violation = json.loads(line) - except json.JSONDecodeError: - violations.append(json.loads(line) if line else {}) - continue - - rule_id = violation.get("rule_id") - if not rule_id or rule_id not in rules_lookup: - continue - - rule_info = rules_lookup[rule_id] - old_cat = violation.get("category", "") - old_subcat = violation.get("subcategory", "unknown") - old_weight = violation.get("weight", 1) - - violation["category"] = rule_info["category"] - violation["subcategory"] = rule_info["subcategory"] - violation["weight"] = rule_info["weight"] - - if ( - old_cat != rule_info["category"] - or old_subcat != rule_info["subcategory"] - or old_weight != rule_info["weight"] - ): - updated_count += 1 - - violations.append(violation) - - except OSError as e: - logger.warning( - "Failed to read ast_grep.jsonl", - path=str(jsonl_path), - error=str(e), - ) - return 0, 0 - - if not violations: - return 0, 0 - - # Write back atomically using temp file - try: - parent_dir = jsonl_path.parent - with tempfile.NamedTemporaryFile( - mode="w", - encoding="utf-8", - dir=parent_dir, - suffix=".jsonl.tmp", - delete=False, - ) as tmp: - for violation in violations: - tmp.write(json.dumps(violation) + "\n") - tmp_path = Path(tmp.name) - - tmp_path.replace(jsonl_path) - except OSError as e: - logger.warning( - "Failed to write updated ast_grep.jsonl", - path=str(jsonl_path), - error=str(e), - ) - return len(violations), 0 - - # Update overall_quality.json with new category aggregates - overall_path = jsonl_path.parent / QUALITY_METRIC_SAVENAME - if overall_path.exists(): - _update_overall_quality_ast_grep(overall_path, violations, logger) - - return len(violations), updated_count - - -def _update_overall_quality_ast_grep( - overall_path: Path, - violations: list[dict], - logger: Any, -) -> None: - """Update the ast_grep section of overall_quality.json. - - Recalculates category_counts and category_weighted from violations. - """ - try: - with overall_path.open("r", encoding="utf-8") as f: - overall = json.load(f) - except (OSError, json.JSONDecodeError) as e: - logger.warning( - "Failed to read overall_quality.json", - path=str(overall_path), - error=str(e), - ) - return - - # Recalculate category aggregates - category_counts: dict[str, int] = {} - category_weighted: dict[str, int] = {} - total_weighted = 0 - rule_counts: dict[str, int] = {} - - for v in violations: - cat = v.get("category", "") - weight = v.get("weight", 1) - rule_id = v.get("rule_id", "") - - if cat: - category_counts[cat] = category_counts.get(cat, 0) + 1 - category_weighted[cat] = category_weighted.get(cat, 0) + weight - total_weighted += weight - if rule_id: - rule_counts[rule_id] = rule_counts.get(rule_id, 0) + 1 - - # Update ast_grep section - if "ast_grep" not in overall: - overall["ast_grep"] = {} - - overall["ast_grep"]["violations"] = len(violations) - overall["ast_grep"]["weighted"] = total_weighted - overall["ast_grep"]["category_counts"] = category_counts - overall["ast_grep"]["category_weighted"] = category_weighted - overall["ast_grep"]["counts"] = rule_counts - - # Write back atomically - try: - parent_dir = overall_path.parent - with tempfile.NamedTemporaryFile( - mode="w", - encoding="utf-8", - dir=parent_dir, - suffix=".json.tmp", - delete=False, - ) as tmp: - json.dump(overall, tmp, indent=2, sort_keys=True) - tmp_path = Path(tmp.name) - - tmp_path.replace(overall_path) - except OSError as e: - logger.warning( - "Failed to write updated overall_quality.json", - path=str(overall_path), - error=str(e), - ) - - -def _backfill_ast_grep_for_run( - results_dir: Path, - rules_lookup: RuleLookup, - logger: Any, -) -> tuple[int, int, int]: - """Backfill ast_grep.jsonl files in all checkpoints of a run. - - Args: - results_dir: Path to the run directory. - rules_lookup: Mapping of rule_id to category/subcategory/weight. - logger: Logger instance. - - Returns: - Tuple of (files_processed, total_violations, violations_updated) - """ - files_processed = 0 - total_violations = 0 - total_updated = 0 - - # Find all ast_grep.jsonl files in checkpoint quality_analysis dirs - pattern = "*/checkpoint_*/quality_analysis/ast_grep.jsonl" - for jsonl_path in results_dir.glob(pattern): - violations, updated = _update_ast_grep_jsonl( - jsonl_path, rules_lookup, logger - ) - if violations > 0: - files_processed += 1 - total_violations += violations - total_updated += updated - - return files_processed, total_violations, total_updated - - def _recalculate_evaluation_counts_dict(data: dict) -> bool: """Recalculate pass/total counts from grouped tests, excluding skipped.""" pass_counts: dict[str, int] = {} @@ -661,14 +454,6 @@ def backfill_reports( raise RuntimeError("backfill-reports requires standard logging") logger.info("Backfilling high-level reports", results_dir=results_dir) - # Build AST-grep rules lookup for backfilling ast_grep.jsonl - rules_lookup = build_ast_grep_rules_lookup() - if rules_lookup: - logger.info( - "Loaded AST-grep rules for backfill", - rules_count=len(rules_lookup), - ) - if not results_dir.exists(): typer.echo( typer.style( @@ -726,19 +511,6 @@ def backfill_reports( typer.echo(f"Reports written to {report_file}") typer.echo(f"Processed {problems_processed} problem(s)") - # Backfill ast_grep.jsonl files - if rules_lookup: - sg_files, sg_violations, sg_updated = ( - _backfill_ast_grep_for_run( - single_run_dir, rules_lookup, logger - ) - ) - if sg_files > 0: - typer.echo( - f"Updated {sg_updated}/{sg_violations} " - f"ast-grep violations in {sg_files} file(s)" - ) - # Display errors for this run if all_errors: typer.echo( @@ -850,17 +622,6 @@ def backfill_reports( typer.echo(f"Reports written to {report_file}") typer.echo(f"Processed {problems_processed} problem(s)") - # Backfill ast_grep.jsonl files - if rules_lookup: - sg_files, sg_violations, sg_updated = _backfill_ast_grep_for_run( - results_dir, rules_lookup, logger - ) - if sg_files > 0: - typer.echo( - f"Updated {sg_updated}/{sg_violations} " - f"ast-grep violations in {sg_files} file(s)" - ) - # Display error summary at end if all_errors: typer.echo( diff --git a/src/slop_code/entrypoints/commands/consolidate_runs.py b/src/slop_code/entrypoints/commands/consolidate_runs.py index ed62fcdc..566a2ab3 100644 --- a/src/slop_code/entrypoints/commands/consolidate_runs.py +++ b/src/slop_code/entrypoints/commands/consolidate_runs.py @@ -81,8 +81,6 @@ COMPLEXITY_PREFIXES = [ "cc_", "lint_", - "ast_grep_", - "sg_", ] # Mass cols (important complexity metrics - warn if missing) @@ -134,7 +132,7 @@ def consolidate_runs( bool, typer.Option( "--skip-quality", - help="Skip quality analysis files (files.jsonl, symbols.jsonl, ast_grep.jsonl)", + help="Skip quality analysis files (files.jsonl, symbols.jsonl)", ), ] = False, skip_rubric: Annotated[ @@ -193,7 +191,6 @@ def consolidate_runs( all_runs: list[dict[str, Any]] = [] all_quality_files: list[dict[str, Any]] = [] all_quality_symbols: list[dict[str, Any]] = [] - all_ast_violations: list[dict[str, Any]] = [] all_evaluations: list[dict[str, Any]] = [] all_rubric: list[dict[str, Any]] = [] runs_missing_mass: set[str] = set() # Track runs missing mass columns @@ -272,16 +269,6 @@ def consolidate_runs( record["checkpoint"] = checkpoint all_quality_symbols.append(record) - # Load ast_grep.jsonl - ast_grep_path = quality_dir / "ast_grep.jsonl" - if ast_grep_path.exists(): - for record in load_jsonl(ast_grep_path): - record["setup"] = setup - record["run_id"] = run_id - record["problem"] = problem - record["checkpoint"] = checkpoint - all_ast_violations.append(record) - # Process evaluation.json if not skipped if not skip_evaluations: problem = checkpoint_record.get("problem") @@ -357,17 +344,6 @@ def consolidate_runs( ) typer.echo(f" quality_symbols.csv.gz: {len(df)} rows (compressed)") - if all_ast_violations: - df = pd.DataFrame(all_ast_violations) - df.to_csv( - output_dir / "quality_ast_violations.csv.gz", - index=False, - compression="gzip", - ) - typer.echo( - f" quality_ast_violations.csv.gz: {len(df)} rows (compressed)" - ) - # Write evaluations if all_evaluations: df = pd.DataFrame(all_evaluations) @@ -388,7 +364,6 @@ def consolidate_runs( "checkpoints": len(all_checkpoints), "quality_files": len(all_quality_files), "quality_symbols": len(all_quality_symbols), - "ast_violations": len(all_ast_violations), "evaluations": len(all_evaluations), "rubric_entries": len(all_rubric), } @@ -796,11 +771,6 @@ def generate_manifest( "key_columns": KEY_COLS + ["name", "type", "file_path"], "compressed": True, }, - "quality_ast_violations.csv.gz": { - "description": "AST-grep rule violations (gzip compressed)", - "key_columns": KEY_COLS + ["rule_id", "file_path"], - "compressed": True, - }, "evaluations.csv.gz": { "description": "Individual test results (gzip compressed)", "key_columns": KEY_COLS + ["group", "test_id"], diff --git a/src/slop_code/entrypoints/commands/static.py b/src/slop_code/entrypoints/commands/static.py index 374d9cc0..0985a22c 100644 --- a/src/slop_code/entrypoints/commands/static.py +++ b/src/slop_code/entrypoints/commands/static.py @@ -3,7 +3,6 @@ from __future__ import annotations import traceback -from collections import Counter from concurrent.futures import ProcessPoolExecutor from dataclasses import dataclass from dataclasses import field @@ -14,7 +13,6 @@ import yaml from pydantic import ValidationError from rich.console import Console -from rich.table import Table from slop_code.common import CHECKPOINT_RESULTS_FILENAME from slop_code.common import CONFIG_FILENAME @@ -45,8 +43,6 @@ class RunProcessingResult: run_dir: Path success: bool - slop_rule_counts: Counter[str] = field(default_factory=Counter) - slop_rows: list[tuple[str, str, int, int]] = field(default_factory=list) all_reports: list[dict] = field(default_factory=list) report_errors: list[tuple[str, str]] = field(default_factory=list) total_checkpoints: int = 0 @@ -78,8 +74,6 @@ def _process_single_run_worker( log = setup_logging(log_dir=None, verbosity=0) try: - slop_rule_counts: Counter[str] = Counter() - slop_rows: list[tuple[str, str, int, int]] = [] all_reports: list[dict] = [] report_errors: list[tuple[str, str]] = [] total_checkpoints = 0 @@ -138,22 +132,9 @@ def _process_single_run_worker( total_checkpoints += len(checkpoint_results) total_files_saved += files_saved - for checkpoint_name, metrics in checkpoint_results.items(): - slop_rule_counts.update(metrics.ast_grep.counts) - slop_rows.append( - ( - problem_dir.name, - checkpoint_name, - metrics.ast_grep.violations, - metrics.ast_grep.rules_checked, - ) - ) - return RunProcessingResult( run_dir=run_dir, success=True, - slop_rule_counts=slop_rule_counts, - slop_rows=slop_rows, all_reports=all_reports, report_errors=report_errors, total_checkpoints=total_checkpoints, @@ -184,8 +165,6 @@ def _process_single_run( problem_name: str | None, console: Console, ) -> tuple[ - Counter[str], - list[tuple[str, str, int, int]], list[dict], list[tuple[str, str]], int, @@ -200,11 +179,9 @@ def _process_single_run( console: Rich console for output. Returns: - Tuple of (slop_rule_counts, slop_rows, all_reports, report_errors, - total_checkpoints, total_files_saved) + Tuple of (all_reports, report_errors, total_checkpoints, + total_files_saved). """ - slop_rule_counts: Counter[str] = Counter() - slop_rows: list[tuple[str, str, int, int]] = [] all_reports: list[dict] = [] report_errors: list[tuple[str, str]] = [] total_checkpoints = 0 @@ -216,8 +193,6 @@ def _process_single_run( if not problems: console.print(f"[red]No problems found in {run_dir}[/red]") return ( - slop_rule_counts, - slop_rows, all_reports, report_errors, total_checkpoints, @@ -232,8 +207,6 @@ def _process_single_run( f"[red]Problem '{problem_name}' not found in {run_dir}[/red]" ) return ( - slop_rule_counts, - slop_rows, all_reports, report_errors, total_checkpoints, @@ -280,20 +253,7 @@ def _process_single_run( f"{files_saved} files saved" ) - for checkpoint_name, metrics in checkpoint_results.items(): - slop_rule_counts.update(metrics.ast_grep.counts) - slop_rows.append( - ( - problem_dir.name, - checkpoint_name, - metrics.ast_grep.violations, - metrics.ast_grep.rules_checked, - ) - ) - return ( - slop_rule_counts, - slop_rows, all_reports, report_errors, total_checkpoints, @@ -381,20 +341,15 @@ def static_metrics( ) raise typer.Exit(1) - slop_rule_counts: Counter[str] = Counter() - slop_rows: list[tuple[str, str, int, int]] = [] _run_extension_scan( console, run_dir, just_static_extension, - slop_rule_counts, - slop_rows, ) console.print( "[green]Done. Saved quality metrics for 1 snapshot " - "(2 files).[/green]" + "(3 files).[/green]" ) - _print_slop_summary(console, slop_rows, slop_rule_counts) return # Handle collection mode @@ -469,10 +424,6 @@ def static_metrics( f"[green]Saved quality metrics for {result.total_checkpoints} " f"checkpoints ({result.total_files_saved} files).[/green]" ) - _print_slop_summary( - console, result.slop_rows, result.slop_rule_counts - ) - update_results_jsonl(report_file, result.all_reports) console.print( f"Backfilled reports for {len(result.all_reports)} " @@ -537,8 +488,6 @@ def static_metrics( # Single run mode (default) ( - slop_rule_counts, - slop_rows, all_reports, report_errors, total_checkpoints, @@ -555,8 +504,6 @@ def static_metrics( f"[green]Done. Saved quality metrics for {total_checkpoints} " f"checkpoints ({total_files_saved} files).[/green]" ) - _print_slop_summary(console, slop_rows, slop_rule_counts) - update_results_jsonl(report_file, all_reports) console.print( f"Backfilled reports for {len(all_reports)} checkpoint(s) at " @@ -607,8 +554,6 @@ def _run_extension_scan( console: Console, run_dir: Path, raw_extension: str, - slop_rule_counts: Counter[str], - slop_rows: list[tuple[str, str, int, int]], ) -> None: extension = _normalize_extension(raw_extension) language = get_language_by_extension(extension) @@ -641,46 +586,3 @@ def _run_extension_scan( f"Scanned snapshot at {run_dir} " f"({len(file_metrics_list)} files, entry {entry_file.name})" ) - - slop_rule_counts.update(quality_result.ast_grep.counts) - slop_rows.append( - ( - run_dir.name, - "snapshot", - quality_result.ast_grep.violations, - quality_result.ast_grep.rules_checked, - ) - ) - - -def _print_slop_summary( - console: Console, - slop_rows: list[tuple[str, str, int, int]], - slop_rule_counts: Counter[str], -) -> None: - if slop_rule_counts: - table = Table(title="Slop Violations by Rule") - table.add_column("Rule") - table.add_column("Count", justify="right") - for rule_id, count in sorted( - slop_rule_counts.items(), key=lambda item: (-item[1], item[0]) - ): - table.add_row(rule_id, str(count)) - console.print(table) - else: - console.print("[green]No slop violations detected.[/green]") - - if slop_rows: - table = Table(title="Slop Violations by Checkpoint") - table.add_column("Problem") - table.add_column("Checkpoint") - table.add_column("Violations", justify="right") - table.add_column("Rules Checked", justify="right") - for problem, checkpoint, violations, rules_checked in slop_rows: - table.add_row( - problem, - checkpoint, - str(violations), - str(rules_checked), - ) - console.print(table) diff --git a/src/slop_code/entrypoints/commands/variance.py b/src/slop_code/entrypoints/commands/variance.py index 81c77f1a..0672fafa 100644 --- a/src/slop_code/entrypoints/commands/variance.py +++ b/src/slop_code/entrypoints/commands/variance.py @@ -65,8 +65,6 @@ class VariancePreset(str, Enum): CV_SUMMARY_METRICS: dict[str, str] = { "Pass rate": "strict_pass_rate.cv", "Lint": "lint_per_loc.cv", - "Violation %": "violation_pct.cv", - "Lint+Violation %": "normalized.lint_violation_pct.cv", "LOC": "lines.loc.cv", "High CC count": "cc.high_count.cv", } @@ -86,7 +84,7 @@ class VariancePreset(str, Enum): DELTA_CI_METRICS: dict[str, str] = { "delta.loc": "LOC delta", - "delta.ast_grep_violations": "Slop delta", + "delta.verbosity": "Verbosity delta", "delta.churn_ratio": "Churn ratio", } CI_METRIC_LABELS: dict[str, str] = { @@ -297,13 +295,6 @@ def _metric_value(row: dict[str, Any], name: str) -> float | None: return None return float(flags) / float(loc) - if name == "normalized.lint_violation_pct": - lint = row.get("lint_per_loc") - violation_pct = row.get("violation_pct") - if not _is_number(lint) or not _is_number(violation_pct): - return None - return float(lint) + float(violation_pct) - if name.startswith("tests.") and name.endswith(".pass_rate"): bucket = name.split(".")[1] if bucket == "total": @@ -395,7 +386,6 @@ def _build_metric_specs( "duration", "steps", "lint_per_loc", - "violation_pct", "loc", "lines.churn", "lines.added", @@ -406,10 +396,8 @@ def _build_metric_specs( "delta.cc_mean", "delta.cc_max", "delta.loc", - "delta.ast_grep_violations", "delta.churn_ratio", "lint_errors", - "ast_grep_violations", "rubric_total_flags", ] specs = [MetricSpec(name=k) for k in base_keys] diff --git a/src/slop_code/entrypoints/utils.py b/src/slop_code/entrypoints/utils.py index 394563f3..de72abd7 100644 --- a/src/slop_code/entrypoints/utils.py +++ b/src/slop_code/entrypoints/utils.py @@ -355,11 +355,6 @@ def render_summary_table(summary: RunSummary, console: Console) -> None: "Mean lint:loc", summary.ratios.lint.format_display(4), ) - if summary.ratios.violation_pct.count > 0: - table.add_row( - "Mean violation pct", - summary.ratios.violation_pct.format_display(4), - ) if summary.verbosity.count > 0: table.add_row( "Mean verbosity score", diff --git a/src/slop_code/metrics/__init__.py b/src/slop_code/metrics/__init__.py index 50dbe9c1..2d787ee7 100644 --- a/src/slop_code/metrics/__init__.py +++ b/src/slop_code/metrics/__init__.py @@ -30,8 +30,6 @@ # Models - Quality # Models - Summary -from slop_code.metrics.models import AstGrepMetrics -from slop_code.metrics.models import AstGrepViolation from slop_code.metrics.models import CostsStats from slop_code.metrics.models import CyclomaticComplexityStats from slop_code.metrics.models import FileMetrics @@ -79,8 +77,6 @@ "load_checkpoint_data", "save_summary_json", # Models - Quality - "AstGrepMetrics", - "AstGrepViolation", "FileMetrics", "LineCountMetrics", "LintMetrics", diff --git a/src/slop_code/metrics/checkpoint/__init__.py b/src/slop_code/metrics/checkpoint/__init__.py index beeab66e..fc868e07 100644 --- a/src/slop_code/metrics/checkpoint/__init__.py +++ b/src/slop_code/metrics/checkpoint/__init__.py @@ -1,7 +1,5 @@ """Checkpoint metric extraction from disk.""" -from slop_code.metrics.checkpoint.composites import compute_checkpoint_erosion -from slop_code.metrics.checkpoint.composites import compute_checkpoint_verbosity from slop_code.metrics.checkpoint.delta import DELTA_METRIC_KEYS from slop_code.metrics.checkpoint.delta import compute_checkpoint_delta from slop_code.metrics.checkpoint.driver import get_checkpoint_metrics @@ -14,9 +12,7 @@ __all__ = [ "DELTA_METRIC_KEYS", "compute_checkpoint_delta", - "compute_checkpoint_erosion", "compute_mass_metrics", - "compute_checkpoint_verbosity", "get_checkpoint_metrics", "get_evaluation_metrics", "get_inference_metrics", diff --git a/src/slop_code/metrics/checkpoint/composites.py b/src/slop_code/metrics/checkpoint/composites.py deleted file mode 100644 index 32142a9b..00000000 --- a/src/slop_code/metrics/checkpoint/composites.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Checkpoint-owned composite metric helpers.""" - -from __future__ import annotations - -from typing import Any - - -def compute_checkpoint_verbosity( - metrics: dict[str, Any], -) -> float | None: - """Compute checkpoint verbosity from saved checkpoint metrics.""" - verbosity_flagged_pct = metrics.get("verbosity_flagged_pct") - if isinstance(verbosity_flagged_pct, int | float): - return float(verbosity_flagged_pct) - - loc = metrics.get("loc") - if not isinstance(loc, int | float) or loc <= 0: - return None - - clone_lines = metrics.get("clone_lines", 0) - violation_pct = metrics.get("violation_pct") - if not isinstance(clone_lines, int | float) or not isinstance( - violation_pct, int | float - ): - return None - - clone_ratio = clone_lines / loc - return clone_ratio + float(violation_pct) - - -def compute_checkpoint_erosion( - metrics: dict[str, Any], -) -> float | None: - """Compute checkpoint erosion from the saved high-CC mass ratio.""" - score = metrics.get("mass.high_cc_pct") - if not isinstance(score, int | float): - return None - - if 0.0 <= score <= 1.0: - return float(score) - return None diff --git a/src/slop_code/metrics/checkpoint/delta.py b/src/slop_code/metrics/checkpoint/delta.py index bb203bd3..b53718d8 100644 --- a/src/slop_code/metrics/checkpoint/delta.py +++ b/src/slop_code/metrics/checkpoint/delta.py @@ -13,7 +13,7 @@ # Only keys that are actually consumed by dashboard/summary/variance. DELTA_METRIC_KEYS: tuple[str, ...] = ( "loc", - "ast_grep_violations", + "verbosity", ) diff --git a/src/slop_code/metrics/checkpoint/driver.py b/src/slop_code/metrics/checkpoint/driver.py index 939efeff..642778cf 100644 --- a/src/slop_code/metrics/checkpoint/driver.py +++ b/src/slop_code/metrics/checkpoint/driver.py @@ -25,7 +25,7 @@ # between releases, so an unpinned `uvx` invocation makes verbosity # incomparable across runs. Bump this deliberately; the value is recorded # with every checkpoint so past results stay interpretable. -SCB_CHECK_VERSION = "0.2.0" +SCB_CHECK_VERSION = "0.1.3" SCB_CHECK_VERSION_KEY = "scb_check_version" diff --git a/src/slop_code/metrics/checkpoint/extractors.py b/src/slop_code/metrics/checkpoint/extractors.py index 3896aab2..c4c7e1e0 100644 --- a/src/slop_code/metrics/checkpoint/extractors.py +++ b/src/slop_code/metrics/checkpoint/extractors.py @@ -94,19 +94,6 @@ def _compute_distributions( } -def _extract_ast_grep_categories(ast_grep: dict) -> dict[str, int]: - """Extract AST-grep violation counts for the slop ruleset. - - Args: - ast_grep: AST-grep metrics dictionary. - - Returns: - Dictionary with the single exported slop violation count. - """ - category_counts = ast_grep.get("category_counts", {}) - return {"sg_slop_violations": category_counts.get("slop", 0)} - - def _build_metrics_from_snapshot( snapshot: dict, distributions: dict[str, Any] ) -> dict[str, Any]: @@ -128,7 +115,6 @@ def _build_metrics_from_snapshot( functions = snapshot["functions"] waste = snapshot["waste"] redundancy = snapshot["redundancy"] - ast_grep = snapshot["ast_grep"] source_loc = lines["loc"] total_loc = lines["total_lines"] @@ -167,26 +153,14 @@ def _build_metrics_from_snapshot( # Redundancy "clone_lines": redundancy["clone_lines"], "cloned_sloc_lines": redundancy.get("cloned_sloc_lines", 0), - # AST-grep - "ast_grep_violations": ast_grep["violations"], - "verbosity_flagged_sloc_lines": snapshot.get( - "verbosity_flagged_sloc_lines", 0 - ), } - # Add AST-grep category counts - result.update(_extract_ast_grep_categories(ast_grep)) - # Per-LOC normalized metrics if total_loc > 0: result["lint_per_loc"] = lint["errors"] / total_loc - result["violation_pct"] = ast_grep.get("violation_lines", 0) / total_loc result["cloned_pct"] = ( redundancy.get("cloned_sloc_lines", 0) / total_loc ) - result["verbosity_flagged_pct"] = ( - snapshot.get("verbosity_flagged_sloc_lines", 0) / total_loc - ) # Graph metrics (optional, may be None for non-Python) if graph := snapshot.get("graph"): @@ -321,7 +295,6 @@ def get_quality_metrics( - symbols.*: Symbol type counts and complexity ratings - waste.*: Abstraction waste metrics - redundancy.*: Code clone metrics - - ast_grep.*: AST-grep violation metrics """ if thresholds is None: thresholds = MetricsThresholds() diff --git a/src/slop_code/metrics/driver.py b/src/slop_code/metrics/driver.py index dda6ed0c..1b9a240c 100644 --- a/src/slop_code/metrics/driver.py +++ b/src/slop_code/metrics/driver.py @@ -27,7 +27,6 @@ from slop_code.metrics.checkpoint.mass import compute_top20_share from slop_code.metrics.languages import get_language_by_extension from slop_code.metrics.languages.python import extract_imports -from slop_code.metrics.models import AstGrepAggregates from slop_code.metrics.models import ClassStats from slop_code.metrics.models import ComplexityAggregates from slop_code.metrics.models import FileMetrics @@ -84,7 +83,6 @@ def _calculate_file_metrics( redundancy = language.redundancy(file_path) if language.redundancy else None waste = language.waste(file_path, symbols) if language.waste else None type_check = language.type_check(file_path) if language.type_check else None - ast_grep = language.ast_grep(file_path) if language.ast_grep else None return FileMetrics( symbols=symbols, lines=line_metrics, @@ -97,8 +95,6 @@ def _calculate_file_metrics( redundancy=redundancy, waste=waste, type_check=type_check, - ast_grep_violations=ast_grep.violations if ast_grep else [], - ast_grep_rules_checked=ast_grep.rules_checked if ast_grep else 0, ) @@ -155,8 +151,6 @@ def __init__( complexity: ComplexityAggregates, waste: WasteAggregates, redundancy: RedundancyAggregates, - ast_grep: AstGrepAggregates, - verbosity_flagged_sloc_lines: int, ): self.file_count = file_count self.symbols = symbols @@ -165,8 +159,6 @@ def __init__( self.complexity = complexity self.waste = waste self.redundancy = redundancy - self.ast_grep = ast_grep - self.verbosity_flagged_sloc_lines = verbosity_flagged_sloc_lines HIGH_CC_THRESHOLD = 10 @@ -248,20 +240,6 @@ def _collect_clone_sloc_lines( return cloned_lines -def _collect_ast_grep_sloc_lines( - fm: FileMetrics, sloc_lines: set[int] -) -> set[int]: - """Return the AST-grep-covered SLOC lines for a file.""" - flagged_lines: set[int] = set() - for violation in fm.ast_grep_violations: - start = violation.line + 1 - end = violation.end_line + 1 - for line_no in range(start, end + 1): - if line_no in sloc_lines: - flagged_lines.add(line_no) - return flagged_lines - - def _normalized_complexity(cc_values: list[int], max_cc: int = 50) -> float: """Compute normalized complexity score (0 = ideal, 1 = worst case).""" if not cc_values: @@ -453,35 +431,22 @@ def compute_aggregates( clone_ratio_sum=0.0, files_with_clones=0, ) - ast_grep = AstGrepAggregates( - violations=0, - rules_checked=0, - counts={}, - ) - # Collect raw values for function/class stats computation func_cc: list[int] = [] func_depth: list[int] = [] func_lines: list[int] = [] class_method_counts: list[int] = [] class_attribute_counts: list[int] = [] - verbosity_flagged_sloc_lines = 0 - # Single pass through all files for path_str, fm in file_metrics.items(): symbols.update(fm) complexity.update(fm, thresholds) waste.update(fm) redundancy.update(fm) - ast_grep.update(fm) source_path = snapshot_dir / path_str sloc_lines = _get_sloc_lines(source_path) clone_sloc_lines = _collect_clone_sloc_lines(fm, sloc_lines) - ast_grep_sloc_lines = _collect_ast_grep_sloc_lines(fm, sloc_lines) redundancy.cloned_sloc_lines += len(clone_sloc_lines) - verbosity_flagged_sloc_lines += len( - clone_sloc_lines | ast_grep_sloc_lines - ) # Collect function/method metrics for sym in fm.symbols: @@ -515,8 +480,6 @@ def compute_aggregates( complexity=complexity, waste=waste, redundancy=redundancy, - ast_grep=ast_grep, - verbosity_flagged_sloc_lines=verbosity_flagged_sloc_lines, ) @@ -666,8 +629,6 @@ def measure_snapshot_quality( complexity=agg.complexity, waste=agg.waste, redundancy=agg.redundancy, - ast_grep=agg.ast_grep, - verbosity_flagged_sloc_lines=agg.verbosity_flagged_sloc_lines, graph=graph_metrics, source_files=traced_source_files, ) diff --git a/src/slop_code/metrics/languages/python/__init__.py b/src/slop_code/metrics/languages/python/__init__.py index 6db914f5..9a4c1265 100644 --- a/src/slop_code/metrics/languages/python/__init__.py +++ b/src/slop_code/metrics/languages/python/__init__.py @@ -7,14 +7,6 @@ from __future__ import annotations -from slop_code.metrics.languages.python.ast_grep import AST_GREP_RULES_DIR -from slop_code.metrics.languages.python.ast_grep import AST_GREP_RULES_PATH -from slop_code.metrics.languages.python.ast_grep import _get_ast_grep_rules_dir -from slop_code.metrics.languages.python.ast_grep import _get_ast_grep_rules_path -from slop_code.metrics.languages.python.ast_grep import _is_sg_available -from slop_code.metrics.languages.python.ast_grep import ( - calculate_ast_grep_metrics, -) from slop_code.metrics.languages.python.constants import EXTENSIONS from slop_code.metrics.languages.python.imports import extract_imports from slop_code.metrics.languages.python.imports import trace_source_files @@ -48,14 +40,11 @@ redundancy=calculate_redundancy_metrics, waste=calculate_waste_metrics, type_check=calculate_type_check_metrics, - ast_grep=calculate_ast_grep_metrics, ), ) __all__ = [ # Constants - "AST_GREP_RULES_DIR", - "AST_GREP_RULES_PATH", "EXTENSIONS", # Line metrics "calculate_line_metrics", @@ -70,11 +59,6 @@ "calculate_waste_metrics", # Type check metrics "calculate_type_check_metrics", - # AST-grep metrics - "calculate_ast_grep_metrics", - "_get_ast_grep_rules_dir", - "_get_ast_grep_rules_path", - "_is_sg_available", # Import extraction and tracing "extract_imports", "trace_source_files", diff --git a/src/slop_code/metrics/languages/python/ast_grep.py b/src/slop_code/metrics/languages/python/ast_grep.py deleted file mode 100644 index 7db88795..00000000 --- a/src/slop_code/metrics/languages/python/ast_grep.py +++ /dev/null @@ -1,259 +0,0 @@ -"""AST-grep based pattern detection metrics.""" - -from __future__ import annotations - -import json -import os -import shutil -import subprocess -from collections import Counter -from pathlib import Path -from typing import TypedDict - -import yaml - -from slop_code.logging import get_logger -from slop_code.metrics.models import AstGrepMetrics -from slop_code.metrics.models import AstGrepViolation - -logger = get_logger(__name__) - - -class _RuleInfo(TypedDict, total=False): - category: str - subcategory: str - weight: int - min_file_count: int - - -RuleLookup = dict[str, _RuleInfo] - -AST_GREP_CATEGORY = "slop" -_EMPTY_RULE_INFO: _RuleInfo = {} - -# Default rules file (relative to project root). -AST_GREP_RULES_PATH = Path(__file__).parents[5] / "configs" / "slop_rules.yaml" -# Back-compat export; this now points at the single rules file. -AST_GREP_RULES_DIR = AST_GREP_RULES_PATH - - -def _is_sg_available() -> bool: - """Check if ast-grep (sg) is available on the system.""" - return shutil.which("sg") is not None - - -def _get_ast_grep_rules_path() -> Path: - """Get the AST-grep rules file, with env var override.""" - override = os.environ.get("AST_GREP_RULES_PATH") - if override: - return Path(override) - override = os.environ.get("AST_GREP_RULES_DIR") - if override: - return Path(override) - return AST_GREP_RULES_PATH - - -def _get_ast_grep_rules_dir() -> Path: - """Back-compat wrapper returning the configured rules file path.""" - return _get_ast_grep_rules_path() - - -def _count_rules_in_file(rule_file: Path) -> int: - """Return the number of AST-grep rules in a (possibly multi-doc) file.""" - try: - with rule_file.open(encoding="utf-8") as handle: - return sum(1 for doc in yaml.safe_load_all(handle) if doc) - except (OSError, yaml.YAMLError) as exc: - logger.debug( - "Failed to read AST-grep rule file", - rule_file=str(rule_file), - error=str(exc), - ) - return 0 - - -def calculate_ast_grep_metrics(source: Path) -> AstGrepMetrics: - """Calculate AST-grep metrics using ast-grep rules. - - Runs ast-grep scan with rules from the configured slop rules file - and parses the JSON output to produce AstGrepMetrics. - - Args: - source: Path to the Python source file. - - Returns: - AstGrepMetrics with violations found, or empty metrics if sg - unavailable. - """ - if not _is_sg_available(): - logger.debug("ast-grep (sg) not available, skipping ast-grep metrics") - return AstGrepMetrics( - violations=[], total_violations=0, counts={}, rules_checked=0 - ) - - rules_path = _get_ast_grep_rules_path() - if not rules_path.exists(): - logger.warning( - "AST-grep rules file not found", - rules_path=str(rules_path), - ) - return AstGrepMetrics( - violations=[], total_violations=0, counts={}, rules_checked=0 - ) - - rules_checked = _count_rules_in_file(rules_path) - - if rules_checked == 0: - return AstGrepMetrics( - violations=[], total_violations=0, counts={}, rules_checked=0 - ) - - # Build lookup to get correct category/subcategory/weight from rule files - # (ast-grep's JSON output doesn't include rule metadata) - rules_lookup = build_ast_grep_rules_lookup() - - raw_violations: list[AstGrepViolation] = [] - raw_counts: Counter[str] = Counter() - logger.debug("Running ast-grep rules", rules_path=str(rules_path)) - try: - result = subprocess.run( # noqa: S603 - [ # noqa: S607 - "sg", - "scan", - "--json=stream", - "-r", - str(rules_path), - str(source.absolute()), - ], - capture_output=True, - text=True, - check=False, - ) - except OSError as e: - logger.debug( - "Failed to run ast-grep rules", - rules_path=str(rules_path), - error=str(e), - ) - return AstGrepMetrics( - violations=[], - total_violations=0, - counts={}, - rules_checked=rules_checked, - ) - if result.returncode != 0: - logger.warning( - "Failed to run ast-grep rules", - rules_path=str(rules_path), - error=result.stderr, - ) - return AstGrepMetrics( - violations=[], - total_violations=0, - counts={}, - rules_checked=rules_checked, - ) - # Parse JSON stream output (one JSON object per line) - for line in result.stdout.strip().split("\n"): - if not line: - continue - try: - match = json.loads(line) - rule_id = match.get("ruleId", AST_GREP_CATEGORY) - rule_info = rules_lookup.get(rule_id, _EMPTY_RULE_INFO) - violation = AstGrepViolation( - rule_id=rule_id, - severity=match.get("severity", "warning"), - category=rule_info.get("category", AST_GREP_CATEGORY), - subcategory=rule_info.get("subcategory", AST_GREP_CATEGORY), - weight=rule_info.get("weight", 1), - line=match["range"]["start"]["line"], - column=match["range"]["start"]["column"], - end_line=match["range"]["end"]["line"], - end_column=match["range"]["end"]["column"], - ) - raw_violations.append(violation) - raw_counts[violation.rule_id] += 1 - except (json.JSONDecodeError, KeyError) as e: - logger.warning( - "Failed to parse ast-grep output", - line=line, - error=str(e), - ) - continue - - violations = [ - violation - for violation in raw_violations - if _exceeds_min_file_count( - raw_counts[violation.rule_id], - rules_lookup.get(violation.rule_id, _EMPTY_RULE_INFO), - ) - ] - counts = Counter(violation.rule_id for violation in violations) - - return AstGrepMetrics( - violations=violations, - total_violations=len(violations), - counts=dict(counts), - rules_checked=rules_checked, - ) - - -def _exceeds_min_file_count(count: int, rule_info: _RuleInfo) -> bool: - """Return whether a rule's file-local matches clear its threshold.""" - min_file_count = rule_info.get("min_file_count") - if not isinstance(min_file_count, int): - return True - return count > min_file_count - - -def build_ast_grep_rules_lookup() -> RuleLookup: - """Build lookup table mapping rule_id to category/subcategory/weight. - - Parses the configured slop rules file and extracts metadata for each rule. - This is used for backfilling ast_grep.jsonl files with correct metadata. - - Returns: - Dict mapping rule_id to metadata including category, subcategory, - weight, and optional min_file_count threshold. - """ - rules_path = _get_ast_grep_rules_path() - if not rules_path.exists(): - logger.warning( - "AST-grep rules file not found", - rules_path=str(rules_path), - ) - return {} - - lookup: RuleLookup = {} - try: - with rules_path.open(encoding="utf-8") as handle: - for doc in yaml.safe_load_all(handle): - if not doc: - continue - rule_id = doc.get("id") - if not rule_id: - continue - metadata = doc.get("metadata", {}) - lookup[rule_id] = { - "category": AST_GREP_CATEGORY, - "subcategory": metadata.get("category", AST_GREP_CATEGORY), - "weight": metadata.get("weight", 1), - } - min_file_count = metadata.get("min_file_count") - if isinstance(min_file_count, int): - lookup[rule_id]["min_file_count"] = min_file_count - except (OSError, yaml.YAMLError) as exc: - logger.warning( - "Failed to parse AST-grep rules file for lookup", - rules_path=str(rules_path), - error=str(exc), - ) - return {} - - logger.debug( - "Built AST-grep rules lookup", - rules_count=len(lookup), - ) - return lookup diff --git a/src/slop_code/metrics/models.py b/src/slop_code/metrics/models.py index f2893aee..c3fe3153 100644 --- a/src/slop_code/metrics/models.py +++ b/src/slop_code/metrics/models.py @@ -233,8 +233,6 @@ class FileMetrics(BaseModel): redundancy: RedundancyMetrics | None = None waste: WasteMetrics | None = None type_check: TypeCheckMetrics | None = None - ast_grep_violations: list[AstGrepViolation] = [] - ast_grep_rules_checked: int = 0 class CodeClone(BaseModel): @@ -313,48 +311,6 @@ class TypeCheckMetrics(BaseModel): counts: dict[str, int] # rule_id -> count -class AstGrepViolation(BaseModel): - """A single AST-grep pattern violation. - - Attributes: - rule_id: The rule identifier (e.g., "bare-except-pass"). - severity: Severity level (warning, error, info, hint). - category: Overall category from rule filename (e.g., "verbosity", "safety"). - subcategory: Sub-category from rule metadata.category field. - weight: Rule weight from metadata (1-4, higher = more important). - line: Starting line number from ast-grep output (0-indexed). - column: Starting column number (0-indexed). - end_line: Ending line number from ast-grep output (0-indexed). - end_column: Ending column number (0-indexed). - """ - - rule_id: str - severity: str - category: str = "" - subcategory: str = "unknown" - weight: int = 1 - line: int - column: int - end_line: int - end_column: int - - -class AstGrepMetrics(BaseModel): - """AST-grep pattern detection results. - - Attributes: - violations: List of individual violations found. - total_violations: Total number of violations. - counts: Mapping of rule_id to violation count. - rules_checked: Number of rules that were applied. - """ - - violations: list[AstGrepViolation] - total_violations: int - counts: dict[str, int] - rules_checked: int - - class FunctionStats(BaseModel): """Pre-computed statistics for functions and methods across all files. @@ -502,36 +458,6 @@ def update(self, fm: FileMetrics) -> None: self.files_with_clones += 1 -class AstGrepAggregates(BaseModel): - """Aggregate AST-grep pattern violation metrics.""" - - violations: int - rules_checked: int - counts: dict[str, int] = {} - weighted: int = 0 - violation_lines: int = 0 - category_counts: dict[str, int] = {} - category_weighted: dict[str, int] = {} - - def update(self, fm: FileMetrics) -> None: - """Update aggregates from a FileMetrics instance.""" - if fm.ast_grep_violations: - self.violations += len(fm.ast_grep_violations) - flagged_lines: set[int] = set() - for v in fm.ast_grep_violations: - self.counts[v.rule_id] = self.counts.get(v.rule_id, 0) + 1 - self.weighted += v.weight - flagged_lines.update(range(v.line, v.end_line + 1)) - self.category_counts[v.category] = ( - self.category_counts.get(v.category, 0) + 1 - ) - self.category_weighted[v.category] = ( - self.category_weighted.get(v.category, 0) + v.weight - ) - self.violation_lines += len(flagged_lines) - self.rules_checked = max(self.rules_checked, fm.ast_grep_rules_checked) - - class TypeCheckAggregates(BaseModel): """Aggregate type checking metrics.""" @@ -586,7 +512,6 @@ class SnapshotMetrics(BaseModel): complexity: CC and MI rating distributions and stats. waste: Waste detection totals. redundancy: Clone detection totals. - ast_grep: AST-grep pattern violation totals. graph: Dependency graph metrics (None if not computed). source_files: Relative paths of files traced from the entrypoint. """ @@ -600,8 +525,6 @@ class SnapshotMetrics(BaseModel): complexity: ComplexityAggregates waste: WasteAggregates redundancy: RedundancyAggregates - ast_grep: AstGrepAggregates - verbosity_flagged_sloc_lines: int = 0 graph: GraphMetrics | None = None source_files: set[str] | None = None @@ -615,8 +538,6 @@ class SnapshotQualityReport(BaseModel): lint_fixable: int cc_counts: dict[Literal["A", "B", "C", "D", "E", "F"], int] mi: dict[Literal["A", "B", "C"], int] - ast_grep_violations: int = 0 - ast_grep_rules_checked: int = 0 graph: GraphMetrics | None = None @classmethod @@ -631,8 +552,6 @@ def from_snapshot_metrics( lint_fixable=snapshot_metrics.lint.fixable, cc_counts=snapshot_metrics.complexity.cc_ratings, mi=snapshot_metrics.complexity.mi_ratings, - ast_grep_violations=snapshot_metrics.ast_grep.violations, - ast_grep_rules_checked=snapshot_metrics.ast_grep.rules_checked, graph=snapshot_metrics.graph, ) @@ -656,7 +575,6 @@ class LanguageSpec(BaseModel): redundancy: Callable[[Path], RedundancyMetrics] | None = None waste: Callable[[Path, list[SymbolMetrics]], WasteMetrics] | None = None type_check: Callable[[Path], TypeCheckMetrics] | None = None - ast_grep: Callable[[Path], AstGrepMetrics] | None = None # ----------------------------------------------------------------------------- @@ -767,7 +685,6 @@ class RatiosStats(BaseModel): rubric: MetricStats = Field(default_factory=MetricStats) lint: MetricStats = Field(default_factory=MetricStats) - violation_pct: MetricStats = Field(default_factory=MetricStats) class RunSummary(BaseModel): @@ -820,7 +737,7 @@ class RunSummary(BaseModel): default_factory=CyclomaticComplexityStats ) - # Quality ratios (per LOC): {rubric, lint, ast_grep} + # Quality ratios (per LOC): {rubric, lint} ratios: RatiosStats = Field(default_factory=RatiosStats) # Composite quality scores diff --git a/src/slop_code/metrics/quality_io.py b/src/slop_code/metrics/quality_io.py index 3d5fc1c6..90c91940 100644 --- a/src/slop_code/metrics/quality_io.py +++ b/src/slop_code/metrics/quality_io.py @@ -9,7 +9,6 @@ import json from pathlib import Path -from slop_code.common import AST_GREP_QUALITY_SAVENAME from slop_code.common import FILES_QUALITY_SAVENAME from slop_code.common import QUALITY_DIR from slop_code.common import QUALITY_METRIC_SAVENAME @@ -32,7 +31,6 @@ def save_quality_metrics( - overall_quality.json: Aggregate snapshot metrics - files.jsonl: Flat file metrics (one row per file) - symbols.jsonl: Symbol metrics with file_path (one row per symbol) - - ast_grep.jsonl: AST-grep violations with file_path (one row per violation) Args: save_dir: Directory to save quality_analysis/ into. @@ -75,8 +73,6 @@ def save_quality_metrics( "import_count": fm.import_count, "global_count": fm.global_count, "symbol_count": len(fm.symbols), - "ast_grep_violation_count": len(fm.ast_grep_violations), - "ast_grep_rules_checked": fm.ast_grep_rules_checked, "clone_lines": fm.redundancy.clone_lines if fm.redundancy else 0, @@ -105,15 +101,6 @@ def save_quality_metrics( f.write(json.dumps(data) + "\n") files_saved += 1 - # Save AST-grep violations with file_path - with (quality_dir / AST_GREP_QUALITY_SAVENAME).open("w") as f: - for fm in file_metrics: - for v in fm.ast_grep_violations: - data = v.model_dump(mode="json") - data["file_path"] = fm.file_path - f.write(json.dumps(data) + "\n") - files_saved += 1 - logger.debug( "Saved quality metrics", save_dir=str(save_dir), diff --git a/src/slop_code/metrics/summary/aggregators.py b/src/slop_code/metrics/summary/aggregators.py index 1c530eac..514251e4 100644 --- a/src/slop_code/metrics/summary/aggregators.py +++ b/src/slop_code/metrics/summary/aggregators.py @@ -295,12 +295,10 @@ def compute_ratios_stats(checkpoints: list[dict[str, Any]]) -> RatiosStats: checkpoints, "rubric_total_flags", "loc" ) lint_ratios = compute_ratio_values(checkpoints, "lint_errors", "loc") - violation_pct_values = extract_metric_values(checkpoints, "violation_pct") return RatiosStats( rubric=compute_metric_stats(rubric_ratios), lint=compute_metric_stats(lint_ratios), - violation_pct=compute_metric_stats(violation_pct_values), ) diff --git a/src/slop_code/visualization/diff_viewer.py b/src/slop_code/visualization/diff_viewer.py index 80e5e4dc..623c6f74 100644 --- a/src/slop_code/visualization/diff_viewer.py +++ b/src/slop_code/visualization/diff_viewer.py @@ -447,13 +447,11 @@ def generate_modern_diff(a, b, name_a, name_b, filename=""): with col_ov2: st.subheader("Quality Analysis") if quality_data: - ast_grep = quality_data.get("ast_grep", {}) lint = quality_data.get("lint", {}) complexity = quality_data.get("complexity", {}) lines = quality_data.get("lines", {}) metrics = { - "Violations (AST)": ast_grep.get("violations"), "Lint Errors": lint.get("errors"), "Complexity (CC Sum)": complexity.get("cc_sum"), "LOC": lines.get("loc"), diff --git a/tests/dashboard/graphs/scatter_test.py b/tests/dashboard/graphs/scatter_test.py index 77890f40..98e0177a 100644 --- a/tests/dashboard/graphs/scatter_test.py +++ b/tests/dashboard/graphs/scatter_test.py @@ -35,7 +35,6 @@ def test_aggregate_erosion_vs_solve_uses_saved_verbosity_mean(): "verbosity.mean": 0.75, "erosion.mean": 55.0, "ratios.rubric.mean": 10.0, - "ratios.violation_pct.mean": 20.0, "ratios.lint.mean": 30.0, "pct_checkpoints_solved": 80.0, } @@ -65,7 +64,6 @@ def test_aggregate_erosion_vs_problem_test_pass_rate_uses_saved_erosion_mean(): "verbosity.mean": 0.75, "erosion.mean": 55.0, "ratios.rubric.mean": 10.0, - "ratios.violation_pct.mean": 20.0, "ratios.lint.mean": 30.0, "pass_rates.problem.total": 0.6, } diff --git a/tests/entrypoints/commands/test_backfill_reports.py b/tests/entrypoints/commands/test_backfill_reports.py index 2efc3921..98924816 100644 --- a/tests/entrypoints/commands/test_backfill_reports.py +++ b/tests/entrypoints/commands/test_backfill_reports.py @@ -12,79 +12,6 @@ from slop_code.entrypoints.commands.backfill_reports import ( _recategorize_evaluation_tests, ) -from slop_code.entrypoints.commands.backfill_reports import ( - _update_ast_grep_jsonl, -) - - -def _write_jsonl(path: Path, rows: list[dict]) -> None: - path.write_text("".join(json.dumps(row) + "\n" for row in rows)) - - -def test_update_ast_grep_jsonl_filters_out_unknown_rules( - tmp_path: Path, -) -> None: - quality_dir = tmp_path / "quality_analysis" - quality_dir.mkdir() - jsonl_path = quality_dir / "ast_grep.jsonl" - overall_path = quality_dir / "overall_quality.json" - - _write_jsonl( - jsonl_path, - [ - { - "rule_id": "manual-sum-loop", - "category": "verbosity", - "subcategory": "verbosity", - "weight": 4, - "line": 1, - "end_line": 1, - }, - { - "rule_id": "bare-except-pass", - "category": "safety", - "subcategory": "safety", - "weight": 4, - "line": 2, - "end_line": 2, - }, - ], - ) - overall_path.write_text(json.dumps({"ast_grep": {}})) - - rules_lookup: dict[str, dict[str, str | int]] = { - "manual-sum-loop": { - "category": "slop", - "subcategory": "slop", - "weight": 4, - } - } - - total, updated = _update_ast_grep_jsonl( - jsonl_path, - rules_lookup, - MagicMock(), - ) - - assert total == 1 - assert updated == 1 - - lines = [json.loads(line) for line in jsonl_path.read_text().splitlines()] - assert lines == [ - { - "rule_id": "manual-sum-loop", - "category": "slop", - "subcategory": "slop", - "weight": 4, - "line": 1, - "end_line": 1, - } - ] - - overall = json.loads(overall_path.read_text()) - assert overall["ast_grep"]["violations"] == 1 - assert overall["ast_grep"]["category_counts"] == {"slop": 1} - assert overall["ast_grep"]["category_weighted"] == {"slop": 4} def test_recategorize_evaluation_tests_dict_format_excludes_skipped_from_counts( @@ -157,11 +84,6 @@ def test_backfill_reports_preserves_costs_for_all_agents( "_process_single_run_backfill", lambda problem_root, results_dir, logger: (reports, [], 1), ) - monkeypatch.setattr( - backfill_reports_module, - "build_ast_grep_rules_lookup", - lambda: {}, - ) monkeypatch.setattr( backfill_reports_module, "update_results_jsonl", diff --git a/tests/entrypoints/commands/test_variance.py b/tests/entrypoints/commands/test_variance.py index 450c8bea..9526590c 100644 --- a/tests/entrypoints/commands/test_variance.py +++ b/tests/entrypoints/commands/test_variance.py @@ -42,27 +42,6 @@ def test_discovers_runs_nested_under_model_dirs(tmp_path: Path) -> None: assert discovered == sorted([run_a, run_b]) -def test_metric_value_combines_lint_and_violation_pct() -> None: - row = { - "lint_per_loc": 0.2, - "violation_pct": 0.3, - } - - value = _metric_value(row, "normalized.lint_violation_pct") - - assert value == 0.5 - - -def test_metric_value_combines_lint_and_violation_pct_missing_returns_none() -> ( - None -): - row = {"lint_per_loc": 0.2} - - value = _metric_value(row, "normalized.lint_violation_pct") - - assert value is None - - def test_metric_value_reads_strict_and_isolated_pass_rates() -> None: row = { "strict_pass_rate": 0.7, @@ -79,8 +58,6 @@ def test_render_problem_cv_summary_splits_tables() -> None: "sample": { "Pass rate": [0.3], "Lint": [0.1], - "Violation %": [0.2], - "Lint+Violation %": [0.4], "LOC": [0.5], } } @@ -88,8 +65,6 @@ def test_render_problem_cv_summary_splits_tables() -> None: "sample": { "Pass rate": [0.05], "Lint": [0.01], - "Violation %": [0.02], - "Lint+Violation %": [0.04], "LOC": [0.05], } } @@ -157,10 +132,10 @@ def test_collect_ci_entries_canonicalizes_metric_names() -> None: record = { "problem": "prob", "run_count": 2, - "final.delta.ast_grep_violations.ci95_low": 0.4, - "final.delta.ast_grep_violations.ci95_high": 0.6, - "delta.ast_grep_violations.final.ci95_low": 0.5, - "delta.ast_grep_violations.final.ci95_high": 0.7, + "final.delta.verbosity.ci95_low": 0.4, + "final.delta.verbosity.ci95_high": 0.6, + "delta.verbosity.final.ci95_low": 0.5, + "delta.verbosity.final.ci95_high": 0.7, } key = RunGroupKey( model="m", diff --git a/tests/metrics/checkpoint/composites_test.py b/tests/metrics/checkpoint/composites_test.py deleted file mode 100644 index a934473e..00000000 --- a/tests/metrics/checkpoint/composites_test.py +++ /dev/null @@ -1,38 +0,0 @@ -from __future__ import annotations - -import pytest - -from slop_code.metrics.checkpoint.composites import compute_checkpoint_erosion -from slop_code.metrics.checkpoint.composites import compute_checkpoint_verbosity - - -def test_compute_checkpoint_verbosity_uses_violation_pct_and_clone_ratio_only(): - metrics = { - "loc": 100, - "clone_lines": 20, - "violation_pct": 0.15, - "verbosity_flagged_pct": 0.22, - "functions": 10, - "methods": 5, - "trivial_wrappers": 99, - "single_use_functions": 99, - } - - result = compute_checkpoint_verbosity(metrics) - - assert result == pytest.approx(0.22) - - -def test_compute_checkpoint_erosion_preserves_high_cc_pct_ratio(): - metrics = {"mass.high_cc_pct": 0.42} - - result = compute_checkpoint_erosion(metrics) - - assert result == pytest.approx(0.42) - - -def test_compute_checkpoint_erosion_accepts_zero_to_one_only(): - assert compute_checkpoint_erosion( - {"mass.high_cc_pct": 1.0} - ) == pytest.approx(1.0) - assert compute_checkpoint_erosion({"mass.high_cc_pct": 100.0}) is None diff --git a/tests/metrics/checkpoint_results_test.py b/tests/metrics/checkpoint_results_test.py index d9606700..3880b2fc 100644 --- a/tests/metrics/checkpoint_results_test.py +++ b/tests/metrics/checkpoint_results_test.py @@ -8,7 +8,6 @@ import pytest -from slop_code.common import AST_GREP_QUALITY_SAVENAME from slop_code.common import FILES_QUALITY_SAVENAME from slop_code.common import QUALITY_DIR from slop_code.common import QUALITY_METRIC_SAVENAME @@ -125,7 +124,6 @@ def _create_quality_test_files(tmp_path: Path, files_data: dict) -> Path: - quality_analysis/overall_quality.json - quality_analysis/files.jsonl (flat file metrics) - quality_analysis/symbols.jsonl (flat symbol metrics) - - quality_analysis/ast_grep.jsonl (flat AST-grep violations) """ # Compute aggregates from file data file_count = len(files_data) @@ -241,7 +239,7 @@ def _create_quality_test_files(tmp_path: Path, files_data: dict) -> Path: elif sym_type == "type_alias": type_alias_count += 1 - # Waste/redundancy/ast_grep aggregates + # Waste/redundancy aggregates total_single_use_functions = sum( f.get("waste", {}).get("single_use_count", 0) for f in files_data.values() @@ -262,21 +260,6 @@ def _create_quality_test_files(tmp_path: Path, files_data: dict) -> Path: files_with_clones = sum( 1 for f in files_data.values() if f.get("redundancy") ) - total_ast_grep_violations = sum( - f.get("ast_grep", {}).get("total_violations", 0) - for f in files_data.values() - ) - total_ast_grep_violation_lines = sum( - f.get("ast_grep", {}).get("violation_lines", 0) - for f in files_data.values() - ) - ast_grep_rules_checked = max( - ( - f.get("ast_grep", {}).get("rules_checked", 0) - for f in files_data.values() - ), - default=0, - ) total_imports = sum(f.get("import_count", 0) for f in files_data.values()) max_imports = max( (f.get("import_count", 0) for f in files_data.values()), default=0 @@ -381,12 +364,6 @@ def _create_quality_test_files(tmp_path: Path, files_data: dict) -> Path: "clone_ratio_sum": clone_ratio_sum, "files_with_clones": files_with_clones, }, - "ast_grep": { - "violations": total_ast_grep_violations, - "violation_lines": total_ast_grep_violation_lines, - "rules_checked": ast_grep_rules_checked, - "counts": {}, - }, "source_files": None, } @@ -401,7 +378,7 @@ def _create_quality_test_files(tmp_path: Path, files_data: dict) -> Path: # Write flat files.jsonl with (quality_dir / FILES_QUALITY_SAVENAME).open("w") as f: for file_path, fm in files_data.items(): - # Create flat file metrics (no nested symbols/ast_grep lists) + # Create flat file metrics (no nested symbol lists) flat_fm = { "file_path": file_path, "loc": fm["lines"]["loc"], @@ -419,12 +396,6 @@ def _create_quality_test_files(tmp_path: Path, files_data: dict) -> Path: "import_count": fm.get("import_count", 0), "global_count": fm.get("global_count", 0), "symbol_count": len(fm["symbols"]), - "ast_grep_violation_count": fm.get("ast_grep", {}).get( - "total_violations", 0 - ), - "ast_grep_rules_checked": fm.get("ast_grep", {}).get( - "rules_checked", 0 - ), "clone_instances": fm.get("redundancy", {}).get( "total_clone_instances", 0 ), @@ -471,12 +442,6 @@ def _create_quality_test_files(tmp_path: Path, files_data: dict) -> Path: } f.write(json.dumps(flat_sym) + "\n") - # Write flat ast_grep.jsonl (empty for most tests, but structure is there) - with (quality_dir / AST_GREP_QUALITY_SAVENAME).open("w") as f: - # AST-grep violations are typically from the ast_grep field in files_data - # For now we just create an empty file (violations are aggregated) - pass - return tmp_path @@ -554,12 +519,6 @@ def sample_quality_file(self, tmp_path: Path) -> Path: "clone_lines": 10, "clone_ratio": 0.1, }, - "ast_grep": { - "total_violations": 3, - "violation_lines": 5, - "rules_checked": 33, - "counts": {"bare-except": 2, "len-comparison": 1}, - }, "import_count": 5, "global_count": 1, }, @@ -660,12 +619,6 @@ def test_redundancy_namespace(self, sample_quality_file: Path): assert result["clone_lines"] == 10 assert "clone_instances" not in result - def test_ast_grep_namespace(self, sample_quality_file: Path): - result = get_quality_metrics(sample_quality_file) - - assert result["ast_grep_violations"] == 3 - assert result["sg_slop_violations"] == 0 - def test_globals_namespace(self, sample_quality_file: Path): result = get_quality_metrics(sample_quality_file) @@ -679,10 +632,8 @@ def test_derived_ratios(self, sample_quality_file: Path): assert "methods_per_class" not in result assert "attributes_per_class" not in result assert result["lint_per_loc"] == pytest.approx(3 / 150) - assert result["violation_pct"] == pytest.approx(5 / 150) - assert "ast_grep_per_loc" not in result - def test_exposes_cloned_and_union_pct_for_later_analysis( + def test_exposes_cloned_pct_for_later_analysis( self, sample_quality_file: Path ): overall_path = ( @@ -690,15 +641,12 @@ def test_exposes_cloned_and_union_pct_for_later_analysis( ) overall = json.loads(overall_path.read_text()) overall["redundancy"]["cloned_sloc_lines"] = 9 - overall["verbosity_flagged_sloc_lines"] = 14 overall_path.write_text(json.dumps(overall)) result = get_quality_metrics(sample_quality_file) assert result["cloned_sloc_lines"] == 9 assert result["cloned_pct"] == pytest.approx(9 / 150) - assert result["verbosity_flagged_sloc_lines"] == 14 - assert result["verbosity_flagged_pct"] == pytest.approx(14 / 150) def test_missing_file(self, tmp_path: Path): result = get_quality_metrics(tmp_path) @@ -708,7 +656,6 @@ def test_removed_fields_are_absent(self, sample_quality_file: Path): result = get_quality_metrics(sample_quality_file) removed_fields = { - "ast_grep_violation_lines", "branches_mean", "clone_instances", "comparisons_mean", @@ -822,7 +769,7 @@ def test_empty_checkpoint(self, tmp_path: Path): # May also include erosion metrics with default values assert "erosion_velocity" in result or len(result) == 2 - def test_uses_scb_check_report_for_composite_quality_numbers( + def test_uses_pinned_scb_check_0_1_3_report_for_composite_quality_numbers( self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ): (tmp_path / "snapshot").mkdir() @@ -841,9 +788,7 @@ def test_uses_scb_check_report_for_composite_quality_numbers( "get_quality_metrics", lambda checkpoint_dir: { "loc": 100, - "verbosity_flagged_pct": 0.01, "mass.high_cc_pct": 0.99, - "cloned_pct": 0.01, }, ) monkeypatch.setattr( @@ -872,19 +817,18 @@ def fake_run(command, **kwargs): result = get_checkpoint_metrics(tmp_path) + assert checkpoint_driver.SCB_CHECK_VERSION == "0.1.3" assert commands == [ [ "uvx", - f"scb-check=={checkpoint_driver.SCB_CHECK_VERSION}", + "scb-check==0.1.3", "check", "--report", "--include-all", str(tmp_path / "snapshot"), ] ] - assert ( - result["scb_check_version"] == checkpoint_driver.SCB_CHECK_VERSION - ) + assert result["scb_check_version"] == "0.1.3" assert result["verbosity"] == pytest.approx(0.5) assert result["erosion"] == pytest.approx(0.2) assert result["cloned_sloc_lines"] == 10 @@ -948,65 +892,65 @@ def test_percentage_deltas(self): """Test percentage change calculation.""" prev = { "loc": 100, - "ast_grep_violations": 5, + "verbosity": 0.2, "total_lines": 120, } curr = { "loc": 150, - "ast_grep_violations": 10, + "verbosity": 0.4, "lines_added": 20, "lines_removed": 10, } result = compute_checkpoint_delta(prev, curr) assert result["delta.loc"] == 50.0 # (150-100)/100 * 100 - assert result["delta.ast_grep_violations"] == 100.0 + assert result["delta.verbosity"] == 100.0 def test_zero_prev_returns_inf(self): """Test handling of zero previous values.""" prev = { - "ast_grep_violations": 0, + "verbosity": 0, "total_lines": 100, } curr = { - "ast_grep_violations": 5, + "verbosity": 0.5, "lines_added": 10, "lines_removed": 5, } result = compute_checkpoint_delta(prev, curr) # Should be inf when prev is 0 but curr > 0 - assert result["delta.ast_grep_violations"] == float("inf") + assert result["delta.verbosity"] == float("inf") def test_zero_both_returns_zero(self): """Test handling when both prev and curr are zero.""" prev = { - "ast_grep_violations": 0, + "verbosity": 0, "total_lines": 100, } curr = { - "ast_grep_violations": 0, + "verbosity": 0, "lines_added": 0, "lines_removed": 0, } result = compute_checkpoint_delta(prev, curr) - assert result["delta.ast_grep_violations"] == 0.0 + assert result["delta.verbosity"] == 0.0 def test_negative_delta(self): """Test negative percentage changes.""" prev = { - "ast_grep_violations": 10, + "verbosity": 0.4, "total_lines": 100, } curr = { - "ast_grep_violations": 5, + "verbosity": 0.2, "lines_added": 10, "lines_removed": 5, } result = compute_checkpoint_delta(prev, curr) - assert result["delta.ast_grep_violations"] == -50.0 + assert result["delta.verbosity"] == -50.0 def test_churn_ratio(self): """Test churn ratio calculation.""" @@ -1028,19 +972,19 @@ def test_all_delta_keys_present(self): """Test that all expected delta keys are present in output.""" prev = { "loc": 100, - "ast_grep_violations": 10, + "verbosity": 0.2, "total_lines": 120, } curr = { "loc": 150, - "ast_grep_violations": 12, + "verbosity": 0.3, "lines_added": 20, "lines_removed": 10, } result = compute_checkpoint_delta(prev, curr) assert "delta.loc" in result - assert "delta.ast_grep_violations" in result + assert "delta.verbosity" in result assert "delta.churn_ratio" in result removed_delta_keys = { diff --git a/tests/metrics/driver_test.py b/tests/metrics/driver_test.py index 3604d16b..9f287005 100644 --- a/tests/metrics/driver_test.py +++ b/tests/metrics/driver_test.py @@ -11,7 +11,6 @@ from slop_code.metrics.driver import measure_snapshot_quality from slop_code.metrics.languages import EXT_TO_LANGUAGE from slop_code.metrics.languages import LANGUAGE_REGISTRY -from slop_code.metrics.models import AstGrepViolation from slop_code.metrics.models import CodeClone from slop_code.metrics.models import FileMetrics from slop_code.metrics.models import LanguageSpec @@ -434,7 +433,7 @@ def test_measure_snapshot_quality_aggregates_metrics(self, tmp_path): assert snapshot.lint.counts["E501"] == 2 # 1 + 1 assert snapshot.lint.counts["E302"] == 2 # 1 + 1 - def test_measure_snapshot_quality_tracks_clone_and_union_sloc_coverage( + def test_measure_snapshot_quality_tracks_clone_sloc_coverage( self, tmp_path, monkeypatch ): source = tmp_path / "file1.py" @@ -479,16 +478,6 @@ def fake_calculate( clone_lines=6, clone_ratio=1.0, ), - ast_grep_violations=[ - AstGrepViolation( - rule_id="rule-a", - severity="warning", - line=5, - column=0, - end_line=7, - end_column=0, - ) - ], ) monkeypatch.setattr( @@ -499,7 +488,6 @@ def fake_calculate( snapshot, _ = measure_snapshot_quality("file1.py", tmp_path) assert snapshot.redundancy.cloned_sloc_lines == 6 - assert snapshot.verbosity_flagged_sloc_lines == 6 def test_measure_snapshot_quality_excludes_patterns(self, tmp_path): """Test that default exclude patterns are applied. diff --git a/tests/metrics/language/py_ast_grep_test.py b/tests/metrics/language/py_ast_grep_test.py deleted file mode 100644 index cae5c31f..00000000 --- a/tests/metrics/language/py_ast_grep_test.py +++ /dev/null @@ -1,713 +0,0 @@ -"""Exhaustive tests for AST-grep metrics. - -Tests all functions in slop_code.metrics.languages.python.ast_grep including: -- Public API: calculate_ast_grep_metrics, build_ast_grep_rules_lookup -- Helper functions: _is_sg_available, _get_ast_grep_rules_path, _count_rules_in_file -""" - -from __future__ import annotations - -import collections -import json -import shutil -import subprocess -from pathlib import Path -from unittest.mock import MagicMock -from unittest.mock import patch - -import pytest -import yaml - -from slop_code.metrics.languages.python import AST_GREP_RULES_DIR -from slop_code.metrics.languages.python import AST_GREP_RULES_PATH -from slop_code.metrics.languages.python import _get_ast_grep_rules_dir -from slop_code.metrics.languages.python import _get_ast_grep_rules_path -from slop_code.metrics.languages.python import _is_sg_available -from slop_code.metrics.languages.python import calculate_ast_grep_metrics -from slop_code.metrics.languages.python.ast_grep import ( - build_ast_grep_rules_lookup, -) -from slop_code.metrics.models import AstGrepMetrics -from slop_code.metrics.models import AstGrepViolation - - -def _write(tmp_path: Path, name: str, content: str) -> Path: - """Helper to write a file and return its path.""" - path = tmp_path / name - path.write_text(content) - return path - - -# ============================================================================= -# sg Availability Tests -# ============================================================================= - - -class TestSgAvailability: - """Tests for _is_sg_available.""" - - def test_sg_available_when_installed(self) -> None: - with patch("shutil.which", return_value="/usr/bin/sg"): - assert _is_sg_available() is True - - def test_sg_unavailable_when_not_installed(self) -> None: - with patch("shutil.which", return_value=None): - assert _is_sg_available() is False - - -# ============================================================================= -# Rules Directory Tests -# ============================================================================= - - -class TestGetAstGrepRulesPath: - """Tests for _get_ast_grep_rules_path.""" - - def test_default_rules_path(self) -> None: - with patch.dict("os.environ", {}, clear=True): - rules_path = _get_ast_grep_rules_path() - assert rules_path.name == "slop_rules.yaml" - assert rules_path.parent.name == "configs" - assert "configs" in str(rules_path) - assert _get_ast_grep_rules_dir() == rules_path - - def test_env_override(self, tmp_path: Path) -> None: - custom_file = tmp_path / "custom-rules.yaml" - custom_file.write_text("id: test-rule\n") - with patch.dict( - "os.environ", {"AST_GREP_RULES_PATH": str(custom_file)} - ): - assert _get_ast_grep_rules_path() == custom_file - - def test_default_rules_path_exists(self) -> None: - """Verify the default rules file actually exists.""" - assert AST_GREP_RULES_PATH.exists(), ( - f"Expected {AST_GREP_RULES_PATH} to exist" - ) - assert AST_GREP_RULES_PATH.is_file() - assert AST_GREP_RULES_DIR == AST_GREP_RULES_PATH - - -class TestBuildAstGrepRulesLookup: - """Tests for build_ast_grep_rules_lookup.""" - - def test_uses_curated_slop_ruleset(self) -> None: - lookup = build_ast_grep_rules_lookup() - - included_rules = { - "manual-sum-loop", - "guard-return-none", - "nested-if-no-else", - "json-roundtrip-dumps-loads", - "dict-str-any", - "object-type-annotation", - } - excluded_rules = { - "bare-except-pass", - "mutable-default-arg", - "pandas-iterrows", - "untyped-list-annotation", - } - - for rule_id in included_rules: - assert lookup[rule_id]["category"] == "slop" - assert lookup[rule_id]["subcategory"] == "slop" - for rule_id in excluded_rules: - assert rule_id not in lookup - - -# ============================================================================= -# Calculate AST-grep Metrics Tests -# ============================================================================= - - -class TestCalculateAstGrepMetrics: - """Tests for calculate_ast_grep_metrics.""" - - def test_returns_empty_when_sg_unavailable(self, tmp_path: Path) -> None: - source = _write(tmp_path, "test.py", "x = 1") - with patch( - "slop_code.metrics.languages.python.ast_grep.shutil.which", - return_value=None, - ): - result = calculate_ast_grep_metrics(source) - - assert result.total_violations == 0 - assert result.violations == [] - assert result.counts == {} - assert result.rules_checked == 0 - - def test_returns_empty_when_rules_dir_missing(self, tmp_path: Path) -> None: - source = _write(tmp_path, "test.py", "x = 1") - with ( - patch( - "slop_code.metrics.languages.python.ast_grep.shutil.which", - return_value="/usr/bin/sg", - ), - patch.dict( - "os.environ", {"AST_GREP_RULES_PATH": "/nonexistent.yaml"} - ), - ): - result = calculate_ast_grep_metrics(source) - - assert result.total_violations == 0 - assert result.rules_checked == 0 - - def test_returns_empty_when_no_rules(self, tmp_path: Path) -> None: - source = _write(tmp_path, "test.py", "x = 1") - rules_path = tmp_path / "empty-rules.yaml" - rules_path.write_text("") - - with ( - patch( - "slop_code.metrics.languages.python.ast_grep.shutil.which", - return_value="/usr/bin/sg", - ), - patch.dict("os.environ", {"AST_GREP_RULES_PATH": str(rules_path)}), - ): - result = calculate_ast_grep_metrics(source) - - assert result.total_violations == 0 - assert result.rules_checked == 0 - - def test_parses_violations_from_sg_output(self, tmp_path: Path) -> None: - source = _write( - tmp_path, - "test.py", - """ -def bad(): - try: - pass - except: - pass -""", - ) - rules_path = tmp_path / "rules.yaml" - # Rule YAML includes metadata - rules_path.write_text( - "id: bare-except-pass\n" - "language: python\n" - "metadata:\n" - " category: safety\n" - " weight: 2\n" - "rule:\n" - " pattern: 'pass'" - ) - - mock_output = ( - '{"ruleId": "bare-except-pass", ' - '"severity": "warning", ' - '"range": {"start": {"line": 5, "column": 8}, ' - '"end": {"line": 5, "column": 12}}}' - ) - - with ( - patch( - "slop_code.metrics.languages.python.ast_grep.shutil.which", - return_value="/usr/bin/sg", - ), - patch.dict("os.environ", {"AST_GREP_RULES_PATH": str(rules_path)}), - patch( - "slop_code.metrics.languages.python.ast_grep.subprocess.run" - ) as mock_run, - ): - mock_run.return_value = MagicMock( - stdout=mock_output, - stderr="", - returncode=0, - ) - result = calculate_ast_grep_metrics(source) - - assert result.total_violations == 1 - assert result.rules_checked == 1 - assert len(result.violations) == 1 - assert result.violations[0].rule_id == "bare-except-pass" - assert result.violations[0].severity == "warning" - assert result.violations[0].line == 5 - assert result.violations[0].column == 8 - assert result.counts == {"bare-except-pass": 1} - - def test_aggregates_multiple_violations(self, tmp_path: Path) -> None: - source = _write(tmp_path, "test.py", "x = 1") - rules_path = tmp_path / "rules.yaml" - rules_path.write_text("id: test-rule") - - mock_output = "\n".join( - [ - '{"ruleId": "test-rule", "severity": "warning", ' - '"range": {"start": {"line": 1, "column": 0}, ' - '"end": {"line": 1, "column": 5}}}', - '{"ruleId": "test-rule", "severity": "warning", ' - '"range": {"start": {"line": 2, "column": 0}, ' - '"end": {"line": 2, "column": 5}}}', - ] - ) - - with ( - patch( - "slop_code.metrics.languages.python.ast_grep.shutil.which", - return_value="/usr/bin/sg", - ), - patch.dict("os.environ", {"AST_GREP_RULES_PATH": str(rules_path)}), - patch( - "slop_code.metrics.languages.python.ast_grep.subprocess.run" - ) as mock_run, - ): - mock_run.return_value = MagicMock( - stdout=mock_output, - stderr="", - returncode=0, - ) - result = calculate_ast_grep_metrics(source) - - assert result.total_violations == 2 - assert result.counts == {"test-rule": 2} - - def test_filters_min_file_count_rules_until_threshold_exceeded( - self, tmp_path: Path - ) -> None: - source = _write(tmp_path, "test.py", "x = 1") - rules_path = tmp_path / "rules.yaml" - rules_path.write_text( - "---\n" - "id: threshold-rule\n" - "metadata:\n" - " min_file_count: 2\n" - "---\n" - "id: noisy-rule\n" - "metadata:\n" - " min_file_count: 2\n" - "---\n" - "id: regular-rule\n" - ) - - mock_output = "\n".join( - [ - '{"ruleId": "threshold-rule", "severity": "warning", ' - '"range": {"start": {"line": 1, "column": 0}, ' - '"end": {"line": 1, "column": 5}}}', - '{"ruleId": "threshold-rule", "severity": "warning", ' - '"range": {"start": {"line": 2, "column": 0}, ' - '"end": {"line": 2, "column": 5}}}', - '{"ruleId": "noisy-rule", "severity": "warning", ' - '"range": {"start": {"line": 3, "column": 0}, ' - '"end": {"line": 3, "column": 5}}}', - '{"ruleId": "noisy-rule", "severity": "warning", ' - '"range": {"start": {"line": 4, "column": 0}, ' - '"end": {"line": 4, "column": 5}}}', - '{"ruleId": "noisy-rule", "severity": "warning", ' - '"range": {"start": {"line": 5, "column": 0}, ' - '"end": {"line": 5, "column": 5}}}', - '{"ruleId": "regular-rule", "severity": "warning", ' - '"range": {"start": {"line": 6, "column": 0}, ' - '"end": {"line": 6, "column": 5}}}', - ] - ) - - with ( - patch( - "slop_code.metrics.languages.python.ast_grep.shutil.which", - return_value="/usr/bin/sg", - ), - patch.dict("os.environ", {"AST_GREP_RULES_PATH": str(rules_path)}), - patch( - "slop_code.metrics.languages.python.ast_grep.subprocess.run" - ) as mock_run, - ): - mock_run.return_value = MagicMock( - stdout=mock_output, - stderr="", - returncode=0, - ) - result = calculate_ast_grep_metrics(source) - - assert result.total_violations == 4 - assert result.counts == {"noisy-rule": 3, "regular-rule": 1} - assert [violation.rule_id for violation in result.violations] == [ - "noisy-rule", - "noisy-rule", - "noisy-rule", - "regular-rule", - ] - - def test_counts_multiple_rules_in_one_file(self, tmp_path: Path) -> None: - source = _write(tmp_path, "test.py", "x = 1") - rules_path = tmp_path / "rules.yaml" - rules_path.write_text( - "---\n" - "id: first-rule\n" - "language: python\n" - "rule:\n" - " pattern: x\n" - "---\n" - "id: second-rule\n" - "language: python\n" - "rule:\n" - " pattern: y\n" - ) - - mock_output = "\n".join( - [ - '{"ruleId": "first-rule", "severity": "warning", ' - '"range": {"start": {"line": 1, "column": 0}, ' - '"end": {"line": 1, "column": 5}}}', - '{"ruleId": "second-rule", "severity": "warning", ' - '"range": {"start": {"line": 2, "column": 0}, ' - '"end": {"line": 2, "column": 5}}}', - ] - ) - - with ( - patch( - "slop_code.metrics.languages.python.ast_grep.shutil.which", - return_value="/usr/bin/sg", - ), - patch.dict("os.environ", {"AST_GREP_RULES_PATH": str(rules_path)}), - patch( - "slop_code.metrics.languages.python.ast_grep.subprocess.run" - ) as mock_run, - ): - mock_run.return_value = MagicMock( - stdout=mock_output, - stderr="", - returncode=0, - ) - result = calculate_ast_grep_metrics(source) - - assert mock_run.call_count == 1 - assert result.rules_checked == 2 - assert result.counts == {"first-rule": 1, "second-rule": 1} - - def test_handles_subprocess_error_gracefully(self, tmp_path: Path) -> None: - source = _write(tmp_path, "test.py", "x = 1") - rules_path = tmp_path / "rules.yaml" - rules_path.write_text( - "id: test\nlanguage: python\nrule:\n pattern: 'x'" - ) - - with ( - patch( - "slop_code.metrics.languages.python.ast_grep.shutil.which", - return_value="/usr/bin/sg", - ), - patch.dict("os.environ", {"AST_GREP_RULES_PATH": str(rules_path)}), - patch( - "slop_code.metrics.languages.python.ast_grep.subprocess.run" - ) as mock_run, - ): - mock_run.side_effect = OSError("sg not found") - result = calculate_ast_grep_metrics(source) - - assert result.total_violations == 0 - assert result.rules_checked == 1 # We tried to check 1 rule - - def test_handles_malformed_json_gracefully(self, tmp_path: Path) -> None: - source = _write(tmp_path, "test.py", "x = 1") - rules_path = tmp_path / "rules.yaml" - rules_path.write_text("id: test") - - with ( - patch( - "slop_code.metrics.languages.python.ast_grep.shutil.which", - return_value="/usr/bin/sg", - ), - patch.dict("os.environ", {"AST_GREP_RULES_PATH": str(rules_path)}), - patch( - "slop_code.metrics.languages.python.ast_grep.subprocess.run" - ) as mock_run, - ): - mock_run.return_value = MagicMock( - stdout="not valid json", - stderr="", - returncode=0, - ) - result = calculate_ast_grep_metrics(source) - - assert result.total_violations == 0 - - def test_handles_missing_json_fields_gracefully( - self, tmp_path: Path - ) -> None: - source = _write(tmp_path, "test.py", "x = 1") - rules_path = tmp_path / "rules.yaml" - rules_path.write_text("id: test") - - # Missing 'range' field - mock_output = '{"ruleId": "test", "severity": "warning"}' - - with ( - patch( - "slop_code.metrics.languages.python.ast_grep.shutil.which", - return_value="/usr/bin/sg", - ), - patch.dict("os.environ", {"AST_GREP_RULES_PATH": str(rules_path)}), - patch( - "slop_code.metrics.languages.python.ast_grep.subprocess.run" - ) as mock_run, - ): - mock_run.return_value = MagicMock( - stdout=mock_output, - stderr="", - returncode=0, - ) - result = calculate_ast_grep_metrics(source) - - assert result.total_violations == 0 - - def test_handles_empty_output(self, tmp_path: Path) -> None: - source = _write(tmp_path, "test.py", "x = 1") - rules_path = tmp_path / "rules.yaml" - rules_path.write_text("id: test") - - with ( - patch( - "slop_code.metrics.languages.python.ast_grep.shutil.which", - return_value="/usr/bin/sg", - ), - patch.dict("os.environ", {"AST_GREP_RULES_PATH": str(rules_path)}), - patch( - "slop_code.metrics.languages.python.ast_grep.subprocess.run" - ) as mock_run, - ): - mock_run.return_value = MagicMock( - stdout="", - stderr="", - returncode=0, - ) - result = calculate_ast_grep_metrics(source) - - assert result.total_violations == 0 - assert result.rules_checked == 1 - - -# ============================================================================= -# AST-grep Metrics Model Tests -# ============================================================================= - - -class TestAstGrepMetricsModel: - """Tests for AstGrepMetrics and AstGrepViolation models.""" - - def test_ast_grep_violation_serialization(self) -> None: - violation = AstGrepViolation( - rule_id="test-rule", - severity="warning", - category="verbosity", - subcategory="verbose-code", - weight=3, - line=10, - column=4, - end_line=10, - end_column=8, - ) - - data = violation.model_dump() - assert data["rule_id"] == "test-rule" - assert data["severity"] == "warning" - assert data["category"] == "verbosity" - assert data["subcategory"] == "verbose-code" - assert data["weight"] == 3 - assert data["line"] == 10 - assert data["column"] == 4 - assert data["end_line"] == 10 - assert data["end_column"] == 8 - - def test_ast_grep_violation_defaults(self) -> None: - """Test that category and subcategory have sensible defaults.""" - violation = AstGrepViolation( - rule_id="test-rule", - severity="warning", - line=1, - column=0, - end_line=1, - end_column=5, - ) - - assert violation.category == "" - assert violation.subcategory == "unknown" - assert violation.weight == 1 - - def test_ast_grep_metrics_serialization(self) -> None: - violation = AstGrepViolation( - rule_id="test-rule", - severity="warning", - line=10, - column=4, - end_line=10, - end_column=8, - ) - metrics = AstGrepMetrics( - violations=[violation], - total_violations=1, - counts={"test-rule": 1}, - rules_checked=5, - ) - - data = metrics.model_dump() - assert data["total_violations"] == 1 - assert data["rules_checked"] == 5 - assert len(data["violations"]) == 1 - assert data["violations"][0]["rule_id"] == "test-rule" - assert data["counts"] == {"test-rule": 1} - - def test_ast_grep_metrics_empty(self) -> None: - metrics = AstGrepMetrics( - violations=[], - total_violations=0, - counts={}, - rules_checked=0, - ) - - data = metrics.model_dump() - assert data["total_violations"] == 0 - assert data["rules_checked"] == 0 - assert data["violations"] == [] - assert data["counts"] == {} - - -# ============================================================================= -# Integration Tests -# ============================================================================= - - -class TestAstGrepMetricsIntegration: - """Integration tests that use actual ast-grep if available.""" - - @pytest.mark.skipif( - not shutil.which("sg"), reason="ast-grep (sg) not installed" - ) - def test_real_sg_scan_with_clean_code(self, tmp_path: Path) -> None: - """Test with actual ast-grep scanning on clean code.""" - source = _write( - tmp_path, - "clean.py", - """ -def greet(name: str) -> str: - return f"Hello, {name}!" -""", - ) - - result = calculate_ast_grep_metrics(source) - - # Should have checked rules but found few/no violations in clean code - assert result.rules_checked > 0 - - @pytest.mark.skipif( - not shutil.which("sg"), reason="ast-grep (sg) not installed" - ) - def test_real_sg_scan_with_bad_code(self, tmp_path: Path) -> None: - """Test with actual ast-grep scanning on code with bad patterns.""" - source = _write( - tmp_path, - "bad.py", - """ -def example(): - try: - do_something() - except: - pass - -def check_value(x): - if x == True: - return True - else: - return False -""", - ) - - result = calculate_ast_grep_metrics(source) - - # Should have checked rules - assert result.rules_checked > 0 - # This code has patterns that should be caught: - # - bare except with pass - # - comparing to True - # - if/else returning True/False - - -# ============================================================================= -# Shipped Ruleset Integrity Tests -# ============================================================================= - - -class TestSlopRulesFileIntegrity: - """Tests that the shipped ``slop_rules.yaml`` is well-formed.""" - - def test_rule_ids_are_unique(self) -> None: - """Every rule id in the shipped ruleset must appear exactly once. - - Duplicate ids make :func:`build_ast_grep_rules_lookup` - non-deterministic (the last occurrence silently wins the - weight / ``min_file_count`` used to score every match of that id) and - inflate ``rules_checked``. - """ - with AST_GREP_RULES_PATH.open(encoding="utf-8") as handle: - ids = [ - doc["id"] - for doc in yaml.safe_load_all(handle) - if doc and "id" in doc - ] - - duplicates = { - rule_id: count - for rule_id, count in collections.Counter(ids).items() - if count > 1 - } - assert not duplicates, f"Duplicate rule ids in slop_rules.yaml: {duplicates}" - - @pytest.mark.skipif( - not shutil.which("sg"), reason="ast-grep (sg) not installed" - ) - def test_frozen_micro_dataclass_matches_tiny_not_large( - self, tmp_path: Path - ) -> None: - """``frozen-micro-dataclass`` must fire on a tiny frozen dataclass but - not on a larger one. - - Regression test: the original regex required ``\\n`` after every field - followed by ``$``, but the ``decorated_definition`` node ends at the - last field with no trailing newline, so the rule matched nothing. - """ - tiny = _write( - tmp_path, - "tiny.py", - "from dataclasses import dataclass\n\n" - "@dataclass(frozen=True, slots=True)\n" - "class Report:\n" - " name: str\n" - " value: float\n", - ) - large = _write( - tmp_path, - "large.py", - "from dataclasses import dataclass\n\n" - "@dataclass(frozen=True)\n" - "class Report:\n" - " a: str\n b: int\n c: int\n d: int\n e: int\n", - ) - - def fired_rule_ids(source: Path) -> set[str]: - # Call sg directly so the assertion targets the rule's regex, - # independent of the min_file_count scoring filter applied by - # calculate_ast_grep_metrics. - result = subprocess.run( # noqa: S603 - [ # noqa: S607 - "sg", - "scan", - "--json=stream", - "-r", - str(AST_GREP_RULES_PATH), - str(source), - ], - capture_output=True, - text=True, - check=False, - ) - return { - json.loads(line)["ruleId"] - for line in result.stdout.splitlines() - if line.strip() - } - - assert "frozen-micro-dataclass" in fired_rule_ids(tiny) - assert "frozen-micro-dataclass" not in fired_rule_ids(large) diff --git a/tests/metrics/models_test.py b/tests/metrics/models_test.py index 91fca04d..0944765f 100644 --- a/tests/metrics/models_test.py +++ b/tests/metrics/models_test.py @@ -12,8 +12,6 @@ from slop_code.metrics.languages import get_language from slop_code.metrics.languages import get_language_by_extension from slop_code.metrics.languages import register_language -from slop_code.metrics.models import AstGrepAggregates -from slop_code.metrics.models import AstGrepViolation from slop_code.metrics.models import ClassStats from slop_code.metrics.models import ComplexityAggregates from slop_code.metrics.models import FileMetrics @@ -160,7 +158,6 @@ def _create_snapshot_from_files( clone_ratio_sum=0.0, files_with_clones=0, ), - ast_grep=AstGrepAggregates(violations=0, rules_checked=0), source_files=source_files, ) @@ -449,95 +446,6 @@ def test_from_snapshot_metrics_empty(self): "F": 0, } assert report.mi == {"A": 0, "B": 0, "C": 0} - assert report.ast_grep_violations == 0 - assert report.ast_grep_rules_checked == 0 - - -def test_ast_grep_aggregates_count_unique_flagged_lines_per_file(): - aggregates = AstGrepAggregates(violations=0, rules_checked=0) - file_metrics = FileMetrics( - symbols=[], - lines=LineCountMetrics( - total_lines=20, - loc=15, - comments=2, - multi_comment=1, - single_comment=1, - ), - lint=LintMetrics(errors=0, fixable=0, counts={}), - mi=20.0, - depth=1, - ast_grep_violations=[ - AstGrepViolation( - rule_id="rule-a", - severity="warning", - line=10, - column=0, - end_line=12, - end_column=0, - ), - AstGrepViolation( - rule_id="rule-b", - severity="warning", - line=12, - column=0, - end_line=13, - end_column=0, - ), - AstGrepViolation( - rule_id="rule-c", - severity="warning", - line=10, - column=0, - end_line=10, - end_column=0, - ), - ], - ) - - aggregates.update(file_metrics) - - assert aggregates.violations == 3 - assert aggregates.violation_lines == 4 - - def test_from_snapshot_metrics_with_ast_grep(self): - """Test AST-grep metrics from aggregates are included in report.""" - files = { - "file.py": FileMetrics( - symbols=[], - lines=LineCountMetrics( - total_lines=10, - loc=8, - comments=1, - multi_comment=0, - single_comment=1, - ), - lint=LintMetrics(errors=0, fixable=0, counts={}), - mi=20.0, - depth=1, - ), - } - snapshot = _create_snapshot_from_files(files) - # Set ast_grep values directly on the ast_grep aggregate section - snapshot = snapshot.model_copy( - update={ - "ast_grep": AstGrepAggregates(violations=15, rules_checked=34) - } - ) - - report = SnapshotQualityReport.from_snapshot_metrics(snapshot) - - assert report.ast_grep_violations == 15 - assert report.ast_grep_rules_checked == 34 - - def test_from_snapshot_metrics_no_ast_grep(self): - """Test report with no AST-grep metrics defaults to zero.""" - snapshot = _create_snapshot_from_files({}) - - report = SnapshotQualityReport.from_snapshot_metrics(snapshot) - - assert report.ast_grep_violations == 0 - assert report.ast_grep_rules_checked == 0 class TestLanguageRegistry: diff --git a/tests/metrics/summary_test.py b/tests/metrics/summary_test.py index ee0f70d5..741d0147 100644 --- a/tests/metrics/summary_test.py +++ b/tests/metrics/summary_test.py @@ -28,7 +28,7 @@ def test_does_not_serialize_delta_stats(self, mock_config): "idx": 1, "strict_pass_rate": 0.8, "delta.loc": 50.0, - "delta.ast_grep_violations": -20.0, + "delta.verbosity": -20.0, "delta.churn_ratio": 0.1, } ] @@ -486,7 +486,6 @@ def test_uses_saved_checkpoint_verbosity_and_erosion(self, mock_config): "isolated_pass_rate": 1.0, "verbosity": 0.95, "erosion": 0.6, - "ast_grep_violations": 999, "rubric_total_flags": 999, "mass.high_cc_pct": 0.01, } From c983ce79ebed38208c4a1d4cbf599201cfc90fae Mon Sep 17 00:00:00 2001 From: gabeorlanski Date: Tue, 28 Jul 2026 12:35:41 -0500 Subject: [PATCH 3/5] Simplify scb-check version handling --- src/slop_code/metrics/checkpoint/driver.py | 3 +-- tests/metrics/checkpoint_results_test.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/slop_code/metrics/checkpoint/driver.py b/src/slop_code/metrics/checkpoint/driver.py index 642778cf..289d8b6d 100644 --- a/src/slop_code/metrics/checkpoint/driver.py +++ b/src/slop_code/metrics/checkpoint/driver.py @@ -26,7 +26,6 @@ # incomparable across runs. Bump this deliberately; the value is recorded # with every checkpoint so past results stay interpretable. SCB_CHECK_VERSION = "0.1.3" -SCB_CHECK_VERSION_KEY = "scb_check_version" def _number(value: Any) -> float | None: @@ -110,7 +109,7 @@ def _get_scb_check_metrics(checkpoint_dir: Path) -> dict[str, Any]: return {} metrics = _scb_check_metrics_from_report(report) - metrics[SCB_CHECK_VERSION_KEY] = SCB_CHECK_VERSION + metrics["scb_check_version"] = SCB_CHECK_VERSION return metrics diff --git a/tests/metrics/checkpoint_results_test.py b/tests/metrics/checkpoint_results_test.py index 3880b2fc..4227ae53 100644 --- a/tests/metrics/checkpoint_results_test.py +++ b/tests/metrics/checkpoint_results_test.py @@ -769,7 +769,7 @@ def test_empty_checkpoint(self, tmp_path: Path): # May also include erosion metrics with default values assert "erosion_velocity" in result or len(result) == 2 - def test_uses_pinned_scb_check_0_1_3_report_for_composite_quality_numbers( + def test_uses_pinned_scb_check_report( self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ): (tmp_path / "snapshot").mkdir() @@ -817,7 +817,6 @@ def fake_run(command, **kwargs): result = get_checkpoint_metrics(tmp_path) - assert checkpoint_driver.SCB_CHECK_VERSION == "0.1.3" assert commands == [ [ "uvx", From d1a346be3603bd0b8830db6a239d3c7e865f6b87 Mon Sep 17 00:00:00 2001 From: gabeorlanski Date: Tue, 28 Jul 2026 13:12:40 -0500 Subject: [PATCH 4/5] Remove local clone analysis --- docs/metrics-reference.md | 4 +- docs/metrics/README.md | 10 +- docs/metrics/checkpoint-results.md | 8 +- docs/metrics/interpreting-results.md | 14 +- docs/metrics/output-files.md | 14 - docs/metrics/run-results.md | 6 +- .../entrypoints/commands/variance.py | 1 - .../metrics/checkpoint/extractors.py | 10 +- src/slop_code/metrics/driver.py | 116 +--- .../metrics/languages/python/__init__.py | 6 - .../metrics/languages/python/constants.py | 12 - .../metrics/languages/python/redundancy.py | 258 --------- src/slop_code/metrics/models.py | 41 -- src/slop_code/metrics/quality_io.py | 6 - tests/metrics/checkpoint_results_test.py | 53 +- tests/metrics/driver_test.py | 58 -- tests/metrics/language/py_redundancy_test.py | 526 ------------------ tests/metrics/models_test.py | 6 - tests/metrics/summary_test.py | 2 - 19 files changed, 23 insertions(+), 1128 deletions(-) delete mode 100644 src/slop_code/metrics/languages/python/redundancy.py delete mode 100644 tests/metrics/language/py_redundancy_test.py diff --git a/docs/metrics-reference.md b/docs/metrics-reference.md index 980e2c70..5ddb18e9 100644 --- a/docs/metrics-reference.md +++ b/docs/metrics-reference.md @@ -142,14 +142,12 @@ Per function/method, radon scale. | `lint_fixable` | Auto-fixable violations | | `lint_per_loc` | `lint_errors / loc` | -### Redundancy & Waste +### `scb-check` & Waste | Key | Description | |-----|-------------| -| `clone_lines` | Total duplicated lines | | `cloned_sloc_lines` | Duplicated SLOC lines reported by `scb-check` | | `cloned_pct` | `scb-check` clone LOC / total LOC | -| `clone_ratio` | Cloned SLOC / file SLOC (per-file metric; aggregated separately) | | `verbosity_flagged_sloc_lines` | Verbosity-flagged SLOC reported by `scb-check` | | `verbosity_flagged_pct` | `scb-check` flagged LOC / total LOC | | `single_use_functions` | Functions called only once | diff --git a/docs/metrics/README.md b/docs/metrics/README.md index db7e28db..aaaea86b 100644 --- a/docs/metrics/README.md +++ b/docs/metrics/README.md @@ -5,7 +5,9 @@ last_updated: 2025-12-17 # Metrics System Documentation -The metrics system automatically measures code quality for agent submissions, tracking everything from lines of code to cyclomatic complexity to code clones. +The metrics system automatically measures code quality for agent submissions, +tracking everything from lines of code and cyclomatic complexity to clone +coverage reported by `scb-check`. ## 30-Second Overview @@ -14,7 +16,7 @@ When an agent completes a checkpoint, the metrics system analyzes the submitted - **Lint metrics**: Ruff errors and violations - **Complexity metrics**: Cyclomatic complexity (A-F ratings), nesting depth - **Composite quality**: Verbosity and erosion from pinned `scb-check` -- **Code quality**: Waste detection (trivial wrappers, single-use functions), code clones +- **Code quality**: Waste detection (trivial wrappers, single-use functions) - **Dependencies**: Graph metrics for import relationships Results are saved to JSON/JSONL files in each checkpoint's `quality_analysis/` directory. @@ -51,7 +53,7 @@ Results are saved to JSON/JSONL files in each checkpoint's `quality_analysis/` d | **Maintainability Index (MI)** | Composite score of code maintainability (A >= 19) | | **Verbosity** | Code-bloat score produced by pinned `scb-check` | | **Waste** | Abstraction inefficiencies (trivial wrappers, single-use functions) | -| **Clones** | Duplicate code blocks detected via AST hashing | +| **Clones** | Clone coverage reported by pinned `scb-check` | | **Delta Metrics** | Percentage changes between checkpoints | | **Pass Rate** | Percentage of tests passing by category (CORE, FUNCTIONALITY, etc.) | | **Solve Rate** | Percentage of checkpoints/problems meeting success criteria | @@ -63,7 +65,7 @@ Results are saved to JSON/JSONL files in each checkpoint's `quality_analysis/` d - **Lint errors**: Lower is better - **Verbosity**: Lower is better - **Waste metrics**: Fewer trivial wrappers and single-use functions -- **Clone ratio**: Lower percentage means less duplication +- **Cloned percentage**: Lower `scb-check` percentage means less duplication ### Where do I find metrics for my run? diff --git a/docs/metrics/checkpoint-results.md b/docs/metrics/checkpoint-results.md index a9d6307a..3cdfebd1 100644 --- a/docs/metrics/checkpoint-results.md +++ b/docs/metrics/checkpoint-results.md @@ -157,7 +157,6 @@ This file contains aggregated metrics across the entire checkpoint. It's organiz | **classes** | count, method_counts_mean, attribute_counts_mean | Class structure | | **complexity** | cc_ratings, mi_ratings, cc_max, cc_mean | Complexity distribution | | **waste** | single_use_functions, trivial_wrappers, single_method_classes | Abstraction efficiency | -| **redundancy** | clone_instances, clone_lines, clone_ratio_sum, cloned_sloc_lines | Code duplication | | **graph** | node_count, edge_count, cyclic_dependency_mass | Dependency analysis | Example excerpt: @@ -205,7 +204,6 @@ Example excerpt: - See [Interpreting Results](interpreting-results.md) for detailed metric meanings - High `cc_max` (>20) indicates complex functions that need review - Positive `waste` metrics suggest over-abstraction -- `clone_ratio` > 10% indicates duplication opportunities ### files.jsonl @@ -223,8 +221,6 @@ Per-file metrics, one JSON object per line: "depth": 1, "is_entry_language": true, "symbol_count": 42, - "clone_instances": 8, - "clone_lines": 30, "single_use_count": 5, "trivial_wrapper_count": 0, "single_method_class_count": 1 @@ -234,7 +230,7 @@ Per-file metrics, one JSON object per line: **Use this to:** - Identify which files have the most complexity or violations - Compare file-level metrics across checkpoints -- Spot files with many clones or waste patterns +- Spot files with high complexity or waste patterns ### symbols.jsonl @@ -519,6 +515,6 @@ For detailed interpretation of specific metrics, see [Interpreting Results](inte - **Correctness**: Are all CORE tests passing? Are REGRESSION tests still passing? - **Complexity**: Is `cc_max` under control? Are most functions rated A/B? -- **Duplication**: Is `clone_ratio` under 10%? Any patterns in cloned code? +- **Duplication**: Is the `scb-check` `cloned_pct` trending upward? - **Violations**: Are critical violations (weight 3-4) being addressed? - **Trends**: Are delta metrics improving or degrading? Is code growing too fast? diff --git a/docs/metrics/interpreting-results.md b/docs/metrics/interpreting-results.md index 05a14678..6ebf09c1 100644 --- a/docs/metrics/interpreting-results.md +++ b/docs/metrics/interpreting-results.md @@ -125,19 +125,17 @@ Detects potential over-abstraction or unnecessary indirection. - Trivial wrappers add cognitive overhead without benefit - Single-method classes might be better as functions -## Redundancy Metrics +## Clone Metrics -Detects duplicate code using AST hashing. +Clone coverage comes exclusively from the pinned `scb-check` report. | Metric | Description | |--------|-------------| -| `clone_instances` | Total duplicate code blocks | -| `clone_lines` | Lines of code in duplicates | -| `clone_ratio` | Percentage of code that's duplicated | -| `files_with_clones` | Number of files containing duplicates | +| `cloned_sloc_lines` | Source lines covered by clones | +| `cloned_pct` | Clone LOC divided by total LOC | **Interpretation:** -- High `clone_ratio` (> 10%) suggests refactoring opportunities +- High `cloned_pct` suggests refactoring opportunities - Duplicates often indicate missing abstractions ## Graph Metrics @@ -197,7 +195,7 @@ the exact checker release used for the checkpoint scores. | `cc_max` | < 15 | > 30 | | `lint_errors` | 0 | > 10 | | `verbosity` | Lower relative to comparable runs | Higher relative to comparable runs | -| `clone_ratio` | < 5% | > 15% | +| `cloned_pct` | < 5% | > 15% | | `trivial_wrappers` | 0 | > 3 | | `cyclic_dependency_mass` | 0 | > 0.1 | diff --git a/docs/metrics/output-files.md b/docs/metrics/output-files.md index 5de3a173..e6b508e6 100644 --- a/docs/metrics/output-files.md +++ b/docs/metrics/output-files.md @@ -104,13 +104,6 @@ Aggregated metrics for the entire snapshot. This is the primary file for checkpo "trivial_wrappers": 1, "single_method_classes": 0 }, - "redundancy": { - "clone_instances": 4, - "clone_lines": 32, - "cloned_sloc_lines": 24, - "clone_ratio_sum": 0.03, - "files_with_clones": 2 - }, "graph": { "node_count": 8, "edge_count": 15, @@ -133,7 +126,6 @@ Aggregated metrics for the entire snapshot. This is the primary file for checkpo | `classes` | Class statistics | | `complexity` | CC and MI distributions | | `waste` | Abstraction waste counts | -| `redundancy` | Code clone aggregates, including filtered cloned SLOC | | `graph` | Dependency graph metrics (optional) | ## files.jsonl @@ -163,12 +155,6 @@ Per-file metrics in JSON Lines format (one JSON object per line). "is_entry_language": true, "import_count": 8, "global_count": 0, - "redundancy": { - "clones": [...], - "total_clone_instances": 2, - "clone_lines": 16, - "clone_ratio": 0.13 - }, "waste": { "single_use_functions": [...], "trivial_wrappers": [...], diff --git a/docs/metrics/run-results.md b/docs/metrics/run-results.md index 8f1fae25..e617d32b 100644 --- a/docs/metrics/run-results.md +++ b/docs/metrics/run-results.md @@ -84,8 +84,8 @@ One JSON object per line, one line per checkpoint across all problems. "cc_std": 6.372353102110006, "cc_high_count": 5, "cc_extreme_count": 0, - "clone_instances": 0, - "clone_lines": 0, + "cloned_sloc_lines": 0, + "cloned_pct": 0.0, "verbosity": 0.26, "erosion": 0.11, "scb_check_version": "0.1.3", @@ -136,7 +136,7 @@ One JSON object per line, one line per checkpoint across all problems. - `cc_std`: Standard deviation of complexity - `cc_high_count`: Functions with CC > 10 - `cc_extreme_count`: Functions with CC > 30 -- `clone_instances`: Duplicate code blocks +- `cloned_sloc_lines` / `cloned_pct`: Clone coverage from `scb-check` - `verbosity`: Code-bloat score from `scb-check` - `erosion`: Structural-degradation score from `scb-check` - `scb_check_version`: Pinned `scb-check` release that produced the scores diff --git a/src/slop_code/entrypoints/commands/variance.py b/src/slop_code/entrypoints/commands/variance.py index 0672fafa..d1ac9b94 100644 --- a/src/slop_code/entrypoints/commands/variance.py +++ b/src/slop_code/entrypoints/commands/variance.py @@ -424,7 +424,6 @@ def _build_metric_specs( "cc.", "symbols.", "waste.", - "redundancy.", "lines.", "imports.", "normalized.", diff --git a/src/slop_code/metrics/checkpoint/extractors.py b/src/slop_code/metrics/checkpoint/extractors.py index c4c7e1e0..6e68a5e6 100644 --- a/src/slop_code/metrics/checkpoint/extractors.py +++ b/src/slop_code/metrics/checkpoint/extractors.py @@ -101,7 +101,7 @@ def _build_metrics_from_snapshot( Args: snapshot: Dict from SnapshotMetrics.model_dump() with nested fields: - - lines, lint, symbols, functions, classes, waste, redundancy, etc. + - lines, lint, symbols, functions, classes, waste, etc. distributions: Pre-computed distribution metrics from file iteration. Returns: @@ -114,7 +114,6 @@ def _build_metrics_from_snapshot( symbols = snapshot["symbols"] functions = snapshot["functions"] waste = snapshot["waste"] - redundancy = snapshot["redundancy"] source_loc = lines["loc"] total_loc = lines["total_lines"] @@ -150,17 +149,11 @@ def _build_metrics_from_snapshot( "single_use_functions": waste["single_use_functions"], "trivial_wrappers": waste["trivial_wrappers"], "unused_variables": waste.get("unused_variables", 0), - # Redundancy - "clone_lines": redundancy["clone_lines"], - "cloned_sloc_lines": redundancy.get("cloned_sloc_lines", 0), } # Per-LOC normalized metrics if total_loc > 0: result["lint_per_loc"] = lint["errors"] / total_loc - result["cloned_pct"] = ( - redundancy.get("cloned_sloc_lines", 0) / total_loc - ) # Graph metrics (optional, may be None for non-Python) if graph := snapshot.get("graph"): @@ -294,7 +287,6 @@ def get_quality_metrics( - files.*: File change counts - symbols.*: Symbol type counts and complexity ratings - waste.*: Abstraction waste metrics - - redundancy.*: Code clone metrics """ if thresholds is None: thresholds = MetricsThresholds() diff --git a/src/slop_code/metrics/driver.py b/src/slop_code/metrics/driver.py index 1b9a240c..4127ecc5 100644 --- a/src/slop_code/metrics/driver.py +++ b/src/slop_code/metrics/driver.py @@ -7,20 +7,12 @@ from __future__ import annotations -import ast import fnmatch import json from collections import Counter from collections.abc import Callable from collections.abc import Generator from pathlib import Path -from token import COMMENT -from token import DEDENT -from token import ENDMARKER -from token import INDENT -from token import NEWLINE -from token import NL -from tokenize import generate_tokens from slop_code.common import RUBRIC_FILENAME from slop_code.logging import get_logger @@ -35,7 +27,6 @@ from slop_code.metrics.models import LineCountMetrics from slop_code.metrics.models import LintMetrics from slop_code.metrics.models import MetricsThresholds -from slop_code.metrics.models import RedundancyAggregates from slop_code.metrics.models import SnapshotMetrics from slop_code.metrics.models import SymbolAggregates from slop_code.metrics.models import WasteAggregates @@ -43,15 +34,6 @@ logger = get_logger(__name__) -IGNORED_SLOC_TOKEN_TYPES = { - COMMENT, - DEDENT, - ENDMARKER, - INDENT, - NEWLINE, - NL, -} - def _calculate_file_metrics( file_path: Path, depth: int, *, is_entry_language: bool = False @@ -80,7 +62,6 @@ def _calculate_file_metrics( imports = extract_imports(file_path) if file_path.suffix == ".py" else [] import_count = len(imports) global_count = sum(1 for symbol in symbols if symbol.type == "variable") - redundancy = language.redundancy(file_path) if language.redundancy else None waste = language.waste(file_path, symbols) if language.waste else None type_check = language.type_check(file_path) if language.type_check else None return FileMetrics( @@ -92,7 +73,6 @@ def _calculate_file_metrics( is_entry_language=is_entry_language, import_count=import_count, global_count=global_count, - redundancy=redundancy, waste=waste, type_check=type_check, ) @@ -150,7 +130,6 @@ def __init__( classes: ClassStats, complexity: ComplexityAggregates, waste: WasteAggregates, - redundancy: RedundancyAggregates, ): self.file_count = file_count self.symbols = symbols @@ -158,88 +137,12 @@ def __init__( self.classes = classes self.complexity = complexity self.waste = waste - self.redundancy = redundancy HIGH_CC_THRESHOLD = 10 EXTREME_CC_THRESHOLD = 30 -def _iter_docstring_ranges(tree: ast.AST) -> list[tuple[int, int]]: - """Return inclusive docstring line ranges for a parsed Python AST.""" - ranges: list[tuple[int, int]] = [] - for node in ast.walk(tree): - body = getattr(node, "body", None) - if not isinstance(body, list) or not body: - continue - first = body[0] - if not isinstance(first, ast.Expr): - continue - value = first.value - if not isinstance(value, ast.Constant) or not isinstance( - value.value, str - ): - continue - if value.lineno is None or value.end_lineno is None: - continue - ranges.append((value.lineno, value.end_lineno)) - return ranges - - -def _python_sloc_lines(source_path: Path) -> set[int]: - """Return 1-indexed SLOC lines for a Python source file.""" - source = source_path.read_text() - source_lines = source.splitlines(keepends=True) - sloc_lines: set[int] = set() - for token in generate_tokens(iter(source_lines).__next__): - if token.type not in IGNORED_SLOC_TOKEN_TYPES: - sloc_lines.add(token.start[0]) - - tree = ast.parse(source) - for start, end in _iter_docstring_ranges(tree): - for line_no in range(start, end + 1): - sloc_lines.discard(line_no) - return sloc_lines - - -def _fallback_sloc_lines(source_path: Path) -> set[int]: - """Return an approximate 1-indexed SLOC line set for non-Python files.""" - sloc_lines: set[int] = set() - for line_no, line in enumerate( - source_path.read_text().splitlines(), start=1 - ): - stripped = line.strip() - if stripped and not stripped.startswith("#"): - sloc_lines.add(line_no) - return sloc_lines - - -def _get_sloc_lines(source_path: Path) -> set[int]: - """Return 1-indexed SLOC lines for a source file.""" - try: - if source_path.suffix == ".py": - return _python_sloc_lines(source_path) - return _fallback_sloc_lines(source_path) - except (OSError, SyntaxError, UnicodeDecodeError): - return _fallback_sloc_lines(source_path) - - -def _collect_clone_sloc_lines( - fm: FileMetrics, sloc_lines: set[int] -) -> set[int]: - """Return the clone-covered SLOC lines for a file.""" - if fm.redundancy is None: - return set() - - cloned_lines: set[int] = set() - for clone in fm.redundancy.clones: - for start, end in clone.locations: - for line_no in range(start, end + 1): - if line_no in sloc_lines: - cloned_lines.add(line_no) - return cloned_lines - - def _normalized_complexity(cc_values: list[int], max_cc: int = 50) -> float: """Compute normalized complexity score (0 = ideal, 1 = worst case).""" if not cc_values: @@ -380,15 +283,12 @@ def _compute_class_stats( def compute_aggregates( file_metrics: dict[str, FileMetrics], - source_files: set[str] | None, - snapshot_dir: Path, thresholds: MetricsThresholds | None = None, ) -> _AggregateResult: """Compute aggregate metrics from file metrics in a single pass. Args: file_metrics: Dictionary mapping file paths to their metrics. - source_files: Set of source file paths traced from entrypoint, or None. thresholds: Configurable thresholds for metrics (uses defaults if None). Returns: @@ -426,11 +326,6 @@ def compute_aggregates( single_use_functions=0, trivial_wrappers=0, ) - redundancy = RedundancyAggregates( - clone_lines=0, - clone_ratio_sum=0.0, - files_with_clones=0, - ) # Collect raw values for function/class stats computation func_cc: list[int] = [] func_depth: list[int] = [] @@ -438,15 +333,10 @@ def compute_aggregates( class_method_counts: list[int] = [] class_attribute_counts: list[int] = [] # Single pass through all files - for path_str, fm in file_metrics.items(): + for fm in file_metrics.values(): symbols.update(fm) complexity.update(fm, thresholds) waste.update(fm) - redundancy.update(fm) - source_path = snapshot_dir / path_str - sloc_lines = _get_sloc_lines(source_path) - clone_sloc_lines = _collect_clone_sloc_lines(fm, sloc_lines) - redundancy.cloned_sloc_lines += len(clone_sloc_lines) # Collect function/method metrics for sym in fm.symbols: @@ -479,7 +369,6 @@ def compute_aggregates( classes=classes, complexity=complexity, waste=waste, - redundancy=redundancy, ) @@ -614,7 +503,7 @@ def measure_snapshot_quality( graph_metrics = compute_graph_metrics(dependency_graph) # Compute aggregates using the dedicated function - agg = compute_aggregates(files_metrics, traced_source_files, snapshot_dir) + agg = compute_aggregates(files_metrics) # Build file list for JSONL output file_metrics_list = list(files_metrics.values()) @@ -628,7 +517,6 @@ def measure_snapshot_quality( classes=agg.classes, complexity=agg.complexity, waste=agg.waste, - redundancy=agg.redundancy, graph=graph_metrics, source_files=traced_source_files, ) diff --git a/src/slop_code/metrics/languages/python/__init__.py b/src/slop_code/metrics/languages/python/__init__.py index 9a4c1265..df6a3ed1 100644 --- a/src/slop_code/metrics/languages/python/__init__.py +++ b/src/slop_code/metrics/languages/python/__init__.py @@ -17,9 +17,6 @@ from slop_code.metrics.languages.python.lint_metrics import ( calculate_lint_metrics, ) -from slop_code.metrics.languages.python.redundancy import ( - calculate_redundancy_metrics, -) from slop_code.metrics.languages.python.symbols import get_symbols from slop_code.metrics.languages.python.type_check import ( calculate_type_check_metrics, @@ -37,7 +34,6 @@ lint=calculate_lint_metrics, symbol=get_symbols, mi=calculate_mi, - redundancy=calculate_redundancy_metrics, waste=calculate_waste_metrics, type_check=calculate_type_check_metrics, ), @@ -53,8 +49,6 @@ "calculate_lint_metrics", # Symbol extraction "get_symbols", - # Redundancy detection - "calculate_redundancy_metrics", # Waste detection "calculate_waste_metrics", # Type check metrics diff --git a/src/slop_code/metrics/languages/python/constants.py b/src/slop_code/metrics/languages/python/constants.py index 4917ae3c..c7c9351e 100644 --- a/src/slop_code/metrics/languages/python/constants.py +++ b/src/slop_code/metrics/languages/python/constants.py @@ -174,15 +174,3 @@ "generator_expression", } ) - -CLONE_NODE_TYPES = frozenset( - { - "function_definition", - "if_statement", - "for_statement", - "while_statement", - "with_statement", - "try_statement", - "match_statement", - } -) diff --git a/src/slop_code/metrics/languages/python/redundancy.py b/src/slop_code/metrics/languages/python/redundancy.py deleted file mode 100644 index d0958cfa..00000000 --- a/src/slop_code/metrics/languages/python/redundancy.py +++ /dev/null @@ -1,258 +0,0 @@ -"""Code clone detection via AST hashing. - -Clones are measured in lines. When clone groups overlap (e.g. a cloned -function contains a cloned if-block), lines are deduplicated via a -line-number union so no line is counted twice. -""" - -from __future__ import annotations - -import hashlib -from pathlib import Path -from token import COMMENT -from token import DEDENT -from token import ENDMARKER -from token import INDENT -from token import NEWLINE -from token import NL -from tokenize import TokenError -from tokenize import generate_tokens -from typing import TYPE_CHECKING - -from slop_code.metrics.languages.python.constants import CLONE_NODE_TYPES -from slop_code.metrics.languages.python.line_metrics import ( - calculate_line_metrics, -) -from slop_code.metrics.languages.python.parser import get_python_parser -from slop_code.metrics.languages.python.utils import read_python_code -from slop_code.metrics.models import CodeClone -from slop_code.metrics.models import RedundancyMetrics - -if TYPE_CHECKING: - from tree_sitter import Node - - -IGNORED_SLOC_TOKEN_TYPES = { - COMMENT, - DEDENT, - ENDMARKER, - INDENT, - NEWLINE, - NL, -} - -_LITERAL_TOKENS = { - "string": "$STR", - "string_content": "$STR", - "f_string": "$STR", - "string_fragment": "$STR", - "bytes": "$STR", - "integer": "$INT", - "float": "$FLOAT", - "imaginary": "$FLOAT", - "true": "$BOOL", - "false": "$BOOL", - "none": "$NONE", -} - - -def _normalize_ast(node: Node) -> str: - """Create normalized string representation of AST subtree.""" - variable_map: dict[str, str] = {} - variable_counter = 0 - - def normalize(current: Node) -> str: - nonlocal variable_counter - if current.type == "identifier": - if current.text is None: - return "$VAR0" - name = current.text.decode("utf-8") - if name not in variable_map: - variable_counter += 1 - variable_map[name] = f"$VAR{variable_counter}" - return variable_map[name] - - if current.type in _LITERAL_TOKENS: - return _LITERAL_TOKENS[current.type] - - children = tuple( - child - for child in current.children - if child.type != "comment" and not _is_plain_string_statement(child) - ) - if not children: - return current.type - - child_parts = [normalize(child) for child in children] - return f"{current.type}({','.join(child_parts)})" - - return normalize(node) - - -def _hash_ast_subtree(node: Node) -> str: - """Generate hash of normalized AST subtree.""" - normalized = _normalize_ast(node) - return hashlib.md5( - normalized.encode("utf-8"), - usedforsecurity=False, - ).hexdigest()[:12] - - -def _is_type_checking_block(node: Node) -> bool: - condition = node.child_by_field_name("condition") - text = condition.text if condition is not None else None - return text in {b"TYPE_CHECKING", b"typing.TYPE_CHECKING"} - - -def _is_plain_string_statement(node: Node) -> bool: - if node.type == "string": - expression = node - elif node.type == "expression_statement" and len(node.named_children) == 1: - expression = node.named_children[0] - else: - return False - - if expression.type != "string": - return False - - text = expression.text - if text is None: - return False - - prefix = _string_prefix(text.decode("utf-8")) - return "b" not in prefix and "f" not in prefix - - -def _string_prefix(literal: str) -> str: - prefix_chars: list[str] = [] - for character in literal: - if character in {'"', "'"}: - break - prefix_chars.append(character.lower()) - return "".join(prefix_chars) - - -def _sloc_line_numbers(source: str, root: Node) -> frozenset[int]: - source_lines = source.splitlines(keepends=True) - text_lines = source.splitlines() - lines: set[int] = set() - try: - for token in generate_tokens(iter(source_lines).__next__): - if token.type not in IGNORED_SLOC_TOKEN_TYPES: - lines.add(token.start[0]) - except TokenError: - return frozenset(lines) - - for start, end in _plain_string_statement_ranges(root, text_lines): - for line_number in range(start, end + 1): - lines.discard(line_number) - return frozenset(lines) - - -def _plain_string_statement_ranges( - root: Node, - source_lines: list[str], -) -> tuple[tuple[int, int], ...]: - ranges: list[tuple[int, int]] = [] - stack = [root] - while stack: - node = stack.pop() - if _is_plain_string_statement(node): - literal = node if node.type == "string" else node.named_children[0] - if _owns_line(literal, source_lines): - ranges.append( - (literal.start_point[0] + 1, literal.end_point[0] + 1), - ) - stack.extend(node.named_children) - return tuple(ranges) - - -def _owns_line(node: Node, source_lines: list[str]) -> bool: - start_row, start_col = node.start_point - end_row, end_col = node.end_point - start_line = ( - source_lines[start_row] if start_row < len(source_lines) else "" - ) - end_line = source_lines[end_row] if end_row < len(source_lines) else "" - return not start_line[:start_col].strip() and not end_line[end_col:].strip() - - -def detect_code_clones(source: Path, min_lines: int = 3) -> RedundancyMetrics: - """Detect duplicate code blocks via AST hashing.""" - code = read_python_code(source) - if not code.strip(): - return RedundancyMetrics( - clones=[], total_clone_instances=0, clone_lines=0, clone_ratio=0.0 - ) - - parser = get_python_parser() - tree = parser.parse(code.encode("utf-8")) - source_lines = code.splitlines() - sloc_lines = _sloc_line_numbers(code, tree.root_node) - - groups: dict[str, list[tuple[Node, str, int]]] = {} - stack = [tree.root_node] - - while stack: - current = stack.pop() - if current.type in CLONE_NODE_TYPES and not _is_type_checking_block( - current - ): - start_line = current.start_point[0] + 1 - end_line = current.end_point[0] + 1 - sloc_count = sum( - 1 for line in sloc_lines if start_line <= line <= end_line - ) - if sloc_count >= min_lines: - ast_hash = _hash_ast_subtree(current) - line_count = end_line - start_line + 1 - groups.setdefault(ast_hash, []).append( - (current, current.type, line_count) - ) - stack.extend(current.children) - - clones: list[CodeClone] = [] - total_instances = 0 - clone_line_set: set[int] = set() - for ast_hash, nodes in groups.items(): - if len(nodes) < 2: - continue - locations = [ - (n.start_point[0] + 1, n.end_point[0] + 1) for n, _, _ in nodes - ] - node_type = nodes[0][1] - line_count = nodes[0][2] - clones.append( - CodeClone( - ast_hash=ast_hash, - locations=locations, - node_type=node_type, - line_count=line_count, - ) - ) - total_instances += len(nodes) - for n, _, _ in nodes: - clone_line_set.update(range(n.start_point[0], n.end_point[0] + 1)) - - total_lines = calculate_line_metrics(source).loc - num_clone_lines = len(clone_line_set) - clone_sloc_lines = sum( - 1 - for line_number in clone_line_set - if line_number < len(source_lines) - and (stripped := source_lines[line_number].strip()) - and not stripped.startswith("#") - ) - clone_ratio = (clone_sloc_lines / total_lines) if total_lines else 0.0 - - return RedundancyMetrics( - clones=clones, - total_clone_instances=total_instances, - clone_lines=num_clone_lines, - clone_ratio=clone_ratio, - ) - - -def calculate_redundancy_metrics(source: Path) -> RedundancyMetrics: - """Calculate redundancy metrics for a Python file.""" - return detect_code_clones(source) diff --git a/src/slop_code/metrics/models.py b/src/slop_code/metrics/models.py index c3fe3153..b67321b4 100644 --- a/src/slop_code/metrics/models.py +++ b/src/slop_code/metrics/models.py @@ -230,32 +230,10 @@ class FileMetrics(BaseModel): is_entry_language: bool = False import_count: int = 0 global_count: int = 0 - redundancy: RedundancyMetrics | None = None waste: WasteMetrics | None = None type_check: TypeCheckMetrics | None = None -class CodeClone(BaseModel): - """A group of duplicate code blocks with identical AST structure.""" - - ast_hash: str - locations: list[tuple[int, int]] - node_type: str - line_count: int - - -class RedundancyMetrics(BaseModel): - """Per-file redundancy analysis results. - - Clones are measured in lines (deduplicated by line number). - """ - - clones: list[CodeClone] - total_clone_instances: int - clone_lines: int - clone_ratio: float - - class SingleUseFunction(BaseModel): """A function that is only called once.""" @@ -442,22 +420,6 @@ def update(self, fm: FileMetrics) -> None: self.unused_variables += fm.waste.unused_variable_count -class RedundancyAggregates(BaseModel): - """Aggregate redundancy/clone detection metrics.""" - - clone_lines: int - clone_ratio_sum: float - files_with_clones: int - cloned_sloc_lines: int = 0 - - def update(self, fm: FileMetrics) -> None: - """Update aggregates from a FileMetrics instance.""" - if fm.redundancy: - self.clone_lines += fm.redundancy.clone_lines - self.clone_ratio_sum += fm.redundancy.clone_ratio - self.files_with_clones += 1 - - class TypeCheckAggregates(BaseModel): """Aggregate type checking metrics.""" @@ -511,7 +473,6 @@ class SnapshotMetrics(BaseModel): classes: Pre-computed statistics for classes. complexity: CC and MI rating distributions and stats. waste: Waste detection totals. - redundancy: Clone detection totals. graph: Dependency graph metrics (None if not computed). source_files: Relative paths of files traced from the entrypoint. """ @@ -524,7 +485,6 @@ class SnapshotMetrics(BaseModel): classes: ClassStats complexity: ComplexityAggregates waste: WasteAggregates - redundancy: RedundancyAggregates graph: GraphMetrics | None = None source_files: set[str] | None = None @@ -572,7 +532,6 @@ class LanguageSpec(BaseModel): lint: Callable[[Path], LintMetrics] symbol: Callable[[Path], list[SymbolMetrics]] mi: Callable[[Path], float] - redundancy: Callable[[Path], RedundancyMetrics] | None = None waste: Callable[[Path, list[SymbolMetrics]], WasteMetrics] | None = None type_check: Callable[[Path], TypeCheckMetrics] | None = None diff --git a/src/slop_code/metrics/quality_io.py b/src/slop_code/metrics/quality_io.py index 90c91940..e36c2a8a 100644 --- a/src/slop_code/metrics/quality_io.py +++ b/src/slop_code/metrics/quality_io.py @@ -73,12 +73,6 @@ def save_quality_metrics( "import_count": fm.import_count, "global_count": fm.global_count, "symbol_count": len(fm.symbols), - "clone_lines": fm.redundancy.clone_lines - if fm.redundancy - else 0, - "clone_ratio": ( - fm.redundancy.clone_ratio if fm.redundancy else 0.0 - ), "single_use_count": ( fm.waste.single_use_count if fm.waste else 0 ), diff --git a/tests/metrics/checkpoint_results_test.py b/tests/metrics/checkpoint_results_test.py index 4227ae53..1fdb42d7 100644 --- a/tests/metrics/checkpoint_results_test.py +++ b/tests/metrics/checkpoint_results_test.py @@ -239,7 +239,7 @@ def _create_quality_test_files(tmp_path: Path, files_data: dict) -> Path: elif sym_type == "type_alias": type_alias_count += 1 - # Waste/redundancy aggregates + # Waste aggregates total_single_use_functions = sum( f.get("waste", {}).get("single_use_count", 0) for f in files_data.values() @@ -248,18 +248,6 @@ def _create_quality_test_files(tmp_path: Path, files_data: dict) -> Path: f.get("waste", {}).get("trivial_wrapper_count", 0) for f in files_data.values() ) - total_clone_lines = sum( - f.get("redundancy", {}).get("clone_lines", 0) - for f in files_data.values() - ) - clone_ratio_sum = sum( - f.get("redundancy", {}).get("clone_ratio", 0) - for f in files_data.values() - if f.get("redundancy") - ) - files_with_clones = sum( - 1 for f in files_data.values() if f.get("redundancy") - ) total_imports = sum(f.get("import_count", 0) for f in files_data.values()) max_imports = max( (f.get("import_count", 0) for f in files_data.values()), default=0 @@ -359,11 +347,6 @@ def _create_quality_test_files(tmp_path: Path, files_data: dict) -> Path: "trivial_wrappers": total_trivial_wrappers, "unused_variables": 0, }, - "redundancy": { - "clone_lines": total_clone_lines, - "clone_ratio_sum": clone_ratio_sum, - "files_with_clones": files_with_clones, - }, "source_files": None, } @@ -396,11 +379,6 @@ def _create_quality_test_files(tmp_path: Path, files_data: dict) -> Path: "import_count": fm.get("import_count", 0), "global_count": fm.get("global_count", 0), "symbol_count": len(fm["symbols"]), - "clone_instances": fm.get("redundancy", {}).get( - "total_clone_instances", 0 - ), - "clone_lines": fm.get("redundancy", {}).get("clone_lines", 0), - "clone_ratio": fm.get("redundancy", {}).get("clone_ratio", 0.0), "single_use_count": fm.get("waste", {}).get( "single_use_count", 0 ), @@ -514,11 +492,6 @@ def sample_quality_file(self, tmp_path: Path) -> Path: "trivial_wrapper_count": 0, "single_method_class_count": 0, }, - "redundancy": { - "total_clone_instances": 2, - "clone_lines": 10, - "clone_ratio": 0.1, - }, "import_count": 5, "global_count": 1, }, @@ -613,12 +586,6 @@ def test_waste_namespace(self, sample_quality_file: Path): assert result["trivial_wrappers"] == 0 assert "single_method_classes" not in result - def test_redundancy_namespace(self, sample_quality_file: Path): - result = get_quality_metrics(sample_quality_file) - - assert result["clone_lines"] == 10 - assert "clone_instances" not in result - def test_globals_namespace(self, sample_quality_file: Path): result = get_quality_metrics(sample_quality_file) @@ -633,21 +600,6 @@ def test_derived_ratios(self, sample_quality_file: Path): assert "attributes_per_class" not in result assert result["lint_per_loc"] == pytest.approx(3 / 150) - def test_exposes_cloned_pct_for_later_analysis( - self, sample_quality_file: Path - ): - overall_path = ( - sample_quality_file / QUALITY_DIR / QUALITY_METRIC_SAVENAME - ) - overall = json.loads(overall_path.read_text()) - overall["redundancy"]["cloned_sloc_lines"] = 9 - overall_path.write_text(json.dumps(overall)) - - result = get_quality_metrics(sample_quality_file) - - assert result["cloned_sloc_lines"] == 9 - assert result["cloned_pct"] == pytest.approx(9 / 150) - def test_missing_file(self, tmp_path: Path): result = get_quality_metrics(tmp_path) assert result == {} @@ -657,7 +609,6 @@ def test_removed_fields_are_absent(self, sample_quality_file: Path): removed_fields = { "branches_mean", - "clone_instances", "comparisons_mean", "control_mean", "single_method_classes", @@ -874,6 +825,7 @@ def fake_run(command, **kwargs): assert result["total_tests"] == 1 assert "verbosity" not in result assert "erosion" not in result + assert "cloned_sloc_lines" not in result assert "cloned_pct" not in result assert "scb_check_version" not in result @@ -988,7 +940,6 @@ def test_all_delta_keys_present(self): removed_delta_keys = { "delta.cc_high_count", - "delta.clone_instances", "delta.comparisons", "delta.functions", "delta.lint_errors", diff --git a/tests/metrics/driver_test.py b/tests/metrics/driver_test.py index 9f287005..2ddb2cb7 100644 --- a/tests/metrics/driver_test.py +++ b/tests/metrics/driver_test.py @@ -11,12 +11,10 @@ from slop_code.metrics.driver import measure_snapshot_quality from slop_code.metrics.languages import EXT_TO_LANGUAGE from slop_code.metrics.languages import LANGUAGE_REGISTRY -from slop_code.metrics.models import CodeClone from slop_code.metrics.models import FileMetrics from slop_code.metrics.models import LanguageSpec from slop_code.metrics.models import LineCountMetrics from slop_code.metrics.models import LintMetrics -from slop_code.metrics.models import RedundancyMetrics from slop_code.metrics.models import SymbolMetrics @@ -433,62 +431,6 @@ def test_measure_snapshot_quality_aggregates_metrics(self, tmp_path): assert snapshot.lint.counts["E501"] == 2 # 1 + 1 assert snapshot.lint.counts["E302"] == 2 # 1 + 1 - def test_measure_snapshot_quality_tracks_clone_sloc_coverage( - self, tmp_path, monkeypatch - ): - source = tmp_path / "file1.py" - source.write_text( - '"""doc"""\n' - "def alpha():\n" - " value = 1\n" - " return value\n" - "\n" - "def beta():\n" - " value = 1\n" - " return value\n" - ) - - def fake_calculate( - file_path: Path, depth: int, *, is_entry_language: bool = False - ) -> FileMetrics: - assert file_path == source - return FileMetrics( - symbols=[], - lines=LineCountMetrics( - total_lines=8, - loc=6, - comments=1, - multi_comment=1, - single_comment=0, - ), - lint=LintMetrics(errors=0, fixable=0, counts={}), - mi=25.0, - depth=depth, - is_entry_language=is_entry_language, - redundancy=RedundancyMetrics( - clones=[ - CodeClone( - ast_hash="clone", - locations=[(2, 4), (6, 8)], - node_type="function_definition", - line_count=3, - ) - ], - total_clone_instances=2, - clone_lines=6, - clone_ratio=1.0, - ), - ) - - monkeypatch.setattr( - "slop_code.metrics.driver._calculate_file_metrics", - fake_calculate, - ) - - snapshot, _ = measure_snapshot_quality("file1.py", tmp_path) - - assert snapshot.redundancy.cloned_sloc_lines == 6 - def test_measure_snapshot_quality_excludes_patterns(self, tmp_path): """Test that default exclude patterns are applied. diff --git a/tests/metrics/language/py_redundancy_test.py b/tests/metrics/language/py_redundancy_test.py deleted file mode 100644 index e7099c5c..00000000 --- a/tests/metrics/language/py_redundancy_test.py +++ /dev/null @@ -1,526 +0,0 @@ -"""Exhaustive tests for Python code clone detection. - -Tests all functions in slop_code.metrics.languages.python.redundancy including: -- Public API: calculate_redundancy_metrics, detect_code_clones -- Helper functions: _normalize_ast, _hash_ast_subtree -""" - -from __future__ import annotations - -from contextlib import suppress -from textwrap import dedent - -import pytest - -from slop_code.metrics.languages.python import calculate_redundancy_metrics - -# ============================================================================= -# Basic Clone Detection Tests -# ============================================================================= - - -class TestCalculateRedundancyMetrics: - """Tests for calculate_redundancy_metrics function.""" - - def test_no_duplicates(self, tmp_path): - """Test file with no duplicate code.""" - source = tmp_path / "unique.py" - source.write_text( - dedent(""" - def func_a(): - return 1 - - def func_b(): - return 2 * 3 - - def func_c(): - x = 1 - y = 2 - return x + y - """) - ) - - metrics = calculate_redundancy_metrics(source) - - # No clones expected - assert metrics.total_clone_instances == 0 - assert metrics.clone_ratio == 0.0 - - def test_identical_code_blocks(self, tmp_path): - """Test detection of identical code blocks.""" - source = tmp_path / "clones.py" - source.write_text( - dedent(""" - def first(): - if True: - return 1 - - def second(): - if True: - return 1 - """) - ) - - metrics = calculate_redundancy_metrics(source) - - assert metrics.clones - assert metrics.total_clone_instances >= 2 - assert metrics.clone_lines > 0 - assert metrics.clone_ratio > 0.0 - - def test_renamed_variables_detected_as_clones(self, tmp_path): - """Test that code with renamed variables is detected as clones.""" - source = tmp_path / "renamed.py" - source.write_text( - dedent(""" - def process_a(): - data = [] - for item in items: - data.append(item) - return data - - def process_b(): - result = [] - for element in items: - result.append(element) - return result - """) - ) - - metrics = calculate_redundancy_metrics(source) - - # Should detect these as clones due to same structure - assert metrics.clones - assert metrics.total_clone_instances >= 2 - - def test_operator_changes_are_not_clones(self, tmp_path): - """Different arithmetic operators do not form structural clones.""" - source = tmp_path / "operators.py" - source.write_text( - dedent(""" - def add(left, right): - result = left + right - return result - - def subtract(left, right): - result = left - right - return result - """) - ) - - metrics = calculate_redundancy_metrics(source) - - assert metrics.clones == [] - assert metrics.total_clone_instances == 0 - - def test_normalizes_literals_for_clone_detection(self, tmp_path): - """Renamed variables and changed literals form structural clones.""" - source = tmp_path / "literals.py" - source.write_text( - dedent(""" - def first(left, right): - result = left + 1 - return result - - def second(alpha, beta): - total = alpha + 2 - return total - """) - ) - - metrics = calculate_redundancy_metrics(source) - - assert metrics.total_clone_instances == 2 - - def test_minimum_line_threshold(self, tmp_path): - """Test that clones below minimum line threshold are not detected.""" - source = tmp_path / "small.py" - source.write_text( - dedent(""" - def a(): pass - def b(): pass - def c(): pass - """) - ) - - calculate_redundancy_metrics(source) - - # Single-line functions shouldn't be detected as clones - # (depends on min_lines setting) - - def test_single_occurrence_not_clone(self, tmp_path): - """Test that single occurrence is not marked as clone.""" - source = tmp_path / "single.py" - source.write_text( - dedent(""" - def unique_function(): - x = 1 - y = 2 - z = x + y - return z - """) - ) - - metrics = calculate_redundancy_metrics(source) - - # Single occurrence = no clones - assert metrics.total_clone_instances == 0 - - def test_empty_file(self, tmp_path): - """Test with empty file.""" - source = tmp_path / "empty.py" - source.write_text("") - - metrics = calculate_redundancy_metrics(source) - - assert metrics.total_clone_instances == 0 - assert metrics.clone_ratio == 0.0 - assert metrics.clones == [] - - -# ============================================================================= -# Clone Location Tests -# ============================================================================= - - -class TestCloneLocation: - """Tests for clone location tracking.""" - - def test_clone_locations_tracked(self, tmp_path): - """Test that clone locations are properly tracked.""" - source = tmp_path / "clones.py" - source.write_text( - dedent(""" - def first(): - if True: - x = 1 - return x - - def second(): - if True: - y = 1 - return y - """) - ) - - metrics = calculate_redundancy_metrics(source) - - if metrics.clones: - for clone in metrics.clones: - # Each clone should have valid location info - assert hasattr(clone, "node_type") or hasattr( - clone, "locations" - ) - assert clone.line_count > 0 - - -# ============================================================================= -# Clone Metrics Tests -# ============================================================================= - - -class TestCloneMetrics: - """Tests for clone-related metrics.""" - - def test_clone_ratio_calculation(self, tmp_path): - """Test clone ratio is calculated correctly.""" - source = tmp_path / "test.py" - source.write_text( - dedent(""" - def first(): - if True: - return 1 - - def second(): - if True: - return 1 - - def unique(): - return 42 - """) - ) - - metrics = calculate_redundancy_metrics(source) - - # clone_ratio should be clone_lines / file SLOC - if metrics.clone_lines > 0: - assert 0.0 < metrics.clone_ratio <= 1.0 - - def test_clone_ratio_uses_sloc_denominator(self, tmp_path): - source = tmp_path / "test.py" - source.write_text( - dedent(""" - def first(): - # comment - if True: - return 1 - - def second(): - # comment - if True: - return 1 - """) - ) - - metrics = calculate_redundancy_metrics(source) - - assert metrics.clone_lines == 8 - assert metrics.clone_ratio == pytest.approx(1.0) - - def test_total_clone_instances(self, tmp_path): - """Test total clone instances count.""" - source = tmp_path / "multi.py" - source.write_text( - dedent(""" - def a(): - x = 1 - y = 2 - return x + y - - def b(): - a = 1 - b = 2 - return a + b - - def c(): - m = 1 - n = 2 - return m + n - """) - ) - - metrics = calculate_redundancy_metrics(source) - - # If all three are clones of each other - if metrics.clones: - assert metrics.total_clone_instances >= 2 - - -# ============================================================================= -# Complex Code Patterns -# ============================================================================= - - -class TestComplexCodePatterns: - """Tests for clone detection in complex code patterns.""" - - def test_nested_loops_clone(self, tmp_path): - """Test clone detection with nested loops.""" - source = tmp_path / "nested.py" - source.write_text( - dedent(""" - def matrix_sum_a(matrix): - total = 0 - for row in matrix: - for cell in row: - total += cell - return total - - def matrix_sum_b(data): - result = 0 - for r in data: - for c in r: - result += c - return result - """) - ) - - metrics = calculate_redundancy_metrics(source) - - # Should detect as clones (same structure, renamed variables) - assert metrics.clones - - def test_try_except_clone(self, tmp_path): - """Test clone detection with try/except blocks.""" - source = tmp_path / "exceptions.py" - source.write_text( - dedent(""" - def safe_divide_a(a, b): - try: - return a / b - except ZeroDivisionError: - return 0 - - def safe_divide_b(x, y): - try: - return x / y - except ZeroDivisionError: - return 0 - """) - ) - - metrics = calculate_redundancy_metrics(source) - - # Should detect as clones - assert metrics.clones - - def test_comprehension_clone(self, tmp_path): - """Test clone detection with list comprehensions. - - Note: Single-line comprehensions may not meet minimum line threshold - for clone detection (typically 3 lines). - """ - source = tmp_path / "comprehension.py" - source.write_text( - dedent(""" - def filter_positive_a(items): - return [x for x in items if x > 0] - - def filter_positive_b(values): - return [v for v in values if v > 0] - """) - ) - - metrics = calculate_redundancy_metrics(source) - - # Single-line functions may not meet minimum line threshold - # This is expected behavior - assert isinstance(metrics.clones, list) - - def test_class_method_clones(self, tmp_path): - """Test clone detection in class methods.""" - source = tmp_path / "classes.py" - source.write_text( - dedent(""" - class ClassA: - def process(self, data): - result = [] - for item in data: - if item > 0: - result.append(item) - return result - - class ClassB: - def filter(self, items): - output = [] - for elem in items: - if elem > 0: - output.append(elem) - return output - """) - ) - - metrics = calculate_redundancy_metrics(source) - - # Methods with same structure should be detected - assert metrics.clones - - -# ============================================================================= -# Edge Cases -# ============================================================================= - - -class TestRedundancyEdgeCases: - """Edge case tests for redundancy detection.""" - - def test_single_function(self, tmp_path): - """Test file with single function.""" - source = tmp_path / "single.py" - source.write_text( - dedent(""" - def only_function(): - x = 1 - y = 2 - return x + y - """) - ) - - metrics = calculate_redundancy_metrics(source) - - # No clones possible with single function - assert metrics.total_clone_instances == 0 - - def test_syntax_error_handling(self, tmp_path): - """Test graceful handling of syntax errors.""" - source = tmp_path / "broken.py" - source.write_text("def broken(:\n pass\n") - - # Should handle gracefully. If it returns, metrics should be empty/zero; - # raising is also acceptable for malformed source. - with suppress(Exception): - calculate_redundancy_metrics(source) - - def test_docstrings_do_not_make_short_functions_clones(self, tmp_path): - """Docstring lines do not count toward clone candidate size.""" - source = tmp_path / "docs.py" - source.write_text( - dedent(''' - import math - - class Metrics: - def cc_mass(self) -> float: - """Return the cyclomatic complexity mass.""" - return self.cyc_complexity * math.sqrt(self.sloc) - - def cog_mass(self) -> float: - """Return the cognitive complexity mass.""" - return self.cog_complexity * math.sqrt(self.sloc) - ''') - ) - - metrics = calculate_redundancy_metrics(source) - - assert metrics.clones == [] - assert metrics.total_clone_instances == 0 - - def test_docstrings_do_not_affect_clone_hash(self, tmp_path): - """Docstring presence does not change clone structure.""" - source = tmp_path / "hash_docs.py" - source.write_text( - dedent(''' - def first(value): - """Explain the first function.""" - current = value + 1 - doubled = current * 2 - return doubled - - def second(value): - current = value + 2 - doubled = current * 2 - return doubled - ''') - ) - - metrics = calculate_redundancy_metrics(source) - - assert metrics.total_clone_instances == 2 - - def test_type_checking_import_blocks_are_not_clones(self, tmp_path): - """TYPE_CHECKING import blocks are excluded from clone detection.""" - source = tmp_path / "type_checking.py" - source.write_text( - dedent(""" - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from one import Thing - from two import Other - - if TYPE_CHECKING: - from one import Thing - from two import Other - """) - ) - - metrics = calculate_redundancy_metrics(source) - - assert metrics.clones == [] - assert metrics.total_clone_instances == 0 - - def test_imports_not_clones(self, tmp_path): - """Test that similar imports don't create false clones.""" - source = tmp_path / "imports.py" - source.write_text( - dedent(""" - import os - - def func(): - import os - return os.getcwd() - """) - ) - - calculate_redundancy_metrics(source) - - # Imports shouldn't be marked as clones diff --git a/tests/metrics/models_test.py b/tests/metrics/models_test.py index 0944765f..e29353ea 100644 --- a/tests/metrics/models_test.py +++ b/tests/metrics/models_test.py @@ -19,7 +19,6 @@ from slop_code.metrics.models import LanguageSpec from slop_code.metrics.models import LineCountMetrics from slop_code.metrics.models import LintMetrics -from slop_code.metrics.models import RedundancyAggregates from slop_code.metrics.models import SnapshotMetrics from slop_code.metrics.models import SnapshotQualityReport from slop_code.metrics.models import SymbolAggregates @@ -153,11 +152,6 @@ def _create_snapshot_from_files( single_use_functions=0, trivial_wrappers=0, ), - redundancy=RedundancyAggregates( - clone_lines=0, - clone_ratio_sum=0.0, - files_with_clones=0, - ), source_files=source_files, ) diff --git a/tests/metrics/summary_test.py b/tests/metrics/summary_test.py index 741d0147..4a10de66 100644 --- a/tests/metrics/summary_test.py +++ b/tests/metrics/summary_test.py @@ -511,7 +511,6 @@ def test_composites_ignore_raw_checkpoint_fields(self, mock_config): "verbosity": 0.95, "erosion": 0.2, "loc": 1, - "clone_lines": 999, "functions": 1, "methods": 0, "trivial_wrappers": 999, @@ -527,7 +526,6 @@ def test_composites_ignore_raw_checkpoint_fields(self, mock_config): "verbosity": 0.35, "erosion": 0.4, "loc": 1, - "clone_lines": 999, "functions": 1, "methods": 0, "trivial_wrappers": 999, From 41b0a508f7f1f5b5619cce04fc4a51d20b2b50d4 Mon Sep 17 00:00:00 2001 From: gabeorlanski Date: Tue, 28 Jul 2026 13:32:38 -0500 Subject: [PATCH 5/5] Remove scb-check dashboard dependencies --- src/slop_code/dashboard/components.py | 10 -- src/slop_code/dashboard/data.py | 4 - src/slop_code/dashboard/graphs/__init__.py | 18 ---- src/slop_code/dashboard/graphs/boxplot.py | 16 ++- src/slop_code/dashboard/graphs/comparison.py | 102 +++++++----------- src/slop_code/dashboard/graphs/erosion.py | 49 --------- .../dashboard/graphs/quality_deltas.py | 1 - src/slop_code/dashboard/graphs/scatter.py | 61 ----------- src/slop_code/dashboard/pages/erosion.py | 26 ----- .../dashboard/pages/head_to_head_evolution.py | 33 +----- .../dashboard/pages/head_to_head_quality.py | 42 +------- .../dashboard/pages/run_analysis_quality.py | 16 +-- src/slop_code/dashboard/pages/scatter.py | 2 +- tests/dashboard/data_test.py | 33 ++++++ tests/dashboard/graphs/quality_test.py | 77 +++++++++++++ tests/dashboard/graphs/scatter_test.py | 71 ++---------- 16 files changed, 172 insertions(+), 389 deletions(-) delete mode 100644 src/slop_code/dashboard/graphs/erosion.py delete mode 100644 src/slop_code/dashboard/pages/erosion.py create mode 100644 tests/dashboard/graphs/quality_test.py diff --git a/src/slop_code/dashboard/components.py b/src/slop_code/dashboard/components.py index bb4dcebe..ad524da9 100644 --- a/src/slop_code/dashboard/components.py +++ b/src/slop_code/dashboard/components.py @@ -201,16 +201,6 @@ def sidebar(): href="/quality-deltas", active="exact", ), - dbc.NavLink( - [ - html.I( - className="fas fa-chart-line me-2" - ), - "Erosion", - ], - href="/erosion", - active="exact", - ), ], vertical=True, pills=True, diff --git a/src/slop_code/dashboard/data.py b/src/slop_code/dashboard/data.py index cc2df738..7e24a355 100644 --- a/src/slop_code/dashboard/data.py +++ b/src/slop_code/dashboard/data.py @@ -848,10 +848,6 @@ def compute_problem_deltas(df: pd.DataFrame) -> pd.DataFrame: prev_row.get("lint_errors", 0), curr_row.get("lint_errors", 0), ), - "verbosity_delta_pct": _safe_delta_pct( - prev_row.get("verbosity", 0), - curr_row.get("verbosity", 0), - ), "complex_delta_pct": _safe_delta_pct( prev_row["cc_high_count"], curr_row["cc_high_count"], diff --git a/src/slop_code/dashboard/graphs/__init__.py b/src/slop_code/dashboard/graphs/__init__.py index e02bf854..a589e5dd 100644 --- a/src/slop_code/dashboard/graphs/__init__.py +++ b/src/slop_code/dashboard/graphs/__init__.py @@ -1,19 +1 @@ """Graph components for the dashboard.""" - -from slop_code.dashboard.graphs.erosion import build_delta_vs_solve_scatter -from slop_code.dashboard.graphs.erosion import build_mass_delta_bars -from slop_code.dashboard.graphs.erosion import build_mass_delta_boxplots -from slop_code.dashboard.graphs.erosion import build_mass_delta_heatmap -from slop_code.dashboard.graphs.erosion import build_other_mass_metrics -from slop_code.dashboard.graphs.erosion import build_symbol_sprawl -from slop_code.dashboard.graphs.erosion import build_velocity_metrics - -__all__ = [ - "build_delta_vs_solve_scatter", - "build_mass_delta_bars", - "build_mass_delta_boxplots", - "build_mass_delta_heatmap", - "build_other_mass_metrics", - "build_symbol_sprawl", - "build_velocity_metrics", -] diff --git a/src/slop_code/dashboard/graphs/boxplot.py b/src/slop_code/dashboard/graphs/boxplot.py index c282020d..6fb77d00 100644 --- a/src/slop_code/dashboard/graphs/boxplot.py +++ b/src/slop_code/dashboard/graphs/boxplot.py @@ -26,12 +26,12 @@ def build_checkpoint_delta_boxplot(context: ChartContext) -> go.Figure: fig = make_subplots( rows=2, cols=3, + specs=[[{}, {}, {}], [{}, {}, None]], vertical_spacing=0.1, horizontal_spacing=0.05, subplot_titles=[ "LOC", "Lint", - "Verbosity", "Rubric", "Novel Flags", "High Complexity Symbols", @@ -41,10 +41,9 @@ def build_checkpoint_delta_boxplot(context: ChartContext) -> go.Figure: metrics = [ ("lines_delta_pct", 1, 1), ("lint_delta_pct", 1, 2), - ("verbosity_delta_pct", 1, 3), - ("rubric_delta_pct", 2, 1), - ("rubric_non_carryover", 2, 2), - ("complex_delta_pct", 2, 3), + ("rubric_delta_pct", 1, 3), + ("rubric_non_carryover", 2, 1), + ("complex_delta_pct", 2, 2), ] variation_info = analyze_model_variations(df) @@ -105,11 +104,8 @@ def build_checkpoint_delta_boxplot(context: ChartContext) -> go.Figure: fig.update_yaxes(row=1, col=3, gridcolor="lightgray") fig.update_yaxes(row=2, col=1, gridcolor="lightgray") fig.update_yaxes(row=2, col=2, gridcolor="lightgray") - fig.update_yaxes(row=2, col=3, gridcolor="lightgray") - - for r in [1, 2]: - for c in [1, 2, 3]: - fig.update_xaxes(row=r, col=c, showticklabels=False) + for _, row, col in metrics: + fig.update_xaxes(row=row, col=col, showticklabels=False) for annotation in fig.layout.annotations: annotation.y = annotation.y + 0.05 diff --git a/src/slop_code/dashboard/graphs/comparison.py b/src/slop_code/dashboard/graphs/comparison.py index e01e96e7..6a35c6c0 100644 --- a/src/slop_code/dashboard/graphs/comparison.py +++ b/src/slop_code/dashboard/graphs/comparison.py @@ -30,12 +30,11 @@ def build_problem_comparison_chart( df = df.sort_values(sort_col) fig = make_subplots( - rows=6, - cols=3, + rows=4, + cols=4, subplot_titles=( "LOC", "Lint Errors", - "Verbosity", "High Complexity", "Total Rubric Flags", "New Rubric Flags", @@ -48,10 +47,8 @@ def build_problem_comparison_chart( "Pass Rate: Error (%)", "Mass: CC", "Δ LOC (%)", - "Δ Verbosity (%)", "Δ Churn Ratio", "CC Concentration", - "Mass: CC", ), shared_xaxes=True, vertical_spacing=0.06, @@ -107,21 +104,7 @@ def build_problem_comparison_chart( carried_over = run_df.get( "rubric_carried_over", pd.Series([0] * len(run_df)) ).fillna(0) - new_flags = total_flags - carried_over - - # Complexity - complexity = run_df["cc_high_count"] - - # Helper for test pass rates - def calc_pass_rate(prefix): - passed = run_df.get( - f"{prefix}_passed", pd.Series([0] * len(run_df)) - ).fillna(0) - total = run_df.get( - f"{prefix}_total", pd.Series([1] * len(run_df)) - ).fillna(1) - total = total.replace(0, 1) - return (passed / total) * 100 + run_df["_new_flags"] = total_flags - carried_over # Test Pass Rates def add_pass_rate_col(prefix): @@ -140,8 +123,6 @@ def add_pass_rate_col(prefix): run_df[f"_pr_{prefix}"] = (passed / total) * 100 - add_pass_rate_col("total") - # passed_tests/total_tests is already there but let's be consistent run_df["_pr_all"] = ( run_df["passed_tests"] / run_df["total_tests"].replace(0, 1) ) * 100 @@ -153,7 +134,6 @@ def add_pass_rate_col(prefix): zeroes = pd.Series([0.0] * len(run_df), index=run_df.index) run_df["_mass_cc"] = run_df.get("mass.cc", zeroes) run_df["_delta_loc"] = run_df.get("delta.loc", zeroes) - run_df["_delta_verbosity"] = run_df.get("delta.verbosity", zeroes) run_df["_delta_churn_ratio"] = run_df.get("delta.churn_ratio", zeroes) run_df["_cc_concentration"] = run_df.get("cc_concentration", zeroes) @@ -198,73 +178,63 @@ def add_trace(col, row, col_idx, show_legend=False): # Row 1 add_trace("loc", 1, 1, show_legend=True) add_trace("lint_errors", 1, 2) - add_trace("verbosity", 1, 3) + add_trace("cc_high_count", 1, 3) + add_trace("_total_flags", 1, 4) # Row 2 - add_trace("cc_high_count", 2, 1) - add_trace("_total_flags", 2, 2) - add_trace("_new_flags", 2, 3) + add_trace("_new_flags", 2, 1) + add_trace("output", 2, 2) + add_trace("cost", 2, 3) + add_trace("_pr_all", 2, 4) # Row 3 - add_trace("output", 3, 1) - add_trace("cost", 3, 2) - add_trace("_pr_all", 3, 3) + add_trace("_pr_core", 3, 1) + add_trace("_pr_functionality", 3, 2) + add_trace("_pr_regression", 3, 3) + add_trace("_pr_error", 3, 4) # Row 4 - add_trace("_pr_core", 4, 1) - add_trace("_pr_functionality", 4, 2) - add_trace("_pr_regression", 4, 3) - - # Row 5 - add_trace("_pr_error", 5, 1) - add_trace("_mass_cc", 5, 2) - add_trace("_delta_loc", 5, 3) - - # Row 6 - add_trace("_delta_verbosity", 6, 1) - add_trace("_delta_churn_ratio", 6, 2) - add_trace("_cc_concentration", 6, 3) + add_trace("_mass_cc", 4, 1) + add_trace("_delta_loc", 4, 2) + add_trace("_delta_churn_ratio", 4, 3) + add_trace("_cc_concentration", 4, 4) # Update y-axes titles fig.update_yaxes(title_text="Lines", row=1, col=1, gridcolor="lightgray") fig.update_yaxes(title_text="Errors", row=1, col=2, gridcolor="lightgray") - fig.update_yaxes(title_text="Score", row=1, col=3, gridcolor="lightgray") - - fig.update_yaxes(title_text="Count", row=2, col=1, gridcolor="lightgray") - fig.update_yaxes(title_text="Flags", row=2, col=2, gridcolor="lightgray") + fig.update_yaxes(title_text="Count", row=1, col=3, gridcolor="lightgray") + fig.update_yaxes(title_text="Flags", row=1, col=4, gridcolor="lightgray") fig.update_yaxes( - title_text="New Flags", row=2, col=3, gridcolor="lightgray" + title_text="New Flags", row=2, col=1, gridcolor="lightgray" ) - - fig.update_yaxes(title_text="Tokens", row=3, col=1, gridcolor="lightgray") - fig.update_yaxes(title_text="Cost ($)", row=3, col=2, gridcolor="lightgray") + fig.update_yaxes(title_text="Tokens", row=2, col=2, gridcolor="lightgray") + fig.update_yaxes(title_text="Cost ($)", row=2, col=3, gridcolor="lightgray") fig.update_yaxes( - title_text="%", row=3, col=3, gridcolor="lightgray", range=[0, 105] + title_text="%", row=2, col=4, gridcolor="lightgray", range=[0, 105] ) - for r, c in [(4, 1), (4, 2), (4, 3), (5, 1)]: + for col in range(1, 5): fig.update_yaxes( - title_text="%", row=r, col=c, gridcolor="lightgray", range=[0, 105] + title_text="%", + row=3, + col=col, + gridcolor="lightgray", + range=[0, 105], ) - fig.update_yaxes(title_text="Mass", row=5, col=2, gridcolor="lightgray") - fig.update_yaxes(title_text="%", row=5, col=3, gridcolor="lightgray") - fig.update_yaxes(title_text="%", row=6, col=1, gridcolor="lightgray") - fig.update_yaxes(title_text="Ratio", row=6, col=2, gridcolor="lightgray") + fig.update_yaxes(title_text="Mass", row=4, col=1, gridcolor="lightgray") + fig.update_yaxes(title_text="%", row=4, col=2, gridcolor="lightgray") + fig.update_yaxes(title_text="Ratio", row=4, col=3, gridcolor="lightgray") fig.update_yaxes( - title_text="Ratio", row=6, col=3, gridcolor="lightgray", range=[0, 1] + title_text="Ratio", row=4, col=4, gridcolor="lightgray", range=[0, 1] ) # Update x-axes titles (only bottom row needs labels) - for col in range(1, 4): + for col in range(1, 5): fig.update_xaxes( - title_text="Checkpoint", row=5, col=col, gridcolor="lightgray" + title_text="Checkpoint", row=4, col=col, gridcolor="lightgray" ) - fig.update_layout( - **get_base_layout( - None, 1400, 1.0, f"Problem: {problem}" - ) # Increased height for more rows - ) + fig.update_layout(**get_base_layout(None, 1100, 1.0, f"Problem: {problem}")) fig.update_layout(legend=GROUPED_VERTICAL_LEGEND) return fig diff --git a/src/slop_code/dashboard/graphs/erosion.py b/src/slop_code/dashboard/graphs/erosion.py deleted file mode 100644 index 14a9a773..00000000 --- a/src/slop_code/dashboard/graphs/erosion.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Retired erosion graphs for removed delta-mass metrics.""" - -from __future__ import annotations - -import plotly.graph_objects as go - -from slop_code.dashboard.data import ChartContext - - -def _retired_figure(title: str) -> go.Figure: - fig = go.Figure() - fig.update_layout(title=title) - fig.add_annotation( - text="Retired: delta-mass and symbol-churn metrics are no longer exported.", - showarrow=False, - x=0.5, - y=0.5, - xref="paper", - yref="paper", - ) - return fig - - -def build_mass_delta_bars(context: ChartContext) -> go.Figure: - return _retired_figure("Erosion Overview") - - -def build_mass_delta_heatmap(context: ChartContext) -> go.Figure: - return _retired_figure("Erosion Heatmap") - - -def build_delta_vs_solve_scatter(context: ChartContext) -> go.Figure: - return _retired_figure("Erosion vs Solve Rate") - - -def build_mass_delta_boxplots(context: ChartContext) -> go.Figure: - return _retired_figure("Erosion Distributions") - - -def build_other_mass_metrics(context: ChartContext) -> go.Figure: - return _retired_figure("Additional Erosion Metrics") - - -def build_velocity_metrics(context: ChartContext) -> go.Figure: - return _retired_figure("Erosion Velocity") - - -def build_symbol_sprawl(context: ChartContext) -> go.Figure: - return _retired_figure("Symbol Sprawl") diff --git a/src/slop_code/dashboard/graphs/quality_deltas.py b/src/slop_code/dashboard/graphs/quality_deltas.py index 072cb15a..594f5d12 100644 --- a/src/slop_code/dashboard/graphs/quality_deltas.py +++ b/src/slop_code/dashboard/graphs/quality_deltas.py @@ -9,7 +9,6 @@ METRICS = [ ("lint_delta_pct", "Lint Δ%"), - ("verbosity_delta_pct", "Verbosity Δ%"), ("rubric_delta_pct", "Rubric Δ%"), ("complex_delta_pct", "Complexity Δ%"), ("rubric_non_carryover", "Novel Flags (count)"), diff --git a/src/slop_code/dashboard/graphs/scatter.py b/src/slop_code/dashboard/graphs/scatter.py index b6847b21..ce0b2dbf 100644 --- a/src/slop_code/dashboard/graphs/scatter.py +++ b/src/slop_code/dashboard/graphs/scatter.py @@ -247,17 +247,6 @@ def _aggregate_scatter_metrics( return metrics -def aggregate_erosion_vs_solve( - context: ChartContext, - solve_type: Literal["all", "iso", "core"] = "all", -) -> list[ScatterMetrics]: - return _aggregate_scatter_metrics( - context, - lambda df: df["verbosity.mean"].fillna(0).mean(), - solve_type=solve_type, - ) - - def aggregate_high_complexity_vs_solve( context: ChartContext, ) -> list[ScatterMetrics]: @@ -311,28 +300,6 @@ def aggregate_rubric_vs_solve(context: ChartContext) -> list[ScatterMetrics]: ) -def aggregate_erosion_vs_problem_test_pass_rate( - context: ChartContext, -) -> list[ScatterMetrics]: - return _aggregate_scatter_metrics( - context, - x_value_fn=lambda df: df["erosion.mean"].fillna(0).mean(), - y_value_fn=lambda df: df["pass_rates.problem.total"].fillna(0).mean() - * 100, - ) - - -def aggregate_erosion_vs_checkpoint_test_pass_rate( - context: ChartContext, -) -> list[ScatterMetrics]: - return _aggregate_scatter_metrics( - context, - x_value_fn=lambda df: df["erosion.mean"].fillna(0).mean(), - y_value_fn=lambda df: df["pass_rates.checkpoint.total"].fillna(0).mean() - * 100, - ) - - class ScatterChartRenderer: def __init__(self, x_axis: AxisConfig, y_axis: AxisConfig): self.x_axis = x_axis @@ -358,9 +325,6 @@ def add_traces( # If not provided, track locally for this call only # (standard single-plot behavior) seen_models = set() - track_globally = False - else: - track_globally = True for m in metrics: is_first_for_model = m.model_name not in seen_models @@ -438,36 +402,11 @@ class ScatterChartConfig: SCATTER_CHARTS: dict[str, ScatterChartConfig] = { - "verbosity_vs_solve": ScatterChartConfig( - aggregator=aggregate_erosion_vs_solve, - x_axis=AxisConfig("Verbosity Score", "log"), - y_axis=AxisConfig("% Checkpoints Solved"), - ), - "verbosity_vs_core_solve": ScatterChartConfig( - aggregator=lambda context: aggregate_erosion_vs_solve(context, "core"), - x_axis=AxisConfig("Verbosity Score", "log"), - y_axis=AxisConfig("% Checkpoints Core Solved"), - ), - "verbosity_vs_iso_solve": ScatterChartConfig( - aggregator=lambda context: aggregate_erosion_vs_solve(context, "iso"), - x_axis=AxisConfig("Verbosity Score", "log"), - y_axis=AxisConfig("% Checkpoints ISO Solved"), - ), "high_complexity_vs_solve": ScatterChartConfig( aggregator=aggregate_high_complexity_vs_solve, x_axis=AxisConfig("High Complexity", "log"), y_axis=AxisConfig("% Checkpoints Solved"), ), - "erosion_vs_problem_test_pass_rate": ScatterChartConfig( - aggregator=aggregate_erosion_vs_problem_test_pass_rate, - x_axis=AxisConfig("Erosion Score", "log"), - y_axis=AxisConfig("% Problem Test Pass Rate"), - ), - "erosion_vs_checkpoint_test_pass_rate": ScatterChartConfig( - aggregator=aggregate_erosion_vs_checkpoint_test_pass_rate, - x_axis=AxisConfig("Erosion Score", "log"), - y_axis=AxisConfig("% Checkpoint Test Pass Rate"), - ), "cost_vs_solve": ScatterChartConfig( aggregator=aggregate_cost_vs_solve, x_axis=AxisConfig("$ / CHKPT", "log"), diff --git a/src/slop_code/dashboard/pages/erosion.py b/src/slop_code/dashboard/pages/erosion.py deleted file mode 100644 index baf3dee0..00000000 --- a/src/slop_code/dashboard/pages/erosion.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Retired erosion page.""" - -import dash -import dash_bootstrap_components as dbc -from dash import html - -dash.register_page(__name__, path="/erosion") - -layout = dbc.Container( - dbc.Alert( - [ - html.H4("Erosion Metrics Retired", className="alert-heading"), - html.P( - "Delta-mass and symbol-churn metrics were removed from checkpoint exports." - ), - html.P( - "Use the comparison and quality pages for the remaining CC and mass.cc views.", - className="mb-0", - ), - ], - color="secondary", - className="mt-4", - ), - fluid=True, - className="py-3", -) diff --git a/src/slop_code/dashboard/pages/head_to_head_evolution.py b/src/slop_code/dashboard/pages/head_to_head_evolution.py index 028d9429..5a8c712d 100644 --- a/src/slop_code/dashboard/pages/head_to_head_evolution.py +++ b/src/slop_code/dashboard/pages/head_to_head_evolution.py @@ -78,24 +78,7 @@ ], className="mb-4 shadow-sm border-0 h-100", ), - md=6, - ), - dbc.Col( - dbc.Card( - [ - dbc.CardHeader( - "Verbosity Trajectory", - className="fw-bold", - ), - dbc.CardBody( - loading_graph( - "h2h-verbosity-trajectory", height="400px" - ) - ), - ], - className="mb-4 shadow-sm border-0 h-100", - ), - md=6, + md=12, ), ] ), @@ -110,12 +93,11 @@ Output("h2h-loc-trajectory", "figure"), Output("h2h-complexity-trajectory", "figure"), Output("h2h-lint-trajectory", "figure"), - Output("h2h-verbosity-trajectory", "figure"), ], [Input("h2h-run-a", "value"), Input("h2h-run-b", "value")], ) def update_evolution(run_a_path, run_b_path): - default_outs = [empty_figure()] * 4 + default_outs = [empty_figure()] * 3 if not run_a_path or not run_b_path: return default_outs @@ -148,8 +130,6 @@ def update_evolution(run_a_path, run_b_path): state_cols = ["loc", "cc_high_count"] if "lint_errors" in df_a_raw.columns: state_cols.append("lint_errors") - if "verbosity" in df_a_raw.columns: - state_cols.append("verbosity") # 1. Add Progress (Create Scatter Data) def add_progress(df): @@ -283,11 +263,4 @@ def make_trajectory(col, title, y_axis): "lint_errors", "Avg Lint Errors per Progress Step", "Errors" ) - # 4. Verbosity - verbosity_traj = make_trajectory( - "verbosity", - "Avg Verbosity per Progress Step", - "Verbosity", - ) - - return loc_traj, comp_traj, lint_traj, verbosity_traj + return loc_traj, comp_traj, lint_traj diff --git a/src/slop_code/dashboard/pages/head_to_head_quality.py b/src/slop_code/dashboard/pages/head_to_head_quality.py index 2b6944f6..fd143c8d 100644 --- a/src/slop_code/dashboard/pages/head_to_head_quality.py +++ b/src/slop_code/dashboard/pages/head_to_head_quality.py @@ -116,24 +116,7 @@ def make_quality_scatter_checkpoint_level( ], className="mb-4 shadow-sm border-0 h-100", ), - md=4, - ), - dbc.Col( - dbc.Card( - [ - dbc.CardHeader( - "Verbosity Comparison", - className="fw-bold", - ), - dbc.CardBody( - loading_graph( - "h2h-verbosity-scatter", height="400px" - ) - ), - ], - className="mb-4 shadow-sm border-0 h-100", - ), - md=4, + md=6, ), dbc.Col( dbc.Card( @@ -149,7 +132,7 @@ def make_quality_scatter_checkpoint_level( ], className="mb-4 shadow-sm border-0 h-100", ), - md=4, + md=6, ), ] ), @@ -162,13 +145,12 @@ def make_quality_scatter_checkpoint_level( @callback( [ Output("h2h-lint-scatter", "figure"), - Output("h2h-verbosity-scatter", "figure"), Output("h2h-complexity-scatter", "figure"), ], [Input("h2h-run-a", "value"), Input("h2h-run-b", "value")], ) def update_quality(run_a_path, run_b_path): - default_outs = [empty_figure()] * 3 + default_outs = [empty_figure()] * 2 if not run_a_path or not run_b_path: return default_outs @@ -195,12 +177,6 @@ def update_quality(run_a_path, run_b_path): name_a = get_display_annotation(row_a).split(" - ")[0] name_b = get_display_annotation(row_b).split(" - ")[0] - def short_label(name): - return (name[:20] + "..") if len(name) > 20 else name - - label_a = short_label(name_a) - label_b = short_label(name_b) - # Calculate complexity column on the full df_chk for accurate lookup df_chk["_complex"] = df_chk["cc_high_count"] @@ -214,16 +190,6 @@ def short_label(name): run_a_path, run_b_path, ) - verbosity_fig = make_quality_scatter_checkpoint_level( - df_chk, - "verbosity", - "Verbosity per Checkpoint", - "Verbosity", - name_a, - name_b, - run_a_path, - run_b_path, - ) comp_fig = make_quality_scatter_checkpoint_level( df_chk, "_complex", @@ -235,4 +201,4 @@ def short_label(name): run_b_path, ) - return lint_fig, verbosity_fig, comp_fig + return lint_fig, comp_fig diff --git a/src/slop_code/dashboard/pages/run_analysis_quality.py b/src/slop_code/dashboard/pages/run_analysis_quality.py index d3febac7..dc4d8432 100644 --- a/src/slop_code/dashboard/pages/run_analysis_quality.py +++ b/src/slop_code/dashboard/pages/run_analysis_quality.py @@ -24,11 +24,8 @@ ), dbc.Row( [ - dbc.Col(loading_graph("ra-lint-dist", height="350px"), md=4), - dbc.Col( - loading_graph("ra-verbosity-dist", height="350px"), md=4 - ), - dbc.Col(loading_graph("ra-complex-dist", height="350px"), md=4), + dbc.Col(loading_graph("ra-lint-dist", height="350px"), md=6), + dbc.Col(loading_graph("ra-complex-dist", height="350px"), md=6), ], className="mb-4", ), @@ -74,7 +71,6 @@ def build_histogram(df, col, title, color): @callback( [ Output("ra-lint-dist", "figure"), - Output("ra-verbosity-dist", "figure"), Output("ra-complex-dist", "figure"), Output("ra-func-loc-dist", "figure"), Output("ra-cmp-loc-dist", "figure"), @@ -85,11 +81,11 @@ def build_histogram(df, col, title, color): ) def update_quality(run_path): if not run_path: - return [empty_figure()] * 7 + return [empty_figure()] * 6 context = build_context([run_path]) if context is None or context.checkpoints.empty: - return [empty_figure()] * 7 + return [empty_figure()] * 6 df = context.checkpoints @@ -97,9 +93,6 @@ def update_quality(run_path): lint_hist = build_histogram( df, "lint_errors", "Lint Errors Distribution", "#d62728" ) - verbosity_hist = build_histogram( - df, "verbosity", "Verbosity Distribution", "#ff7f0e" - ) comp_hist = build_histogram( df, "cc_high_count", "Complexity Distribution", "#9467bd" ) @@ -147,7 +140,6 @@ def update_quality(run_path): return ( lint_hist, - verbosity_hist, comp_hist, func_loc_hist, cmp_loc_hist, diff --git a/src/slop_code/dashboard/pages/scatter.py b/src/slop_code/dashboard/pages/scatter.py index ca34d2ea..a9d849dd 100644 --- a/src/slop_code/dashboard/pages/scatter.py +++ b/src/slop_code/dashboard/pages/scatter.py @@ -47,7 +47,7 @@ dbc.Tabs( tabs, id="scatter-tabs", - active_tab="verbosity_vs_solve", + active_tab="high_complexity_vs_solve", className="nav-fill border-0", ), className="bg-light", diff --git a/tests/dashboard/data_test.py b/tests/dashboard/data_test.py index 69c699ea..7fc77d06 100644 --- a/tests/dashboard/data_test.py +++ b/tests/dashboard/data_test.py @@ -1,5 +1,8 @@ from __future__ import annotations +import pandas as pd + +from slop_code.dashboard.data import compute_problem_deltas from slop_code.dashboard.data import process_checkpoint_row @@ -14,3 +17,33 @@ def test_process_checkpoint_row_uses_new_pass_rate_names() -> None: ) assert processed["passed_chkpt"] is True + + +def test_problem_deltas_need_no_scb_metrics() -> None: + deltas = compute_problem_deltas( + pd.DataFrame( + [ + { + "display_name": "Run A", + "problem": "prob1", + "idx": 1, + "loc": 10, + "lint_errors": 2, + "cc_high_count": 1, + "cc_max": 3, + }, + { + "display_name": "Run A", + "problem": "prob1", + "idx": 2, + "loc": 20, + "lint_errors": 1, + "cc_high_count": 2, + "cc_max": 4, + }, + ] + ) + ) + + assert deltas["lines_delta_pct"].item() == 100 + assert "verbosity_delta_pct" not in deltas diff --git a/tests/dashboard/graphs/quality_test.py b/tests/dashboard/graphs/quality_test.py new file mode 100644 index 00000000..049738ea --- /dev/null +++ b/tests/dashboard/graphs/quality_test.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +import pandas as pd +import plotly.graph_objects as go + +from slop_code.dashboard.data import ChartContext +from slop_code.dashboard.graphs.boxplot import build_checkpoint_delta_boxplot +from slop_code.dashboard.graphs.comparison import ( + build_problem_comparison_chart, +) + + +def _titles(figure: go.Figure) -> set[str]: + return { + annotation.text + for annotation in figure.layout.annotations + if annotation.text + } + + +def test_quality_charts_need_no_scb_metrics() -> None: + metadata = { + "display_name": "Run A", + "model_name": "model-a", + "_thinking_sort_key": 0, + "thinking": "none", + "prompt_template": "default", + "agent_type": "codex", + "agent_version": "1", + "run_date": "2026-07-28", + } + checkpoints = pd.DataFrame( + [ + { + **metadata, + "problem": "prob1", + "idx": idx, + "loc": 10 * idx, + "lint_errors": 3 - idx, + "cc_high_count": idx, + "cc_max": idx + 2, + "rubric_total_flags": idx, + "rubric_carried_over": idx - 1, + "output": 100 * idx, + "cost": 0.1 * idx, + "passed_tests": idx, + "total_tests": 2, + "core_passed": idx, + "core_total": 2, + "functionality_passed": idx, + "functionality_total": 2, + "regression_passed": idx, + "regression_total": 2, + "error_passed": idx, + "error_total": 2, + "mass.cc": 0.1 * idx, + "delta.loc": 0.2 * idx, + "delta.churn_ratio": 0.3 * idx, + "cc_concentration": 0.4 * idx, + } + for idx in (1, 2) + ] + ) + context = ChartContext( + checkpoints=checkpoints, + run_summaries=pd.DataFrame([metadata]), + color_map={"Run A": "#123456"}, + base_color_map={"Run A": "#123456"}, + ) + + comparison = build_problem_comparison_chart(context, "prob1") + deltas = build_checkpoint_delta_boxplot(context) + + assert comparison.data + assert deltas.data + assert "Verbosity" not in _titles(comparison) + assert "Verbosity" not in _titles(deltas) diff --git a/tests/dashboard/graphs/scatter_test.py b/tests/dashboard/graphs/scatter_test.py index 98e0177a..fcc4b34c 100644 --- a/tests/dashboard/graphs/scatter_test.py +++ b/tests/dashboard/graphs/scatter_test.py @@ -1,13 +1,9 @@ from __future__ import annotations import pandas as pd -import pytest from slop_code.dashboard.data import ChartContext -from slop_code.dashboard.graphs.scatter import ( - aggregate_erosion_vs_problem_test_pass_rate, -) -from slop_code.dashboard.graphs.scatter import aggregate_erosion_vs_solve +from slop_code.dashboard.graphs.scatter import SCATTER_CHARTS from slop_code.dashboard.graphs.scatter import aggregate_time_vs_solve @@ -20,64 +16,6 @@ def _build_context(run_summaries: pd.DataFrame) -> ChartContext: ) -def test_aggregate_erosion_vs_solve_uses_saved_verbosity_mean(): - context = _build_context( - pd.DataFrame( - [ - { - "display_name": "Run A", - "model_name": "model-a", - "thinking": "none", - "prompt_template": "default", - "agent_type": "codex", - "agent_version": "1", - "run_date": "2026-03-20", - "verbosity.mean": 0.75, - "erosion.mean": 55.0, - "ratios.rubric.mean": 10.0, - "ratios.lint.mean": 30.0, - "pct_checkpoints_solved": 80.0, - } - ] - ) - ) - - metrics = aggregate_erosion_vs_solve(context) - - assert len(metrics) == 1 - assert metrics[0].x_value == pytest.approx(0.75) - assert metrics[0].y_value == pytest.approx(80.0) - - -def test_aggregate_erosion_vs_problem_test_pass_rate_uses_saved_erosion_mean(): - context = _build_context( - pd.DataFrame( - [ - { - "display_name": "Run A", - "model_name": "model-a", - "thinking": "none", - "prompt_template": "default", - "agent_type": "codex", - "agent_version": "1", - "run_date": "2026-03-20", - "verbosity.mean": 0.75, - "erosion.mean": 55.0, - "ratios.rubric.mean": 10.0, - "ratios.lint.mean": 30.0, - "pass_rates.problem.total": 0.6, - } - ] - ) - ) - - metrics = aggregate_erosion_vs_problem_test_pass_rate(context) - - assert len(metrics) == 1 - assert metrics[0].x_value == pytest.approx(55.0) - assert metrics[0].y_value == pytest.approx(60.0) - - def test_aggregate_time_vs_solve_skips_missing_time() -> None: context = _build_context( pd.DataFrame( @@ -99,3 +37,10 @@ def test_aggregate_time_vs_solve_skips_missing_time() -> None: metrics = aggregate_time_vs_solve(context) assert metrics == [] + + +def test_scatter_catalog_needs_no_scb_metrics() -> None: + assert all( + "verbosity" not in chart and "erosion" not in chart + for chart in SCATTER_CHARTS + )