2026-08-03 20:56:36 +00:00
// Package shape validates the candidate shape of D&D enemy-event artifacts.
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"
enemyeventmodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/enemyevents"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
)
const (
Key = "extract/dnd/enemy-events/shape"
ReasonCode = "invalid_enemy_event_shape"
policy = "dnd.enemy_events.validator.shape.v1"
)
type Options struct { }
type Validator struct { }
var _ contracts . TypedValidator [ dnd . EnemyEventList ] = ( * 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 . EnemyEventList ] ) ( contracts . ValidationResult , error ) {
if err := Validate ( req . Value ) ; err != nil {
2026-08-27 11:50:37 +00:00
return contracts . ValidationResult { Approved : false , ReasonCode : ReasonCode , Message : err . Error ( ) , CorrectionGuidance : "Return a complete enemy-event list with every required field present, valid contextual NPC names, supported event values, and valid source references." } , nil
2026-08-03 20:56:36 +00:00
}
return contracts . ValidationResult { Approved : true } , nil
}
func Validate ( value dnd . EnemyEventList ) error {
issues := issuesFor ( value )
if len ( issues ) == 0 {
return nil
}
return fmt . Errorf ( "%s" , diagnostics . Aggregate ( "invalid enemy event shape" , issues ) )
}
func issuesFor ( value dnd . EnemyEventList ) [ ] string {
if value . Events == nil {
return [ ] string { "events must be present" }
}
issues := make ( [ ] string , 0 )
for eventIndex , event := range value . Events {
prefix := fmt . Sprintf ( "events[%d]" , eventIndex )
if strings . TrimSpace ( event . Name ) == "" {
issues = append ( issues , prefix + ".name must not be empty: " + diagnostics . Quote ( event . Name ) )
}
if ! enemyeventmodel . SupportedKind ( event . Kind ) {
issues = append ( issues , prefix + ".kind is unsupported: " + diagnostics . Quote ( string ( event . Kind ) ) )
}
if len ( event . SourceRefs ) == 0 {
issues = append ( issues , prefix + ".source_refs must contain at least one reference" )
}
}
return issues
}
func Spec ( ) pipeline . ValidatorSpec {
return pipeline . ValidatorSpec { Key : Key , ExecutionClass : contracts . ExecutionClassDeterministic }
}
func Register ( registry * pipeline . ValidatorRegistry ) error {
return pipeline . RegisterTypedValidatorBuilder ( registry , dnd . EnemyEventListKind , Spec ( ) , validateOptions , func ( request pipeline . BuildRequest ) ( contracts . TypedValidator [ dnd . EnemyEventList ] , 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 }