136 lines
4.7 KiB
Go
136 lines
4.7 KiB
Go
// 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
|
|
}
|
|
|
|
// NormalizationDiagnostics classifies deterministic normalization findings as
|
|
// observations, except for explicitly identified unresolved-quality findings.
|
|
func NormalizationDiagnostics(findings []contracts.Warning, advisoryReasonCodes ...string) ([]contracts.ProducerDiagnostic, error) {
|
|
advisoryReasons := make(map[string]struct{}, len(advisoryReasonCodes))
|
|
for _, reasonCode := range advisoryReasonCodes {
|
|
advisoryReasons[reasonCode] = struct{}{}
|
|
}
|
|
observations := make([]contracts.Warning, 0, len(findings))
|
|
advisories := make([]contracts.Warning, 0)
|
|
for _, finding := range findings {
|
|
if _, advisory := advisoryReasons[finding.ReasonCode]; advisory {
|
|
advisories = append(advisories, finding)
|
|
continue
|
|
}
|
|
observations = append(observations, finding)
|
|
}
|
|
diagnostics, err := Collect(observations, contracts.DiagnosticDispositionObservation, contracts.DiagnosticCategoryNormalization)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
qualityDiagnostics, err := Collect(advisories, contracts.DiagnosticDispositionAdvisory, contracts.DiagnosticCategoryDataQuality)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return append(diagnostics, qualityDiagnostics...), 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
|
|
}
|