Finalize structured diagnostic aggregation
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// Package diagnostics provides bounded, safe text for deterministic D&D
|
||||
// decisions and warnings.
|
||||
// decisions and findings.
|
||||
package diagnostics
|
||||
|
||||
import (
|
||||
@@ -13,15 +13,22 @@ import (
|
||||
|
||||
const (
|
||||
MaxIssues = 20
|
||||
MaxWarnings = 20
|
||||
MaxDisplayedRunes = 128
|
||||
MaxMessageBytes = 4096
|
||||
)
|
||||
|
||||
// Finding is a local, ungrouped D&D diagnostic fact. Producers convert these
|
||||
// to bounded framework diagnostics at their result boundary.
|
||||
type Finding struct {
|
||||
Scope string
|
||||
ReasonCode string
|
||||
Message string
|
||||
}
|
||||
|
||||
// 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) {
|
||||
func DataQualityResult(findings []Finding) (contracts.ValidationResult, error) {
|
||||
diagnostics, err := Collect(findings, contracts.DiagnosticDispositionAdvisory, contracts.DiagnosticCategoryDataQuality)
|
||||
if err != nil {
|
||||
return contracts.ValidationResult{}, err
|
||||
@@ -31,7 +38,7 @@ func DataQualityResult(findings []contracts.Warning) (contracts.ValidationResult
|
||||
|
||||
// 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) {
|
||||
func Collect(findings []Finding, disposition contracts.DiagnosticDisposition, category contracts.DiagnosticCategory) ([]contracts.ProducerDiagnostic, error) {
|
||||
collector := frameworkdiagnostics.NewCollector()
|
||||
for _, finding := range findings {
|
||||
if err := collector.Add(contracts.ProducerDiagnostic{
|
||||
@@ -49,13 +56,13 @@ func Collect(findings []contracts.Warning, disposition contracts.DiagnosticDispo
|
||||
|
||||
// NormalizationDiagnostics classifies deterministic normalization findings as
|
||||
// observations, except for explicitly identified unresolved-quality findings.
|
||||
func NormalizationDiagnostics(findings []contracts.Warning, advisoryReasonCodes ...string) ([]contracts.ProducerDiagnostic, error) {
|
||||
func NormalizationDiagnostics(findings []Finding, 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)
|
||||
observations := make([]Finding, 0, len(findings))
|
||||
advisories := make([]Finding, 0)
|
||||
for _, finding := range findings {
|
||||
if _, advisory := advisoryReasons[finding.ReasonCode]; advisory {
|
||||
advisories = append(advisories, finding)
|
||||
@@ -97,28 +104,6 @@ func Aggregate(prefix string, issues []string) string {
|
||||
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 {
|
||||
|
||||
@@ -2,7 +2,6 @@ package diagnostics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
@@ -30,30 +29,8 @@ func TestAggregateEnforcesByteBudgetAndReportsOmissions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimitWarningsBoundsOutputAndReportsOmissions(t *testing.T) {
|
||||
warnings := make([]contracts.Warning, MaxWarnings+3)
|
||||
for index := range warnings {
|
||||
warnings[index] = contracts.Warning{ReasonCode: fmt.Sprintf("warning-%d", index)}
|
||||
}
|
||||
before := append([]contracts.Warning(nil), warnings...)
|
||||
|
||||
got := LimitWarnings(warnings, "records", "warnings_omitted")
|
||||
if len(got) != MaxWarnings {
|
||||
t.Fatalf("LimitWarnings() count = %d, want %d", len(got), MaxWarnings)
|
||||
}
|
||||
summary := got[len(got)-1]
|
||||
wantOmitted := len(warnings) - (MaxWarnings - 1)
|
||||
if summary.Scope != "records" || summary.ReasonCode != "warnings_omitted" ||
|
||||
summary.Message != fmt.Sprintf("%d additional warning(s) omitted", wantOmitted) {
|
||||
t.Fatalf("summary = %#v", summary)
|
||||
}
|
||||
if !reflect.DeepEqual(warnings, before) {
|
||||
t.Fatal("LimitWarnings() mutated its input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataQualityResultGroupsFindingsWithoutWarnings(t *testing.T) {
|
||||
result, err := DataQualityResult([]contracts.Warning{
|
||||
result, err := DataQualityResult([]Finding{
|
||||
{Scope: "records[0]", ReasonCode: "not_near_source", Message: "first"},
|
||||
{Scope: "records[0]", ReasonCode: "not_near_source", Message: "first"},
|
||||
{Scope: "records[1]", ReasonCode: "not_near_source", Message: "second"},
|
||||
@@ -61,7 +38,7 @@ func TestDataQualityResultGroupsFindingsWithoutWarnings(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 {
|
||||
if !result.Approved || len(result.Diagnostics) != 1 {
|
||||
t.Fatalf("result = %#v", result)
|
||||
}
|
||||
diagnostic := result.Diagnostics[0]
|
||||
@@ -71,9 +48,9 @@ func TestDataQualityResultGroupsFindingsWithoutWarnings(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCollectGroupsNormalizationFindingsWithBoundedSamples(t *testing.T) {
|
||||
findings := make([]contracts.Warning, 5)
|
||||
findings := make([]Finding, 5)
|
||||
for index := range findings {
|
||||
findings[index] = contracts.Warning{
|
||||
findings[index] = Finding{
|
||||
Scope: fmt.Sprintf("records[%d]", index),
|
||||
ReasonCode: "record_normalized",
|
||||
Message: fmt.Sprintf("record %d normalized", index),
|
||||
@@ -94,7 +71,7 @@ func TestCollectGroupsNormalizationFindingsWithBoundedSamples(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNormalizationDiagnosticsCombinesQualityAndCleanupWithoutWarnings(t *testing.T) {
|
||||
groups, err := NormalizationDiagnostics([]contracts.Warning{
|
||||
groups, err := NormalizationDiagnostics([]Finding{
|
||||
{Scope: "spell_casts[0]", ReasonCode: "spell_name_canonicalized", Message: "canonicalized spell name"},
|
||||
{Scope: "spell_casts[1]", ReasonCode: "spell_name_unresolved", Message: "spell was not in the catalog"},
|
||||
}, "spell_name_unresolved")
|
||||
|
||||
Reference in New Issue
Block a user