2026-07-21 05:08:02 +00:00
package shape
import (
"context"
"fmt"
"strings"
"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"
)
const (
Key = "extract/dnd/combat-turns/shape"
ReasonCode = "invalid_combat_turn_shape"
2026-08-29 01:24:45 +00:00
policy = "dnd.combat_turns.validator.shape.v2"
2026-07-21 05:08:02 +00:00
)
type Options struct { }
type Validator struct { }
var _ contracts . TypedValidator [ dnd . CombatTurnList ] = ( * Validator ) ( nil )
var _ pipeline . CheckpointFingerprintProvider = ( * Validator ) ( nil )
func New ( Options ) * Validator { return & Validator { } }
func ( v * Validator ) Name ( ) string { return Key }
func ( v * Validator ) ExecutionClass ( ) contracts . ExecutionClass {
return contracts . ExecutionClassDeterministic
}
func ( v * Validator ) CheckpointFingerprints ( ) [ ] pipeline . CheckpointFingerprint {
return [ ] pipeline . CheckpointFingerprint { { Name : "policy" , Value : policy } }
}
func ( v * Validator ) Validate ( _ context . Context , req contracts . TypedValidationRequest [ dnd . CombatTurnList ] ) ( contracts . ValidationResult , error ) {
2026-08-29 01:24:45 +00:00
issues , corrections := assess ( req . Value )
if len ( issues ) != 0 {
return rejection (
diagnostics . Aggregate ( "invalid combat turn shape" , issues ) ,
corrections . Guidance ( "Correct every rejected combat turn and return the complete replacement combat-turn list" ) ,
) , nil
2026-07-21 05:08:02 +00:00
}
return contracts . ValidationResult { Approved : true } , nil
}
func Validate ( value dnd . CombatTurnList ) error {
2026-08-29 01:24:45 +00:00
issues , _ := assess ( value )
2026-07-21 05:08:02 +00:00
if len ( issues ) == 0 {
return nil
}
return fmt . Errorf ( "%s" , diagnostics . Aggregate ( "invalid combat turn shape" , issues ) )
}
2026-08-29 01:24:45 +00:00
func assess ( value dnd . CombatTurnList ) ( [ ] string , diagnostics . Corrections ) {
var corrections diagnostics . Corrections
2026-07-21 05:08:02 +00:00
if value . CombatTurns == nil {
2026-08-29 01:24:45 +00:00
corrections . Add ( "list" , "Return a `combat_turns` array; use an empty array when the scene contains no combat turns." , "" )
return [ ] string { "combat_turns must be present" } , corrections
2026-07-21 05:08:02 +00:00
}
issues := make ( [ ] string , 0 )
for turnIndex , turn := range value . CombatTurns {
prefix := fmt . Sprintf ( "combat_turns[%d]" , turnIndex )
2026-08-29 01:24:45 +00:00
record := fmt . Sprintf ( "Affected %s for actor %s %s." , diagnostics . Quote ( string ( turn . TurnKind ) ) , diagnostics . Quote ( strings . TrimSpace ( turn . Actor ) ) , diagnostics . SourceRange ( turn . SourceRefs ) )
2026-07-21 05:08:02 +00:00
if strings . TrimSpace ( turn . Actor ) == "" {
issues = append ( issues , prefix + ".actor must not be empty: " + diagnostics . Quote ( turn . Actor ) )
2026-08-29 01:24:45 +00:00
corrections . Add ( "actor" , "Provide the contextual combatant name for every combat turn." , record )
2026-07-21 05:08:02 +00:00
}
if ! validTurnKind ( turn . TurnKind ) {
issues = append ( issues , prefix + ".turn_kind is unsupported: " + diagnostics . Quote ( string ( turn . TurnKind ) ) )
2026-08-29 01:24:45 +00:00
corrections . Add ( "kind" , "Set `turn_kind` to exactly one of `turn`, `reaction`, `legendary_action`, `lair_action`, or `other`." , record )
2026-07-21 05:08:02 +00:00
}
if len ( turn . SourceRefs ) == 0 {
issues = append ( issues , prefix + ".source_refs must contain at least one reference" )
2026-08-29 01:24:45 +00:00
corrections . Add ( "source-refs" , "Provide at least one transcript source range that directly supports every combat turn." , record )
2026-07-21 05:08:02 +00:00
}
}
2026-08-29 01:24:45 +00:00
return issues , corrections
2026-07-21 05:08:02 +00:00
}
func validTurnKind ( value dnd . CombatTurnKind ) bool {
switch value {
case dnd . CombatTurnKindTurn , dnd . CombatTurnKindReaction , dnd . CombatTurnKindLegendaryAction , dnd . CombatTurnKindLairAction , dnd . CombatTurnKindOther :
return true
default :
return false
}
}
func Spec ( ) pipeline . ValidatorSpec {
return pipeline . ValidatorSpec { Key : Key , ExecutionClass : contracts . ExecutionClassDeterministic }
}
func Register ( registry * pipeline . ValidatorRegistry ) error {
return pipeline . RegisterTypedValidatorBuilder ( registry , dnd . CombatTurnListKind , Spec ( ) , validateOptions , func ( request pipeline . BuildRequest ) ( contracts . TypedValidator [ dnd . CombatTurnList ] , error ) {
options , err := DecodeOptions ( request . Options )
if err != nil {
return nil , err
}
return New ( options ) , nil
} )
}
func DecodeOptions ( options map [ string ] any ) ( Options , error ) {
if err := pipeline . RejectUnknownOptions ( options ) ; err != nil {
return Options { } , err
}
return Options { } , nil
}
func validateOptions ( options map [ string ] any ) error { _ , err := DecodeOptions ( options ) ; return err }
2026-08-29 01:24:45 +00:00
func rejection ( message , guidance string ) contracts . ValidationResult {
return contracts . ValidationResult { Approved : false , ReasonCode : ReasonCode , Message : message , CorrectionGuidance : guidance }
2026-07-21 05:08:02 +00:00
}