2026-08-05 20:00:20 +00:00
// Package invariants validates normalized D&D item-occurrence artifacts.
2026-07-25 22:00:32 +00:00
package invariants
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-05 20:00:20 +00:00
itemoccurrencemodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/itemoccurrences"
2026-07-25 22:00:32 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
2026-08-05 20:00:20 +00:00
itemoccurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/shape"
2026-07-25 22:00:32 +00:00
)
const (
2026-08-05 20:00:20 +00:00
Key = "normalize/dnd/item-occurrences/invariants"
ReasonCode = "invalid_normalized_item_occurrence_invariants"
policy = "dnd.item_occurrences.normalize_invariants.v1"
2026-07-25 22:00:32 +00:00
)
type Options struct { }
type Validator struct { }
2026-08-05 19:55:34 +00:00
var _ contracts . TypedValidator [ dnd . ItemOccurrenceList ] = ( * Validator ) ( nil )
2026-07-25 22:00:32 +00:00
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 } }
}
2026-08-05 19:55:34 +00:00
func ( v * Validator ) Validate ( _ context . Context , req contracts . TypedValidationRequest [ dnd . ItemOccurrenceList ] ) ( contracts . ValidationResult , error ) {
2026-07-25 22:00:32 +00:00
if err := Validate ( req . Source , req . Value ) ; err != nil {
2026-08-27 11:50:37 +00:00
return contracts . ValidationResult { Approved : false , ReasonCode : ReasonCode , Message : err . Error ( ) , CorrectionGuidance : "Return item occurrences in canonical transcript order, without duplicate occurrences, using consistent contextual item names and valid event transitions." } , nil
2026-07-25 22:00:32 +00:00
}
return contracts . ValidationResult { Approved : true } , nil
}
2026-08-05 20:00:20 +00:00
// Validate checks only invariants introduced by item-occurrence normalization.
2026-07-25 22:00:32 +00:00
// Shape and source-reference failures remain owned by their earlier validators.
2026-08-05 19:55:34 +00:00
func Validate ( doc * source . SourceDocument , value dnd . ItemOccurrenceList ) error {
2026-08-05 20:00:20 +00:00
if itemoccurrenceshape . Validate ( value ) != nil {
2026-07-25 22:00:32 +00:00
return nil
}
index := source . NewDocumentIndex ( doc )
if ! sourceRefsValid ( index , value ) {
return nil
}
issues := issuesFor ( shared . NewSourceRefOrderFromIndex ( index ) , value )
if len ( issues ) == 0 {
return nil
}
2026-08-05 19:55:34 +00:00
return fmt . Errorf ( "%s" , diagnostics . Aggregate ( "invalid normalized item occurrence invariants" , issues ) )
2026-07-25 22:00:32 +00:00
}
2026-08-05 19:55:34 +00:00
func issuesFor ( order shared . SourceRefOrder , value dnd . ItemOccurrenceList ) [ ] string {
2026-07-25 22:00:32 +00:00
issues := make ( [ ] string , 0 )
2026-08-09 02:36:53 +00:00
canonicalEvidence := make ( [ ] bool , len ( value . Occurrences ) )
seenIdentity := make ( map [ string ] [ ] int )
2026-08-05 20:00:20 +00:00
for occurrenceIndex , occurrence := range value . Occurrences {
prefix := fmt . Sprintf ( "occurrences[%d]" , occurrenceIndex )
2026-08-09 02:36:53 +00:00
canonicalEvidence [ occurrenceIndex ] = true
2026-08-05 20:00:20 +00:00
if occurrence . Name != itemoccurrencemodel . DisplayValue ( occurrence . Name ) {
2026-07-25 22:00:32 +00:00
issues = append ( issues , prefix + ".name is not display-normalized" )
}
2026-08-05 20:00:20 +00:00
if occurrence . From != itemoccurrencemodel . DisplayValue ( occurrence . From ) {
2026-07-25 22:00:32 +00:00
issues = append ( issues , prefix + ".from is not display-normalized" )
}
2026-08-05 20:00:20 +00:00
if occurrence . To != itemoccurrencemodel . DisplayValue ( occurrence . To ) {
2026-07-25 22:00:32 +00:00
issues = append ( issues , prefix + ".to is not display-normalized" )
}
2026-08-05 20:00:20 +00:00
for refIndex := 1 ; refIndex < len ( occurrence . SourceRefs ) ; refIndex ++ {
previous := occurrence . SourceRefs [ refIndex - 1 ]
current := occurrence . SourceRefs [ refIndex ]
2026-07-25 22:00:32 +00:00
if order . Less ( current , previous ) {
issues = append ( issues , fmt . Sprintf ( "%s.source_refs are not in canonical order at index %d" , prefix , refIndex ) )
2026-08-09 02:36:53 +00:00
canonicalEvidence [ occurrenceIndex ] = false
2026-07-25 22:00:32 +00:00
} else if current == previous {
issues = append ( issues , fmt . Sprintf ( "%s.source_refs[%d] duplicates the previous reference" , prefix , refIndex ) )
2026-08-09 02:36:53 +00:00
canonicalEvidence [ occurrenceIndex ] = false
2026-07-25 22:00:32 +00:00
}
}
2026-08-09 02:36:53 +00:00
if canonicalEvidence [ occurrenceIndex ] {
key := itemoccurrencemodel . CanonicalExactIdentity ( occurrence )
for _ , previous := range seenIdentity [ key ] {
if itemoccurrencemodel . CanonicalExactEqual ( value . Occurrences [ previous ] , occurrence ) {
issues = append ( issues , fmt . Sprintf ( "%s duplicates item occurrence %d under normalized identity" , prefix , previous ) )
break
}
}
seenIdentity [ key ] = append ( seenIdentity [ key ] , occurrenceIndex )
2026-07-25 22:00:32 +00:00
}
}
2026-08-05 20:00:20 +00:00
for occurrenceIndex := 1 ; occurrenceIndex < len ( value . Occurrences ) ; occurrenceIndex ++ {
2026-08-09 02:36:53 +00:00
less := itemoccurrencemodel . Less
if canonicalEvidence [ occurrenceIndex ] && canonicalEvidence [ occurrenceIndex - 1 ] {
less = itemoccurrencemodel . CanonicalLess
}
if less ( order , value . Occurrences [ occurrenceIndex ] , value . Occurrences [ occurrenceIndex - 1 ] ) {
2026-08-05 20:00:20 +00:00
issues = append ( issues , fmt . Sprintf ( "occurrences[%d] is out of canonical order" , occurrenceIndex ) )
2026-07-25 22:00:32 +00:00
}
}
return issues
}
2026-08-05 19:55:34 +00:00
func sourceRefsValid ( index source . DocumentIndex , value dnd . ItemOccurrenceList ) bool {
2026-08-05 20:00:20 +00:00
for _ , occurrence := range value . Occurrences {
if ! itemoccurrencemodel . ValidSourceRefs ( index , occurrence . SourceRefs ) {
2026-07-25 22:00:32 +00:00
return false
}
}
return true
}
func Spec ( ) pipeline . ValidatorSpec {
return pipeline . ValidatorSpec { Key : Key , ExecutionClass : contracts . ExecutionClassDeterministic }
}
func Register ( registry * pipeline . ValidatorRegistry ) error {
2026-08-05 19:55:34 +00:00
return pipeline . RegisterTypedValidatorBuilder ( registry , dnd . ItemOccurrenceListKind , Spec ( ) , validateOptions , func ( request pipeline . BuildRequest ) ( contracts . TypedValidator [ dnd . ItemOccurrenceList ] , error ) {
2026-07-25 22:00:32 +00:00
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 }