2026-07-07 21:37:58 +00:00
package shape
import (
"context"
2026-07-17 07:02:30 +00:00
"fmt"
"strings"
2026-07-07 21:37:58 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
2026-07-17 07:02:30 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
2026-08-29 01:24:45 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
2026-07-07 21:37:58 +00:00
)
2026-07-21 14:43:20 +00:00
const (
Key = "extract/dnd/spells/shape"
ReasonCode = "invalid_spell_shape"
2026-08-29 01:24:45 +00:00
policy = "dnd.spells.validator.shape.v2"
2026-07-21 14:43:20 +00:00
)
2026-07-07 21:37:58 +00:00
2026-07-17 07:02:30 +00:00
type Options struct { }
2026-07-07 21:37:58 +00:00
type Validator struct { }
2026-07-17 07:02:30 +00:00
var _ contracts . TypedValidator [ dnd . SpellList ] = ( * Validator ) ( nil )
2026-07-21 14:43:20 +00:00
var _ pipeline . CheckpointFingerprintProvider = ( * Validator ) ( nil )
2026-07-07 21:37:58 +00:00
2026-07-17 07:02:30 +00:00
func New ( Options ) * Validator { return & Validator { } }
func ( v * Validator ) Name ( ) string { return Key }
2026-07-07 21:37:58 +00:00
func ( v * Validator ) ExecutionClass ( ) contracts . ExecutionClass {
return contracts . ExecutionClassDeterministic
}
2026-07-21 14:43:20 +00:00
func ( v * Validator ) CheckpointFingerprints ( ) [ ] pipeline . CheckpointFingerprint {
return [ ] pipeline . CheckpointFingerprint { { Name : "policy" , Value : policy } }
}
2026-07-17 07:02:30 +00:00
func ( v * Validator ) Validate ( _ context . Context , req contracts . TypedValidationRequest [ dnd . SpellList ] ) ( contracts . ValidationResult , error ) {
2026-08-29 01:24:45 +00:00
assessment := assess ( req . Value )
if len ( assessment . issues ) != 0 {
return rejection ( assessment . message ( ) , assessment . corrections . Guidance ( "Correct every rejected spell cast and return the complete replacement list" ) ) , nil
2026-07-07 21:37:58 +00:00
}
return contracts . ValidationResult { Approved : true } , nil
}
2026-07-17 07:02:30 +00:00
func Validate ( value dnd . SpellList ) error {
2026-08-29 01:24:45 +00:00
assessment := assess ( value )
if len ( assessment . issues ) == 0 {
return nil
}
return fmt . Errorf ( "%s" , assessment . message ( ) )
}
type validationAssessment struct {
issues [ ] string
corrections diagnostics . Corrections
}
func assess ( value dnd . SpellList ) validationAssessment {
var assessment validationAssessment
2026-07-17 07:02:30 +00:00
if value . SpellCasts == nil {
2026-08-29 01:24:45 +00:00
assessment . issues = append ( assessment . issues , "spell_casts must be present" )
assessment . corrections . Add ( "list" , "Return a `spell_casts` array; use an empty array when the transcript establishes no spell casts." , "" )
return assessment
2026-07-17 07:02:30 +00:00
}
for index , spell := range value . SpellCasts {
2026-08-29 01:24:45 +00:00
context := spellContext ( spell )
2026-07-17 07:02:30 +00:00
if strings . TrimSpace ( spell . Caster ) == "" {
2026-08-29 01:24:45 +00:00
assessment . issues = append ( assessment . issues , fmt . Sprintf ( "spell_casts[%d].caster must not be empty" , index ) )
assessment . corrections . Add ( "caster" , "Provide the contextual caster name for every spell cast." , context )
2026-07-17 07:02:30 +00:00
}
if strings . TrimSpace ( spell . Spell ) == "" {
2026-08-29 01:24:45 +00:00
assessment . issues = append ( assessment . issues , fmt . Sprintf ( "spell_casts[%d].spell must not be empty" , index ) )
assessment . corrections . Add ( "spell" , "Provide the transcript-supported spell name for every spell cast." , context )
2026-07-17 07:02:30 +00:00
}
if len ( spell . SourceRefs ) == 0 {
2026-08-29 01:24:45 +00:00
assessment . issues = append ( assessment . issues , fmt . Sprintf ( "spell_casts[%d].source_refs must not be empty" , index ) )
assessment . corrections . Add ( "source-refs" , "Provide at least one transcript source range that directly supports every spell cast." , context )
2026-07-17 07:02:30 +00:00
}
2026-07-07 21:37:58 +00:00
}
2026-08-29 01:24:45 +00:00
return assessment
}
func ( assessment validationAssessment ) message ( ) string {
return diagnostics . Aggregate ( "invalid spell shape" , assessment . issues )
}
func spellContext ( spell dnd . SpellCast ) string {
name := strings . TrimSpace ( spell . Spell )
if name == "" {
name = "blank spell name"
} else {
name = "spell " + diagnostics . Quote ( name )
}
caster := strings . TrimSpace ( spell . Caster )
if caster == "" {
caster = "blank caster name"
} else {
caster = "caster " + diagnostics . Quote ( caster )
}
return "Affected cast: " + name + " with " + caster + " " + diagnostics . SourceRange ( spell . SourceRefs ) + "."
2026-07-07 21:37:58 +00:00
}
2026-07-17 07:02:30 +00:00
func Spec ( ) pipeline . ValidatorSpec {
return pipeline . ValidatorSpec { Key : Key , ExecutionClass : contracts . ExecutionClassDeterministic }
}
2026-07-07 21:37:58 +00:00
func Register ( registry * pipeline . ValidatorRegistry ) error {
2026-07-17 07:02:30 +00:00
return pipeline . RegisterTypedValidatorBuilder ( registry , dnd . SpellListKind , Spec ( ) , validateOptions , func ( request pipeline . BuildRequest ) ( contracts . TypedValidator [ dnd . SpellList ] , error ) {
options , err := DecodeOptions ( request . Options )
if err != nil {
return nil , err
}
return New ( options ) , nil
2026-07-07 21:37:58 +00:00
} )
}
2026-07-17 07:02:30 +00:00
func DecodeOptions ( options map [ string ] any ) ( Options , error ) {
if err := pipeline . RejectUnknownOptions ( options ) ; err != nil {
return Options { } , err
2026-07-07 21:37:58 +00:00
}
2026-07-17 07:02:30 +00:00
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-07 21:37:58 +00:00
}