Finalize structured diagnostic aggregation

This commit is contained in:
2026-08-27 16:40:17 +00:00
parent 480680b257
commit 4dbbf68051
112 changed files with 656 additions and 893 deletions

View File

@@ -148,23 +148,23 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
return retryResult(recordList(applied), findings, reconciliation)
}
func (n *Normalizer) invalidStructuredResult(value dnd.LocationRegistry, findings []contracts.Warning) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) {
func (n *Normalizer) invalidStructuredResult(value dnd.LocationRegistry, findings []diagnostics.Finding) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) {
return retryResultWithFallback(value, findings, nil, "semantic proposal requires retry: invalid structured output", semanticFallbackFinding(-1))
}
func retryResult(value dnd.LocationRegistry, findings []contracts.Warning, reconciliation semanticreconcile.Result) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) {
func retryResult(value dnd.LocationRegistry, findings []diagnostics.Finding, reconciliation semanticreconcile.Result) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) {
return retryResultWithFallback(value, findings, reconciliation.ModelCandidate(), diagnostics.Aggregate("semantic proposal requires retry", semanticreconcile.IssueDetails(reconciliation.Issues())), semanticFallbackFinding(reconciliation.DiscardedGroupCount()))
}
func semanticFallbackFinding(discarded int) contracts.Warning {
func semanticFallbackFinding(discarded int) diagnostics.Finding {
message := "semantic proposal could not be applied"
if discarded >= 0 {
message = fmt.Sprintf("%d proposal group(s) omitted after semantic proposal retry exhaustion", discarded)
}
return contracts.Warning{Scope: "locations", ReasonCode: ReasonCodeLocationSemanticReconciliationExhausted, Message: message}
return diagnostics.Finding{Scope: "locations", ReasonCode: ReasonCodeLocationSemanticReconciliationExhausted, Message: message}
}
func normalizationResult(value dnd.LocationRegistry, findings []contracts.Warning, candidate *contracts.ModelCandidate) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) {
func normalizationResult(value dnd.LocationRegistry, findings []diagnostics.Finding, candidate *contracts.ModelCandidate) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) {
diagnosticGroups, err := diagnostics.Collect(findings, contracts.DiagnosticDispositionObservation, contracts.DiagnosticCategoryNormalization)
if err != nil {
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, normalizerErrorf("collect normalization diagnostics: %w", err)
@@ -172,12 +172,12 @@ func normalizationResult(value dnd.LocationRegistry, findings []contracts.Warnin
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{Value: value, Diagnostics: diagnosticGroups, ModelCandidate: candidate}, nil
}
func fallbackResult(value dnd.LocationRegistry, findings []contracts.Warning, fallback contracts.Warning) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) {
func fallbackResult(value dnd.LocationRegistry, findings []diagnostics.Finding, fallback diagnostics.Finding) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) {
result, err := normalizationResult(value, findings, nil)
if err != nil {
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, err
}
fallbackGroups, err := diagnostics.Collect([]contracts.Warning{fallback}, contracts.DiagnosticDispositionWarning, contracts.DiagnosticCategoryFallback)
fallbackGroups, err := diagnostics.Collect([]diagnostics.Finding{fallback}, contracts.DiagnosticDispositionWarning, contracts.DiagnosticCategoryFallback)
if err != nil {
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, normalizerErrorf("collect fallback diagnostic: %w", err)
}
@@ -185,12 +185,12 @@ func fallbackResult(value dnd.LocationRegistry, findings []contracts.Warning, fa
return result, nil
}
func retryResultWithFallback(value dnd.LocationRegistry, findings []contracts.Warning, candidate *contracts.ModelCandidate, message string, fallback contracts.Warning) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) {
func retryResultWithFallback(value dnd.LocationRegistry, findings []diagnostics.Finding, candidate *contracts.ModelCandidate, message string, fallback diagnostics.Finding) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) {
result, err := normalizationResult(value, findings, candidate)
if err != nil {
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, err
}
fallbackGroups, err := diagnostics.Collect([]contracts.Warning{fallback}, contracts.DiagnosticDispositionWarning, contracts.DiagnosticCategoryFallback)
fallbackGroups, err := diagnostics.Collect([]diagnostics.Finding{fallback}, contracts.DiagnosticDispositionWarning, contracts.DiagnosticCategoryFallback)
if err != nil {
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, normalizerErrorf("collect fallback diagnostic: %w", err)
}
@@ -204,23 +204,23 @@ type normalizedRecord struct {
earliest int
}
func preprocessRecords(input dnd.LocationRegistry, order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning) {
func preprocessRecords(input dnd.LocationRegistry, order shared.SourceRefOrder) ([]normalizedRecord, []diagnostics.Finding) {
if input.Locations == nil {
return nil, nil
}
records := make([]normalizedRecord, len(input.Locations))
warnings := make([]contracts.Warning, 0)
warnings := make([]diagnostics.Finding, 0)
for index, inputLocation := range input.Locations {
location, fieldsChanged, refsChanged := normalizeRecord(inputLocation, order)
records[index] = normalizedRecord{location: location, inputIndexes: []int{index}, earliest: index}
if fieldsChanged {
warnings = append(warnings, contracts.Warning{Scope: locationScope(index), ReasonCode: ReasonCodeLocationFieldsNormalized, Message: fmt.Sprintf("input index %d: location name normalized for %s", index, diagnostics.Quote(inputLocation.Name))})
warnings = append(warnings, diagnostics.Finding{Scope: locationScope(index), ReasonCode: ReasonCodeLocationFieldsNormalized, Message: fmt.Sprintf("input index %d: location name normalized for %s", index, diagnostics.Quote(inputLocation.Name))})
}
if refsChanged {
warnings = append(warnings, contracts.Warning{Scope: locationScope(index), ReasonCode: ReasonCodeSourceReferencesNormalized, Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)", index, len(inputLocation.SourceRefs), len(location.SourceRefs))})
warnings = append(warnings, diagnostics.Finding{Scope: locationScope(index), ReasonCode: ReasonCodeSourceReferencesNormalized, Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)", index, len(inputLocation.SourceRefs), len(location.SourceRefs))})
}
if inputLocation.ID != location.ID {
warnings = append(warnings, contracts.Warning{Scope: locationScope(index), ReasonCode: ReasonCodeLocationIDRecomputed, Message: fmt.Sprintf("input index %d: location ID recomputed from %s", index, diagnostics.Quote(location.Name))})
warnings = append(warnings, diagnostics.Finding{Scope: locationScope(index), ReasonCode: ReasonCodeLocationIDRecomputed, Message: fmt.Sprintf("input index %d: location ID recomputed from %s", index, diagnostics.Quote(location.Name))})
}
}
groups := exactDuplicateGroups(records)
@@ -342,7 +342,7 @@ func recordList(records []normalizedRecord) dnd.LocationRegistry {
return dnd.LocationRegistry{Locations: recordValues(records)}
}
func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
const maxDisplayedIndices = 20
displayed := removed
if len(displayed) > maxDisplayedIndices {
@@ -356,7 +356,7 @@ func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
if omitted := len(removed) - len(displayed); omitted > 0 {
message += fmt.Sprintf("; %d additional removed input indices omitted", omitted)
}
return contracts.Warning{Scope: locationScope(retainedIndex), ReasonCode: ReasonCodeDuplicateLocationCollapsed, Message: message}
return diagnostics.Finding{Scope: locationScope(retainedIndex), ReasonCode: ReasonCodeDuplicateLocationCollapsed, Message: message}
}
func locationScope(index int) string { return fmt.Sprintf("locations[%d]", index) }

View File

@@ -3,7 +3,6 @@ package locationregistry
import (
"fmt"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/semanticreconcile"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/locations/identity"
@@ -28,7 +27,7 @@ func reconciliationInputs(records []normalizedRecord) ([]semanticreconcile.Candi
return candidates, envelopes, nil
}
func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRecord, envelopes []semanticreconcile.Record[dnd.Location], order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning, error) {
func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRecord, envelopes []semanticreconcile.Record[dnd.Location], order shared.SourceRefOrder) ([]normalizedRecord, []diagnostics.Finding, error) {
application, err := semanticreconcile.ApplyPlan(plan, envelopes, semanticreconcile.ApplicationPolicy[dnd.Location]{
CloneValue: cloneLocation,
ConsolidateGroup: func(members []dnd.Location, canonical dnd.Location) (dnd.Location, error) {
@@ -55,7 +54,7 @@ func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRe
earliest: record.EarliestInputPosition(),
}
}
warnings := make([]contracts.Warning, 0, len(application.AppliedGroups()))
warnings := make([]diagnostics.Finding, 0, len(application.AppliedGroups()))
for _, event := range application.AppliedGroups() {
provenance := event.Provenance()
warnings = append(warnings, semanticDuplicateWarning(provenance, records[provenance.CanonicalPosition()]))
@@ -63,7 +62,7 @@ func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRe
return output, warnings, nil
}
func semanticDuplicateWarning(provenance semanticreconcile.GroupProvenance, canonical normalizedRecord) contracts.Warning {
func semanticDuplicateWarning(provenance semanticreconcile.GroupProvenance, canonical normalizedRecord) diagnostics.Finding {
inputIndexes := provenance.OriginalInputIndexes()
details := make([]string, 0, len(inputIndexes)+1)
for _, inputIndex := range inputIndexes {
@@ -72,7 +71,7 @@ func semanticDuplicateWarning(provenance semanticreconcile.GroupProvenance, cano
if canonical.earliest != provenance.EarliestInputPosition() {
details = append(details, fmt.Sprintf("canonical display name from input index %d", canonical.earliest))
}
return contracts.Warning{
return diagnostics.Finding{
Scope: locationScope(provenance.EarliestInputPosition()),
ReasonCode: ReasonCodeDuplicateLocationCollapsed,
Message: diagnostics.Aggregate("semantic duplicate consolidation", details),