Add diagnostic contract primitives

This commit is contained in:
2026-08-27 15:19:56 +00:00
parent 610dd3d7c3
commit acb04954eb
6 changed files with 751 additions and 1 deletions

View File

@@ -0,0 +1,93 @@
// Package diagnostics provides bounded local grouping for producer and
// validator diagnostic results.
package diagnostics
import (
"errors"
"fmt"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
)
// Collector merges local producer diagnostics by their semantic identity. Its
// zero value is ready for use.
type Collector struct {
diagnostics []contracts.ProducerDiagnostic
indices map[key]int
}
// NewCollector returns an empty local diagnostic collector.
func NewCollector() *Collector {
return &Collector{}
}
// Add validates and merges one producer diagnostic. Every occurrence remains
// counted, while the first three distinct samples in input order are retained.
func (collector *Collector) Add(diagnostic contracts.ProducerDiagnostic) error {
if err := diagnostic.Validate(); err != nil {
return fmt.Errorf("producer diagnostic: %w", err)
}
if collector.indices == nil {
collector.indices = make(map[key]int)
}
diagnosticKey := key{disposition: diagnostic.Disposition, category: diagnostic.Category, reasonCode: diagnostic.ReasonCode}
index, exists := collector.indices[diagnosticKey]
if !exists {
if len(collector.diagnostics) >= contracts.MaxProducerDiagnosticGroups {
return errors.New("producer diagnostics exceed maximum group count")
}
collector.indices[diagnosticKey] = len(collector.diagnostics)
collector.diagnostics = append(collector.diagnostics, contracts.CloneProducerDiagnostics([]contracts.ProducerDiagnostic{diagnostic})[0])
return nil
}
current := &collector.diagnostics[index]
if diagnostic.OccurrenceCount > maximumInt()-current.OccurrenceCount {
return errors.New("producer diagnostic occurrence count overflow")
}
current.OccurrenceCount += diagnostic.OccurrenceCount
for _, sample := range diagnostic.Samples {
if len(current.Samples) == contracts.MaxDiagnosticSamples || containsSample(current.Samples, sample) {
continue
}
current.Samples = append(current.Samples, cloneSample(sample))
}
current.OmittedSampleCount = current.OccurrenceCount - len(current.Samples)
return nil
}
// Diagnostics returns an independently owned snapshot in first-occurrence
// order.
func (collector *Collector) Diagnostics() []contracts.ProducerDiagnostic {
if collector == nil {
return nil
}
return contracts.CloneProducerDiagnostics(collector.diagnostics)
}
func containsSample(samples []contracts.DiagnosticSample, candidate contracts.DiagnosticSample) bool {
for _, sample := range samples {
if sample.Scope == candidate.Scope && sample.Message == candidate.Message {
return true
}
}
return false
}
func cloneSample(sample contracts.DiagnosticSample) contracts.DiagnosticSample {
if sample.ChunkIndex != nil {
chunkIndex := *sample.ChunkIndex
sample.ChunkIndex = &chunkIndex
}
return sample
}
func maximumInt() int {
return int(^uint(0) >> 1)
}
type key struct {
disposition contracts.DiagnosticDisposition
category contracts.DiagnosticCategory
reasonCode string
}

View File

@@ -0,0 +1,103 @@
package diagnostics
import (
"testing"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
)
func TestCollectorCountsOccurrencesAndRetainsDistinctSamplesInOrder(t *testing.T) {
collector := NewCollector()
for _, diagnostic := range []contracts.ProducerDiagnostic{
advisory("one", "first"),
advisory("one", "first"),
advisory("two", "second"),
advisory("three", "third"),
advisory("four", "fourth"),
} {
if err := collector.Add(diagnostic); err != nil {
t.Fatalf("Add() error = %v", err)
}
}
diagnostics := collector.Diagnostics()
if len(diagnostics) != 1 {
t.Fatalf("group count = %d, want 1", len(diagnostics))
}
group := diagnostics[0]
if group.OccurrenceCount != 5 || group.OmittedSampleCount != 2 {
t.Fatalf("group counts = %#v, want five occurrences and two omitted samples", group)
}
if got := []string{group.Samples[0].Scope, group.Samples[1].Scope, group.Samples[2].Scope}; !equalStrings(got, []string{"one", "two", "three"}) {
t.Fatalf("sample order = %#v, want first three distinct samples", got)
}
}
func TestCollectorSeparatesGroupsAndRejectsInvalidOrExcessiveGroups(t *testing.T) {
collector := NewCollector()
if err := collector.Add(advisory("one", "first")); err != nil {
t.Fatalf("Add() error = %v", err)
}
warning := advisory("two", "second")
warning.Disposition = contracts.DiagnosticDispositionWarning
warning.Category = contracts.DiagnosticCategoryFallback
if err := collector.Add(warning); err != nil {
t.Fatalf("Add() error = %v", err)
}
if got := len(collector.Diagnostics()); got != 2 {
t.Fatalf("group count = %d, want 2", got)
}
invalid := advisory("bad", "bad")
invalid.ReasonCode = ""
if err := collector.Add(invalid); err == nil {
t.Fatal("Add() error = nil, want invalid diagnostic error")
}
limited := NewCollector()
for index := 0; index < contracts.MaxProducerDiagnosticGroups; index++ {
diagnostic := advisory("scope", "message")
diagnostic.ReasonCode = "reason-" + string(rune('a'+index))
if err := limited.Add(diagnostic); err != nil {
t.Fatalf("Add(%d) error = %v", index, err)
}
}
if err := limited.Add(advisory("overflow", "overflow")); err == nil {
t.Fatal("Add() error = nil, want local group limit error")
}
}
func TestCollectorReturnsIndependentSnapshots(t *testing.T) {
collector := NewCollector()
if err := collector.Add(advisory("scope", "message")); err != nil {
t.Fatalf("Add() error = %v", err)
}
first := collector.Diagnostics()
first[0].Samples[0].Message = "changed"
second := collector.Diagnostics()
if second[0].Samples[0].Message != "message" {
t.Fatalf("collector snapshot changed = %#v", second)
}
}
func advisory(scope string, message string) contracts.ProducerDiagnostic {
return contracts.ProducerDiagnostic{
Disposition: contracts.DiagnosticDispositionAdvisory,
Category: contracts.DiagnosticCategoryDataQuality,
ReasonCode: "source_unrelated",
OccurrenceCount: 1,
Samples: []contracts.DiagnosticSample{{Scope: scope, Message: message}},
}
}
func equalStrings(left []string, right []string) bool {
if len(left) != len(right) {
return false
}
for index := range left {
if left[index] != right[index] {
return false
}
}
return true
}