Finalize structured diagnostic aggregation
This commit is contained in:
@@ -179,9 +179,6 @@ func TestPlanReturnsAnnotationFreeSceneRangesFromStructuredOutput(t *testing.T)
|
||||
if len(result.Plan.Annotations) != 0 {
|
||||
t.Fatalf("plan annotations = %#v, want absent", result.Plan.Annotations)
|
||||
}
|
||||
if len(result.Warnings) != 0 {
|
||||
t.Fatalf("warnings = %#v, want absent", result.Warnings)
|
||||
}
|
||||
wantCandidate, err := json.Marshal(client.response)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal expected candidate: %v", err)
|
||||
|
||||
@@ -310,10 +310,10 @@ func TestExtractAppliesSceneEligibilityBeforePromptConstruction(t *testing.T) {
|
||||
t.Fatal("CombatTurns = nil, want accepted non-nil empty list")
|
||||
}
|
||||
if test.wantWarning != "" {
|
||||
if len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].Samples[0].Scope != SceneDescriptionReferenceSlot || result.Diagnostics[0].ReasonCode != test.wantWarning || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionWarning || result.Diagnostics[0].Category != contracts.DiagnosticCategoryDegradation {
|
||||
if len(result.Diagnostics) != 1 || result.Diagnostics[0].Samples[0].Scope != SceneDescriptionReferenceSlot || result.Diagnostics[0].ReasonCode != test.wantWarning || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionWarning || result.Diagnostics[0].Category != contracts.DiagnosticCategoryDegradation {
|
||||
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
||||
}
|
||||
} else if len(result.Warnings) != 0 || len(result.Diagnostics) != 0 {
|
||||
} else if len(result.Diagnostics) != 0 {
|
||||
t.Fatalf("diagnostics = %#v, want none", result.Diagnostics)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -98,10 +98,10 @@ func TestExtractSkipsModelForIneligibleScenes(t *testing.T) {
|
||||
t.Fatalf("result = %#v, calls = %d", result, len(client.requests))
|
||||
}
|
||||
if test.wantWarning {
|
||||
if len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != "scene_classification_unavailable" || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionWarning || result.Diagnostics[0].Category != contracts.DiagnosticCategoryDegradation || result.Diagnostics[0].Samples[0].Scope != SceneDescriptionReferenceSlot {
|
||||
if len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != "scene_classification_unavailable" || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionWarning || result.Diagnostics[0].Category != contracts.DiagnosticCategoryDegradation || result.Diagnostics[0].Samples[0].Scope != SceneDescriptionReferenceSlot {
|
||||
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
||||
}
|
||||
} else if len(result.Warnings) != 0 || len(result.Diagnostics) != 0 {
|
||||
} else if len(result.Diagnostics) != 0 {
|
||||
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -40,10 +40,6 @@ func TestExtractReturnsCanonicalSpellListFromPrivateResponse(t *testing.T) {
|
||||
if !reflect.DeepEqual(result.Value, want) {
|
||||
t.Fatalf("Value = %#v, want %#v", result.Value, want)
|
||||
}
|
||||
if len(result.Warnings) != 0 {
|
||||
t.Fatalf("Warnings = %#v, want none", result.Warnings)
|
||||
}
|
||||
|
||||
if len(client.requests) != 1 {
|
||||
t.Fatalf("LLM calls = %d, want 1", len(client.requests))
|
||||
}
|
||||
|
||||
@@ -132,13 +132,13 @@ type actorCanonicalization struct {
|
||||
to string
|
||||
}
|
||||
|
||||
func normalizeList(input dnd.CombatTurnList, documentIndex source.DocumentIndex, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.CombatTurnList, []contracts.Warning) {
|
||||
func normalizeList(input dnd.CombatTurnList, documentIndex source.DocumentIndex, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.CombatTurnList, []diagnostics.Finding) {
|
||||
if input.CombatTurns == nil {
|
||||
return dnd.CombatTurnList{}, nil
|
||||
}
|
||||
|
||||
records := make([]normalizedRecord, len(input.CombatTurns))
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
for index, inputTurn := range input.CombatTurns {
|
||||
turn, actorChange, refsChanged := normalizeTurn(inputTurn, order, registry)
|
||||
earliest, hasEvidence := order.EarliestValid(turn.SourceRefs)
|
||||
@@ -149,7 +149,7 @@ func normalizeList(input dnd.CombatTurnList, documentIndex source.DocumentIndex,
|
||||
hasEvidence: hasEvidence,
|
||||
}
|
||||
if actorChange != nil {
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: turnScope(index),
|
||||
ReasonCode: ReasonCodeActorCanonicalized,
|
||||
Message: fmt.Sprintf("input index %d: actor canonicalized from %s to %s",
|
||||
@@ -157,7 +157,7 @@ func normalizeList(input dnd.CombatTurnList, documentIndex source.DocumentIndex,
|
||||
})
|
||||
}
|
||||
if refsChanged {
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: turnScope(index),
|
||||
ReasonCode: ReasonCodeSourceRefsNormalized,
|
||||
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)",
|
||||
@@ -179,7 +179,7 @@ func normalizeList(input dnd.CombatTurnList, documentIndex source.DocumentIndex,
|
||||
if position == record.inputIndex {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: turnScope(record.inputIndex),
|
||||
ReasonCode: ReasonCodeTurnsReordered,
|
||||
Message: fmt.Sprintf("input index %d moved to normalized position %d by source chronology",
|
||||
@@ -236,7 +236,7 @@ type duplicateGroup struct {
|
||||
removed []int
|
||||
}
|
||||
|
||||
func collapseDuplicates(records []normalizedRecord, documentIndex source.DocumentIndex) ([]dnd.CombatTurn, []contracts.Warning) {
|
||||
func collapseDuplicates(records []normalizedRecord, documentIndex source.DocumentIndex) ([]dnd.CombatTurn, []diagnostics.Finding) {
|
||||
if len(records) == 0 {
|
||||
return make([]dnd.CombatTurn, 0), nil
|
||||
}
|
||||
@@ -267,7 +267,7 @@ func collapseDuplicates(records []normalizedRecord, documentIndex source.Documen
|
||||
}
|
||||
}
|
||||
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
for _, group := range groups {
|
||||
if len(group.removed) == 0 {
|
||||
continue
|
||||
@@ -309,12 +309,12 @@ func writeKeyInt(builder *strings.Builder, value int) {
|
||||
builder.WriteByte(';')
|
||||
}
|
||||
|
||||
func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
|
||||
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
|
||||
issues := make([]string, len(removed))
|
||||
for index, removedIndex := range removed {
|
||||
issues[index] = fmt.Sprintf("removed input index %d", removedIndex)
|
||||
}
|
||||
return contracts.Warning{
|
||||
return diagnostics.Finding{
|
||||
Scope: turnScope(retainedIndex),
|
||||
ReasonCode: ReasonCodeDuplicateCollapsed,
|
||||
Message: diagnostics.Aggregate(
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
|
||||
)
|
||||
|
||||
func TestNormalizeCanonicalizesFieldsAndRegistryIdentities(t *testing.T) {
|
||||
@@ -61,7 +60,7 @@ func TestNormalizeCanonicalizesFieldsAndRegistryIdentities(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNormalizeLimitsWarningsWithoutChangingCombatTurnValues(t *testing.T) {
|
||||
units := make([]source.SourceUnit, diagnostics.MaxWarnings+1)
|
||||
units := make([]source.SourceUnit, contracts.MaxDiagnosticSamples+1)
|
||||
turns := make([]dnd.CombatTurn, len(units))
|
||||
for index := range units {
|
||||
unitID := index + 1
|
||||
@@ -82,7 +81,7 @@ func TestNormalizeLimitsWarningsWithoutChangingCombatTurnValues(t *testing.T) {
|
||||
if len(result.Value.CombatTurns) != len(turns) || result.Value.CombatTurns[0].Actor != "Aria" {
|
||||
t.Fatalf("normalized turns = %#v, want canonicalized values", result.Value.CombatTurns)
|
||||
}
|
||||
if len(result.Warnings) != 0 || len(result.Diagnostics) == 0 || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionObservation || result.Diagnostics[0].OccurrenceCount != len(turns) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||
if len(result.Diagnostics) == 0 || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionObservation || result.Diagnostics[0].OccurrenceCount != len(turns) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,17 +123,17 @@ type nameCanonicalization struct {
|
||||
to string
|
||||
}
|
||||
|
||||
func normalizeList(input dnd.EnemyEventList, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.EnemyEventList, []contracts.Warning) {
|
||||
func normalizeList(input dnd.EnemyEventList, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.EnemyEventList, []diagnostics.Finding) {
|
||||
if input.Events == nil {
|
||||
return dnd.EnemyEventList{}, nil
|
||||
}
|
||||
records := make([]normalizedRecord, len(input.Events))
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
for index, inputEvent := range input.Events {
|
||||
event, nameChange, refsChanged := normalizeEvent(inputEvent, order, registry)
|
||||
records[index] = normalizedRecord{event: event, identity: enemyeventmodel.CanonicalIdentity(event), inputIndex: index}
|
||||
if nameChange != nil {
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: eventScope(index),
|
||||
ReasonCode: ReasonCodeNameCanonicalized,
|
||||
Message: fmt.Sprintf("input index %d: subject canonicalized from %s to %s",
|
||||
@@ -141,7 +141,7 @@ func normalizeList(input dnd.EnemyEventList, order shared.SourceRefOrder, regist
|
||||
})
|
||||
}
|
||||
if refsChanged {
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: eventScope(index),
|
||||
ReasonCode: ReasonCodeSourceRefsNormalized,
|
||||
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)",
|
||||
@@ -157,7 +157,7 @@ func normalizeList(input dnd.EnemyEventList, order shared.SourceRefOrder, regist
|
||||
if position == record.inputIndex {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: eventScope(record.inputIndex),
|
||||
ReasonCode: ReasonCodeEventsReordered,
|
||||
Message: fmt.Sprintf("input index %d moved to normalized position %d", record.inputIndex, position),
|
||||
@@ -208,7 +208,7 @@ type duplicateGroup struct {
|
||||
removed []int
|
||||
}
|
||||
|
||||
func collapseDuplicates(records []normalizedRecord) ([]dnd.EnemyEvent, []contracts.Warning) {
|
||||
func collapseDuplicates(records []normalizedRecord) ([]dnd.EnemyEvent, []diagnostics.Finding) {
|
||||
if len(records) == 0 {
|
||||
return make([]dnd.EnemyEvent, 0), nil
|
||||
}
|
||||
@@ -235,7 +235,7 @@ func collapseDuplicates(records []normalizedRecord) ([]dnd.EnemyEvent, []contrac
|
||||
for index, record := range kept {
|
||||
output[index] = cloneEvent(record.event)
|
||||
}
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
for _, group := range groups {
|
||||
if len(group.removed) == 0 {
|
||||
continue
|
||||
@@ -244,7 +244,7 @@ func collapseDuplicates(records []normalizedRecord) ([]dnd.EnemyEvent, []contrac
|
||||
for index, removed := range group.removed {
|
||||
issues[index] = fmt.Sprintf("removed input index %d", removed)
|
||||
}
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: eventScope(group.retainedIndex),
|
||||
ReasonCode: ReasonCodeDuplicateCollapsed,
|
||||
Message: diagnostics.Aggregate(
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
|
||||
)
|
||||
|
||||
func TestNormalizeCanonicalizesSubjectsEvidenceOrderAndDuplicates(t *testing.T) {
|
||||
@@ -162,7 +161,7 @@ func TestNormalizerContractAndWarningBound(t *testing.T) {
|
||||
t.Fatalf("unsafe metadata = %s, %v", encoded, err)
|
||||
}
|
||||
|
||||
count := diagnostics.MaxWarnings + 5
|
||||
count := contracts.MaxDiagnosticSamples + 5
|
||||
document := &source.SourceDocument{ID: "session", Units: make([]source.SourceUnit, count)}
|
||||
input := dnd.EnemyEventList{Events: make([]dnd.EnemyEvent, count)}
|
||||
for index := range document.Units {
|
||||
@@ -170,7 +169,7 @@ func TestNormalizerContractAndWarningBound(t *testing.T) {
|
||||
input.Events[index] = dnd.EnemyEvent{Name: " ÁRIA ", Kind: dnd.EnemyEventKindEngaged, SourceRefs: []source.SourceRef{{SourceID: document.ID, StartUnitID: count - index, EndUnitID: count - index}}}
|
||||
}
|
||||
result, err := normalizer.Normalize(context.Background(), normalizeRequest(document, input, contracts.ReferenceSet{}))
|
||||
if err != nil || len(result.Warnings) != 0 || len(result.Diagnostics) == 0 {
|
||||
if err != nil || len(result.Diagnostics) == 0 {
|
||||
t.Fatalf("diagnostics = %#v, %v", result.Diagnostics, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,18 +120,18 @@ type normalizedRecord struct {
|
||||
inputIndex int
|
||||
}
|
||||
|
||||
func normalizeList(input dnd.ItemOccurrenceList, index source.DocumentIndex, order shared.SourceRefOrder, registry *itemregistry.Registry) (dnd.ItemOccurrenceList, []contracts.Warning) {
|
||||
func normalizeList(input dnd.ItemOccurrenceList, index source.DocumentIndex, order shared.SourceRefOrder, registry *itemregistry.Registry) (dnd.ItemOccurrenceList, []diagnostics.Finding) {
|
||||
if input.Occurrences == nil {
|
||||
return dnd.ItemOccurrenceList{}, nil
|
||||
}
|
||||
|
||||
records := make([]normalizedRecord, len(input.Occurrences))
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
for index, inputOccurrence := range input.Occurrences {
|
||||
occurrence, changedFields, found, refsChanged := normalizeOccurrence(inputOccurrence, order, registry)
|
||||
records[index] = normalizedRecord{occurrence: occurrence, identity: itemoccurrencemodel.CanonicalExactIdentity(occurrence), inputIndex: index}
|
||||
if len(changedFields) != 0 {
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: occurrenceScope(index),
|
||||
ReasonCode: ReasonCodeNameCanonicalized,
|
||||
Message: fmt.Sprintf("input index %d: normalized display whitespace in %s", index,
|
||||
@@ -139,15 +139,15 @@ func normalizeList(input dnd.ItemOccurrenceList, index source.DocumentIndex, ord
|
||||
})
|
||||
}
|
||||
if found && inputOccurrence.Name != occurrence.Name {
|
||||
warnings = append(warnings, contracts.Warning{Scope: occurrenceScope(index), ReasonCode: ReasonCodeNameCanonicalized,
|
||||
warnings = append(warnings, diagnostics.Finding{Scope: occurrenceScope(index), ReasonCode: ReasonCodeNameCanonicalized,
|
||||
Message: fmt.Sprintf("input index %d: item name canonicalized from %s to %s", index, diagnostics.Quote(inputOccurrence.Name), diagnostics.Quote(occurrence.Name))})
|
||||
}
|
||||
if !found {
|
||||
warnings = append(warnings, contracts.Warning{Scope: occurrenceScope(index), ReasonCode: ReasonCodeUnknownItemID,
|
||||
warnings = append(warnings, diagnostics.Finding{Scope: occurrenceScope(index), ReasonCode: ReasonCodeUnknownItemID,
|
||||
Message: fmt.Sprintf("input index %d: item ID %s is not in the supplied registry", index, diagnostics.Quote(inputOccurrence.ItemID))})
|
||||
}
|
||||
if refsChanged {
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: occurrenceScope(index),
|
||||
ReasonCode: ReasonCodeSourceRefsNormalized,
|
||||
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)",
|
||||
@@ -163,7 +163,7 @@ func normalizeList(input dnd.ItemOccurrenceList, index source.DocumentIndex, ord
|
||||
if position == record.inputIndex {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: occurrenceScope(record.inputIndex),
|
||||
ReasonCode: ReasonCodeOccurrencesReordered,
|
||||
Message: fmt.Sprintf("input index %d moved to normalized position %d", record.inputIndex, position),
|
||||
@@ -217,7 +217,7 @@ type duplicateGroup struct {
|
||||
removed []int
|
||||
}
|
||||
|
||||
func collapseDuplicates(records []normalizedRecord, index source.DocumentIndex) ([]dnd.ItemOccurrence, []contracts.Warning) {
|
||||
func collapseDuplicates(records []normalizedRecord, index source.DocumentIndex) ([]dnd.ItemOccurrence, []diagnostics.Finding) {
|
||||
if len(records) == 0 {
|
||||
return make([]dnd.ItemOccurrence, 0), nil
|
||||
}
|
||||
@@ -253,7 +253,7 @@ func collapseDuplicates(records []normalizedRecord, index source.DocumentIndex)
|
||||
output = append(output, cloneOccurrence(record.occurrence))
|
||||
}
|
||||
}
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
for _, group := range groups {
|
||||
if len(group.removed) != 0 {
|
||||
warnings = append(warnings, duplicateWarning(group.retainedIndex, group.removed))
|
||||
@@ -262,12 +262,12 @@ func collapseDuplicates(records []normalizedRecord, index source.DocumentIndex)
|
||||
return output, warnings
|
||||
}
|
||||
|
||||
func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
|
||||
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
|
||||
issues := make([]string, len(removed))
|
||||
for index, removedIndex := range removed {
|
||||
issues[index] = fmt.Sprintf("removed input index %d", removedIndex)
|
||||
}
|
||||
return contracts.Warning{
|
||||
return diagnostics.Finding{
|
||||
Scope: occurrenceScope(retainedIndex),
|
||||
ReasonCode: ReasonCodeDuplicateCollapsed,
|
||||
Message: diagnostics.Aggregate(
|
||||
|
||||
@@ -149,11 +149,11 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
||||
return retryResult(recordList(applied), findings, advisoryFindings, reconciliation, rejectedGroups)
|
||||
}
|
||||
|
||||
func (n *Normalizer) invalidStructuredResult(value dnd.ItemRegistry, findings []contracts.Warning) (contracts.TypedNormalizeResult[dnd.ItemRegistry], error) {
|
||||
func (n *Normalizer) invalidStructuredResult(value dnd.ItemRegistry, findings []diagnostics.Finding) (contracts.TypedNormalizeResult[dnd.ItemRegistry], error) {
|
||||
return retryResultWithFallback(value, findings, nil, nil, ReasonCodeItemSemanticRetryProposalInvalid, "semantic proposal requires retry: invalid structured output", semanticFallbackFinding(-1))
|
||||
}
|
||||
|
||||
func retryResult(value dnd.ItemRegistry, findings, advisoryFindings []contracts.Warning, reconciliation semanticreconcile.Result, rejectedGroups int) (contracts.TypedNormalizeResult[dnd.ItemRegistry], error) {
|
||||
func retryResult(value dnd.ItemRegistry, findings, advisoryFindings []diagnostics.Finding, reconciliation semanticreconcile.Result, rejectedGroups int) (contracts.TypedNormalizeResult[dnd.ItemRegistry], error) {
|
||||
details := semanticreconcile.IssueDetails(reconciliation.Issues())
|
||||
if rejectedGroups > 0 {
|
||||
details = append(details, "currency may only be consolidated with aliases of one denomination")
|
||||
@@ -162,15 +162,15 @@ func retryResult(value dnd.ItemRegistry, findings, advisoryFindings []contracts.
|
||||
return retryResultWithFallback(value, findings, advisoryFindings, reconciliation.ModelCandidate(), ReasonCodeItemSemanticRetryProposalInvalid, diagnostics.Aggregate("semantic proposal requires retry", details), semanticFallbackFinding(discardedGroups))
|
||||
}
|
||||
|
||||
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: "items", ReasonCode: ReasonCodeItemSemanticReconciliationExhausted, Message: message}
|
||||
return diagnostics.Finding{Scope: "items", ReasonCode: ReasonCodeItemSemanticReconciliationExhausted, Message: message}
|
||||
}
|
||||
|
||||
func normalizationResult(value dnd.ItemRegistry, findings, advisoryFindings []contracts.Warning, candidate *contracts.ModelCandidate) (contracts.TypedNormalizeResult[dnd.ItemRegistry], error) {
|
||||
func normalizationResult(value dnd.ItemRegistry, findings, advisoryFindings []diagnostics.Finding, candidate *contracts.ModelCandidate) (contracts.TypedNormalizeResult[dnd.ItemRegistry], error) {
|
||||
diagnosticGroups, err := diagnostics.Collect(findings, contracts.DiagnosticDispositionObservation, contracts.DiagnosticCategoryNormalization)
|
||||
if err != nil {
|
||||
return contracts.TypedNormalizeResult[dnd.ItemRegistry]{}, normalizerErrorf("collect normalization diagnostics: %w", err)
|
||||
@@ -183,12 +183,12 @@ func normalizationResult(value dnd.ItemRegistry, findings, advisoryFindings []co
|
||||
return contracts.TypedNormalizeResult[dnd.ItemRegistry]{Value: value, Diagnostics: diagnosticGroups, ModelCandidate: candidate}, nil
|
||||
}
|
||||
|
||||
func fallbackResult(value dnd.ItemRegistry, findings, advisoryFindings []contracts.Warning, fallback contracts.Warning) (contracts.TypedNormalizeResult[dnd.ItemRegistry], error) {
|
||||
func fallbackResult(value dnd.ItemRegistry, findings, advisoryFindings []diagnostics.Finding, fallback diagnostics.Finding) (contracts.TypedNormalizeResult[dnd.ItemRegistry], error) {
|
||||
result, err := normalizationResult(value, findings, advisoryFindings, nil)
|
||||
if err != nil {
|
||||
return contracts.TypedNormalizeResult[dnd.ItemRegistry]{}, 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.ItemRegistry]{}, normalizerErrorf("collect fallback diagnostic: %w", err)
|
||||
}
|
||||
@@ -196,12 +196,12 @@ func fallbackResult(value dnd.ItemRegistry, findings, advisoryFindings []contrac
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func retryResultWithFallback(value dnd.ItemRegistry, findings, advisoryFindings []contracts.Warning, candidate *contracts.ModelCandidate, reasonCode, message string, fallback contracts.Warning) (contracts.TypedNormalizeResult[dnd.ItemRegistry], error) {
|
||||
func retryResultWithFallback(value dnd.ItemRegistry, findings, advisoryFindings []diagnostics.Finding, candidate *contracts.ModelCandidate, reasonCode, message string, fallback diagnostics.Finding) (contracts.TypedNormalizeResult[dnd.ItemRegistry], error) {
|
||||
result, err := normalizationResult(value, findings, advisoryFindings, candidate)
|
||||
if err != nil {
|
||||
return contracts.TypedNormalizeResult[dnd.ItemRegistry]{}, 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.ItemRegistry]{}, normalizerErrorf("collect fallback diagnostic: %w", err)
|
||||
}
|
||||
@@ -215,23 +215,23 @@ type normalizedRecord struct {
|
||||
earliest int
|
||||
}
|
||||
|
||||
func preprocessRecords(input dnd.ItemRegistry, order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning) {
|
||||
func preprocessRecords(input dnd.ItemRegistry, order shared.SourceRefOrder) ([]normalizedRecord, []diagnostics.Finding) {
|
||||
if input.Items == nil {
|
||||
return nil, nil
|
||||
}
|
||||
records := make([]normalizedRecord, len(input.Items))
|
||||
findings := make([]contracts.Warning, 0)
|
||||
findings := make([]diagnostics.Finding, 0)
|
||||
for index, inputItem := range input.Items {
|
||||
item, fieldsChanged, refsChanged := normalizeRecord(inputItem, order)
|
||||
records[index] = normalizedRecord{item: item, inputIndexes: []int{index}, earliest: index}
|
||||
if fieldsChanged {
|
||||
findings = append(findings, contracts.Warning{Scope: itemScope(index), ReasonCode: ReasonCodeItemFieldsNormalized, Message: fmt.Sprintf("input index %d: item name normalized for %s", index, diagnostics.Quote(inputItem.Name))})
|
||||
findings = append(findings, diagnostics.Finding{Scope: itemScope(index), ReasonCode: ReasonCodeItemFieldsNormalized, Message: fmt.Sprintf("input index %d: item name normalized for %s", index, diagnostics.Quote(inputItem.Name))})
|
||||
}
|
||||
if refsChanged {
|
||||
findings = append(findings, contracts.Warning{Scope: itemScope(index), ReasonCode: ReasonCodeSourceReferencesNormalized, Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)", index, len(inputItem.SourceRefs), len(item.SourceRefs))})
|
||||
findings = append(findings, diagnostics.Finding{Scope: itemScope(index), ReasonCode: ReasonCodeSourceReferencesNormalized, Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)", index, len(inputItem.SourceRefs), len(item.SourceRefs))})
|
||||
}
|
||||
if inputItem.ID != item.ID {
|
||||
findings = append(findings, contracts.Warning{Scope: itemScope(index), ReasonCode: ReasonCodeItemIDRecomputed, Message: fmt.Sprintf("input index %d: item ID recomputed from %s", index, diagnostics.Quote(item.Name))})
|
||||
findings = append(findings, diagnostics.Finding{Scope: itemScope(index), ReasonCode: ReasonCodeItemIDRecomputed, Message: fmt.Sprintf("input index %d: item ID recomputed from %s", index, diagnostics.Quote(item.Name))})
|
||||
}
|
||||
}
|
||||
groups := comparisonNameGroups(records)
|
||||
@@ -328,7 +328,7 @@ func recordList(records []normalizedRecord) dnd.ItemRegistry {
|
||||
return dnd.ItemRegistry{Items: 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 {
|
||||
@@ -342,7 +342,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: itemScope(retainedIndex), ReasonCode: ReasonCodeDuplicateItemCollapsed, Message: message}
|
||||
return diagnostics.Finding{Scope: itemScope(retainedIndex), ReasonCode: ReasonCodeDuplicateItemCollapsed, Message: message}
|
||||
}
|
||||
func itemScope(index int) string { return fmt.Sprintf("items[%d]", index) }
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"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/items/identity"
|
||||
@@ -31,7 +30,7 @@ func reconciliationInputs(records []normalizedRecord) ([]semanticreconcile.Candi
|
||||
return candidates, envelopes, nil
|
||||
}
|
||||
|
||||
func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRecord, envelopes []semanticreconcile.Record[dnd.Item], order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning, []contracts.Warning, int, error) {
|
||||
func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRecord, envelopes []semanticreconcile.Record[dnd.Item], order shared.SourceRefOrder) ([]normalizedRecord, []diagnostics.Finding, []diagnostics.Finding, int, error) {
|
||||
application, err := semanticreconcile.ApplyPlan(plan, envelopes, semanticreconcile.ApplicationPolicy[dnd.Item]{
|
||||
CloneValue: cloneItem,
|
||||
RejectGroup: func(members []dnd.Item, _ dnd.Item) semanticreconcile.RejectionCategory {
|
||||
@@ -66,7 +65,7 @@ func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRe
|
||||
}
|
||||
type orderedFinding struct {
|
||||
position int
|
||||
finding contracts.Warning
|
||||
finding diagnostics.Finding
|
||||
}
|
||||
observations := make([]orderedFinding, 0, len(application.AppliedGroups()))
|
||||
advisories := make([]orderedFinding, 0, len(application.RejectedGroups()))
|
||||
@@ -81,7 +80,7 @@ func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRe
|
||||
provenance := event.Provenance()
|
||||
advisories = append(advisories, orderedFinding{
|
||||
position: provenance.EarliestInputPosition(),
|
||||
finding: contracts.Warning{
|
||||
finding: diagnostics.Finding{
|
||||
Scope: itemScope(provenance.EarliestInputPosition()),
|
||||
ReasonCode: ReasonCodeItemSemanticProposalInvalid,
|
||||
Message: "proposal group preserved because currency may only be consolidated with aliases of one denomination",
|
||||
@@ -90,11 +89,11 @@ func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRe
|
||||
}
|
||||
sort.SliceStable(observations, func(left, right int) bool { return observations[left].position < observations[right].position })
|
||||
sort.SliceStable(advisories, func(left, right int) bool { return advisories[left].position < advisories[right].position })
|
||||
observationFindings := make([]contracts.Warning, len(observations))
|
||||
observationFindings := make([]diagnostics.Finding, len(observations))
|
||||
for index, entry := range observations {
|
||||
observationFindings[index] = entry.finding
|
||||
}
|
||||
advisoryFindings := make([]contracts.Warning, len(advisories))
|
||||
advisoryFindings := make([]diagnostics.Finding, len(advisories))
|
||||
for index, entry := range advisories {
|
||||
advisoryFindings[index] = entry.finding
|
||||
}
|
||||
@@ -139,7 +138,7 @@ func currencyDenomination(name string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func semanticDuplicateFinding(provenance semanticreconcile.GroupProvenance, canonical normalizedRecord) contracts.Warning {
|
||||
func semanticDuplicateFinding(provenance semanticreconcile.GroupProvenance, canonical normalizedRecord) diagnostics.Finding {
|
||||
inputIndexes := provenance.OriginalInputIndexes()
|
||||
details := make([]string, 0, len(inputIndexes)+1)
|
||||
for _, inputIndex := range inputIndexes {
|
||||
@@ -148,7 +147,7 @@ func semanticDuplicateFinding(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: itemScope(provenance.EarliestInputPosition()),
|
||||
ReasonCode: ReasonCodeDuplicateItemCollapsed,
|
||||
Message: diagnostics.Aggregate("semantic duplicate consolidation", details),
|
||||
|
||||
@@ -122,25 +122,25 @@ type normalizedRecord struct {
|
||||
|
||||
type nameCanonicalization struct{ from, to string }
|
||||
|
||||
func normalizeList(input dnd.LocationOccurrenceList, documentIndex source.DocumentIndex, order shared.SourceRefOrder, registry *locationregistry.Registry) (dnd.LocationOccurrenceList, []contracts.Warning) {
|
||||
func normalizeList(input dnd.LocationOccurrenceList, documentIndex source.DocumentIndex, order shared.SourceRefOrder, registry *locationregistry.Registry) (dnd.LocationOccurrenceList, []diagnostics.Finding) {
|
||||
if input.Occurrences == nil {
|
||||
return dnd.LocationOccurrenceList{}, nil
|
||||
}
|
||||
records := make([]normalizedRecord, len(input.Occurrences))
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
for index, inputOccurrence := range input.Occurrences {
|
||||
occurrence, change, found, refsChanged := normalizeOccurrence(inputOccurrence, order, registry)
|
||||
records[index] = normalizedRecord{occurrence: occurrence, inputIndex: index}
|
||||
if change != nil {
|
||||
warnings = append(warnings, contracts.Warning{Scope: occurrenceScope(index), ReasonCode: ReasonCodeNameCanonicalized,
|
||||
warnings = append(warnings, diagnostics.Finding{Scope: occurrenceScope(index), ReasonCode: ReasonCodeNameCanonicalized,
|
||||
Message: fmt.Sprintf("input index %d: location name canonicalized from %s to %s", index, diagnostics.Quote(change.from), diagnostics.Quote(change.to))})
|
||||
}
|
||||
if !found {
|
||||
warnings = append(warnings, contracts.Warning{Scope: occurrenceScope(index), ReasonCode: ReasonCodeUnknownLocationID,
|
||||
warnings = append(warnings, diagnostics.Finding{Scope: occurrenceScope(index), ReasonCode: ReasonCodeUnknownLocationID,
|
||||
Message: fmt.Sprintf("input index %d: location ID %s is not in the supplied registry", index, diagnostics.Quote(inputOccurrence.LocationID))})
|
||||
}
|
||||
if refsChanged {
|
||||
warnings = append(warnings, contracts.Warning{Scope: occurrenceScope(index), ReasonCode: ReasonCodeSourceRefsNormalized,
|
||||
warnings = append(warnings, diagnostics.Finding{Scope: occurrenceScope(index), ReasonCode: ReasonCodeSourceRefsNormalized,
|
||||
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)", index, len(inputOccurrence.SourceRefs), len(occurrence.SourceRefs))})
|
||||
}
|
||||
}
|
||||
@@ -149,7 +149,7 @@ func normalizeList(input dnd.LocationOccurrenceList, documentIndex source.Docume
|
||||
})
|
||||
for position, record := range records {
|
||||
if position != record.inputIndex {
|
||||
warnings = append(warnings, contracts.Warning{Scope: occurrenceScope(record.inputIndex), ReasonCode: ReasonCodeOccurrencesReordered,
|
||||
warnings = append(warnings, diagnostics.Finding{Scope: occurrenceScope(record.inputIndex), ReasonCode: ReasonCodeOccurrencesReordered,
|
||||
Message: fmt.Sprintf("input index %d moved to normalized position %d by canonical occurrence order", record.inputIndex, position)})
|
||||
}
|
||||
}
|
||||
@@ -194,7 +194,7 @@ type duplicateGroup struct {
|
||||
removed []int
|
||||
}
|
||||
|
||||
func collapseDuplicates(records []normalizedRecord, documentIndex source.DocumentIndex) ([]dnd.LocationOccurrence, []contracts.Warning) {
|
||||
func collapseDuplicates(records []normalizedRecord, documentIndex source.DocumentIndex) ([]dnd.LocationOccurrence, []diagnostics.Finding) {
|
||||
if len(records) == 0 {
|
||||
return make([]dnd.LocationOccurrence, 0), nil
|
||||
}
|
||||
@@ -222,7 +222,7 @@ func collapseDuplicates(records []normalizedRecord, documentIndex source.Documen
|
||||
output = append(output, cloneOccurrence(record.occurrence))
|
||||
}
|
||||
}
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
for _, group := range groups {
|
||||
if len(group.removed) > 0 {
|
||||
warnings = append(warnings, duplicateWarning(group.retainedIndex, group.removed))
|
||||
@@ -316,12 +316,12 @@ func sourceRefsLess(order shared.SourceRefOrder, left, right []source.SourceRef)
|
||||
return len(left) < len(right)
|
||||
}
|
||||
|
||||
func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
|
||||
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
|
||||
issues := make([]string, len(removed))
|
||||
for index, removedIndex := range removed {
|
||||
issues[index] = fmt.Sprintf("removed input index %d", removedIndex)
|
||||
}
|
||||
return contracts.Warning{Scope: occurrenceScope(retainedIndex), ReasonCode: ReasonCodeDuplicateCollapsed,
|
||||
return diagnostics.Finding{Scope: occurrenceScope(retainedIndex), ReasonCode: ReasonCodeDuplicateCollapsed,
|
||||
Message: diagnostics.Aggregate(fmt.Sprintf("duplicate location occurrence collapsed; retained input index %d", retainedIndex), issues)}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
locationcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationregistry"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/locations/identity"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
|
||||
)
|
||||
|
||||
func TestNormalizeCanonicalizesNamesByIDAndClonesInputs(t *testing.T) {
|
||||
@@ -125,7 +124,7 @@ func TestNormalizerContractsRequiredRegistryAndWarningBounds(t *testing.T) {
|
||||
t.Fatalf("unbound registry error = %v", err)
|
||||
}
|
||||
|
||||
count := diagnostics.MaxWarnings + 5
|
||||
count := contracts.MaxDiagnosticSamples + 5
|
||||
doc := &source.SourceDocument{ID: "session", Units: make([]source.SourceUnit, count)}
|
||||
input := dnd.LocationOccurrenceList{Occurrences: make([]dnd.LocationOccurrence, count)}
|
||||
location := registryLocations("The Mill").Locations[0]
|
||||
@@ -134,7 +133,7 @@ func TestNormalizerContractsRequiredRegistryAndWarningBounds(t *testing.T) {
|
||||
input.Occurrences[index] = dnd.LocationOccurrence{LocationID: location.ID, Name: "not canonical", Kind: dnd.LocationOccurrenceKindMentioned, SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: count - index, EndUnitID: count - index}}}
|
||||
}
|
||||
bounded, err := newNormalizer(t, registryReferences(t, dnd.LocationRegistry{Locations: []dnd.Location{location}})).Normalize(context.Background(), normalizeRequest(input, doc, contracts.ReferenceSet{}))
|
||||
if err != nil || len(bounded.Warnings) != 0 || len(bounded.Diagnostics) == 0 {
|
||||
if err != nil || len(bounded.Diagnostics) == 0 {
|
||||
t.Fatalf("bounded diagnostics = %#v, %v", bounded.Diagnostics, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -124,13 +124,13 @@ type normalizedRecord struct {
|
||||
inputIndex int
|
||||
}
|
||||
|
||||
func normalizeList(input dnd.NPCOccurrenceList, documentIndex source.DocumentIndex, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.NPCOccurrenceList, []contracts.Warning, error) {
|
||||
func normalizeList(input dnd.NPCOccurrenceList, documentIndex source.DocumentIndex, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.NPCOccurrenceList, []diagnostics.Finding, error) {
|
||||
if input.Occurrences == nil {
|
||||
return dnd.NPCOccurrenceList{}, nil, nil
|
||||
}
|
||||
|
||||
records := make([]normalizedRecord, len(input.Occurrences))
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
for index, inputOccurrence := range input.Occurrences {
|
||||
occurrence, refsChanged, err := normalizeOccurrence(inputOccurrence, order, registry)
|
||||
if err != nil {
|
||||
@@ -138,7 +138,7 @@ func normalizeList(input dnd.NPCOccurrenceList, documentIndex source.DocumentInd
|
||||
}
|
||||
records[index] = normalizedRecord{occurrence: occurrence, inputIndex: index}
|
||||
if refsChanged {
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: occurrenceScope(index),
|
||||
ReasonCode: ReasonCodeSourceRefsNormalized,
|
||||
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)",
|
||||
@@ -154,7 +154,7 @@ func normalizeList(input dnd.NPCOccurrenceList, documentIndex source.DocumentInd
|
||||
if position == record.inputIndex {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: occurrenceScope(record.inputIndex),
|
||||
ReasonCode: ReasonCodeOccurrencesReordered,
|
||||
Message: fmt.Sprintf("input index %d moved to normalized position %d by source chronology", record.inputIndex, position),
|
||||
@@ -192,7 +192,7 @@ type duplicateGroup struct {
|
||||
removed []int
|
||||
}
|
||||
|
||||
func collapseDuplicates(records []normalizedRecord, documentIndex source.DocumentIndex) ([]dnd.NPCOccurrence, []contracts.Warning) {
|
||||
func collapseDuplicates(records []normalizedRecord, documentIndex source.DocumentIndex) ([]dnd.NPCOccurrence, []diagnostics.Finding) {
|
||||
if len(records) == 0 {
|
||||
return make([]dnd.NPCOccurrence, 0), nil
|
||||
}
|
||||
@@ -220,7 +220,7 @@ func collapseDuplicates(records []normalizedRecord, documentIndex source.Documen
|
||||
output = append(output, cloneOccurrence(record.occurrence))
|
||||
}
|
||||
}
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
for _, group := range groups {
|
||||
if len(group.removed) != 0 {
|
||||
warnings = append(warnings, duplicateWarning(group.retainedIndex, group.removed))
|
||||
@@ -229,12 +229,12 @@ func collapseDuplicates(records []normalizedRecord, documentIndex source.Documen
|
||||
return output, warnings
|
||||
}
|
||||
|
||||
func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
|
||||
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
|
||||
issues := make([]string, len(removed))
|
||||
for index, removedIndex := range removed {
|
||||
issues[index] = fmt.Sprintf("removed input index %d", removedIndex)
|
||||
}
|
||||
return contracts.Warning{
|
||||
return diagnostics.Finding{
|
||||
Scope: occurrenceScope(retainedIndex),
|
||||
ReasonCode: ReasonCodeDuplicateCollapsed,
|
||||
Message: diagnostics.Aggregate(
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
|
||||
)
|
||||
|
||||
func TestNormalizeValidatesPairsAndClones(t *testing.T) {
|
||||
@@ -141,7 +140,7 @@ func TestNormalizerContractAndDeterministicWarnings(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNormalizeBoundsWarnings(t *testing.T) {
|
||||
count := diagnostics.MaxWarnings + 5
|
||||
count := contracts.MaxDiagnosticSamples + 5
|
||||
doc := &source.SourceDocument{ID: "session", Units: make([]source.SourceUnit, count)}
|
||||
input := dnd.NPCOccurrenceList{Occurrences: make([]dnd.NPCOccurrence, count)}
|
||||
for index := range doc.Units {
|
||||
@@ -163,7 +162,7 @@ func TestNormalizeBoundsWarnings(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(result.Warnings) != 0 || len(result.Diagnostics) == 0 || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionObservation {
|
||||
if len(result.Diagnostics) == 0 || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionObservation {
|
||||
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,23 +147,23 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
||||
return retryResult(recordList(applied), findings, reconciliation)
|
||||
}
|
||||
|
||||
func (n *Normalizer) invalidStructuredResult(value dnd.NPCRegistry, findings []contracts.Warning) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
|
||||
func (n *Normalizer) invalidStructuredResult(value dnd.NPCRegistry, findings []diagnostics.Finding) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
|
||||
return retryResultWithFallback(value, findings, nil, "semantic proposal requires retry: invalid structured output", semanticFallbackFinding(-1))
|
||||
}
|
||||
|
||||
func retryResult(value dnd.NPCRegistry, findings []contracts.Warning, reconciliation semanticreconcile.Result) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
|
||||
func retryResult(value dnd.NPCRegistry, findings []diagnostics.Finding, reconciliation semanticreconcile.Result) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
|
||||
return retryResultWithFallback(value, findings, reconciliation.ModelCandidate(), diagnostics.Aggregate("semantic proposal requires retry", semanticreconcile.IssueDetails(reconciliation.Issues())), semanticFallbackFinding(reconciliation.DiscardedGroupCount()))
|
||||
}
|
||||
|
||||
func semanticFallbackFinding(discardedGroups int) contracts.Warning {
|
||||
func semanticFallbackFinding(discardedGroups int) diagnostics.Finding {
|
||||
message := "semantic proposal could not be applied"
|
||||
if discardedGroups >= 0 {
|
||||
message = fmt.Sprintf("%d proposal group(s) omitted after semantic proposal retry exhaustion", discardedGroups)
|
||||
}
|
||||
return contracts.Warning{Scope: "npcs", ReasonCode: ReasonCodeNPCSemanticReconciliationExhausted, Message: message}
|
||||
return diagnostics.Finding{Scope: "npcs", ReasonCode: ReasonCodeNPCSemanticReconciliationExhausted, Message: message}
|
||||
}
|
||||
|
||||
func normalizationResult(value dnd.NPCRegistry, findings []contracts.Warning, candidate *contracts.ModelCandidate) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
|
||||
func normalizationResult(value dnd.NPCRegistry, findings []diagnostics.Finding, candidate *contracts.ModelCandidate) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
|
||||
diagnosticGroups, err := diagnostics.Collect(findings, contracts.DiagnosticDispositionObservation, contracts.DiagnosticCategoryNormalization)
|
||||
if err != nil {
|
||||
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, normalizerErrorf("collect normalization diagnostics: %w", err)
|
||||
@@ -171,12 +171,12 @@ func normalizationResult(value dnd.NPCRegistry, findings []contracts.Warning, ca
|
||||
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{Value: value, Diagnostics: diagnosticGroups, ModelCandidate: candidate}, nil
|
||||
}
|
||||
|
||||
func fallbackResult(value dnd.NPCRegistry, findings []contracts.Warning, fallback contracts.Warning) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
|
||||
func fallbackResult(value dnd.NPCRegistry, findings []diagnostics.Finding, fallback diagnostics.Finding) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
|
||||
result, err := normalizationResult(value, findings, nil)
|
||||
if err != nil {
|
||||
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, 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.NPCRegistry]{}, normalizerErrorf("collect fallback diagnostic: %w", err)
|
||||
}
|
||||
@@ -184,12 +184,12 @@ func fallbackResult(value dnd.NPCRegistry, findings []contracts.Warning, fallbac
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func retryResultWithFallback(value dnd.NPCRegistry, findings []contracts.Warning, candidate *contracts.ModelCandidate, message string, fallback contracts.Warning) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
|
||||
func retryResultWithFallback(value dnd.NPCRegistry, findings []diagnostics.Finding, candidate *contracts.ModelCandidate, message string, fallback diagnostics.Finding) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
|
||||
result, err := normalizationResult(value, findings, candidate)
|
||||
if err != nil {
|
||||
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, 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.NPCRegistry]{}, normalizerErrorf("collect fallback diagnostic: %w", err)
|
||||
}
|
||||
@@ -203,23 +203,23 @@ type normalizedRecord struct {
|
||||
earliest int
|
||||
}
|
||||
|
||||
func preprocessRecords(input dnd.NPCRegistry, order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning) {
|
||||
func preprocessRecords(input dnd.NPCRegistry, order shared.SourceRefOrder) ([]normalizedRecord, []diagnostics.Finding) {
|
||||
if input.NPCs == nil {
|
||||
return nil, nil
|
||||
}
|
||||
records := make([]normalizedRecord, len(input.NPCs))
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
for index, inputNPC := range input.NPCs {
|
||||
npc, fieldsChanged, referencesChanged := normalizeRecord(inputNPC, order)
|
||||
records[index] = normalizedRecord{npc: npc, inputIndexes: []int{index}, earliest: index}
|
||||
if fieldsChanged {
|
||||
warnings = append(warnings, contracts.Warning{Scope: npcScope(index), ReasonCode: ReasonCodeNPCFieldsNormalized, Message: fmt.Sprintf("input index %d: NPC name normalized for %s", index, diagnostics.Quote(inputNPC.Name))})
|
||||
warnings = append(warnings, diagnostics.Finding{Scope: npcScope(index), ReasonCode: ReasonCodeNPCFieldsNormalized, Message: fmt.Sprintf("input index %d: NPC name normalized for %s", index, diagnostics.Quote(inputNPC.Name))})
|
||||
}
|
||||
if referencesChanged {
|
||||
warnings = append(warnings, contracts.Warning{Scope: npcScope(index), ReasonCode: ReasonCodeSourceReferencesNormalized, Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)", index, len(inputNPC.SourceRefs), len(npc.SourceRefs))})
|
||||
warnings = append(warnings, diagnostics.Finding{Scope: npcScope(index), ReasonCode: ReasonCodeSourceReferencesNormalized, Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)", index, len(inputNPC.SourceRefs), len(npc.SourceRefs))})
|
||||
}
|
||||
if inputNPC.ID != npc.ID {
|
||||
warnings = append(warnings, contracts.Warning{Scope: npcScope(index), ReasonCode: ReasonCodeNPCIDRecomputed, Message: fmt.Sprintf("input index %d: NPC ID recomputed from %s", index, diagnostics.Quote(npc.Name))})
|
||||
warnings = append(warnings, diagnostics.Finding{Scope: npcScope(index), ReasonCode: ReasonCodeNPCIDRecomputed, Message: fmt.Sprintf("input index %d: NPC ID recomputed from %s", index, diagnostics.Quote(npc.Name))})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ func preprocessRecords(input dnd.NPCRegistry, order shared.SourceRefOrder) ([]no
|
||||
output = append(output, consolidated)
|
||||
retainedIndex := consolidated.earliest
|
||||
if referencesChanged {
|
||||
warnings = append(warnings, contracts.Warning{Scope: npcScope(retainedIndex), ReasonCode: ReasonCodeSourceReferencesNormalized, Message: fmt.Sprintf("input index %d: source references normalized during identity consolidation (final count %d)", retainedIndex, len(consolidated.npc.SourceRefs))})
|
||||
warnings = append(warnings, diagnostics.Finding{Scope: npcScope(retainedIndex), ReasonCode: ReasonCodeSourceReferencesNormalized, Message: fmt.Sprintf("input index %d: source references normalized during identity consolidation (final count %d)", retainedIndex, len(consolidated.npc.SourceRefs))})
|
||||
}
|
||||
if len(members) > 1 {
|
||||
warnings = append(warnings, duplicateWarning(retainedIndex, memberInputIndexes(records, members[1:])))
|
||||
@@ -340,7 +340,7 @@ func cloneSourceRefs(input []source.SourceRef) []source.SourceRef {
|
||||
return append([]source.SourceRef(nil), input...)
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -354,7 +354,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: npcScope(retainedIndex), ReasonCode: ReasonCodeDuplicateNPCCollapsed, Message: message}
|
||||
return diagnostics.Finding{Scope: npcScope(retainedIndex), ReasonCode: ReasonCodeDuplicateNPCCollapsed, Message: message}
|
||||
}
|
||||
|
||||
func npcScope(index int) string { return fmt.Sprintf("npcs[%d]", index) }
|
||||
|
||||
@@ -3,7 +3,6 @@ package npcregistry
|
||||
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/npcs/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.NPC], order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning, error) {
|
||||
func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRecord, envelopes []semanticreconcile.Record[dnd.NPC], order shared.SourceRefOrder) ([]normalizedRecord, []diagnostics.Finding, error) {
|
||||
application, err := semanticreconcile.ApplyPlan(plan, envelopes, semanticreconcile.ApplicationPolicy[dnd.NPC]{
|
||||
CloneValue: cloneNPC,
|
||||
ConsolidateGroup: func(members []dnd.NPC, canonical dnd.NPC) (dnd.NPC, 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: npcScope(provenance.EarliestInputPosition()),
|
||||
ReasonCode: ReasonCodeDuplicateNPCCollapsed,
|
||||
Message: diagnostics.Aggregate("semantic duplicate consolidation", details),
|
||||
|
||||
@@ -79,7 +79,7 @@ type normalizedScene struct {
|
||||
inputIndex int
|
||||
}
|
||||
|
||||
func normalizeList(input dnd.SceneDescriptionList, doc *source.SourceDocument) (dnd.SceneDescriptionList, []contracts.Warning, error) {
|
||||
func normalizeList(input dnd.SceneDescriptionList, doc *source.SourceDocument) (dnd.SceneDescriptionList, []diagnostics.Finding, error) {
|
||||
if doc == nil {
|
||||
return dnd.SceneDescriptionList{}, nil, fmt.Errorf("source document must not be nil")
|
||||
}
|
||||
@@ -92,7 +92,7 @@ func normalizeList(input dnd.SceneDescriptionList, doc *source.SourceDocument) (
|
||||
|
||||
documentIndex := source.NewDocumentIndex(doc)
|
||||
records := make([]normalizedScene, len(input.Scenes))
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
for sceneIndex, scene := range input.Scenes {
|
||||
originalTitle, originalSummary := scene.Title, scene.Summary
|
||||
scene.Title = strings.TrimSpace(scene.Title)
|
||||
@@ -104,7 +104,7 @@ func normalizeList(input dnd.SceneDescriptionList, doc *source.SourceDocument) (
|
||||
return dnd.SceneDescriptionList{}, nil, fmt.Errorf("scenes[%d].source_ref: %s", sceneIndex, diagnostics.Truncate(err.Error()))
|
||||
}
|
||||
if originalTitle != scene.Title || originalSummary != scene.Summary {
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: sceneScope(sceneIndex),
|
||||
ReasonCode: ReasonCodeProseNormalized,
|
||||
Message: fmt.Sprintf("input index %d: title and/or summary whitespace normalized", sceneIndex),
|
||||
@@ -125,7 +125,7 @@ func normalizeList(input dnd.SceneDescriptionList, doc *source.SourceDocument) (
|
||||
if position == record.inputIndex {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: sceneScope(record.inputIndex),
|
||||
ReasonCode: ReasonCodeOrderNormalized,
|
||||
Message: fmt.Sprintf("input index %d moved to normalized position %d by canonical scene order",
|
||||
@@ -146,7 +146,7 @@ func normalizeList(input dnd.SceneDescriptionList, doc *source.SourceDocument) (
|
||||
return dnd.SceneDescriptionList{}, nil, fmt.Errorf("source range %s has conflicting records", sourceRefLabel(scene.SourceRef))
|
||||
}
|
||||
if retainedIndex, ok := seen[scene]; ok {
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: sceneScope(record.inputIndex),
|
||||
ReasonCode: ReasonCodeDuplicateCollapsed,
|
||||
Message: fmt.Sprintf("input index %d: exact duplicate scene collapsed; retained input index %d",
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
|
||||
)
|
||||
|
||||
func TestNormalizeTrimsOrdersDeduplicatesAndOwnsOutput(t *testing.T) {
|
||||
@@ -35,7 +34,7 @@ func TestNormalizeTrimsOrdersDeduplicatesAndOwnsOutput(t *testing.T) {
|
||||
if !reflect.DeepEqual(result.Value.Scenes, want) {
|
||||
t.Fatalf("scenes = %#v, want %#v", result.Value.Scenes, want)
|
||||
}
|
||||
if len(result.Warnings) != 0 || len(result.Diagnostics) != 3 || result.Diagnostics[0].ReasonCode != ReasonCodeProseNormalized || result.Diagnostics[0].OccurrenceCount != 4 || result.Diagnostics[1].ReasonCode != ReasonCodeOrderNormalized || result.Diagnostics[1].OccurrenceCount != 2 || result.Diagnostics[2].ReasonCode != ReasonCodeDuplicateCollapsed || result.Diagnostics[2].OccurrenceCount != 1 {
|
||||
if len(result.Diagnostics) != 3 || result.Diagnostics[0].ReasonCode != ReasonCodeProseNormalized || result.Diagnostics[0].OccurrenceCount != 4 || result.Diagnostics[1].ReasonCode != ReasonCodeOrderNormalized || result.Diagnostics[1].OccurrenceCount != 2 || result.Diagnostics[2].ReasonCode != ReasonCodeDuplicateCollapsed || result.Diagnostics[2].OccurrenceCount != 1 {
|
||||
t.Fatalf("diagnostics = %#v, want grouped prose, order, and duplicate observations", result.Diagnostics)
|
||||
}
|
||||
if !reflect.DeepEqual(input, before) {
|
||||
@@ -55,7 +54,7 @@ func TestNormalizeTrimsOrdersDeduplicatesAndOwnsOutput(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNormalizeLimitsCombinedSceneMutationWarnings(t *testing.T) {
|
||||
count := diagnostics.MaxWarnings - 8
|
||||
count := contracts.MaxDiagnosticSamples + 1
|
||||
doc := &source.SourceDocument{ID: "session", Units: make([]source.SourceUnit, count)}
|
||||
input := dnd.SceneDescriptionList{Scenes: make([]dnd.SceneDescription, count)}
|
||||
for index := range input.Scenes {
|
||||
@@ -70,7 +69,7 @@ func TestNormalizeLimitsCombinedSceneMutationWarnings(t *testing.T) {
|
||||
if len(result.Value.Scenes) != count || result.Value.Scenes[0].SourceRef.StartUnitID != 1 {
|
||||
t.Fatalf("normalized scenes = %#v, want unchanged canonical values", result.Value.Scenes)
|
||||
}
|
||||
if len(result.Warnings) != 0 || len(result.Diagnostics) != 2 || result.Diagnostics[0].ReasonCode != ReasonCodeProseNormalized || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionObservation || result.Diagnostics[0].OccurrenceCount != count || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples || result.Diagnostics[1].ReasonCode != ReasonCodeOrderNormalized || result.Diagnostics[1].OccurrenceCount != count {
|
||||
if len(result.Diagnostics) != 2 || result.Diagnostics[0].ReasonCode != ReasonCodeProseNormalized || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionObservation || result.Diagnostics[0].OccurrenceCount != count || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples || result.Diagnostics[1].ReasonCode != ReasonCodeOrderNormalized || result.Diagnostics[1].OccurrenceCount != count {
|
||||
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,8 +110,8 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
||||
return contracts.TypedNormalizeResult[dnd.SpellList]{Value: value, Diagnostics: diagnosticGroups}, nil
|
||||
}
|
||||
|
||||
func normalizeSpellList(input dnd.SpellList, catalog spellcatalog.EffectiveCatalog, order shared.SourceRefOrder) (dnd.SpellList, []contracts.Warning) {
|
||||
var warnings []contracts.Warning
|
||||
func normalizeSpellList(input dnd.SpellList, catalog spellcatalog.EffectiveCatalog, order shared.SourceRefOrder) (dnd.SpellList, []diagnostics.Finding) {
|
||||
var warnings []diagnostics.Finding
|
||||
if input.SpellCasts == nil {
|
||||
return dnd.SpellList{}, nil
|
||||
}
|
||||
@@ -121,7 +121,7 @@ func normalizeSpellList(input dnd.SpellList, catalog spellcatalog.EffectiveCatal
|
||||
cast := cloneSpellCast(inputCast)
|
||||
if canonicalName, ok := catalog.Lookup(inputCast.Spell); ok {
|
||||
if inputCast.Spell != canonicalName {
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: spellCastScope(index),
|
||||
ReasonCode: ReasonCodeSpellNameCanonicalized,
|
||||
Message: fmt.Sprintf("input index %d: spell name canonicalized from %q to %q",
|
||||
@@ -130,7 +130,7 @@ func normalizeSpellList(input dnd.SpellList, catalog spellcatalog.EffectiveCatal
|
||||
}
|
||||
cast.Spell = canonicalName
|
||||
} else {
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: spellCastScope(index),
|
||||
ReasonCode: ReasonCodeSpellNameUnresolved,
|
||||
Message: fmt.Sprintf("input index %d: spell name %q could not be resolved in the effective catalog",
|
||||
@@ -141,7 +141,7 @@ func normalizeSpellList(input dnd.SpellList, catalog spellcatalog.EffectiveCatal
|
||||
canonicalRefs, orderChanged, duplicateCount := canonicalizeSourceRefs(order, inputCast.SourceRefs)
|
||||
cast.SourceRefs = canonicalRefs
|
||||
if orderChanged || duplicateCount > 0 {
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: spellCastScope(index),
|
||||
ReasonCode: ReasonCodeSourceReferencesNormalized,
|
||||
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d, order changed %t, duplicates removed %d)",
|
||||
@@ -179,7 +179,7 @@ type duplicateGroup struct {
|
||||
removed []int
|
||||
}
|
||||
|
||||
func collapseDuplicateSpellCasts(input dnd.SpellList, documentIndex source.DocumentIndex, catalog spellcatalog.EffectiveCatalog) (dnd.SpellList, []contracts.Warning) {
|
||||
func collapseDuplicateSpellCasts(input dnd.SpellList, documentIndex source.DocumentIndex, catalog spellcatalog.EffectiveCatalog) (dnd.SpellList, []diagnostics.Finding) {
|
||||
if len(input.SpellCasts) == 0 {
|
||||
return input, nil
|
||||
}
|
||||
@@ -221,7 +221,7 @@ func collapseDuplicateSpellCasts(input dnd.SpellList, documentIndex source.Docum
|
||||
}
|
||||
}
|
||||
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
for _, group := range groups {
|
||||
if len(group.removed) == 0 {
|
||||
continue
|
||||
@@ -264,7 +264,7 @@ func writeKeyInt(builder *strings.Builder, value int) {
|
||||
builder.WriteByte(';')
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -279,7 +279,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{
|
||||
return diagnostics.Finding{
|
||||
Scope: spellCastScope(retainedIndex),
|
||||
ReasonCode: ReasonCodeDuplicateSpellCastCollapsed,
|
||||
Message: message,
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
|
||||
spellcatalog "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/spells/catalog"
|
||||
)
|
||||
|
||||
@@ -151,13 +150,13 @@ func TestNormalizeCanonicalizesNamesAndReportsUnresolvedNames(t *testing.T) {
|
||||
}
|
||||
canonicalized := diagnosticByReason(result.Diagnostics, ReasonCodeSpellNameCanonicalized)
|
||||
unresolved := diagnosticByReason(result.Diagnostics, ReasonCodeSpellNameUnresolved)
|
||||
if len(result.Warnings) != 0 || canonicalized == nil || canonicalized.Disposition != contracts.DiagnosticDispositionObservation || canonicalized.OccurrenceCount != 3 || unresolved == nil || unresolved.Disposition != contracts.DiagnosticDispositionAdvisory || unresolved.OccurrenceCount != 1 || unresolved.Samples[0].Scope != "spell_casts[3]" || strings.Contains(unresolved.Samples[0].Message, "Mystery\nSpell") || !strings.Contains(unresolved.Samples[0].Message, `Mystery\nSpell\tName`) {
|
||||
if canonicalized == nil || canonicalized.Disposition != contracts.DiagnosticDispositionObservation || canonicalized.OccurrenceCount != 3 || unresolved == nil || unresolved.Disposition != contracts.DiagnosticDispositionAdvisory || unresolved.OccurrenceCount != 1 || unresolved.Samples[0].Scope != "spell_casts[3]" || strings.Contains(unresolved.Samples[0].Message, "Mystery\nSpell") || !strings.Contains(unresolved.Samples[0].Message, `Mystery\nSpell\tName`) {
|
||||
t.Fatalf("diagnostics = %#v, want grouped canonicalization and unresolved-quality diagnostics", result.Diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeLimitsWarningsWithoutChangingSpellValues(t *testing.T) {
|
||||
input := dnd.SpellList{SpellCasts: make([]dnd.SpellCast, diagnostics.MaxWarnings+1)}
|
||||
input := dnd.SpellList{SpellCasts: make([]dnd.SpellCast, contracts.MaxDiagnosticSamples+1)}
|
||||
for index := range input.SpellCasts {
|
||||
input.SpellCasts[index].Spell = fmt.Sprintf("Unknown Spell %d", index)
|
||||
}
|
||||
@@ -168,7 +167,7 @@ func TestNormalizeLimitsWarningsWithoutChangingSpellValues(t *testing.T) {
|
||||
if !reflect.DeepEqual(result.Value, input) {
|
||||
t.Fatalf("normalized value = %#v, want unresolved spell values preserved", result.Value)
|
||||
}
|
||||
if len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != ReasonCodeSpellNameUnresolved || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionAdvisory || result.Diagnostics[0].OccurrenceCount != len(input.SpellCasts) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||
if len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != ReasonCodeSpellNameUnresolved || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionAdvisory || result.Diagnostics[0].OccurrenceCount != len(input.SpellCasts) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
||||
}
|
||||
}
|
||||
@@ -350,7 +349,7 @@ func TestNormalizeCollapsesDuplicateGroupsAfterCanonicalization(t *testing.T) {
|
||||
canonicalized := diagnosticByReason(result.Diagnostics, ReasonCodeSpellNameCanonicalized)
|
||||
refsNormalized := diagnosticByReason(result.Diagnostics, ReasonCodeSourceReferencesNormalized)
|
||||
collapsed := diagnosticByReason(result.Diagnostics, ReasonCodeDuplicateSpellCastCollapsed)
|
||||
if len(result.Warnings) != 0 || canonicalized == nil || canonicalized.OccurrenceCount != 3 || refsNormalized == nil || refsNormalized.OccurrenceCount != 1 || collapsed == nil || collapsed.OccurrenceCount != 2 || !strings.Contains(collapsed.Samples[0].Message, "retained input index 0") {
|
||||
if canonicalized == nil || canonicalized.OccurrenceCount != 3 || refsNormalized == nil || refsNormalized.OccurrenceCount != 1 || collapsed == nil || collapsed.OccurrenceCount != 2 || !strings.Contains(collapsed.Samples[0].Message, "retained input index 0") {
|
||||
t.Fatalf("diagnostics = %#v, want grouped normalization observations", result.Diagnostics)
|
||||
}
|
||||
}
|
||||
@@ -380,10 +379,8 @@ func TestNormalizeKeepsDistinctAndIneligibleCastsSeparate(t *testing.T) {
|
||||
if len(result.Value.SpellCasts) != 2 {
|
||||
t.Fatalf("normalized casts = %#v, want both casts retained", result.Value.SpellCasts)
|
||||
}
|
||||
for _, warning := range result.Warnings {
|
||||
if warning.ReasonCode == ReasonCodeDuplicateSpellCastCollapsed {
|
||||
t.Fatalf("warnings = %#v, want no duplicate collapse", result.Warnings)
|
||||
}
|
||||
if diagnosticByReason(result.Diagnostics, ReasonCodeDuplicateSpellCastCollapsed) != nil {
|
||||
t.Fatalf("diagnostics = %#v, want no duplicate collapse", result.Diagnostics)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -404,10 +401,8 @@ func TestNormalizeDoesNotCollapseAdjacentOrOverlappingEvidence(t *testing.T) {
|
||||
if len(result.Value.SpellCasts) != len(input.SpellCasts) {
|
||||
t.Fatalf("normalized casts = %#v, want adjacent and overlapping evidence retained", result.Value.SpellCasts)
|
||||
}
|
||||
for _, warning := range result.Warnings {
|
||||
if warning.ReasonCode == ReasonCodeDuplicateSpellCastCollapsed {
|
||||
t.Fatalf("warnings = %#v, want no duplicate collapse", result.Warnings)
|
||||
}
|
||||
if diagnosticByReason(result.Diagnostics, ReasonCodeDuplicateSpellCastCollapsed) != nil {
|
||||
t.Fatalf("diagnostics = %#v, want no duplicate collapse", result.Diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,8 +443,8 @@ func TestNormalizeIsIdempotentForAlreadyNormalizedInput(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("second Normalize() error = %v, want nil", err)
|
||||
}
|
||||
if !reflect.DeepEqual(second.Value, first.Value) || len(first.Warnings) != 0 || len(second.Warnings) != 0 {
|
||||
t.Fatalf("first = %#v/%#v, second = %#v/%#v, want identical artifacts without mutation warnings", first.Value, first.Warnings, second.Value, second.Warnings)
|
||||
if !reflect.DeepEqual(second.Value, first.Value) {
|
||||
t.Fatalf("first = %#v, second = %#v, want identical artifacts", first.Value, second.Value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
func TestValidatorApprovesNormalizedCombatTurns(t *testing.T) {
|
||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Source: invariantDocument(), Value: normalizedList()})
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 {
|
||||
if err != nil || !result.Approved {
|
||||
t.Fatalf("Validate() = %#v, %v; want approval without warnings", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
func TestValidatorApprovesWellFormedCombatTurnList(t *testing.T) {
|
||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Value: validCombatTurnList()})
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 {
|
||||
if err != nil || !result.Approved {
|
||||
t.Fatalf("Validate() = %#v, %v; want approval without warnings", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ func TestValidatorEnforcesCurrentChunkEvidenceDuringExtraction(t *testing.T) {
|
||||
func TestValidatorDefersMalformedShape(t *testing.T) {
|
||||
value := dnd.CombatTurnList{CombatTurns: []dnd.CombatTurn{{Actor: "Aria"}}}
|
||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Source: validDocument(), Value: value})
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 {
|
||||
if err != nil || !result.Approved {
|
||||
t.Fatalf("shape deferral = %#v, %v; want approval without source warning", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,13 +50,13 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
}
|
||||
citedTexts[turnIndex] = citedText
|
||||
}
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
for turnIndex, turn := range req.Value.CombatTurns {
|
||||
citedText := citedTexts[turnIndex]
|
||||
if actorAppearsInCitedText(citedText, turn.Actor) {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: fmt.Sprintf("combat_turns[%d]", turnIndex),
|
||||
ReasonCode: ReasonCode,
|
||||
Message: diagnostics.Aggregate("combat turn not near source", []string{
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestValidatorWarnsOncePerTurnForUnrelatedActor(t *testing.T) {
|
||||
SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}, {SourceID: "session", StartUnitID: 1, EndUnitID: 1}},
|
||||
}}}
|
||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Source: relatednessDocument(), Value: value})
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 {
|
||||
if err != nil || !result.Approved || len(result.Diagnostics) != 1 {
|
||||
t.Fatalf("Validate() = %#v, %v; want one advisory for the turn", result, err)
|
||||
}
|
||||
diagnostic := result.Diagnostics[0]
|
||||
@@ -62,7 +62,7 @@ func TestValidatorLimitsUnrelatedActorWarnings(t *testing.T) {
|
||||
if err != nil || !result.Approved {
|
||||
t.Fatalf("Validate() = %#v, %v", result, err)
|
||||
}
|
||||
if len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(turns) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples || result.Diagnostics[0].OmittedSampleCount != len(turns)-contracts.MaxDiagnosticSamples {
|
||||
if len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(turns) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples || result.Diagnostics[0].OmittedSampleCount != len(turns)-contracts.MaxDiagnosticSamples {
|
||||
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ func TestRepresentativeProductionValidatorResultsSatisfyCorrectionContract(t *te
|
||||
name string
|
||||
validate func() (contracts.ValidationResult, error)
|
||||
wantApproved bool
|
||||
wantWarningCount int
|
||||
wantDiagnosticCount int
|
||||
wantDisposition contracts.DiagnosticDisposition
|
||||
}{
|
||||
@@ -123,9 +122,6 @@ func TestRepresentativeProductionValidatorResultsSatisfyCorrectionContract(t *te
|
||||
if result.Approved != test.wantApproved {
|
||||
t.Fatalf("Validate() approved = %t, want %t: %#v", result.Approved, test.wantApproved, result)
|
||||
}
|
||||
if len(result.Warnings) != test.wantWarningCount {
|
||||
t.Fatalf("Validate() warnings = %d, want %d: %#v", len(result.Warnings), test.wantWarningCount, result)
|
||||
}
|
||||
if len(result.Diagnostics) != test.wantDiagnosticCount {
|
||||
t.Fatalf("Validate() diagnostics = %d, want %d: %#v", len(result.Diagnostics), test.wantDiagnosticCount, result)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
func TestValidatorAcceptsEmptyAndWellFormedEventLists(t *testing.T) {
|
||||
for _, value := range []dnd.EnemyEventList{{Events: []dnd.EnemyEvent{}}, validEventList()} {
|
||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.EnemyEventList]{Value: value})
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 {
|
||||
if err != nil || !result.Approved {
|
||||
t.Fatalf("Validate() = %#v, %v", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,13 +42,13 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
if err != nil {
|
||||
return contracts.ValidationResult{Approved: true}, nil
|
||||
}
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
for eventIndex, event := range req.Value.Events {
|
||||
citedText, err := resolver.CitedText(event.SourceRefs)
|
||||
if err != nil || shared.ContainsTokenSequence(citedText, event.Name) {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: fmt.Sprintf("events[%d]", eventIndex),
|
||||
ReasonCode: ReasonCode,
|
||||
Message: diagnostics.Aggregate("enemy event subject not near source", []string{
|
||||
|
||||
@@ -14,7 +14,7 @@ func TestValidatorUsesOnlyCitedTranscriptEvidence(t *testing.T) {
|
||||
value := validEventList()
|
||||
references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{"npc_registry": {Items: []contracts.ReferenceItem{{Content: []byte("Ashfang")}}}}}
|
||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.EnemyEventList]{Source: document("The party waits."), References: references, Value: value})
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != ReasonCode || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionAdvisory {
|
||||
if err != nil || !result.Approved || len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != ReasonCode || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionAdvisory {
|
||||
t.Fatalf("Validate() = %#v, %v", result, err)
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ func TestValidatorDefersMalformedValuesAndBoundsWarnings(t *testing.T) {
|
||||
value.Events[index] = dnd.EnemyEvent{Name: "Missing", Kind: dnd.EnemyEventKindEngaged, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}
|
||||
}
|
||||
result, err = New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.EnemyEventList]{Source: document("none"), Value: value})
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(value.Events) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||
if err != nil || !result.Approved || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(value.Events) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||
t.Fatalf("bounded advisories = %#v, %v", result, err)
|
||||
}
|
||||
registry := pipeline.NewValidatorRegistry()
|
||||
|
||||
@@ -45,13 +45,13 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
return contracts.ValidationResult{Approved: true}, nil
|
||||
}
|
||||
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
for index, occurrence := range req.Value.Occurrences {
|
||||
citedText, err := resolver.CitedText(occurrence.SourceRefs)
|
||||
if err != nil || shared.ContainsTokenSequence(citedText, occurrence.Name) {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: fmt.Sprintf("occurrences[%d]", index),
|
||||
ReasonCode: ReasonCode,
|
||||
Message: fmt.Sprintf("item occurrence name %s was not found in cited source text", diagnostics.Quote(occurrence.Name)),
|
||||
|
||||
@@ -25,7 +25,7 @@ func TestValidatorMatchesTokenSequencesAcrossRanges(t *testing.T) {
|
||||
"glossary": {Items: []contracts.ReferenceItem{{Content: []byte("missing-item")}}},
|
||||
}}
|
||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.ItemOccurrenceList]{Source: doc, References: references, Value: value})
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 {
|
||||
if err != nil || !result.Approved || len(result.Diagnostics) != 1 {
|
||||
t.Fatalf("Validate() = %#v, %v", result, err)
|
||||
}
|
||||
if diagnostic := result.Diagnostics[0]; diagnostic.Samples[0].Scope != "occurrences[1]" || diagnostic.ReasonCode != ReasonCode || !strings.Contains(diagnostic.Samples[0].Message, "missing-item") {
|
||||
@@ -56,7 +56,7 @@ func TestValidatorBoundsWarningsAndRegistersPolicy(t *testing.T) {
|
||||
Source: &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 1, Text: "nothing useful"}}},
|
||||
Value: dnd.ItemOccurrenceList{Occurrences: occurrences},
|
||||
})
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != count || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||
if err != nil || !result.Approved || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != count || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||
t.Fatalf("Validate() = %#v, %v", result, err)
|
||||
}
|
||||
if got := New(Options{}).CheckpointFingerprints(); !reflect.DeepEqual(got, []pipeline.CheckpointFingerprint{{Name: "policy", Value: policy}}) {
|
||||
|
||||
@@ -50,12 +50,12 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
}
|
||||
citedTexts[itemIndex] = citedText
|
||||
}
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
for itemIndex, item := range req.Value.Items {
|
||||
if shared.ContainsTokenSequence(citedTexts[itemIndex], item.Name) {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: fmt.Sprintf("items[%d]", itemIndex), ReasonCode: ReasonCode,
|
||||
Message: fmt.Sprintf("Item %s was not found in cited source text", diagnostics.Quote(item.Name)),
|
||||
})
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestValidatorUsesOnlyCitedTranscriptText(t *testing.T) {
|
||||
value.Items[0].Name = "Glossary Relic"
|
||||
before := value
|
||||
result, err = New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.ItemRegistry]{Source: doc, References: contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{"glossary": {Items: []contracts.ReferenceItem{{Content: []byte("Glossary Relic")}}}}}, Value: value})
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != ReasonCode || !reflect.DeepEqual(value, before) {
|
||||
if err != nil || !result.Approved || len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != ReasonCode || !reflect.DeepEqual(value, before) {
|
||||
t.Fatalf("reference-only match = %#v, %v; want advisory warning", result, err)
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func TestValidatorBoundsWarningsAndRegisters(t *testing.T) {
|
||||
items[index] = dnd.Item{ID: "item", Name: "Missing", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}
|
||||
}
|
||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.ItemRegistry]{Source: &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 1, Text: "Nothing here."}}}, Value: dnd.ItemRegistry{Items: items}})
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(items) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||
if err != nil || !result.Approved || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(items) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||
t.Fatalf("bounded advisories = %#v, %v", result, err)
|
||||
}
|
||||
registry := pipeline.NewValidatorRegistry()
|
||||
|
||||
@@ -50,12 +50,12 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
}
|
||||
citedTexts[index] = citedText
|
||||
}
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
for index, occurrence := range req.Value.Occurrences {
|
||||
if shared.ContainsTokenSequence(citedTexts[index], occurrence.Name) {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, contracts.Warning{Scope: fmt.Sprintf("occurrences[%d]", index), ReasonCode: ReasonCode, Message: fmt.Sprintf("Location occurrence name %s was not found in cited source text", diagnostics.Quote(occurrence.Name))})
|
||||
warnings = append(warnings, diagnostics.Finding{Scope: fmt.Sprintf("occurrences[%d]", index), ReasonCode: ReasonCode, Message: fmt.Sprintf("Location occurrence name %s was not found in cited source text", diagnostics.Quote(occurrence.Name))})
|
||||
}
|
||||
return diagnostics.DataQualityResult(warnings)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func TestValidatorUsesOnlyCitedTranscriptEvidence(t *testing.T) {
|
||||
before := cloneList(value)
|
||||
references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{"glossary": {Items: []contracts.ReferenceItem{{Content: []byte("Glossary Keep")}}}}}
|
||||
result, err = New(Options{}).Validate(context.Background(), request(doc, value, references))
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != ReasonCode || !strings.Contains(result.Diagnostics[0].Samples[0].Message, "Glossary Keep") || !reflect.DeepEqual(value, before) {
|
||||
if err != nil || !result.Approved || len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != ReasonCode || !strings.Contains(result.Diagnostics[0].Samples[0].Message, "Glossary Keep") || !reflect.DeepEqual(value, before) {
|
||||
t.Fatalf("reference-only match = %#v, %v", result, err)
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ func TestValidatorDefersUnreadableEvidenceAndBoundsWarnings(t *testing.T) {
|
||||
occurrences[index] = occurrenceList("Missing").Occurrences[0]
|
||||
}
|
||||
result, err = New(Options{}).Validate(context.Background(), request(document(), dnd.LocationOccurrenceList{Occurrences: occurrences}, contracts.ReferenceSet{}))
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(occurrences) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||
if err != nil || !result.Approved || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(occurrences) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||
t.Fatalf("bounded advisories = %#v, %v", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,12 +50,12 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
}
|
||||
citedTexts[locationIndex] = citedText
|
||||
}
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
for locationIndex, location := range req.Value.Locations {
|
||||
if shared.ContainsTokenSequence(citedTexts[locationIndex], location.Name) {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: fmt.Sprintf("locations[%d]", locationIndex), ReasonCode: ReasonCode,
|
||||
Message: fmt.Sprintf("Location %s was not found in cited source text", diagnostics.Quote(location.Name)),
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ func TestValidatorUsesOnlyCitedTranscriptText(t *testing.T) {
|
||||
before := value
|
||||
references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{"glossary": {Items: []contracts.ReferenceItem{{Content: []byte("Glossary Keep")}}}}}
|
||||
result, err = New(Options{}).Validate(context.Background(), request(doc, value, references))
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != ReasonCode || !strings.Contains(result.Diagnostics[0].Samples[0].Message, "Glossary Keep") || !reflect.DeepEqual(value, before) {
|
||||
if err != nil || !result.Approved || len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != ReasonCode || !strings.Contains(result.Diagnostics[0].Samples[0].Message, "Glossary Keep") || !reflect.DeepEqual(value, before) {
|
||||
t.Fatalf("reference-only match = %#v, %v; want advisory warning", result, err)
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ func TestValidatorBoundsWarnings(t *testing.T) {
|
||||
locations[index] = dnd.Location{ID: "candidate", Name: "Missing", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}
|
||||
}
|
||||
result, err := New(Options{}).Validate(context.Background(), request(document(), dnd.LocationRegistry{Locations: locations}, contracts.ReferenceSet{}))
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(locations) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||
if err != nil || !result.Approved || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(locations) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||
t.Fatalf("bounded advisories = %#v, %v", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,12 +50,12 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
}
|
||||
citedTexts[index] = citedText
|
||||
}
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
for index, occurrence := range req.Value.Occurrences {
|
||||
if shared.ContainsTokenSequence(citedTexts[index], occurrence.Name) {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: fmt.Sprintf("occurrences[%d]", index),
|
||||
ReasonCode: ReasonCode,
|
||||
Message: fmt.Sprintf("NPC occurrence name %s was not found in cited source text", diagnostics.Quote(occurrence.Name)),
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestValidatorUsesOnlyCurrentTranscriptAndWarnsOncePerOccurrence(t *testing.
|
||||
doc := &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 1, Text: "O’Rin Thorn speaks."}, {ID: 2, Text: "The party waits."}}}
|
||||
references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{"glossary": {Items: []contracts.ReferenceItem{{Content: []byte("Missing NPC")}}}}}
|
||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCOccurrenceList]{Source: doc, References: references, Value: value})
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 {
|
||||
if err != nil || !result.Approved || len(result.Diagnostics) != 1 {
|
||||
t.Fatalf("Validate() = %#v, %v", result, err)
|
||||
}
|
||||
if diagnostic := result.Diagnostics[0]; diagnostic.Samples[0].Scope != "occurrences[1]" || diagnostic.ReasonCode != ReasonCode || !strings.Contains(diagnostic.Samples[0].Message, `Missing\nNPC`) {
|
||||
@@ -68,7 +68,7 @@ func TestValidatorBoundsWarnings(t *testing.T) {
|
||||
if err != nil || !result.Approved {
|
||||
t.Fatalf("Validate() = %#v, %v", result, err)
|
||||
}
|
||||
if len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != count || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||
if len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != count || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
func TestValidatorApprovesValidSourceReferences(t *testing.T) {
|
||||
result, err := New(Options{}).Validate(context.Background(), requestWithValue(validDocument(), validNPCRegistry()))
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 {
|
||||
if err != nil || !result.Approved {
|
||||
t.Fatalf("Validate() = %#v, %v; want approval", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,12 +49,12 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
}
|
||||
citedTexts[npcIndex] = citedText
|
||||
}
|
||||
var warnings []contracts.Warning
|
||||
var warnings []diagnostics.Finding
|
||||
for npcIndex, npc := range req.Value.NPCs {
|
||||
if npcAppearsInCitedText(citedTexts[npcIndex], npc) {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
Scope: fmt.Sprintf("npcs[%d]", npcIndex),
|
||||
ReasonCode: ReasonCode,
|
||||
Message: fmt.Sprintf("NPC %s was not found in cited source text", diagnostics.Quote(npc.Name)),
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestValidatorWarnsAtMostOncePerNPCForUnrelatedCitations(t *testing.T) {
|
||||
{ID: "one", Name: "Missing\nName", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}, {SourceID: "session", StartUnitID: 1, EndUnitID: 1}}},
|
||||
}}
|
||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCRegistry]{Source: relatednessDocument(), Value: value})
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 {
|
||||
if err != nil || !result.Approved || len(result.Diagnostics) != 1 {
|
||||
t.Fatalf("Validate() = %#v, %v; want one advisory", result, err)
|
||||
}
|
||||
diagnostic := result.Diagnostics[0]
|
||||
|
||||
@@ -51,7 +51,7 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
if err != nil {
|
||||
return contracts.ValidationResult{Approved: true}, nil
|
||||
}
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
for index, scene := range req.Value.Scenes {
|
||||
citedText, err := resolver.CitedText([]source.SourceRef{scene.SourceRef})
|
||||
if err != nil {
|
||||
@@ -98,8 +98,8 @@ func significant(token string) bool {
|
||||
return !ignored
|
||||
}
|
||||
|
||||
func warning(index int, field string) contracts.Warning {
|
||||
return contracts.Warning{
|
||||
func warning(index int, field string) diagnostics.Finding {
|
||||
return diagnostics.Finding{
|
||||
Scope: fmt.Sprintf("scenes[%d].%s", index, field),
|
||||
ReasonCode: ReasonCode,
|
||||
Message: "scene description " + field + " has no significant token in cited source text",
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestValidatorWarnsIndependentlyForUngroundedTitleAndSummary(t *testing.T) {
|
||||
t.Fatalf("Validate() = %#v, %v", result, err)
|
||||
}
|
||||
wantScopes := []string{"scenes[1].title", "scenes[1].summary", "scenes[2].title", "scenes[2].summary"}
|
||||
if len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(wantScopes) {
|
||||
if len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(wantScopes) {
|
||||
t.Fatalf("diagnostics = %#v, want %d occurrences", result.Diagnostics, len(wantScopes))
|
||||
}
|
||||
for index, scope := range wantScopes[:contracts.MaxDiagnosticSamples] {
|
||||
|
||||
@@ -49,10 +49,10 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
}
|
||||
citedTexts[spellIndex] = citedText
|
||||
}
|
||||
var warnings []contracts.Warning
|
||||
var warnings []diagnostics.Finding
|
||||
for spellIndex, spell := range req.Value.SpellCasts {
|
||||
if !spellAppearsInCitedText(citedTexts[spellIndex], spell) {
|
||||
warnings = append(warnings, contracts.Warning{Scope: fmt.Sprintf("spell_casts[%d]", spellIndex), ReasonCode: ReasonCode, Message: fmt.Sprintf("spell %s was not found in cited source text", diagnostics.Quote(strings.TrimSpace(spell.Spell)))})
|
||||
warnings = append(warnings, diagnostics.Finding{Scope: fmt.Sprintf("spell_casts[%d]", spellIndex), ReasonCode: ReasonCode, Message: fmt.Sprintf("spell %s was not found in cited source text", diagnostics.Quote(strings.TrimSpace(spell.Spell)))})
|
||||
}
|
||||
}
|
||||
return diagnostics.DataQualityResult(warnings)
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestValidatorWarnsWhenSpellDoesNotAppearInCitedText(t *testing.T) {
|
||||
if !result.Approved {
|
||||
t.Fatalf("Approved = false, want true")
|
||||
}
|
||||
if len(result.Warnings) != 0 || len(result.Diagnostics) != 1 {
|
||||
if len(result.Diagnostics) != 1 {
|
||||
t.Fatalf("Diagnostics = %#v, want one advisory", result.Diagnostics)
|
||||
}
|
||||
if result.Diagnostics[0].ReasonCode != ReasonCode || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionAdvisory {
|
||||
@@ -81,7 +81,7 @@ func TestValidatorBoundsHighCardinalitySpellFindings(t *testing.T) {
|
||||
Source: &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 1, Text: "The party waits."}}},
|
||||
Value: dnd.SpellList{SpellCasts: casts},
|
||||
})
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 {
|
||||
if err != nil || !result.Approved || len(result.Diagnostics) != 1 {
|
||||
t.Fatalf("Validate() = %#v, %v", result, err)
|
||||
}
|
||||
diagnostic := result.Diagnostics[0]
|
||||
|
||||
Reference in New Issue
Block a user