package diagnostics import ( "errors" "fmt" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" ) const ( MaxWarningGroups = 128 MaxNonWarningGroups = 256 ) // Aggregator merges origin-enriched diagnostics in caller-supplied canonical // order. Its zero value is ready for use. type Aggregator struct { groups []contracts.DiagnosticGroup indices map[groupKey]int omitted map[groupKey]struct{} warningGroups int nonWarningGroups int unrepresentedOccurrences int } // Add validates and incorporates one final diagnostic group. Actionable // warnings cannot overflow; later non-warning groups are represented by exact // unrepresented-occurrence metadata once their fixed bound is reached. func (aggregator *Aggregator) Add(group contracts.DiagnosticGroup) error { if err := group.Validate(); err != nil { return fmt.Errorf("diagnostic group: %w", err) } if aggregator.indices == nil { aggregator.indices = make(map[groupKey]int) aggregator.omitted = make(map[groupKey]struct{}) } key := groupKeyFromGroup(group) if index, exists := aggregator.indices[key]; exists { return aggregator.merge(index, group) } if _, exists := aggregator.omitted[key]; exists { return aggregator.addUnrepresented(group.OccurrenceCount) } if group.Disposition == contracts.DiagnosticDispositionWarning { if aggregator.warningGroups >= MaxWarningGroups { return errors.New("diagnostic warning groups exceed maximum count") } aggregator.warningGroups++ } else if aggregator.nonWarningGroups >= MaxNonWarningGroups { aggregator.omitted[key] = struct{}{} return aggregator.addUnrepresented(group.OccurrenceCount) } else { aggregator.nonWarningGroups++ } aggregator.indices[key] = len(aggregator.groups) aggregator.groups = append(aggregator.groups, contracts.CloneDiagnosticCollection(contracts.DiagnosticCollection{Groups: []contracts.DiagnosticGroup{group}}).Groups[0]) return nil } // Collection returns an independently owned grouped result in first-occurrence // order. func (aggregator *Aggregator) Collection() contracts.DiagnosticCollection { if aggregator == nil { return contracts.DiagnosticCollection{} } return contracts.CloneDiagnosticCollection(contracts.DiagnosticCollection{ Groups: aggregator.groups, Truncated: aggregator.unrepresentedOccurrences > 0, UnrepresentedOccurrenceCount: aggregator.unrepresentedOccurrences, }) } func (aggregator *Aggregator) merge(index int, incoming contracts.DiagnosticGroup) error { current := &aggregator.groups[index] if incoming.OccurrenceCount > maximumInt()-current.OccurrenceCount { return errors.New("diagnostic occurrence count overflow") } current.OccurrenceCount += incoming.OccurrenceCount for _, sample := range incoming.Samples { if len(current.Samples) == contracts.MaxDiagnosticSamples || containsGroupSample(current.Samples, sample) { continue } current.Samples = append(current.Samples, cloneGroupSample(sample)) } current.OmittedSampleCount = current.OccurrenceCount - len(current.Samples) return nil } func (aggregator *Aggregator) addUnrepresented(count int) error { if count > maximumInt()-aggregator.unrepresentedOccurrences { return errors.New("diagnostic unrepresented occurrence count overflow") } aggregator.unrepresentedOccurrences += count return nil } func containsGroupSample(samples []contracts.DiagnosticSample, candidate contracts.DiagnosticSample) bool { for _, sample := range samples { if sample.Scope != candidate.Scope || sample.Message != candidate.Message || sample.ChunkID != candidate.ChunkID { continue } if sample.ChunkIndex == nil || candidate.ChunkIndex == nil { if sample.ChunkIndex == candidate.ChunkIndex { return true } continue } if *sample.ChunkIndex == *candidate.ChunkIndex { return true } } return false } func cloneGroupSample(sample contracts.DiagnosticSample) contracts.DiagnosticSample { if sample.ChunkIndex != nil { value := *sample.ChunkIndex sample.ChunkIndex = &value } return sample } type groupKey struct { disposition contracts.DiagnosticDisposition category contracts.DiagnosticCategory reasonCode string origin contracts.DiagnosticOrigin } func groupKeyFromGroup(group contracts.DiagnosticGroup) groupKey { return groupKey{disposition: group.Disposition, category: group.Category, reasonCode: group.ReasonCode, origin: group.Origin} }