Harden diagnostic handling and warning presentation
This commit is contained in:
@@ -112,8 +112,8 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
||||
}
|
||||
index := source.NewDocumentIndex(req.Source)
|
||||
order := shared.NewSourceRefOrderFromIndex(index)
|
||||
value, warnings := normalizeList(req.MergeOutput.Value, index, order, npcRegistry)
|
||||
diagnosticGroups, err := diagnostics.NormalizationDiagnostics(warnings)
|
||||
value, findings := normalizeList(req.MergeOutput.Value, index, order, npcRegistry)
|
||||
diagnosticGroups, err := diagnostics.NormalizationDiagnostics(findings)
|
||||
if err != nil {
|
||||
return contracts.TypedNormalizeResult[dnd.CombatTurnList]{}, normalizerErrorf("collect diagnostics: %w", err)
|
||||
}
|
||||
@@ -138,7 +138,7 @@ func normalizeList(input dnd.CombatTurnList, documentIndex source.DocumentIndex,
|
||||
}
|
||||
|
||||
records := make([]normalizedRecord, len(input.CombatTurns))
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := 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, diagnostics.Finding{
|
||||
findings = append(findings, 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, diagnostics.Finding{
|
||||
findings = append(findings, 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, diagnostics.Finding{
|
||||
findings = append(findings, diagnostics.Finding{
|
||||
Scope: turnScope(record.inputIndex),
|
||||
ReasonCode: ReasonCodeTurnsReordered,
|
||||
Message: fmt.Sprintf("input index %d moved to normalized position %d by source chronology",
|
||||
@@ -187,9 +187,9 @@ func normalizeList(input dnd.CombatTurnList, documentIndex source.DocumentIndex,
|
||||
})
|
||||
}
|
||||
|
||||
output, duplicateWarnings := collapseDuplicates(records, documentIndex)
|
||||
warnings = append(warnings, duplicateWarnings...)
|
||||
return dnd.CombatTurnList{CombatTurns: output}, warnings
|
||||
output, duplicateFindings := collapseDuplicates(records, documentIndex)
|
||||
findings = append(findings, duplicateFindings...)
|
||||
return dnd.CombatTurnList{CombatTurns: output}, findings
|
||||
}
|
||||
|
||||
func normalizeTurn(input dnd.CombatTurn, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.CombatTurn, *actorCanonicalization, bool) {
|
||||
@@ -267,14 +267,14 @@ func collapseDuplicates(records []normalizedRecord, documentIndex source.Documen
|
||||
}
|
||||
}
|
||||
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := make([]diagnostics.Finding, 0)
|
||||
for _, group := range groups {
|
||||
if len(group.removed) == 0 {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, duplicateWarning(group.retainedIndex, group.removed))
|
||||
findings = append(findings, duplicateFinding(group.retainedIndex, group.removed))
|
||||
}
|
||||
return output, warnings
|
||||
return output, findings
|
||||
}
|
||||
|
||||
func duplicateKey(turn dnd.CombatTurn, documentIndex source.DocumentIndex) (string, bool) {
|
||||
@@ -309,7 +309,7 @@ func writeKeyInt(builder *strings.Builder, value int) {
|
||||
builder.WriteByte(';')
|
||||
}
|
||||
|
||||
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
|
||||
func duplicateFinding(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)
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestNormalizeCanonicalizesFieldsAndRegistryIdentities(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeLimitsWarningsWithoutChangingCombatTurnValues(t *testing.T) {
|
||||
func TestNormalizeLimitsFindingsWithoutChangingCombatTurnValues(t *testing.T) {
|
||||
units := make([]source.SourceUnit, contracts.MaxDiagnosticSamples+1)
|
||||
turns := make([]dnd.CombatTurn, len(units))
|
||||
for index := range units {
|
||||
@@ -288,23 +288,23 @@ func TestNormalizerPreparationMetadataFingerprintsAndModuleContract(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestDuplicateWarningBoundsDisplayedIndexesAndReportsAllOmissions(t *testing.T) {
|
||||
func TestDuplicateFindingBoundsDisplayedIndexesAndReportsAllOmissions(t *testing.T) {
|
||||
removed := make([]int, 25)
|
||||
for index := range removed {
|
||||
removed[index] = math.MaxInt - index
|
||||
}
|
||||
warning := duplicateWarning(7, removed)
|
||||
if warning.Scope != "combat_turns[7]" || warning.ReasonCode != ReasonCodeDuplicateCollapsed {
|
||||
t.Fatalf("duplicate warning = %#v, want retained-record scope and reason", warning)
|
||||
finding := duplicateFinding(7, removed)
|
||||
if finding.Scope != "combat_turns[7]" || finding.ReasonCode != ReasonCodeDuplicateCollapsed {
|
||||
t.Fatalf("duplicate finding = %#v, want retained-record scope and reason", finding)
|
||||
}
|
||||
if !strings.Contains(warning.Message, "retained input index 7") || !strings.Contains(warning.Message, fmt.Sprintf("removed input index %d", removed[0])) {
|
||||
t.Fatalf("duplicate warning = %q, want retained and displayed removed indexes", warning.Message)
|
||||
if !strings.Contains(finding.Message, "retained input index 7") || !strings.Contains(finding.Message, fmt.Sprintf("removed input index %d", removed[0])) {
|
||||
t.Fatalf("duplicate finding = %q, want retained and displayed removed indexes", finding.Message)
|
||||
}
|
||||
if !strings.Contains(warning.Message, "5 additional issue(s) omitted") {
|
||||
t.Fatalf("duplicate warning = %q, want exact omitted count", warning.Message)
|
||||
if !strings.Contains(finding.Message, "5 additional issue(s) omitted") {
|
||||
t.Fatalf("duplicate finding = %q, want exact omitted count", finding.Message)
|
||||
}
|
||||
if !utf8.ValidString(warning.Message) || len([]byte(warning.Message)) > 4096 {
|
||||
t.Fatalf("duplicate warning length/encoding = %d/%t", len([]byte(warning.Message)), utf8.ValidString(warning.Message))
|
||||
if !utf8.ValidString(finding.Message) || len([]byte(finding.Message)) > 4096 {
|
||||
t.Fatalf("duplicate finding length/encoding = %d/%t", len([]byte(finding.Message)), utf8.ValidString(finding.Message))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,8 +104,8 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
||||
}
|
||||
index := source.NewDocumentIndex(req.Source)
|
||||
order := shared.NewSourceRefOrderFromIndex(index)
|
||||
value, warnings := normalizeList(req.MergeOutput.Value, order, registry)
|
||||
diagnosticGroups, err := diagnostics.NormalizationDiagnostics(warnings)
|
||||
value, findings := normalizeList(req.MergeOutput.Value, order, registry)
|
||||
diagnosticGroups, err := diagnostics.NormalizationDiagnostics(findings)
|
||||
if err != nil {
|
||||
return contracts.TypedNormalizeResult[dnd.EnemyEventList]{}, normalizerErrorf("collect diagnostics: %w", err)
|
||||
}
|
||||
@@ -128,12 +128,12 @@ func normalizeList(input dnd.EnemyEventList, order shared.SourceRefOrder, regist
|
||||
return dnd.EnemyEventList{}, nil
|
||||
}
|
||||
records := make([]normalizedRecord, len(input.Events))
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := 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, diagnostics.Finding{
|
||||
findings = append(findings, 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, diagnostics.Finding{
|
||||
findings = append(findings, diagnostics.Finding{
|
||||
Scope: eventScope(index),
|
||||
ReasonCode: ReasonCodeSourceRefsNormalized,
|
||||
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)",
|
||||
@@ -157,16 +157,16 @@ func normalizeList(input dnd.EnemyEventList, order shared.SourceRefOrder, regist
|
||||
if position == record.inputIndex {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
findings = append(findings, diagnostics.Finding{
|
||||
Scope: eventScope(record.inputIndex),
|
||||
ReasonCode: ReasonCodeEventsReordered,
|
||||
Message: fmt.Sprintf("input index %d moved to normalized position %d", record.inputIndex, position),
|
||||
})
|
||||
}
|
||||
|
||||
output, duplicateWarnings := collapseDuplicates(records)
|
||||
warnings = append(warnings, duplicateWarnings...)
|
||||
return dnd.EnemyEventList{Events: output}, warnings
|
||||
output, duplicateFindings := collapseDuplicates(records)
|
||||
findings = append(findings, duplicateFindings...)
|
||||
return dnd.EnemyEventList{Events: output}, findings
|
||||
}
|
||||
|
||||
func normalizeEvent(input dnd.EnemyEvent, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.EnemyEvent, *nameCanonicalization, bool) {
|
||||
@@ -235,7 +235,7 @@ func collapseDuplicates(records []normalizedRecord) ([]dnd.EnemyEvent, []diagnos
|
||||
for index, record := range kept {
|
||||
output[index] = cloneEvent(record.event)
|
||||
}
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := make([]diagnostics.Finding, 0)
|
||||
for _, group := range groups {
|
||||
if len(group.removed) == 0 {
|
||||
continue
|
||||
@@ -244,14 +244,14 @@ func collapseDuplicates(records []normalizedRecord) ([]dnd.EnemyEvent, []diagnos
|
||||
for index, removed := range group.removed {
|
||||
issues[index] = fmt.Sprintf("removed input index %d", removed)
|
||||
}
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
findings = append(findings, diagnostics.Finding{
|
||||
Scope: eventScope(group.retainedIndex),
|
||||
ReasonCode: ReasonCodeDuplicateCollapsed,
|
||||
Message: diagnostics.Aggregate(
|
||||
fmt.Sprintf("duplicate enemy event collapsed; retained input index %d", group.retainedIndex), issues),
|
||||
})
|
||||
}
|
||||
return output, warnings
|
||||
return output, findings
|
||||
}
|
||||
|
||||
func eventScope(index int) string { return fmt.Sprintf("events[%d]", index) }
|
||||
|
||||
@@ -134,7 +134,7 @@ func TestNormalizeRequiresRegistryAndKeepsOperationContentOutOfMetadata(t *testi
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizerContractAndWarningBound(t *testing.T) {
|
||||
func TestNormalizerContractAndFindingBound(t *testing.T) {
|
||||
normalizer := newNormalizer(t, npcReferences(t))
|
||||
if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil {
|
||||
t.Fatal("DecodeOptions() accepted an unknown option")
|
||||
|
||||
@@ -106,8 +106,8 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
||||
return contracts.TypedNormalizeResult[dnd.ItemOccurrenceList]{}, normalizerErrorf("item registry reference is required")
|
||||
}
|
||||
index := source.NewDocumentIndex(req.Source)
|
||||
value, warnings := normalizeList(req.MergeOutput.Value, index, shared.NewSourceRefOrderFromIndex(index), registry)
|
||||
diagnosticGroups, err := diagnostics.NormalizationDiagnostics(warnings, ReasonCodeUnknownItemID)
|
||||
value, findings := normalizeList(req.MergeOutput.Value, index, shared.NewSourceRefOrderFromIndex(index), registry)
|
||||
diagnosticGroups, err := diagnostics.NormalizationDiagnostics(findings, ReasonCodeUnknownItemID)
|
||||
if err != nil {
|
||||
return contracts.TypedNormalizeResult[dnd.ItemOccurrenceList]{}, normalizerErrorf("collect diagnostics: %w", err)
|
||||
}
|
||||
@@ -126,12 +126,12 @@ func normalizeList(input dnd.ItemOccurrenceList, index source.DocumentIndex, ord
|
||||
}
|
||||
|
||||
records := make([]normalizedRecord, len(input.Occurrences))
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := 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, diagnostics.Finding{
|
||||
findings = append(findings, 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, diagnostics.Finding{Scope: occurrenceScope(index), ReasonCode: ReasonCodeNameCanonicalized,
|
||||
findings = append(findings, 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, diagnostics.Finding{Scope: occurrenceScope(index), ReasonCode: ReasonCodeUnknownItemID,
|
||||
findings = append(findings, 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, diagnostics.Finding{
|
||||
findings = append(findings, diagnostics.Finding{
|
||||
Scope: occurrenceScope(index),
|
||||
ReasonCode: ReasonCodeSourceRefsNormalized,
|
||||
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)",
|
||||
@@ -163,16 +163,16 @@ func normalizeList(input dnd.ItemOccurrenceList, index source.DocumentIndex, ord
|
||||
if position == record.inputIndex {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
findings = append(findings, diagnostics.Finding{
|
||||
Scope: occurrenceScope(record.inputIndex),
|
||||
ReasonCode: ReasonCodeOccurrencesReordered,
|
||||
Message: fmt.Sprintf("input index %d moved to normalized position %d", record.inputIndex, position),
|
||||
})
|
||||
}
|
||||
|
||||
output, duplicateWarnings := collapseDuplicates(records, index)
|
||||
warnings = append(warnings, duplicateWarnings...)
|
||||
return dnd.ItemOccurrenceList{Occurrences: output}, warnings
|
||||
output, duplicateFindings := collapseDuplicates(records, index)
|
||||
findings = append(findings, duplicateFindings...)
|
||||
return dnd.ItemOccurrenceList{Occurrences: output}, findings
|
||||
}
|
||||
|
||||
func normalizeOccurrence(input dnd.ItemOccurrence, order shared.SourceRefOrder, registry *itemregistry.Registry) (dnd.ItemOccurrence, []string, bool, bool) {
|
||||
@@ -253,16 +253,16 @@ func collapseDuplicates(records []normalizedRecord, index source.DocumentIndex)
|
||||
output = append(output, cloneOccurrence(record.occurrence))
|
||||
}
|
||||
}
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := make([]diagnostics.Finding, 0)
|
||||
for _, group := range groups {
|
||||
if len(group.removed) != 0 {
|
||||
warnings = append(warnings, duplicateWarning(group.retainedIndex, group.removed))
|
||||
findings = append(findings, duplicateFinding(group.retainedIndex, group.removed))
|
||||
}
|
||||
}
|
||||
return output, warnings
|
||||
return output, findings
|
||||
}
|
||||
|
||||
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
|
||||
func duplicateFinding(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)
|
||||
|
||||
@@ -247,7 +247,7 @@ func preprocessRecords(input dnd.ItemRegistry, order shared.SourceRefOrder) ([]n
|
||||
retained.inputIndexes = sortedUniqueIndexes(retained.inputIndexes)
|
||||
output = append(output, retained)
|
||||
if len(members) > 1 {
|
||||
findings = append(findings, duplicateWarning(retained.earliest, memberInputIndexes(records, members[1:])))
|
||||
findings = append(findings, duplicateFinding(retained.earliest, memberInputIndexes(records, members[1:])))
|
||||
}
|
||||
}
|
||||
return output, findings
|
||||
@@ -328,7 +328,7 @@ func recordList(records []normalizedRecord) dnd.ItemRegistry {
|
||||
return dnd.ItemRegistry{Items: recordValues(records)}
|
||||
}
|
||||
|
||||
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
|
||||
func duplicateFinding(retainedIndex int, removed []int) diagnostics.Finding {
|
||||
const maxDisplayedIndices = 20
|
||||
displayed := removed
|
||||
if len(displayed) > maxDisplayedIndices {
|
||||
|
||||
@@ -314,7 +314,7 @@ func TestNormalizeLimitSkipDoesNotCallLLMAndAddsBoundedFallbackWarning(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRetryFallbackErrorsWarningsAndIdempotence(t *testing.T) {
|
||||
func TestNormalizeRetryFallbackErrorsAndIdempotence(t *testing.T) {
|
||||
doc := semanticDocument()
|
||||
input := dnd.ItemRegistry{Items: []dnd.Item{{Name: "Star Compass", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}}, {Name: "Compass", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}}}}
|
||||
invalid, err := newNormalizer(t, &recordingNormalizerClient{err: contracts.ErrInvalidStructuredOutput}).Normalize(context.Background(), normalizeRequestWithSource(input, doc))
|
||||
|
||||
@@ -107,8 +107,8 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
||||
return contracts.TypedNormalizeResult[dnd.LocationOccurrenceList]{}, normalizerErrorf("location registry reference is required")
|
||||
}
|
||||
index := source.NewDocumentIndex(req.Source)
|
||||
value, warnings := normalizeList(req.MergeOutput.Value, index, shared.NewSourceRefOrderFromIndex(index), registry)
|
||||
diagnosticGroups, err := diagnostics.NormalizationDiagnostics(warnings, ReasonCodeUnknownLocationID)
|
||||
value, findings := normalizeList(req.MergeOutput.Value, index, shared.NewSourceRefOrderFromIndex(index), registry)
|
||||
diagnosticGroups, err := diagnostics.NormalizationDiagnostics(findings, ReasonCodeUnknownLocationID)
|
||||
if err != nil {
|
||||
return contracts.TypedNormalizeResult[dnd.LocationOccurrenceList]{}, normalizerErrorf("collect diagnostics: %w", err)
|
||||
}
|
||||
@@ -127,20 +127,20 @@ func normalizeList(input dnd.LocationOccurrenceList, documentIndex source.Docume
|
||||
return dnd.LocationOccurrenceList{}, nil
|
||||
}
|
||||
records := make([]normalizedRecord, len(input.Occurrences))
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := 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, diagnostics.Finding{Scope: occurrenceScope(index), ReasonCode: ReasonCodeNameCanonicalized,
|
||||
findings = append(findings, 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, diagnostics.Finding{Scope: occurrenceScope(index), ReasonCode: ReasonCodeUnknownLocationID,
|
||||
findings = append(findings, 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, diagnostics.Finding{Scope: occurrenceScope(index), ReasonCode: ReasonCodeSourceRefsNormalized,
|
||||
findings = append(findings, 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,13 +149,13 @@ func normalizeList(input dnd.LocationOccurrenceList, documentIndex source.Docume
|
||||
})
|
||||
for position, record := range records {
|
||||
if position != record.inputIndex {
|
||||
warnings = append(warnings, diagnostics.Finding{Scope: occurrenceScope(record.inputIndex), ReasonCode: ReasonCodeOccurrencesReordered,
|
||||
findings = append(findings, 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)})
|
||||
}
|
||||
}
|
||||
output, duplicateWarnings := collapseDuplicates(records, documentIndex)
|
||||
warnings = append(warnings, duplicateWarnings...)
|
||||
return dnd.LocationOccurrenceList{Occurrences: output}, warnings
|
||||
output, duplicateFindings := collapseDuplicates(records, documentIndex)
|
||||
findings = append(findings, duplicateFindings...)
|
||||
return dnd.LocationOccurrenceList{Occurrences: output}, findings
|
||||
}
|
||||
|
||||
func normalizeOccurrence(input dnd.LocationOccurrence, order shared.SourceRefOrder, registry *locationregistry.Registry) (dnd.LocationOccurrence, *nameCanonicalization, bool, bool) {
|
||||
@@ -222,13 +222,13 @@ func collapseDuplicates(records []normalizedRecord, documentIndex source.Documen
|
||||
output = append(output, cloneOccurrence(record.occurrence))
|
||||
}
|
||||
}
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := make([]diagnostics.Finding, 0)
|
||||
for _, group := range groups {
|
||||
if len(group.removed) > 0 {
|
||||
warnings = append(warnings, duplicateWarning(group.retainedIndex, group.removed))
|
||||
findings = append(findings, duplicateFinding(group.retainedIndex, group.removed))
|
||||
}
|
||||
}
|
||||
return output, warnings
|
||||
return output, findings
|
||||
}
|
||||
|
||||
func validSourceRefs(index source.DocumentIndex, refs []source.SourceRef) bool {
|
||||
@@ -316,7 +316,7 @@ func sourceRefsLess(order shared.SourceRefOrder, left, right []source.SourceRef)
|
||||
return len(left) < len(right)
|
||||
}
|
||||
|
||||
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
|
||||
func duplicateFinding(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)
|
||||
|
||||
@@ -91,7 +91,7 @@ func TestNormalizePreservesUnknownIDsAndMalformedOperationRegistry(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizerContractsRequiredRegistryAndWarningBounds(t *testing.T) {
|
||||
func TestNormalizerContractsRequiredRegistryAndFindingBounds(t *testing.T) {
|
||||
if _, err := New(Options{}, contracts.ReferenceSet{}, contracts.ReferenceSet{}); err == nil || !strings.Contains(err.Error(), "at most one reference set") {
|
||||
t.Fatalf("New() error = %v", err)
|
||||
}
|
||||
|
||||
@@ -209,18 +209,18 @@ func preprocessRecords(input dnd.LocationRegistry, order shared.SourceRefOrder)
|
||||
return nil, nil
|
||||
}
|
||||
records := make([]normalizedRecord, len(input.Locations))
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := 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, diagnostics.Finding{Scope: locationScope(index), ReasonCode: ReasonCodeLocationFieldsNormalized, Message: fmt.Sprintf("input index %d: location name normalized for %s", index, diagnostics.Quote(inputLocation.Name))})
|
||||
findings = append(findings, 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, 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))})
|
||||
findings = append(findings, 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, diagnostics.Finding{Scope: locationScope(index), ReasonCode: ReasonCodeLocationIDRecomputed, Message: fmt.Sprintf("input index %d: location ID recomputed from %s", index, diagnostics.Quote(location.Name))})
|
||||
findings = append(findings, 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)
|
||||
@@ -233,10 +233,10 @@ func preprocessRecords(input dnd.LocationRegistry, order shared.SourceRefOrder)
|
||||
retained.inputIndexes = sortedUniqueIndexes(retained.inputIndexes)
|
||||
output = append(output, retained)
|
||||
if len(members) > 1 {
|
||||
warnings = append(warnings, duplicateWarning(retained.earliest, memberInputIndexes(records, members[1:])))
|
||||
findings = append(findings, duplicateFinding(retained.earliest, memberInputIndexes(records, members[1:])))
|
||||
}
|
||||
}
|
||||
return output, warnings
|
||||
return output, findings
|
||||
}
|
||||
|
||||
func normalizeRecord(input dnd.Location, order shared.SourceRefOrder) (dnd.Location, bool, bool) {
|
||||
@@ -342,7 +342,7 @@ func recordList(records []normalizedRecord) dnd.LocationRegistry {
|
||||
return dnd.LocationRegistry{Locations: recordValues(records)}
|
||||
}
|
||||
|
||||
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
|
||||
func duplicateFinding(retainedIndex int, removed []int) diagnostics.Finding {
|
||||
const maxDisplayedIndices = 20
|
||||
displayed := removed
|
||||
if len(displayed) > maxDisplayedIndices {
|
||||
|
||||
@@ -73,7 +73,7 @@ func TestNormalizePreparesOnlyExactDuplicatesAndRetainsSameNameAndNestedPlaces(t
|
||||
t.Fatalf("locations = %#v, want same names and nested place retained", got)
|
||||
}
|
||||
if result.Value.Locations[0].ID == result.Value.Locations[1].ID || !hasDiagnostic(result.Diagnostics, ReasonCodeDuplicateLocationCollapsed, contracts.DiagnosticDispositionObservation) || len(client.requests) != 0 {
|
||||
t.Fatalf("result = %#v, want evidence-anchored IDs and exact duplicate warning", result)
|
||||
t.Fatalf("result = %#v, want evidence-anchored IDs and exact duplicate finding", result)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,15 +54,15 @@ func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRe
|
||||
earliest: record.EarliestInputPosition(),
|
||||
}
|
||||
}
|
||||
warnings := make([]diagnostics.Finding, 0, len(application.AppliedGroups()))
|
||||
findings := make([]diagnostics.Finding, 0, len(application.AppliedGroups()))
|
||||
for _, event := range application.AppliedGroups() {
|
||||
provenance := event.Provenance()
|
||||
warnings = append(warnings, semanticDuplicateWarning(provenance, records[provenance.CanonicalPosition()]))
|
||||
findings = append(findings, semanticDuplicateFinding(provenance, records[provenance.CanonicalPosition()]))
|
||||
}
|
||||
return output, warnings, nil
|
||||
return output, findings, nil
|
||||
}
|
||||
|
||||
func semanticDuplicateWarning(provenance semanticreconcile.GroupProvenance, canonical normalizedRecord) diagnostics.Finding {
|
||||
func semanticDuplicateFinding(provenance semanticreconcile.GroupProvenance, canonical normalizedRecord) diagnostics.Finding {
|
||||
inputIndexes := provenance.OriginalInputIndexes()
|
||||
details := make([]string, 0, len(inputIndexes)+1)
|
||||
for _, inputIndex := range inputIndexes {
|
||||
|
||||
@@ -108,11 +108,11 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
||||
}
|
||||
index := source.NewDocumentIndex(req.Source)
|
||||
order := shared.NewSourceRefOrderFromIndex(index)
|
||||
value, warnings, err := normalizeList(req.MergeOutput.Value, index, order, registry)
|
||||
value, findings, err := normalizeList(req.MergeOutput.Value, index, order, registry)
|
||||
if err != nil {
|
||||
return contracts.TypedNormalizeResult[dnd.NPCOccurrenceList]{}, normalizerErrorf("validate NPC registry pairs: %w", err)
|
||||
}
|
||||
diagnosticGroups, err := diagnostics.NormalizationDiagnostics(warnings)
|
||||
diagnosticGroups, err := diagnostics.NormalizationDiagnostics(findings)
|
||||
if err != nil {
|
||||
return contracts.TypedNormalizeResult[dnd.NPCOccurrenceList]{}, normalizerErrorf("collect diagnostics: %w", err)
|
||||
}
|
||||
@@ -130,7 +130,7 @@ func normalizeList(input dnd.NPCOccurrenceList, documentIndex source.DocumentInd
|
||||
}
|
||||
|
||||
records := make([]normalizedRecord, len(input.Occurrences))
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := 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, diagnostics.Finding{
|
||||
findings = append(findings, diagnostics.Finding{
|
||||
Scope: occurrenceScope(index),
|
||||
ReasonCode: ReasonCodeSourceRefsNormalized,
|
||||
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)",
|
||||
@@ -154,16 +154,16 @@ func normalizeList(input dnd.NPCOccurrenceList, documentIndex source.DocumentInd
|
||||
if position == record.inputIndex {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
findings = append(findings, 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),
|
||||
})
|
||||
}
|
||||
|
||||
output, duplicateWarnings := collapseDuplicates(records, documentIndex)
|
||||
warnings = append(warnings, duplicateWarnings...)
|
||||
return dnd.NPCOccurrenceList{Occurrences: output}, warnings, nil
|
||||
output, duplicateFindings := collapseDuplicates(records, documentIndex)
|
||||
findings = append(findings, duplicateFindings...)
|
||||
return dnd.NPCOccurrenceList{Occurrences: output}, findings, nil
|
||||
}
|
||||
|
||||
func normalizeOccurrence(input dnd.NPCOccurrence, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.NPCOccurrence, bool, error) {
|
||||
@@ -220,16 +220,16 @@ func collapseDuplicates(records []normalizedRecord, documentIndex source.Documen
|
||||
output = append(output, cloneOccurrence(record.occurrence))
|
||||
}
|
||||
}
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := make([]diagnostics.Finding, 0)
|
||||
for _, group := range groups {
|
||||
if len(group.removed) != 0 {
|
||||
warnings = append(warnings, duplicateWarning(group.retainedIndex, group.removed))
|
||||
findings = append(findings, duplicateFinding(group.retainedIndex, group.removed))
|
||||
}
|
||||
}
|
||||
return output, warnings
|
||||
return output, findings
|
||||
}
|
||||
|
||||
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
|
||||
func duplicateFinding(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)
|
||||
|
||||
@@ -40,7 +40,7 @@ func TestNormalizeValidatesPairsAndClones(t *testing.T) {
|
||||
}
|
||||
second, err := normalizer.Normalize(context.Background(), contracts.TypedNormalizeRequest[dnd.NPCOccurrenceList]{Source: doc, MergeOutput: contracts.MergeArtifact[dnd.NPCOccurrenceList]{Value: result.Value}})
|
||||
if err != nil || !reflect.DeepEqual(second.Value, result.Value) || len(second.Diagnostics) != 0 {
|
||||
t.Fatalf("second normalization = %#v, %v; want idempotent output without warnings", second, err)
|
||||
t.Fatalf("second normalization = %#v, %v; want idempotent output without findings", second, err)
|
||||
}
|
||||
result.Value.Occurrences[0].SourceRefs[0].StartUnitID = 999
|
||||
if input.Occurrences[0].SourceRefs[0].StartUnitID == 999 {
|
||||
@@ -120,7 +120,7 @@ func TestNormalizeOrdersAndCollapsesExactDuplicatesOnly(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizerContractAndDeterministicWarnings(t *testing.T) {
|
||||
func TestNormalizerContractAndDeterministicFindings(t *testing.T) {
|
||||
normalizer, err := New(Options{}, npcReferences(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -139,7 +139,7 @@ func TestNormalizerContractAndDeterministicWarnings(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeBoundsWarnings(t *testing.T) {
|
||||
func TestNormalizeBoundsFindings(t *testing.T) {
|
||||
count := contracts.MaxDiagnosticSamples + 5
|
||||
doc := &source.SourceDocument{ID: "session", Units: make([]source.SourceUnit, count)}
|
||||
input := dnd.NPCOccurrenceList{Occurrences: make([]dnd.NPCOccurrence, count)}
|
||||
|
||||
@@ -208,18 +208,18 @@ func preprocessRecords(input dnd.NPCRegistry, order shared.SourceRefOrder) ([]no
|
||||
return nil, nil
|
||||
}
|
||||
records := make([]normalizedRecord, len(input.NPCs))
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := 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, diagnostics.Finding{Scope: npcScope(index), ReasonCode: ReasonCodeNPCFieldsNormalized, Message: fmt.Sprintf("input index %d: NPC name normalized for %s", index, diagnostics.Quote(inputNPC.Name))})
|
||||
findings = append(findings, 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, 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))})
|
||||
findings = append(findings, 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, diagnostics.Finding{Scope: npcScope(index), ReasonCode: ReasonCodeNPCIDRecomputed, Message: fmt.Sprintf("input index %d: NPC ID recomputed from %s", index, diagnostics.Quote(npc.Name))})
|
||||
findings = append(findings, diagnostics.Finding{Scope: npcScope(index), ReasonCode: ReasonCodeNPCIDRecomputed, Message: fmt.Sprintf("input index %d: NPC ID recomputed from %s", index, diagnostics.Quote(npc.Name))})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,13 +230,13 @@ func preprocessRecords(input dnd.NPCRegistry, order shared.SourceRefOrder) ([]no
|
||||
output = append(output, consolidated)
|
||||
retainedIndex := consolidated.earliest
|
||||
if referencesChanged {
|
||||
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))})
|
||||
findings = append(findings, 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:])))
|
||||
findings = append(findings, duplicateFinding(retainedIndex, memberInputIndexes(records, members[1:])))
|
||||
}
|
||||
}
|
||||
return output, warnings
|
||||
return output, findings
|
||||
}
|
||||
|
||||
func normalizeRecord(input dnd.NPC, order shared.SourceRefOrder) (dnd.NPC, bool, bool) {
|
||||
@@ -340,7 +340,7 @@ func cloneSourceRefs(input []source.SourceRef) []source.SourceRef {
|
||||
return append([]source.SourceRef(nil), input...)
|
||||
}
|
||||
|
||||
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
|
||||
func duplicateFinding(retainedIndex int, removed []int) diagnostics.Finding {
|
||||
const maxDisplayedIndices = 20
|
||||
displayed := removed
|
||||
if len(displayed) > maxDisplayedIndices {
|
||||
|
||||
@@ -54,15 +54,15 @@ func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRe
|
||||
earliest: record.EarliestInputPosition(),
|
||||
}
|
||||
}
|
||||
warnings := make([]diagnostics.Finding, 0, len(application.AppliedGroups()))
|
||||
findings := make([]diagnostics.Finding, 0, len(application.AppliedGroups()))
|
||||
for _, event := range application.AppliedGroups() {
|
||||
provenance := event.Provenance()
|
||||
warnings = append(warnings, semanticDuplicateWarning(provenance, records[provenance.CanonicalPosition()]))
|
||||
findings = append(findings, semanticDuplicateFinding(provenance, records[provenance.CanonicalPosition()]))
|
||||
}
|
||||
return output, warnings, nil
|
||||
return output, findings, nil
|
||||
}
|
||||
|
||||
func semanticDuplicateWarning(provenance semanticreconcile.GroupProvenance, canonical normalizedRecord) diagnostics.Finding {
|
||||
func semanticDuplicateFinding(provenance semanticreconcile.GroupProvenance, canonical normalizedRecord) diagnostics.Finding {
|
||||
inputIndexes := provenance.OriginalInputIndexes()
|
||||
details := make([]string, 0, len(inputIndexes)+1)
|
||||
for _, inputIndex := range inputIndexes {
|
||||
|
||||
@@ -63,11 +63,11 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
||||
if err := ctx.Err(); err != nil {
|
||||
return contracts.TypedNormalizeResult[dnd.SceneDescriptionList]{}, normalizerErrorf("context error before normalize: %w", err)
|
||||
}
|
||||
value, warnings, err := normalizeList(req.MergeOutput.Value, req.Source)
|
||||
value, findings, err := normalizeList(req.MergeOutput.Value, req.Source)
|
||||
if err != nil {
|
||||
return contracts.TypedNormalizeResult[dnd.SceneDescriptionList]{}, normalizerErrorf("normalize scenes: %w", err)
|
||||
}
|
||||
diagnosticGroups, err := diagnostics.NormalizationDiagnostics(warnings)
|
||||
diagnosticGroups, err := diagnostics.NormalizationDiagnostics(findings)
|
||||
if err != nil {
|
||||
return contracts.TypedNormalizeResult[dnd.SceneDescriptionList]{}, normalizerErrorf("collect diagnostics: %w", err)
|
||||
}
|
||||
@@ -92,7 +92,7 @@ func normalizeList(input dnd.SceneDescriptionList, doc *source.SourceDocument) (
|
||||
|
||||
documentIndex := source.NewDocumentIndex(doc)
|
||||
records := make([]normalizedScene, len(input.Scenes))
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := 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, diagnostics.Finding{
|
||||
findings = append(findings, 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, diagnostics.Finding{
|
||||
findings = append(findings, 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, diagnostics.Finding{
|
||||
findings = append(findings, diagnostics.Finding{
|
||||
Scope: sceneScope(record.inputIndex),
|
||||
ReasonCode: ReasonCodeDuplicateCollapsed,
|
||||
Message: fmt.Sprintf("input index %d: exact duplicate scene collapsed; retained input index %d",
|
||||
@@ -159,7 +159,7 @@ func normalizeList(input dnd.SceneDescriptionList, doc *source.SourceDocument) (
|
||||
seen[scene] = record.inputIndex
|
||||
unique = append(unique, scene)
|
||||
}
|
||||
return dnd.SceneDescriptionList{Scenes: unique}, warnings, nil
|
||||
return dnd.SceneDescriptionList{Scenes: unique}, findings, nil
|
||||
}
|
||||
|
||||
func sceneScope(index int) string { return fmt.Sprintf("scenes[%d]", index) }
|
||||
|
||||
@@ -53,7 +53,7 @@ func TestNormalizeTrimsOrdersDeduplicatesAndOwnsOutput(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeLimitsCombinedSceneMutationWarnings(t *testing.T) {
|
||||
func TestNormalizeLimitsCombinedSceneMutationFindings(t *testing.T) {
|
||||
count := contracts.MaxDiagnosticSamples + 1
|
||||
doc := &source.SourceDocument{ID: "session", Units: make([]source.SourceUnit, count)}
|
||||
input := dnd.SceneDescriptionList{Scenes: make([]dnd.SceneDescription, count)}
|
||||
|
||||
@@ -15,10 +15,10 @@ type normalizerFixtureSet struct {
|
||||
}
|
||||
|
||||
type normalizerFixtureCase struct {
|
||||
Name string `json:"name"`
|
||||
Input dnd.SpellList `json:"input"`
|
||||
Output dnd.SpellList `json:"output"`
|
||||
WarningReasonCodes []string `json:"warning_reason_codes"`
|
||||
Name string `json:"name"`
|
||||
Input dnd.SpellList `json:"input"`
|
||||
Output dnd.SpellList `json:"output"`
|
||||
DiagnosticReasonCodes []string `json:"diagnostic_reason_codes"`
|
||||
}
|
||||
|
||||
func TestNormalizeAcceptedFixtures(t *testing.T) {
|
||||
@@ -48,8 +48,8 @@ func TestNormalizeAcceptedFixtures(t *testing.T) {
|
||||
for _, diagnostic := range result.Diagnostics {
|
||||
gotReasonCodes = append(gotReasonCodes, diagnostic.ReasonCode)
|
||||
}
|
||||
if !reflect.DeepEqual(gotReasonCodes, fixture.WarningReasonCodes) {
|
||||
t.Fatalf("warning reason codes = %#v, want %#v", gotReasonCodes, fixture.WarningReasonCodes)
|
||||
if !reflect.DeepEqual(gotReasonCodes, fixture.DiagnosticReasonCodes) {
|
||||
t.Fatalf("diagnostic reason codes = %#v, want %#v", gotReasonCodes, fixture.DiagnosticReasonCodes)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -100,10 +100,10 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
||||
|
||||
index := source.NewDocumentIndex(req.Source)
|
||||
order := shared.NewSourceRefOrderFromIndex(index)
|
||||
value, warnings := normalizeSpellList(req.MergeOutput.Value, n.effectiveCatalog, order)
|
||||
value, duplicateWarnings := collapseDuplicateSpellCasts(value, index, n.effectiveCatalog)
|
||||
warnings = append(warnings, duplicateWarnings...)
|
||||
diagnosticGroups, err := diagnostics.NormalizationDiagnostics(warnings, ReasonCodeSpellNameUnresolved)
|
||||
value, findings := normalizeSpellList(req.MergeOutput.Value, n.effectiveCatalog, order)
|
||||
value, duplicateFindings := collapseDuplicateSpellCasts(value, index, n.effectiveCatalog)
|
||||
findings = append(findings, duplicateFindings...)
|
||||
diagnosticGroups, err := diagnostics.NormalizationDiagnostics(findings, ReasonCodeSpellNameUnresolved)
|
||||
if err != nil {
|
||||
return contracts.TypedNormalizeResult[dnd.SpellList]{}, normalizerErrorf("collect diagnostics: %w", err)
|
||||
}
|
||||
@@ -111,7 +111,7 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
||||
}
|
||||
|
||||
func normalizeSpellList(input dnd.SpellList, catalog spellcatalog.EffectiveCatalog, order shared.SourceRefOrder) (dnd.SpellList, []diagnostics.Finding) {
|
||||
var warnings []diagnostics.Finding
|
||||
var findings []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, diagnostics.Finding{
|
||||
findings = append(findings, 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, diagnostics.Finding{
|
||||
findings = append(findings, 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, diagnostics.Finding{
|
||||
findings = append(findings, 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)",
|
||||
@@ -150,7 +150,7 @@ func normalizeSpellList(input dnd.SpellList, catalog spellcatalog.EffectiveCatal
|
||||
}
|
||||
output.SpellCasts[index] = cast
|
||||
}
|
||||
return output, warnings
|
||||
return output, findings
|
||||
}
|
||||
|
||||
func cloneSpellCast(input dnd.SpellCast) dnd.SpellCast {
|
||||
@@ -221,14 +221,14 @@ func collapseDuplicateSpellCasts(input dnd.SpellList, documentIndex source.Docum
|
||||
}
|
||||
}
|
||||
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := make([]diagnostics.Finding, 0)
|
||||
for _, group := range groups {
|
||||
if len(group.removed) == 0 {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, duplicateWarning(group.retainedIndex, group.removed))
|
||||
findings = append(findings, duplicateFinding(group.retainedIndex, group.removed))
|
||||
}
|
||||
return output, warnings
|
||||
return output, findings
|
||||
}
|
||||
|
||||
func duplicateKey(cast dnd.SpellCast, documentIndex source.DocumentIndex, catalog spellcatalog.EffectiveCatalog) (string, bool) {
|
||||
@@ -264,7 +264,7 @@ func writeKeyInt(builder *strings.Builder, value int) {
|
||||
builder.WriteByte(';')
|
||||
}
|
||||
|
||||
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
|
||||
func duplicateFinding(retainedIndex int, removed []int) diagnostics.Finding {
|
||||
const maxDisplayedIndices = 20
|
||||
displayed := removed
|
||||
if len(displayed) > maxDisplayedIndices {
|
||||
|
||||
@@ -155,7 +155,7 @@ func TestNormalizeCanonicalizesNamesAndReportsUnresolvedNames(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeLimitsWarningsWithoutChangingSpellValues(t *testing.T) {
|
||||
func TestNormalizeLimitsFindingsWithoutChangingSpellValues(t *testing.T) {
|
||||
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)
|
||||
@@ -241,7 +241,7 @@ func TestNormalizeReportsDuplicateRemovalWithoutOrderChange(t *testing.T) {
|
||||
}
|
||||
message := diagnostic.Samples[0].Message
|
||||
if !strings.Contains(message, "order changed false") || !strings.Contains(message, "duplicates removed 1") {
|
||||
t.Fatalf("warning = %q, want duplicate-only repair without order change", message)
|
||||
t.Fatalf("finding = %q, want duplicate-only repair without order change", message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,7 +406,7 @@ func TestNormalizeDoesNotCollapseAdjacentOrOverlappingEvidence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeBoundsDuplicateWarningIndices(t *testing.T) {
|
||||
func TestNormalizeBoundsDuplicateFindingIndices(t *testing.T) {
|
||||
doc := sourceDocument(2)
|
||||
ref := source.SourceRef{SourceID: "source", StartUnitID: 1, EndUnitID: 1}
|
||||
casts := make([]dnd.SpellCast, 22)
|
||||
@@ -423,7 +423,7 @@ func TestNormalizeBoundsDuplicateWarningIndices(t *testing.T) {
|
||||
}
|
||||
message := diagnostic.Samples[0].Message
|
||||
if !strings.Contains(message, "removed input indices [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]") || strings.Contains(message, ", 21]") || !strings.Contains(message, "1 additional removed input indices omitted") {
|
||||
t.Fatalf("warning message = %q, want 20 displayed indices and exact omitted count", message)
|
||||
t.Fatalf("finding message = %q, want 20 displayed indices and exact omitted count", message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"warning_reason_codes": [
|
||||
"diagnostic_reason_codes": [
|
||||
"spell_name_canonicalized",
|
||||
"source_references_normalized",
|
||||
"duplicate_spell_cast_collapsed"
|
||||
@@ -79,7 +79,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"warning_reason_codes": []
|
||||
"diagnostic_reason_codes": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user