2026-07-23 20:17:18 +00:00
// Package shape validates required D&D scene description fields.
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/scene-descriptions/shape"
ReasonCode = "invalid_scene_description_shape"
2026-08-29 01:24:45 +00:00
policy = "dnd.scene_descriptions.validator.shape.v2"
2026-07-23 20:17:18 +00:00
)
type Options struct { }
type Validator struct { }
var _ contracts . TypedValidator [ dnd . SceneDescriptionList ] = ( * 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 . SceneDescriptionList ] ) ( contracts . ValidationResult , error ) {
2026-08-29 01:24:45 +00:00
issues , corrections := assess ( req . Value , req . Stage == string ( pipeline . StageExtract ) )
if len ( issues ) != 0 {
return contracts . ValidationResult {
Approved : false ,
ReasonCode : ReasonCode ,
Message : diagnostics . Aggregate ( "invalid scene description shape" , issues ) ,
CorrectionGuidance : corrections . Guidance ( "Correct the rejected scene description and return the complete replacement response" ) ,
} , nil
2026-07-23 20:17:18 +00:00
}
return contracts . ValidationResult { Approved : true } , nil
}
func Validate ( value dnd . SceneDescriptionList ) error { return validate ( value , false ) }
func ValidateForStage ( value dnd . SceneDescriptionList , stage string ) error {
return validate ( value , stage == string ( pipeline . StageExtract ) )
}
func validate ( value dnd . SceneDescriptionList , exactlyOne bool ) error {
2026-08-29 01:24:45 +00:00
issues , _ := assess ( value , exactlyOne )
if len ( issues ) == 0 {
return nil
}
return fmt . Errorf ( "%s" , diagnostics . Aggregate ( "invalid scene description shape" , issues ) )
}
func assess ( value dnd . SceneDescriptionList , exactlyOne bool ) ( [ ] string , diagnostics . Corrections ) {
2026-07-23 20:17:18 +00:00
issues := make ( [ ] string , 0 )
2026-08-29 01:24:45 +00:00
var corrections diagnostics . Corrections
2026-07-23 20:17:18 +00:00
if value . Scenes == nil {
issues = append ( issues , "scenes must be present" )
2026-08-29 01:24:45 +00:00
corrections . Add ( "scene" , "Return one scene description for the supplied extraction chunk." , "" )
2026-07-23 20:17:18 +00:00
} else if len ( value . Scenes ) == 0 {
issues = append ( issues , "scenes must not be empty" )
2026-08-29 01:24:45 +00:00
corrections . Add ( "scene" , "Return one scene description for the supplied extraction chunk." , "" )
2026-07-23 20:17:18 +00:00
} else if exactlyOne && len ( value . Scenes ) != 1 {
issues = append ( issues , "extraction must contain exactly one scene" )
2026-08-29 01:24:45 +00:00
corrections . Add ( "scene" , "Return exactly one scene description for the supplied extraction chunk." , "" )
2026-07-23 20:17:18 +00:00
}
for index , scene := range value . Scenes {
prefix := fmt . Sprintf ( "scenes[%d]" , index )
2026-08-29 01:24:45 +00:00
record := fmt . Sprintf ( "Affected scene titled %s with kind %s, citing %s." , diagnostics . Quote ( strings . TrimSpace ( scene . Title ) ) , diagnostics . Quote ( string ( scene . Kind ) ) , diagnostics . SourceRefRange ( scene . SourceRef ) )
2026-07-23 20:17:18 +00:00
if strings . TrimSpace ( scene . ID ) == "" || scene . ID != strings . TrimSpace ( scene . ID ) {
issues = append ( issues , prefix + ".id must be non-empty and trimmed" )
2026-08-29 01:24:45 +00:00
corrections . Add ( "scene" , "Return exactly one scene description for the supplied extraction chunk." , record )
2026-07-23 20:17:18 +00:00
}
if ! ValidKind ( scene . Kind ) {
issues = append ( issues , prefix + ".kind is unsupported: " + diagnostics . Quote ( string ( scene . Kind ) ) )
2026-08-29 01:24:45 +00:00
corrections . Add ( "kind" , "Set `kind` to exactly one of `combat`, `narrative`, `recap`, or `meta`." , record )
2026-07-23 20:17:18 +00:00
}
if strings . TrimSpace ( scene . Title ) == "" || scene . Title != strings . TrimSpace ( scene . Title ) {
issues = append ( issues , prefix + ".title must be non-empty and trimmed" )
2026-08-29 01:24:45 +00:00
corrections . Add ( "title" , "Provide a concise, nonblank, trimmed title grounded in the supplied scene transcript." , record )
2026-07-23 20:17:18 +00:00
}
if strings . TrimSpace ( scene . Summary ) == "" || scene . Summary != strings . TrimSpace ( scene . Summary ) {
issues = append ( issues , prefix + ".summary must be non-empty and trimmed" )
2026-08-29 01:24:45 +00:00
corrections . Add ( "summary" , "Provide a concise, nonblank, trimmed summary grounded in the supplied scene transcript." , record )
2026-07-23 20:17:18 +00:00
}
if strings . TrimSpace ( scene . SourceRef . SourceID ) == "" || scene . SourceRef . SourceID != strings . TrimSpace ( scene . SourceRef . SourceID ) || scene . SourceRef . StartUnitID <= 0 || scene . SourceRef . EndUnitID <= 0 {
issues = append ( issues , prefix + ".source_ref must have a trimmed source ID and positive unit IDs" )
2026-08-29 01:24:45 +00:00
corrections . Add ( "scene" , "Return exactly one scene description for the supplied extraction chunk." , record )
2026-07-23 20:17:18 +00:00
}
}
2026-08-29 01:24:45 +00:00
return issues , corrections
2026-07-23 20:17:18 +00:00
}
func ValidKind ( value dnd . SceneKind ) bool {
switch value {
case dnd . SceneKindCombat , dnd . SceneKindNarrative , dnd . SceneKindRecap , dnd . SceneKindMeta :
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 . SceneDescriptionListKind , Spec ( ) , validateOptions , func ( request pipeline . BuildRequest ) ( contracts . TypedValidator [ dnd . SceneDescriptionList ] , 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 }