|
| 1 | +package rubric |
| 2 | + |
| 3 | +// CoverageSection represents coverage metrics for a single section/area. |
| 4 | +// This is a generic type that can be used for: |
| 5 | +// - Spec coverage (components, foundations, patterns) |
| 6 | +// - Code coverage (functions, lines, branches) |
| 7 | +// - Test coverage (scenarios, edge cases) |
| 8 | +// - Documentation coverage (API docs, guides) |
| 9 | +type CoverageSection struct { |
| 10 | + // Total is the total number of items in this section. |
| 11 | + Total int `json:"total"` |
| 12 | + |
| 13 | + // Complete is the number of items that are complete/covered. |
| 14 | + Complete int `json:"complete"` |
| 15 | + |
| 16 | + // Percentage is the coverage percentage (0-100). |
| 17 | + Percentage int `json:"percentage"` |
| 18 | + |
| 19 | + // Missing lists the IDs or names of missing/incomplete items. |
| 20 | + Missing []string `json:"missing,omitempty"` |
| 21 | +} |
| 22 | + |
| 23 | +// CoverageReport aggregates coverage across multiple sections. |
| 24 | +type CoverageReport struct { |
| 25 | + // Sections contains coverage for each named section. |
| 26 | + // Example keys: "components", "foundations", "functions", "lines" |
| 27 | + Sections map[string]CoverageSection `json:"sections"` |
| 28 | + |
| 29 | + // Overall is the aggregate coverage percentage (0-100). |
| 30 | + Overall int `json:"overall"` |
| 31 | +} |
| 32 | + |
| 33 | +// NewCoverageReport creates an empty coverage report. |
| 34 | +func NewCoverageReport() *CoverageReport { |
| 35 | + return &CoverageReport{ |
| 36 | + Sections: make(map[string]CoverageSection), |
| 37 | + } |
| 38 | +} |
| 39 | + |
| 40 | +// AddSection adds a coverage section to the report. |
| 41 | +func (cr *CoverageReport) AddSection(name string, section CoverageSection) { |
| 42 | + cr.Sections[name] = section |
| 43 | +} |
| 44 | + |
| 45 | +// SetSection is an alias for AddSection for fluent API. |
| 46 | +func (cr *CoverageReport) SetSection(name string, total, complete int, missing []string) *CoverageReport { |
| 47 | + percentage := 0 |
| 48 | + if total > 0 { |
| 49 | + percentage = (complete * 100) / total |
| 50 | + } |
| 51 | + cr.Sections[name] = CoverageSection{ |
| 52 | + Total: total, |
| 53 | + Complete: complete, |
| 54 | + Percentage: percentage, |
| 55 | + Missing: missing, |
| 56 | + } |
| 57 | + return cr |
| 58 | +} |
| 59 | + |
| 60 | +// ComputeOverall calculates the overall coverage percentage. |
| 61 | +// Uses a simple average of all section percentages. |
| 62 | +func (cr *CoverageReport) ComputeOverall() int { |
| 63 | + if len(cr.Sections) == 0 { |
| 64 | + return 0 |
| 65 | + } |
| 66 | + |
| 67 | + total := 0 |
| 68 | + for _, section := range cr.Sections { |
| 69 | + total += section.Percentage |
| 70 | + } |
| 71 | + cr.Overall = total / len(cr.Sections) |
| 72 | + return cr.Overall |
| 73 | +} |
| 74 | + |
| 75 | +// ComputeOverallWeighted calculates overall coverage with weights. |
| 76 | +// The weights map should have keys matching section names. |
| 77 | +// Sections not in the weights map get weight 1.0. |
| 78 | +func (cr *CoverageReport) ComputeOverallWeighted(weights map[string]float64) int { |
| 79 | + if len(cr.Sections) == 0 { |
| 80 | + return 0 |
| 81 | + } |
| 82 | + |
| 83 | + totalWeight := 0.0 |
| 84 | + weightedSum := 0.0 |
| 85 | + |
| 86 | + for name, section := range cr.Sections { |
| 87 | + weight := 1.0 |
| 88 | + if w, ok := weights[name]; ok { |
| 89 | + weight = w |
| 90 | + } |
| 91 | + totalWeight += weight |
| 92 | + weightedSum += float64(section.Percentage) * weight |
| 93 | + } |
| 94 | + |
| 95 | + if totalWeight == 0 { |
| 96 | + return 0 |
| 97 | + } |
| 98 | + cr.Overall = int(weightedSum / totalWeight) |
| 99 | + return cr.Overall |
| 100 | +} |
| 101 | + |
| 102 | +// GetSection retrieves a section by name. |
| 103 | +// Returns an empty section if not found. |
| 104 | +func (cr *CoverageReport) GetSection(name string) CoverageSection { |
| 105 | + if section, ok := cr.Sections[name]; ok { |
| 106 | + return section |
| 107 | + } |
| 108 | + return CoverageSection{} |
| 109 | +} |
| 110 | + |
| 111 | +// HasSection checks if a section exists. |
| 112 | +func (cr *CoverageReport) HasSection(name string) bool { |
| 113 | + _, ok := cr.Sections[name] |
| 114 | + return ok |
| 115 | +} |
| 116 | + |
| 117 | +// AllComplete returns true if all sections have 100% coverage. |
| 118 | +func (cr *CoverageReport) AllComplete() bool { |
| 119 | + for _, section := range cr.Sections { |
| 120 | + if section.Percentage < 100 { |
| 121 | + return false |
| 122 | + } |
| 123 | + } |
| 124 | + return true |
| 125 | +} |
| 126 | + |
| 127 | +// MeetsThreshold returns true if overall coverage meets the threshold. |
| 128 | +func (cr *CoverageReport) MeetsThreshold(threshold int) bool { |
| 129 | + return cr.Overall >= threshold |
| 130 | +} |
| 131 | + |
| 132 | +// SectionsAboveThreshold returns section names with coverage >= threshold. |
| 133 | +func (cr *CoverageReport) SectionsAboveThreshold(threshold int) []string { |
| 134 | + var result []string |
| 135 | + for name, section := range cr.Sections { |
| 136 | + if section.Percentage >= threshold { |
| 137 | + result = append(result, name) |
| 138 | + } |
| 139 | + } |
| 140 | + return result |
| 141 | +} |
| 142 | + |
| 143 | +// SectionsBelowThreshold returns section names with coverage < threshold. |
| 144 | +func (cr *CoverageReport) SectionsBelowThreshold(threshold int) []string { |
| 145 | + var result []string |
| 146 | + for name, section := range cr.Sections { |
| 147 | + if section.Percentage < threshold { |
| 148 | + result = append(result, name) |
| 149 | + } |
| 150 | + } |
| 151 | + return result |
| 152 | +} |
| 153 | + |
| 154 | +// ExtensionKeyCoverage is the standard extension key for coverage data. |
| 155 | +const ExtensionKeyCoverage = "coverage" |
| 156 | + |
| 157 | +// SetCoverage is a convenience method to set coverage on a Rubric. |
| 158 | +func (r *Rubric) SetCoverage(coverage *CoverageReport) { |
| 159 | + r.SetExtension(ExtensionKeyCoverage, coverage) |
| 160 | +} |
| 161 | + |
| 162 | +// GetCoverage retrieves coverage data from a Rubric's extensions. |
| 163 | +// Returns nil if not set or if type assertion fails. |
| 164 | +func (r *Rubric) GetCoverage() *CoverageReport { |
| 165 | + ext := r.GetExtension(ExtensionKeyCoverage) |
| 166 | + if ext == nil { |
| 167 | + return nil |
| 168 | + } |
| 169 | + |
| 170 | + // Handle both direct type and map[string]any (from JSON unmarshal) |
| 171 | + if cr, ok := ext.(*CoverageReport); ok { |
| 172 | + return cr |
| 173 | + } |
| 174 | + |
| 175 | + // Try to convert from map[string]any (common after JSON round-trip) |
| 176 | + if m, ok := ext.(map[string]any); ok { |
| 177 | + return coverageReportFromMap(m) |
| 178 | + } |
| 179 | + |
| 180 | + return nil |
| 181 | +} |
| 182 | + |
| 183 | +// coverageReportFromMap converts a map to CoverageReport. |
| 184 | +// Used when coverage data has been JSON marshaled/unmarshaled. |
| 185 | +func coverageReportFromMap(m map[string]any) *CoverageReport { |
| 186 | + cr := NewCoverageReport() |
| 187 | + |
| 188 | + if overall, ok := m["overall"].(float64); ok { |
| 189 | + cr.Overall = int(overall) |
| 190 | + } |
| 191 | + |
| 192 | + if sections, ok := m["sections"].(map[string]any); ok { |
| 193 | + for name, sectionData := range sections { |
| 194 | + if sectionMap, ok := sectionData.(map[string]any); ok { |
| 195 | + section := CoverageSection{} |
| 196 | + if total, ok := sectionMap["total"].(float64); ok { |
| 197 | + section.Total = int(total) |
| 198 | + } |
| 199 | + if complete, ok := sectionMap["complete"].(float64); ok { |
| 200 | + section.Complete = int(complete) |
| 201 | + } |
| 202 | + if percentage, ok := sectionMap["percentage"].(float64); ok { |
| 203 | + section.Percentage = int(percentage) |
| 204 | + } |
| 205 | + if missing, ok := sectionMap["missing"].([]any); ok { |
| 206 | + for _, item := range missing { |
| 207 | + if s, ok := item.(string); ok { |
| 208 | + section.Missing = append(section.Missing, s) |
| 209 | + } |
| 210 | + } |
| 211 | + } |
| 212 | + cr.Sections[name] = section |
| 213 | + } |
| 214 | + } |
| 215 | + } |
| 216 | + |
| 217 | + return cr |
| 218 | +} |
0 commit comments