2026-07-21 05:08:02 +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"
"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"
2026-07-21 05:08:02 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
combatshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/combatturns/shape"
)
const (
Key = "extract/dnd/combat-turns/source_refs"
ReasonCode = "invalid_combat_turn_source_refs"
2026-08-29 01:24:45 +00:00
policy = "dnd.combat_turns.validator.source_refs.v3"
2026-07-21 05:08:02 +00:00
)
type Options struct { }
type Validator struct { }
var _ contracts . TypedValidator [ dnd . CombatTurnList ] = ( * 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 . CombatTurnList ] ) ( 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 ( "combat-turn source-reference validator requires the current extraction chunk" )
}
2026-07-21 05:08:02 +00:00
if err := combatshape . Validate ( req . Value ) ; err != nil {
return contracts . ValidationResult { Approved : true } , nil
}
2026-08-29 01:24:45 +00:00
var coverage shared . ChunkCoverage
2026-08-09 02:08:11 +00:00
if req . Stage == string ( pipeline . StageExtract ) {
2026-08-29 01:24:45 +00:00
coverage = shared . NewChunkCoverage ( req . Chunk )
2026-08-09 02:08:11 +00:00
}
2026-08-29 01:24:45 +00:00
issues , corrections := sourceRefIssues ( source . NewDocumentIndex ( req . Source ) , req . Source , coverage , req . Stage == string ( pipeline . StageExtract ) , req . Value )
2026-07-21 05:08:02 +00:00
if len ( issues ) == 0 {
return contracts . ValidationResult { Approved : true } , nil
}
return contracts . ValidationResult {
2026-08-27 11:50:37 +00:00
Approved : false ,
ReasonCode : ReasonCode ,
Message : diagnostics . Aggregate ( "invalid combat turn source references" , issues ) ,
2026-08-29 01:24:45 +00:00
CorrectionGuidance : corrections . Guidance ( "Correct every rejected combat-turn citation and return the complete replacement combat-turn list" ) ,
2026-07-21 05:08:02 +00:00
} , nil
}
2026-08-29 01:24:45 +00:00
func sourceRefIssues ( index source . DocumentIndex , doc * source . SourceDocument , coverage shared . ChunkCoverage , checkCoverage bool , value dnd . CombatTurnList ) ( [ ] string , diagnostics . Corrections ) {
2026-07-21 05:08:02 +00:00
issues := make ( [ ] string , 0 )
2026-08-29 01:24:45 +00:00
var corrections diagnostics . Corrections
2026-07-21 05:08:02 +00:00
for turnIndex , turn := range value . CombatTurns {
for refIndex , ref := range turn . SourceRefs {
2026-08-29 01:24:45 +00:00
record := fmt . Sprintf ( "Affected %s for actor %s, citing %s." , diagnostics . Quote ( string ( turn . TurnKind ) ) , diagnostics . Quote ( turn . Actor ) , diagnostics . SourceRefRange ( ref ) )
2026-07-25 12:57:31 +00:00
if err := index . ValidateRef ( ref ) ; err != nil {
2026-07-21 05:08:02 +00:00
issues = append ( issues , fmt . Sprintf ( "combat_turns[%d].source_refs[%d]: %s" , turnIndex , refIndex , diagnostics . Truncate ( err . Error ( ) ) ) )
2026-08-29 01:24:45 +00:00
corrections . Add ( "valid-range" , "Use positive source range endpoints that occur in the supplied transcript, with the earlier unit first." , record )
2026-08-09 02:08:11 +00:00
continue
}
2026-08-29 01:24:45 +00:00
if checkCoverage && ! coverage . Contains ( index , doc , ref ) {
2026-08-09 02:08:11 +00:00
issues = append ( issues , fmt . Sprintf ( "combat_turns[%d].source_refs[%d]: source reference is outside the current extraction chunk" , turnIndex , refIndex ) )
2026-08-29 01:24:45 +00:00
corrections . Add ( "chunk-range" , "Use only source ranges wholly contained in the supplied extraction chunk." , record )
2026-07-21 05:08:02 +00:00
}
}
}
2026-08-29 01:24:45 +00:00
return issues , corrections
2026-08-09 02:08:11 +00:00
}
2026-07-21 05:08:02 +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 . CombatTurnListKind , Spec ( ) , validateOptions , func ( request pipeline . BuildRequest ) ( contracts . TypedValidator [ dnd . CombatTurnList ] , 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 }