// Package diagnostics provides bounded, safe text for deterministic D&D // decisions and warnings. package diagnostics import ( "fmt" "strconv" "strings" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" frameworkdiagnostics "gitea.maximumdirect.net/eric/notarius/internal/framework/diagnostics" ) const ( MaxIssues = 20 MaxWarnings = 20 MaxDisplayedRunes = 128 MaxMessageBytes = 4096 ) // DataQualityResult converts accepted source-quality findings into bounded, // locally grouped advisories. These findings do not indicate process // degradation. func DataQualityResult(findings []contracts.Warning) (contracts.ValidationResult, error) { diagnostics, err := Collect(findings, contracts.DiagnosticDispositionAdvisory, contracts.DiagnosticCategoryDataQuality) if err != nil { return contracts.ValidationResult{}, err } return contracts.ValidationResult{Approved: true, Diagnostics: diagnostics}, nil } // Collect converts individual deterministic findings into bounded, // occurrence-grouped producer diagnostics with one consistent classification. func Collect(findings []contracts.Warning, disposition contracts.DiagnosticDisposition, category contracts.DiagnosticCategory) ([]contracts.ProducerDiagnostic, error) { collector := frameworkdiagnostics.NewCollector() for _, finding := range findings { if err := collector.Add(contracts.ProducerDiagnostic{ Disposition: disposition, Category: category, ReasonCode: finding.ReasonCode, OccurrenceCount: 1, Samples: []contracts.DiagnosticSample{{Scope: finding.Scope, Message: finding.Message}}, }); err != nil { return nil, fmt.Errorf("collect diagnostic: %w", err) } } return collector.Diagnostics(), nil } func Truncate(value string) string { runes := []rune(value) if len(runes) <= MaxDisplayedRunes { return value } return string(runes[:MaxDisplayedRunes-1]) + "…" } func Quote(value string) string { return strconv.Quote(Truncate(value)) } func Aggregate(prefix string, issues []string) string { displayed := make([]string, 0, min(len(issues), MaxIssues)) for len(displayed) < len(issues) && len(displayed) < MaxIssues { issue := Truncate(issues[len(displayed)]) candidate := aggregateMessage(prefix, append(displayed, issue), len(issues)-len(displayed)-1) if len([]byte(candidate)) > MaxMessageBytes { break } displayed = append(displayed, issue) } return aggregateMessage(prefix, displayed, len(issues)-len(displayed)) } // LimitWarnings returns at most MaxWarnings warnings, reserving the final // position for a deterministic omission summary when truncation is required. func LimitWarnings(warnings []contracts.Warning, scope, reasonCode string) []contracts.Warning { if warnings == nil { return nil } if len(warnings) <= MaxWarnings { bounded := make([]contracts.Warning, len(warnings)) copy(bounded, warnings) return bounded } displayed := MaxWarnings - 1 bounded := make([]contracts.Warning, displayed, MaxWarnings) copy(bounded, warnings[:displayed]) bounded = append(bounded, contracts.Warning{ Scope: scope, ReasonCode: reasonCode, Message: fmt.Sprintf("%d additional warning(s) omitted", len(warnings)-displayed), }) return bounded } func aggregateMessage(prefix string, issues []string, omitted int) string { message := prefix + ": " + strings.Join(issues, ", ") if omitted > 0 { message += fmt.Sprintf("; %d additional issue(s) omitted", omitted) } return message } func min(left, right int) int { if left < right { return left } return right }