2026-07-07 21:37:58 +00:00
package sourcerelatedness
import (
"context"
"fmt"
"strings"
"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-07-21 14:37:12 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
2026-08-27 15:49:53 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
2026-07-17 07:02:30 +00:00
spellshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/spells/shape"
2026-07-07 21:37:58 +00:00
)
2026-07-21 14:43:20 +00:00
const (
2026-08-27 15:49:53 +00:00
Key = "extract/dnd/spells/source_relatedness"
ReasonCode = "spell_not_near_source"
policy = "dnd.spells.validator.source_relatedness.v1"
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 ) {
if err := spellshape . Validate ( req . Value ) ; err != nil {
2026-07-07 21:37:58 +00:00
return contracts . ValidationResult { Approved : true } , nil
}
2026-07-25 12:53:48 +00:00
resolver , err := shared . NewCitationResolver ( req . Source )
if err != nil {
return contracts . ValidationResult { Approved : true } , nil
}
2026-07-21 14:37:12 +00:00
citedTexts := make ( [ ] string , len ( req . Value . SpellCasts ) )
for spellIndex , spell := range req . Value . SpellCasts {
2026-07-25 12:53:48 +00:00
citedText , err := resolver . CitedText ( spell . SourceRefs )
2026-07-21 14:37:12 +00:00
if err != nil {
return contracts . ValidationResult { Approved : true } , nil
}
citedTexts [ spellIndex ] = citedText
}
2026-08-27 18:41:59 +00:00
var findings [ ] diagnostics . Finding
2026-07-17 07:02:30 +00:00
for spellIndex , spell := range req . Value . SpellCasts {
2026-07-21 14:37:12 +00:00
if ! spellAppearsInCitedText ( citedTexts [ spellIndex ] , spell ) {
2026-08-27 18:41:59 +00:00
findings = append ( findings , diagnostics . Finding { Scope : fmt . Sprintf ( "spell_casts[%d]" , spellIndex ) , ReasonCode : ReasonCode , Message : fmt . Sprintf ( "spell %s was not found in cited source text" , diagnostics . Quote ( strings . TrimSpace ( spell . Spell ) ) ) } )
2026-07-07 21:37:58 +00:00
}
}
2026-08-27 18:41:59 +00:00
return diagnostics . DataQualityResult ( findings )
2026-07-07 21:37:58 +00:00
}
2026-07-21 14:37:12 +00:00
func spellAppearsInCitedText ( citedText string , spell dnd . SpellCast ) bool {
name := strings . TrimSpace ( spell . Spell )
2026-07-07 21:37:58 +00:00
if name == "" {
return true
}
2026-07-21 14:37:12 +00:00
return shared . ContainsTokenSequence ( citedText , name )
2026-07-17 07:02:30 +00:00
}
func Spec ( ) pipeline . ValidatorSpec {
return pipeline . ValidatorSpec { Key : Key , ExecutionClass : contracts . ExecutionClassDeterministic }
}
func Register ( registry * pipeline . ValidatorRegistry ) error {
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
} )
}
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 }