2026-07-07 21:37:58 +00:00
package sourcerefs
import (
"context"
"fmt"
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"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:43:20 +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 (
Key = "extract/dnd/spells/source_refs"
ReasonCode = "invalid_source_refs"
2026-08-09 02:08:11 +00:00
policy = "dnd.spells.validator.source_refs.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-09 02:08:11 +00:00
if req . Stage == string ( pipeline . StageExtract ) && req . Chunk == nil {
return contracts . ValidationResult { } , fmt . Errorf ( "spell source-reference validator requires the current extraction chunk" )
}
2026-07-17 07:02:30 +00:00
if err := spellshape . Validate ( req . Value ) ; err != nil {
2026-07-21 14:43:20 +00:00
return contracts . ValidationResult { Approved : true } , nil
2026-07-07 21:37:58 +00:00
}
2026-07-25 12:57:31 +00:00
index := source . NewDocumentIndex ( req . Source )
2026-08-09 02:08:11 +00:00
var coverage * chunkCoverage
if req . Stage == string ( pipeline . StageExtract ) {
coverage = newChunkCoverage ( req . Chunk )
}
2026-07-21 14:43:20 +00:00
issues := make ( [ ] string , 0 )
2026-07-17 07:02:30 +00:00
for spellIndex , spell := range req . Value . SpellCasts {
for refIndex , ref := range spell . SourceRefs {
2026-07-25 12:57:31 +00:00
if err := index . ValidateRef ( ref ) ; err != nil {
2026-07-21 14:43:20 +00:00
issues = append ( issues , fmt . Sprintf ( "spell_casts[%d].source_refs[%d]: %s" , spellIndex , refIndex , diagnostics . Truncate ( err . Error ( ) ) ) )
2026-08-09 02:08:11 +00:00
continue
}
if coverage != nil && ! coverage . contains ( req . Source , ref ) {
issues = append ( issues , fmt . Sprintf ( "spell_casts[%d].source_refs[%d]: source reference is outside the current extraction chunk" , spellIndex , refIndex ) )
2026-07-07 21:37:58 +00:00
}
}
}
2026-07-21 14:43:20 +00:00
if len ( issues ) > 0 {
return rejection ( diagnostics . Aggregate ( "invalid spell source references" , issues ) ) , nil
}
2026-07-07 21:37:58 +00:00
return contracts . ValidationResult { Approved : true } , nil
}
2026-08-09 02:08:11 +00:00
type chunkCoverage struct {
sourceID string
unitIDs map [ int ] struct { }
}
func newChunkCoverage ( chunk * source . Chunk ) * chunkCoverage {
coverage := & chunkCoverage { sourceID : chunk . SourceID , unitIDs : make ( map [ int ] struct { } , len ( chunk . Units ) ) }
for _ , unit := range chunk . Units {
coverage . unitIDs [ unit . ID ] = struct { } { }
}
return coverage
}
func ( coverage * chunkCoverage ) contains ( doc * source . SourceDocument , ref source . SourceRef ) bool {
if coverage == nil || doc == nil || ref . SourceID != coverage . sourceID {
return false
}
start , startOK := source . UnitIndex ( doc , ref . StartUnitID )
end , endOK := source . UnitIndex ( doc , ref . EndUnitID )
if ! startOK || ! endOK || start > end {
return false
}
for position := start ; position <= end ; position ++ {
if _ , found := coverage . unitIDs [ doc . Units [ position ] . ID ] ; ! found {
return false
}
}
return true
}
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
}
return Options { } , nil
}
func validateOptions ( options map [ string ] any ) error { _ , err := DecodeOptions ( options ) ; return err }
func rejection ( message string ) contracts . ValidationResult {
2026-08-27 11:50:37 +00:00
return contracts . ValidationResult { Approved : false , ReasonCode : ReasonCode , Message : message , CorrectionGuidance : "Return spell casts whose source references identify valid transcript ranges within the supplied extraction chunk and directly support the named spell and caster." }
2026-07-07 21:37:58 +00:00
}