Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rebalanced Scoring

Rebalanced scoring is a multi-dimensional scoring algorithm that prioritizes actual code quality issues over pure file size concerns. It provides a more nuanced approach to technical debt prioritization by considering complexity, coverage gaps, structural problems, and code smells.

Overview

Traditional scoring often over-emphasizes file size, causing large but simple files to rank higher than complex, untested code. The rebalanced algorithm fixes this by:

  1. De-emphasizing size: Reduces size weight from ~1.5 to 0.3 (80% reduction)
  2. Emphasizing quality: Increases weights for complexity (1.0) and coverage gaps (1.0)
  3. Additive bonuses: Provides +20 bonus for complex + untested code (not multiplicative)
  4. Context-aware detection: Automatically detects and reduces scores for generated code

Source: src/priority/scoring/rebalanced.rs:1-10

Severity Levels

The rebalanced algorithm assigns severity levels based on normalized total scores and risk factors. Scores are normalized to a 0-200 range.

Source: src/priority/scoring/rebalanced.rs:12-30 (Severity enum), src/priority/scoring/rebalanced.rs:416-448 (determine_severity)

SeverityScore ThresholdAdditional CriteriaDescription
CRITICAL> 120OR complexity > 60 AND coverage > 40Requires immediate attention
HIGH> 80OR complexity > 40 AND coverage > 20 OR structural > 50High priority for next sprint
MEDIUM> 40OR single moderate issue (complexity/coverage/structural > 30)Plan for future sprint
LOWEverything else-Minor concerns, size-only issues

Severity Determination Logic (from src/priority/scoring/rebalanced.rs:416-448):

#![allow(unused)]
fn main() {
fn determine_severity(components: &ScoreComponents, ...) -> Severity {
    let total = components.weighted_total(&ScoreWeights::default());

    // CRITICAL: Total score > 120 OR high complexity + low coverage
    if total > 120.0 || (components.complexity_score > 60.0 && components.coverage_score > 40.0) {
        return Severity::Critical;
    }

    // HIGH: Total score > 80 OR moderate complexity + coverage gap OR severe structural issue
    if total > 80.0
        || (components.complexity_score > 40.0 && components.coverage_score > 20.0)
        || components.structural_score > 50.0 {
        return Severity::High;
    }

    // MEDIUM: Total score > 40 OR single moderate issue
    if total > 40.0
        || components.complexity_score > 30.0
        || components.coverage_score > 30.0
        || components.structural_score > 30.0 {
        return Severity::Medium;
    }

    // LOW: Everything else
    Severity::Low
}
}

Score Components

The rebalanced algorithm computes five distinct scoring components, each contributing to the weighted total.

Source: src/priority/scoring/rebalanced.rs:32-55 (ScoreComponents struct)

ComponentWeight RangeDefault WeightDescription
complexity_score0-1001.0Cyclomatic + cognitive complexity combined
coverage_score0-801.0Testing coverage deficit with complexity bonus
structural_score0-600.8God objects and architectural issues
size_score0-300.3File/function size (reduced from legacy ~1.5)
smell_score0-400.6Long functions, deep nesting, impure logic

ScoreComponents Struct

#![allow(unused)]
fn main() {
/// Individual scoring components with their contributions
pub struct ScoreComponents {
    pub complexity_score: f64, // Weight: 0-100
    pub coverage_score: f64,   // Weight: 0-80
    pub structural_score: f64, // Weight: 0-60
    pub size_score: f64,       // Weight: 0-30 (reduced from current)
    pub smell_score: f64,      // Weight: 0-40
}
}

Source: src/priority/scoring/rebalanced.rs:32-40

Weighted Total Calculation

The weighted total is normalized to a 0-200 range:

#![allow(unused)]
fn main() {
pub fn weighted_total(&self, weights: &ScoreWeights) -> f64 {
    let raw_total = self.complexity_score * weights.complexity_weight
        + self.coverage_score * weights.coverage_weight
        + self.structural_score * weights.structural_weight
        + self.size_score * weights.size_weight
        + self.smell_score * weights.smell_weight;

    // Normalize to 0-200 range
    // Theoretical max: 100×1.0 + 80×1.0 + 60×0.8 + 30×0.3 + 40×0.6 = 237
    (raw_total / 237.0) * 200.0
}
}

Source: src/priority/scoring/rebalanced.rs:44-54

Presets

Debtmap provides four weight presets for different prioritization strategies.

Source: src/priority/scoring/rebalanced.rs:73-127 (ScoreWeights presets)

Balanced (Default)

The default preset prioritizes complexity and coverage over size.

[scoring_rebalanced]
preset = "balanced"

Weights:

  • complexity_weight: 1.0
  • coverage_weight: 1.0
  • structural_weight: 0.8
  • size_weight: 0.3
  • smell_weight: 0.6

Use when: Standard development with focus on actual code quality.

Source: src/priority/scoring/rebalanced.rs:74-83

Quality-Focused

Maximum emphasis on code quality, minimal concern for file size.

[scoring_rebalanced]
preset = "quality-focused"

Weights:

  • complexity_weight: 1.2
  • coverage_weight: 1.1
  • structural_weight: 0.9
  • size_weight: 0.2
  • smell_weight: 0.7

Use when: You want maximum emphasis on code quality over size.

Source: src/priority/scoring/rebalanced.rs:85-94

Size-Focused (Legacy)

Restores legacy behavior for backward compatibility.

[scoring_rebalanced]
preset = "size-focused"

Weights:

  • complexity_weight: 0.5
  • coverage_weight: 0.4
  • structural_weight: 0.6
  • size_weight: 1.5
  • smell_weight: 0.3

Use when: Maintaining legacy scoring behavior or when file size is the primary concern.

Source: src/priority/scoring/rebalanced.rs:96-105

Test-Coverage-Focused

Emphasizes testing gaps above all other factors.

[scoring_rebalanced]
preset = "test-coverage"

Weights:

  • complexity_weight: 0.8
  • coverage_weight: 1.3
  • structural_weight: 0.6
  • size_weight: 0.2
  • smell_weight: 0.5

Use when: Prioritizing test coverage improvements.

Source: src/priority/scoring/rebalanced.rs:107-116

Preset Comparison Table

PresetComplexityCoverageStructuralSizeSmellsBest For
balanced1.01.00.80.30.6Standard development
quality-focused1.21.10.90.20.7Quality-first teams
size-focused0.50.40.61.50.3Legacy compatibility
test-coverage0.81.30.60.20.5Coverage campaigns

Preset Name Aliases

Presets support multiple naming conventions for convenience:

Source: src/priority/scoring/rebalanced.rs:118-127

PresetAliases
balancedbalanced
quality-focusedquality-focused, quality_focused, quality
size-focusedsize-focused, size_focused, legacy
test-coveragetest-coverage, test_coverage, testing

Generated Code Detection

The rebalanced scoring automatically detects and reduces scores for generated code, applying a 90% reduction to the size score.

Source: src/priority/scoring/rebalanced.rs:450-467 (is_generated_file), src/priority/scoring/rebalanced.rs:246-249 (reduction logic)

Detection Patterns

Generated files are identified by common naming patterns:

#![allow(unused)]
fn main() {
let generated_patterns = [
    ".generated.rs",
    ".pb.rs",     // Protocol buffers
    ".g.rs",      // Grammar generated files
    "_pb.rs",     // Alternative protobuf naming
    "generated/", // Generated directory
    "/gen/",      // Gen directory
];
}

Source: src/priority/scoring/rebalanced.rs:455-462

Score Reduction

When a file matches a generated pattern, the size score is reduced by 90%:

#![allow(unused)]
fn main() {
// Apply generated code detection and scoring reduction
if is_generated_file(&func.file) {
    // Reduce size score by 90% for generated code
    components.size_score *= 0.1;
}
}

Source: src/priority/scoring/rebalanced.rs:246-249

Examples

File PathPattern MatchSize Score Adjustment
src/proto/api.pb.rs.pb.rs90% reduction
src/generated/schema.rsgenerated/90% reduction
src/parser.g.rs.g.rs90% reduction
src/main.rsNoneNo adjustment

Configuration

Rebalanced scoring is configured in your .debtmap.toml configuration file.

Source: src/config/scoring.rs:495-554 (RebalancedScoringConfig)

Enabling Rebalanced Scoring

Add the [scoring_rebalanced] section to activate rebalanced scoring:

# .debtmap.toml

[scoring_rebalanced]
preset = "balanced"  # Activates rebalanced scoring with balanced preset

Using Presets

Select a preset to use predefined weight configurations:

[scoring_rebalanced]
preset = "quality-focused"

Custom Weight Overrides

Override individual weights while using a preset as base:

[scoring_rebalanced]
preset = "balanced"
complexity_weight = 1.2      # Override complexity weight
coverage_weight = 1.0        # Keep default
# Other weights inherit from preset

Full Custom Configuration

Define all weights manually without a preset:

[scoring_rebalanced]
# Custom weights (no preset)
complexity_weight = 1.0
coverage_weight = 1.0
structural_weight = 0.8
size_weight = 0.3
smell_weight = 0.6

RebalancedScoringConfig Structure

#![allow(unused)]
fn main() {
pub struct RebalancedScoringConfig {
    /// Preset name (balanced, quality-focused, size-focused, test-coverage)
    pub preset: Option<String>,

    /// Custom complexity weight (overrides preset if specified)
    pub complexity_weight: Option<f64>,

    /// Custom coverage weight (overrides preset if specified)
    pub coverage_weight: Option<f64>,

    /// Custom structural weight (overrides preset if specified)
    pub structural_weight: Option<f64>,

    /// Custom size weight (overrides preset if specified)
    pub size_weight: Option<f64>,

    /// Custom smell weight (overrides preset if specified)
    pub smell_weight: Option<f64>,
}
}

Source: src/config/scoring.rs:496-521

Scoring Rationale

Each debt item includes a detailed rationale explaining why a score was assigned. The rationale includes primary factors, bonuses, and context adjustments.

Source: src/priority/scoring/rebalanced.rs:130-223 (ScoringRationale)

ScoringRationale Struct

#![allow(unused)]
fn main() {
pub struct ScoringRationale {
    pub primary_factors: Vec<String>,
    pub bonuses: Vec<String>,
    pub context_adjustments: Vec<String>,
}
}

Source: src/priority/scoring/rebalanced.rs:131-136

Primary Factors

Primary factors are the main contributors to the score:

  • High cyclomatic complexity (complexity_score > 40): "High cyclomatic complexity (+{score})"
  • Significant coverage gap (coverage_score > 30): "Significant coverage gap (+{score})"
  • Structural issues (structural_score > 30): "Structural issues (+{score})"

Bonuses

Additive enhancements to the score:

  • Complex + untested (complexity > 40 AND coverage > 20): "Complex + untested: +20 bonus applied"
  • Code smells (smell_score > 20): "Code smells detected (+{score})"

Context Adjustments

Explanations for score adjustments based on context:

  • Size context-adjusted (0 < size_score < 10): "File size context-adjusted (reduced weight for file type)"
  • Size de-emphasized (size_weight < 0.5): "Size de-emphasized (weight: {weight})"

Example Rationale Output

Debt Item: src/payment/processor.rs:142 - process_payment()
Score: 95.3 (CRITICAL)

Primary factors:
  - High cyclomatic complexity (+100.0)
  - Significant coverage gap (+57.2)

Bonuses:
  - Complex + untested: +20 bonus applied
  - Code smells detected (+25.0)

Context adjustments:
  - Size de-emphasized (weight: 0.3)

Complexity Scoring Details

The complexity score is computed from both cyclomatic and cognitive complexity.

Source: src/priority/scoring/rebalanced.rs:265-317 (score_complexity)

Cyclomatic Complexity Thresholds

Cyclomatic ComplexityBase Score
> 30100.0
> 2080.0
> 1560.0
> 1040.0
> 520.0
<= 50.0

Cognitive Complexity Bonus

An additive bonus is applied based on cognitive complexity:

Cognitive ComplexityBonus
> 50+20.0
> 30+15.0
> 20+10.0
> 15+5.0
<= 150.0

The final complexity score is capped at 100.0:

#![allow(unused)]
fn main() {
(cyclomatic_score + cognitive_bonus).min(100.0)
}

Example Calculations

FunctionCyclomaticCognitiveBase ScoreBonusFinal
Simple getter230.00.00.0
Moderate logic121840.05.045.0
Complex parser254580.015.095.0
Very complex3560100.020.0100.0 (capped)

Score Normalization

Weighted totals are normalized to a 0-200 range for consistent comparison across projects.

Source: src/priority/scoring/rebalanced.rs:44-54

Normalization Formula

normalized_score = (raw_total / 237.0) * 200.0

Theoretical Maximum

The theoretical maximum raw score is 237:

  • Complexity: 100 × 1.0 = 100
  • Coverage: 80 × 1.0 = 80
  • Structural: 60 × 0.8 = 48
  • Size: 30 × 0.3 = 9
  • Smells: 40 × 0.6 = 24
  • Total: 237

This maps to a normalized score of 200.

CLI Usage

Enable Rebalanced Scoring via Config

Create or modify .debtmap.toml:

[scoring_rebalanced]
preset = "balanced"

Compare Standard vs Rebalanced Scoring

# Create test config with rebalanced scoring
cat > .debtmap-rebalanced.toml <<EOF
[scoring_rebalanced]
preset = "balanced"
EOF

# Compare results
debtmap analyze . --format terminal                            # Standard scoring
debtmap analyze . --config .debtmap-rebalanced.toml --format terminal  # Rebalanced scoring

Test Different Presets

# Quality-focused preset
cat > .debtmap-quality.toml <<EOF
[scoring_rebalanced]
preset = "quality-focused"
EOF

debtmap analyze . --config .debtmap-quality.toml --top 10

Best Practices

  1. Start with balanced preset for standard development workflows
  2. Use quality-focused when code quality is the primary concern
  3. Use test-coverage during coverage improvement campaigns
  4. Use size-focused (legacy) only for backward compatibility
  5. Review rationale output to understand why items are prioritized
  6. Combine with file-level analysis for comprehensive debt assessment
  7. Track severity distributions over time to measure improvement

Migration from Legacy Scoring

Breaking Changes

  • Scores will change significantly for all debt items
  • Large files with low complexity will rank lower
  • Complex untested code will rank higher
  • Size-based prioritization reduced by 80%

Restoring Legacy Behavior

[scoring_rebalanced]
preset = "size-focused"

Gradual Migration Steps

  1. Test first: Add [scoring_rebalanced] section to a test config file
  2. Compare: Run analysis with both standard and rebalanced scoring
  3. Evaluate: Review how priorities change
  4. Adopt: Switch your primary config after validation
  5. Tune: Adjust preset or custom weights based on team priorities

See Also