feat: include medium-risk classes in CBO penalty calculation

**Problem:**
CBO analysis was only counting High Risk classes (CBO > 7) for penalty
calculation, completely ignoring Medium Risk classes (3 < CBO ≤ 7).

Example: Project with 364 classes, multiple classes at CBO=6, but score = 100/100
because all were "Medium Risk" (not counted).

**Solution:**
1. Added `MediumCouplingClasses` field to track Medium Risk classes
2. Updated penalty calculation to use weighted ratio:
   - High Risk classes: weight = 1.0
   - Medium Risk classes: weight = 0.5

   Formula: (HighRisk × 1.0 + MediumRisk × 0.5) / TotalClasses

**Impact:**
Projects with many Medium Risk classes will now receive appropriate penalties:
- 10% weighted ratio → -6 points (Low penalty)
- 30% weighted ratio → -12 points (Medium penalty)
- 60% weighted ratio → -20 points (High penalty)

This makes CBO scoring more realistic and catches coupling issues that were
previously ignored.

**Files Changed:**
- domain/analyze.go: Added MediumCouplingClasses field, updated penalty logic
- app/analyze_usecase.go: Set MediumCouplingClasses from CBO analysis
- domain/analyze.go: Added validation for new field

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
DaisukeYoda
2025-10-05 18:04:09 +09:00
parent 182bac8b71
commit 87ebfadb7e
2 changed files with 25 additions and 8 deletions

View File

@@ -497,6 +497,7 @@ func (uc *AnalyzeUseCase) calculateSummary(summary *domain.AnalyzeSummary, respo
if response.CBO != nil {
summary.CBOClasses = response.CBO.Summary.TotalClasses
summary.HighCouplingClasses = response.CBO.Summary.HighRiskClasses
summary.MediumCouplingClasses = response.CBO.Summary.MediumRiskClasses
summary.AverageCoupling = response.CBO.Summary.AverageCBO
}

View File

@@ -112,9 +112,10 @@ type AnalyzeSummary struct {
CloneGroups int `json:"clone_groups" yaml:"clone_groups"`
CodeDuplication float64 `json:"code_duplication_percentage" yaml:"code_duplication_percentage"`
CBOClasses int `json:"cbo_classes" yaml:"cbo_classes"`
HighCouplingClasses int `json:"high_coupling_classes" yaml:"high_coupling_classes"`
AverageCoupling float64 `json:"average_coupling" yaml:"average_coupling"`
CBOClasses int `json:"cbo_classes" yaml:"cbo_classes"`
HighCouplingClasses int `json:"high_coupling_classes" yaml:"high_coupling_classes"` // CBO > 7 (High Risk)
MediumCouplingClasses int `json:"medium_coupling_classes" yaml:"medium_coupling_classes"` // 3 < CBO ≤ 7 (Medium Risk)
AverageCoupling float64 `json:"average_coupling" yaml:"average_coupling"`
// Overall health score (0-100)
HealthScore int `json:"health_score" yaml:"health_score"`
@@ -160,9 +161,19 @@ func (s *AnalyzeSummary) Validate() error {
}
// CBO checks
if s.CBOClasses > 0 && s.HighCouplingClasses > s.CBOClasses {
return fmt.Errorf("HighCouplingClasses (%d) cannot exceed CBOClasses (%d)",
s.HighCouplingClasses, s.CBOClasses)
if s.CBOClasses > 0 {
if s.HighCouplingClasses > s.CBOClasses {
return fmt.Errorf("HighCouplingClasses (%d) cannot exceed CBOClasses (%d)",
s.HighCouplingClasses, s.CBOClasses)
}
if s.MediumCouplingClasses > s.CBOClasses {
return fmt.Errorf("MediumCouplingClasses (%d) cannot exceed CBOClasses (%d)",
s.MediumCouplingClasses, s.CBOClasses)
}
if (s.HighCouplingClasses + s.MediumCouplingClasses) > s.CBOClasses {
return fmt.Errorf("HighCouplingClasses + MediumCouplingClasses (%d) cannot exceed CBOClasses (%d)",
s.HighCouplingClasses+s.MediumCouplingClasses, s.CBOClasses)
}
}
return nil
@@ -206,13 +217,18 @@ func (s *AnalyzeSummary) calculateDuplicationPenalty() int {
}
}
// calculateCouplingPenalty calculates the penalty for class coupling (max 16)
// calculateCouplingPenalty calculates the penalty for class coupling (max 20)
// Considers both High and Medium risk classes with different weights
func (s *AnalyzeSummary) calculateCouplingPenalty() int {
if s.CBOClasses == 0 {
return 0
}
ratio := float64(s.HighCouplingClasses) / float64(s.CBOClasses)
// Calculate combined problematic classes ratio
// Weight: High Risk = 1.0, Medium Risk = 0.5
weightedProblematicClasses := float64(s.HighCouplingClasses) + (0.5 * float64(s.MediumCouplingClasses))
ratio := weightedProblematicClasses / float64(s.CBOClasses)
switch {
case ratio > CouplingRatioHigh:
return CouplingPenaltyHigh