2026-07-21 05:22:12 +00:00
package combatturns
import (
"context"
2026-07-21 07:49:57 -05:00
"fmt"
"math"
2026-07-21 05:22:12 +00:00
"reflect"
2026-07-21 07:49:57 -05:00
"strings"
2026-07-21 05:22:12 +00:00
"testing"
2026-07-21 07:49:57 -05:00
"unicode/utf8"
2026-07-21 05:22:12 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
2026-08-05 18:38:22 +00:00
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
2026-07-21 05:22:12 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
)
func TestNormalizeCanonicalizesFieldsAndRegistryIdentities ( t * testing . T ) {
doc := testDocument ( )
references := npcReferences ( t )
normalizer , err := New ( Options { } , references )
if err != nil {
t . Fatalf ( "New() error = %v" , err )
}
input := dnd . CombatTurnList { CombatTurns : [ ] dnd . CombatTurn { {
2026-07-22 19:14:14 +00:00
Actor : " aria " ,
TurnKind : dnd . CombatTurnKindTurn ,
2026-07-21 05:22:12 +00:00
SourceRefs : [ ] source . SourceRef { { SourceID : doc . ID , StartUnitID : 30 , EndUnitID : 30 } , { SourceID : doc . ID , StartUnitID : 20 , EndUnitID : 20 } , { SourceID : doc . ID , StartUnitID : 30 , EndUnitID : 30 } } ,
} } }
original := cloneCombatTurn ( input . CombatTurns [ 0 ] )
result , err := normalizer . Normalize ( context . Background ( ) , contracts . TypedNormalizeRequest [ dnd . CombatTurnList ] {
Source : doc ,
MergeOutput : contracts . MergeArtifact [ dnd . CombatTurnList ] { Value : input } ,
} )
if err != nil {
t . Fatalf ( "Normalize() error = %v" , err )
}
2026-07-22 19:14:14 +00:00
if got := result . Value . CombatTurns [ 0 ] ; got . Actor != "Aria" {
t . Fatalf ( "normalized turn = %#v, want canonical actor" , got )
2026-07-21 05:22:12 +00:00
}
wantRefs := [ ] source . SourceRef { { SourceID : doc . ID , StartUnitID : 20 , EndUnitID : 20 } , { SourceID : doc . ID , StartUnitID : 30 , EndUnitID : 30 } }
if ! reflect . DeepEqual ( result . Value . CombatTurns [ 0 ] . SourceRefs , wantRefs ) {
t . Fatalf ( "normalized refs = %#v, want %#v" , result . Value . CombatTurns [ 0 ] . SourceRefs , wantRefs )
}
2026-07-22 19:14:14 +00:00
for _ , reason := range [ ] string { ReasonCodeActorCanonicalized , ReasonCodeSourceRefsNormalized } {
2026-08-27 16:04:32 +00:00
if ! hasDiagnosticReason ( result . Diagnostics , reason ) {
t . Fatalf ( "diagnostics = %#v, missing reason %q" , result . Diagnostics , reason )
2026-07-21 05:22:12 +00:00
}
}
if ! reflect . DeepEqual ( input . CombatTurns [ 0 ] , original ) {
t . Fatalf ( "Normalize() mutated input: got %#v, want %#v" , input . CombatTurns [ 0 ] , original )
}
2026-07-22 19:14:14 +00:00
result . Value . CombatTurns [ 0 ] . SourceRefs [ 0 ] . StartUnitID = 999
if input . CombatTurns [ 0 ] . SourceRefs [ 0 ] . StartUnitID == 999 {
t . Fatal ( "normalized source refs share input storage" )
2026-07-21 05:22:12 +00:00
}
}
2026-08-27 18:41:59 +00:00
func TestNormalizeLimitsFindingsWithoutChangingCombatTurnValues ( t * testing . T ) {
2026-08-27 16:40:17 +00:00
units := make ( [ ] source . SourceUnit , contracts . MaxDiagnosticSamples + 1 )
2026-08-09 02:22:23 +00:00
turns := make ( [ ] dnd . CombatTurn , len ( units ) )
for index := range units {
unitID := index + 1
units [ index ] = source . SourceUnit { ID : unitID }
turns [ index ] = validTurn ( " Aria " , source . SourceRef { SourceID : "session" , StartUnitID : unitID , EndUnitID : unitID } )
}
normalizer , err := New ( Options { } )
if err != nil {
t . Fatal ( err )
}
result , err := normalizer . Normalize ( context . Background ( ) , contracts . TypedNormalizeRequest [ dnd . CombatTurnList ] {
Source : & source . SourceDocument { ID : "session" , Units : units } ,
MergeOutput : contracts . MergeArtifact [ dnd . CombatTurnList ] { Value : dnd . CombatTurnList { CombatTurns : turns } } ,
} )
if err != nil {
t . Fatalf ( "Normalize() error = %v" , err )
}
if len ( result . Value . CombatTurns ) != len ( turns ) || result . Value . CombatTurns [ 0 ] . Actor != "Aria" {
t . Fatalf ( "normalized turns = %#v, want canonicalized values" , result . Value . CombatTurns )
}
2026-08-27 16:40:17 +00:00
if len ( result . Diagnostics ) == 0 || result . Diagnostics [ 0 ] . Disposition != contracts . DiagnosticDispositionObservation || result . Diagnostics [ 0 ] . OccurrenceCount != len ( turns ) || len ( result . Diagnostics [ 0 ] . Samples ) != contracts . MaxDiagnosticSamples {
2026-08-27 16:04:32 +00:00
t . Fatalf ( "diagnostics = %#v" , result . Diagnostics )
2026-08-09 02:22:23 +00:00
}
}
2026-07-21 22:12:01 +00:00
func TestNormalizeResolvesOperationNPCOverrideWithoutSingletonMetadata ( t * testing . T ) {
doc := testDocument ( )
normalizer , err := New ( Options { } )
if err != nil {
t . Fatalf ( "New() error = %v" , err )
}
input := dnd . CombatTurnList { CombatTurns : [ ] dnd . CombatTurn { {
2026-07-22 18:51:26 +00:00
Actor : "aria" ,
2026-07-21 22:12:01 +00:00
TurnKind : dnd . CombatTurnKindTurn ,
SourceRefs : [ ] source . SourceRef { { SourceID : doc . ID , StartUnitID : 10 , EndUnitID : 10 } } ,
} } }
result , err := normalizer . Normalize ( context . Background ( ) , contracts . TypedNormalizeRequest [ dnd . CombatTurnList ] {
Source : doc ,
MergeOutput : contracts . MergeArtifact [ dnd . CombatTurnList ] { Value : input } ,
References : npcReferences ( t ) ,
} )
if err != nil {
t . Fatalf ( "Normalize() error = %v" , err )
}
turn := result . Value . CombatTurns [ 0 ]
2026-07-22 19:14:14 +00:00
if turn . Actor != "Aria" {
t . Fatalf ( "operation-normalized turn = %#v, want Aria" , turn )
2026-07-21 22:12:01 +00:00
}
if metadata := normalizer . ManifestMetadata ( ) ; metadata [ "npc_registry_digest" ] != nil || metadata [ "npc_count" ] != nil {
t . Fatalf ( "singleton metadata = %#v, want no operation-varying NPC identity" , metadata )
}
}
2026-07-21 05:22:12 +00:00
func TestNormalizeOrdersBySourcePositionAndCollapsesExactDuplicates ( t * testing . T ) {
doc := & source . SourceDocument { ID : "session" , Units : [ ] source . SourceUnit { { ID : 50 } , { ID : 10 } , { ID : 90 } } }
first := validTurn ( "Aria" , source . SourceRef { SourceID : doc . ID , StartUnitID : 50 , EndUnitID : 50 } )
second := validTurn ( "Aria" , source . SourceRef { SourceID : doc . ID , StartUnitID : 90 , EndUnitID : 90 } )
duplicate := cloneCombatTurn ( first )
invalid := validTurn ( "Unknown" , source . SourceRef { SourceID : doc . ID , StartUnitID : 999 , EndUnitID : 999 } )
input := dnd . CombatTurnList { CombatTurns : [ ] dnd . CombatTurn { second , first , duplicate , invalid } }
normalizer , err := New ( Options { } )
if err != nil {
t . Fatalf ( "New() error = %v" , err )
}
result , err := normalizer . Normalize ( context . Background ( ) , contracts . TypedNormalizeRequest [ dnd . CombatTurnList ] {
Source : doc ,
MergeOutput : contracts . MergeArtifact [ dnd . CombatTurnList ] { Value : input } ,
} )
if err != nil {
t . Fatalf ( "Normalize() error = %v" , err )
}
if len ( result . Value . CombatTurns ) != 3 {
t . Fatalf ( "normalized turn count = %d, want 3" , len ( result . Value . CombatTurns ) )
}
2026-07-22 19:14:14 +00:00
if result . Value . CombatTurns [ 0 ] . SourceRefs [ 0 ] . StartUnitID != 50 || result . Value . CombatTurns [ 1 ] . SourceRefs [ 0 ] . StartUnitID != 90 || result . Value . CombatTurns [ 2 ] . Actor != "Unknown" {
2026-07-21 05:22:12 +00:00
t . Fatalf ( "normalized order/value = %#v, want chronology then invalid evidence" , result . Value . CombatTurns )
}
2026-08-27 16:04:32 +00:00
if ! hasDiagnosticReason ( result . Diagnostics , ReasonCodeTurnsReordered ) || ! hasDiagnosticReason ( result . Diagnostics , ReasonCodeDuplicateCollapsed ) {
t . Fatalf ( "diagnostics = %#v, want reorder and duplicate observations" , result . Diagnostics )
2026-07-21 05:22:12 +00:00
}
2026-08-27 16:04:32 +00:00
for _ , diagnostic := range result . Diagnostics {
if diagnostic . ReasonCode == ReasonCodeDuplicateCollapsed && diagnostic . Samples [ 0 ] . Scope != "combat_turns[1]" {
t . Fatalf ( "duplicate diagnostic = %#v, want retained input scope combat_turns[1]" , diagnostic )
2026-07-21 05:22:12 +00:00
}
}
}
2026-07-24 14:20:10 +00:00
func TestNormalizeUsesDocumentOrderForReferencesAndChronology ( t * testing . T ) {
doc := & source . SourceDocument { ID : "session" , Units : [ ] source . SourceUnit { { ID : 30 } , { ID : 10 } } }
input := dnd . CombatTurnList { CombatTurns : [ ] dnd . CombatTurn {
validTurn ( "Borin" , source . SourceRef { SourceID : doc . ID , StartUnitID : 10 , EndUnitID : 10 } ) ,
validTurn ( "Aria" , source . SourceRef { SourceID : doc . ID , StartUnitID : 30 , EndUnitID : 30 } ) ,
{ Actor : "Goblin" , TurnKind : dnd . CombatTurnKindTurn , SourceRefs : [ ] source . SourceRef {
{ SourceID : doc . ID , StartUnitID : 10 , EndUnitID : 10 } ,
{ SourceID : doc . ID , StartUnitID : 30 , EndUnitID : 30 } ,
{ SourceID : doc . ID , StartUnitID : 999 , EndUnitID : 999 } ,
} } ,
} }
normalizer , err := New ( Options { } )
if err != nil {
t . Fatalf ( "New() error = %v" , err )
}
result , err := normalizer . Normalize ( context . Background ( ) , contracts . TypedNormalizeRequest [ dnd . CombatTurnList ] { Source : doc , MergeOutput : contracts . MergeArtifact [ dnd . CombatTurnList ] { Value : input } } )
if err != nil {
t . Fatalf ( "Normalize() error = %v" , err )
}
if got := result . Value . CombatTurns ; got [ 0 ] . Actor != "Aria" || got [ 1 ] . Actor != "Goblin" || got [ 2 ] . Actor != "Borin" {
t . Fatalf ( "turn order = %#v, want source chronology" , got )
}
refs := result . Value . CombatTurns [ 1 ] . SourceRefs
if refs [ 0 ] . StartUnitID != 30 || refs [ 1 ] . StartUnitID != 10 || refs [ 2 ] . StartUnitID != 999 {
t . Fatalf ( "turn source refs = %#v, want source-document order and invalid fallback" , refs )
}
}
2026-07-21 05:22:12 +00:00
func TestNormalizePreservesStableOrderForEqualEvidencePositions ( t * testing . T ) {
doc := & source . SourceDocument { ID : "session" , Units : [ ] source . SourceUnit { { ID : 50 } } }
input := dnd . CombatTurnList { CombatTurns : [ ] dnd . CombatTurn {
validTurn ( "Aria" , source . SourceRef { SourceID : doc . ID , StartUnitID : 50 , EndUnitID : 50 } ) ,
validTurn ( "Borin" , source . SourceRef { SourceID : doc . ID , StartUnitID : 50 , EndUnitID : 50 } ) ,
} }
normalizer , err := New ( Options { } )
if err != nil {
t . Fatalf ( "New() error = %v" , err )
}
result , err := normalizer . Normalize ( context . Background ( ) , contracts . TypedNormalizeRequest [ dnd . CombatTurnList ] { Source : doc , MergeOutput : contracts . MergeArtifact [ dnd . CombatTurnList ] { Value : input } } )
if err != nil {
t . Fatalf ( "Normalize() error = %v" , err )
}
if got := [ ] string { result . Value . CombatTurns [ 0 ] . Actor , result . Value . CombatTurns [ 1 ] . Actor } ; ! reflect . DeepEqual ( got , [ ] string { "Aria" , "Borin" } ) {
t . Fatalf ( "equal-position order = %#v, want stable input order" , got )
}
2026-08-27 16:04:32 +00:00
if hasDiagnosticReason ( result . Diagnostics , ReasonCodeTurnsReordered ) {
t . Fatalf ( "diagnostics = %#v, equal-position stable sort should not report reordering" , result . Diagnostics )
2026-07-21 05:22:12 +00:00
}
}
func TestNormalizeDoesNotCollapseDifferentIdentityDimensions ( t * testing . T ) {
doc := testDocument ( )
base := validTurn ( "Aria" , source . SourceRef { SourceID : doc . ID , StartUnitID : 10 , EndUnitID : 10 } )
tests := [ ] struct {
name string
other dnd . CombatTurn
} {
{ name : "different actor" , other : withActor ( base , "Borin" ) } ,
{ name : "different turn kind" , other : withKind ( base , dnd . CombatTurnKindReaction ) } ,
{ name : "different evidence" , other : withRef ( base , source . SourceRef { SourceID : doc . ID , StartUnitID : 20 , EndUnitID : 20 } ) } ,
{ name : "invalid evidence" , other : withRef ( base , source . SourceRef { SourceID : doc . ID , StartUnitID : 999 , EndUnitID : 999 } ) } ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
normalizer , err := New ( Options { } )
if err != nil {
t . Fatalf ( "New() error = %v" , err )
}
result , err := normalizer . Normalize ( context . Background ( ) , contracts . TypedNormalizeRequest [ dnd . CombatTurnList ] {
Source : doc ,
MergeOutput : contracts . MergeArtifact [ dnd . CombatTurnList ] { Value : dnd . CombatTurnList { CombatTurns : [ ] dnd . CombatTurn { base , test . other } } } ,
} )
if err != nil {
t . Fatalf ( "Normalize() error = %v" , err )
}
if len ( result . Value . CombatTurns ) != 2 {
t . Fatalf ( "normalized turn count = %d, want distinct records" , len ( result . Value . CombatTurns ) )
}
} )
}
}
func TestNormalizePreservesNilAndPresentEmptyStorage ( t * testing . T ) {
normalizer , err := New ( Options { } )
if err != nil {
t . Fatalf ( "New() error = %v" , err )
}
for _ , input := range [ ] dnd . CombatTurnList {
{ } ,
{ CombatTurns : [ ] dnd . CombatTurn { } } ,
} {
result , err := normalizer . Normalize ( context . Background ( ) , contracts . TypedNormalizeRequest [ dnd . CombatTurnList ] { MergeOutput : contracts . MergeArtifact [ dnd . CombatTurnList ] { Value : input } } )
if err != nil {
t . Fatalf ( "Normalize() error = %v" , err )
}
if ( result . Value . CombatTurns == nil ) != ( input . CombatTurns == nil ) {
t . Fatalf ( "nil/present-empty distinction lost: input %#v output %#v" , input . CombatTurns , result . Value . CombatTurns )
}
}
}
func TestNormalizerPreparationMetadataFingerprintsAndModuleContract ( t * testing . T ) {
unbound , err := New ( Options { } )
if err != nil {
t . Fatalf ( "New() error = %v" , err )
}
2026-07-21 14:58:26 +00:00
if metadata := unbound . ManifestMetadata ( ) ; metadata [ "npc_registry_digest" ] != nil || metadata [ "npc_count" ] != nil || metadata [ "normalization_policy" ] != normalizationPolicy || metadata [ "identity_policy" ] != identity . Policy {
2026-07-21 05:22:12 +00:00
t . Fatalf ( "unbound metadata = %#v" , metadata )
}
2026-07-22 18:51:26 +00:00
if got := unbound . CheckpointFingerprints ( ) ; len ( got ) != 3 || got [ 0 ] . Name != "normalization_policy" || got [ 1 ] . Name != "identity_policy" || got [ 2 ] . Name != "npc_registry" || got [ 2 ] . Value == "" {
2026-07-21 05:22:12 +00:00
t . Fatalf ( "unbound fingerprints = %#v" , got )
}
bound , err := New ( Options { } , npcReferences ( t ) )
if err != nil {
t . Fatalf ( "bound New() error = %v" , err )
}
metadata := bound . ManifestMetadata ( )
if metadata [ "npc_registry_digest" ] == "" || metadata [ "npc_count" ] != 2 {
t . Fatalf ( "bound metadata = %#v" , metadata )
}
if got := bound . CheckpointFingerprints ( ) ; len ( got ) != 3 || got [ 2 ] . Name != "npc_registry" || got [ 2 ] . Value == "" {
t . Fatalf ( "bound fingerprints = %#v" , got )
}
2026-07-21 07:49:57 -05:00
if spec := ModuleSpec ( ) ; spec . Key != Key || spec . Stage != pipeline . StageNormalize || spec . ArtifactKind != dnd . CombatTurnListKind || ! reflect . DeepEqual ( spec . Requires , [ ] string { "merged" } ) || ! reflect . DeepEqual ( spec . Provides , [ ] string { "normalized" } ) || len ( spec . ReferenceSlots ) != 1 || spec . ReferenceSlots [ 0 ] . Name != NPCRegistryReferenceSlot {
2026-07-21 05:22:12 +00:00
t . Fatalf ( "ModuleSpec() = %#v" , spec )
}
registry := pipeline . NewNormalizerRegistry ( )
if err := Register ( registry ) ; err != nil {
t . Fatalf ( "Register() error = %v" , err )
}
if _ , err := DecodeOptions ( map [ string ] any { "unexpected" : true } ) ; err == nil {
t . Fatal ( "DecodeOptions() accepted unknown option" )
}
if _ , err := New ( Options { } , contracts . ReferenceSet { } , contracts . ReferenceSet { } ) ; err == nil {
t . Fatal ( "New() accepted multiple reference sets" )
}
}
2026-08-27 18:41:59 +00:00
func TestDuplicateFindingBoundsDisplayedIndexesAndReportsAllOmissions ( t * testing . T ) {
2026-07-21 07:49:57 -05:00
removed := make ( [ ] int , 25 )
for index := range removed {
removed [ index ] = math . MaxInt - index
}
2026-08-27 18:41:59 +00:00
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 )
2026-07-21 07:49:57 -05:00
}
2026-08-27 18:41:59 +00:00
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 )
2026-07-21 07:49:57 -05:00
}
2026-08-27 18:41:59 +00:00
if ! strings . Contains ( finding . Message , "5 additional issue(s) omitted" ) {
t . Fatalf ( "duplicate finding = %q, want exact omitted count" , finding . Message )
2026-07-21 07:49:57 -05:00
}
2026-08-27 18:41:59 +00:00
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 ) )
2026-07-21 07:49:57 -05:00
}
}
2026-07-21 05:22:12 +00:00
func TestNormalizerRejectsNilAndCanceledCalls ( t * testing . T ) {
normalizer , err := New ( Options { } )
if err != nil {
t . Fatalf ( "New() error = %v" , err )
}
if _ , err := normalizer . Normalize ( nil , contracts . TypedNormalizeRequest [ dnd . CombatTurnList ] { } ) ; err == nil {
t . Fatal ( "Normalize() accepted nil context" )
}
ctx , cancel := context . WithCancel ( context . Background ( ) )
cancel ( )
if _ , err := normalizer . Normalize ( ctx , contracts . TypedNormalizeRequest [ dnd . CombatTurnList ] { } ) ; err == nil {
t . Fatal ( "Normalize() accepted canceled context" )
}
}
func validTurn ( actor string , ref source . SourceRef ) dnd . CombatTurn {
return dnd . CombatTurn {
Actor : actor ,
TurnKind : dnd . CombatTurnKindTurn ,
SourceRefs : [ ] source . SourceRef { ref } ,
}
}
func testDocument ( ) * source . SourceDocument {
return & source . SourceDocument { ID : "session" , Units : [ ] source . SourceUnit { { ID : 10 , Text : "Aria attacks the goblin." } , { ID : 20 , Text : "The goblin is hit." } , { ID : 30 , Text : "The goblin falls." } } }
}
func npcReferences ( t * testing . T ) contracts . ReferenceSet {
t . Helper ( )
2026-08-05 18:25:04 +00:00
npcs := dnd . NPCRegistry { NPCs : [ ] dnd . NPC {
2026-07-22 18:51:26 +00:00
{ ID : identity . DeriveID ( "Aria" ) , Name : "Aria" , SourceRefs : [ ] source . SourceRef { { SourceID : "npc-run" , StartUnitID : 1 , EndUnitID : 1 } } } ,
{ ID : identity . DeriveID ( "Goblin" ) , Name : "Goblin" , SourceRefs : [ ] source . SourceRef { { SourceID : "npc-run" , StartUnitID : 1 , EndUnitID : 1 } } } ,
2026-07-21 05:22:12 +00:00
} }
content , err := npccodec . New ( ) . Encode ( npcs )
if err != nil {
t . Fatalf ( "encode NPC references: %v" , err )
}
return contracts . ReferenceSet { Slots : map [ string ] contracts . ResolvedReferenceSlot {
NPCRegistryReferenceSlot : {
Slot : contracts . ReferenceSlot { Name : NPCRegistryReferenceSlot , AcceptedMediaTypes : [ ] string { npccodec . MediaType } , MaxBytes : NPCRegistryMaxBytes } ,
Items : [ ] contracts . ReferenceItem { { SlotName : NPCRegistryReferenceSlot , MediaType : npccodec . MediaType , Content : content } } ,
} ,
} }
}
2026-08-27 16:04:32 +00:00
func hasDiagnosticReason ( diagnostics [ ] contracts . ProducerDiagnostic , reason string ) bool {
for _ , diagnostic := range diagnostics {
if diagnostic . ReasonCode == reason && diagnostic . Disposition == contracts . DiagnosticDispositionObservation {
2026-07-21 05:22:12 +00:00
return true
}
}
return false
}
func withActor ( turn dnd . CombatTurn , actor string ) dnd . CombatTurn {
turn . Actor = actor
return turn
}
func withKind ( turn dnd . CombatTurn , kind dnd . CombatTurnKind ) dnd . CombatTurn {
turn . TurnKind = kind
return turn
}
func withRef ( turn dnd . CombatTurn , ref source . SourceRef ) dnd . CombatTurn {
turn . SourceRefs = [ ] source . SourceRef { ref }
return turn
}