2026-08-05 19:55:34 +00:00
// Package registry validates item occurrence identity against item grounding.
package registry
import (
"context"
"fmt"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
itemregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/items/registry"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
2026-08-05 20:00:20 +00:00
occurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/shape"
2026-08-05 19:55:34 +00:00
)
const (
2026-08-05 20:00:20 +00:00
Key = "extract/dnd/item-occurrences/registry"
2026-08-05 19:55:34 +00:00
ReasonCode = "invalid_item_occurrence_registry"
2026-08-29 01:24:45 +00:00
policy = "dnd.item_occurrences.validator.registry.v2"
2026-08-05 19:55:34 +00:00
)
type Options struct { }
type Validator struct { itemResolver * itemregistry . Resolver }
var _ contracts . TypedValidator [ dnd . ItemOccurrenceList ] = ( * Validator ) ( nil )
var _ contracts . ManifestMetadataProvider = ( * Validator ) ( nil )
var _ pipeline . CheckpointFingerprintProvider = ( * Validator ) ( nil )
func New ( _ Options , references ... contracts . ReferenceSet ) ( * Validator , error ) {
if len ( references ) > 1 {
return nil , fmt . Errorf ( "item occurrence registry validator accepts at most one reference set" )
}
var referenceSet contracts . ReferenceSet
if len ( references ) == 1 {
referenceSet = references [ 0 ]
}
resolver , err := itemregistry . NewResolver ( referenceSet )
if err != nil {
return nil , fmt . Errorf ( "prepare item registry: %w" , err )
}
return & Validator { itemResolver : resolver } , nil
}
func ( v * Validator ) Name ( ) string { return Key }
func ( v * Validator ) ExecutionClass ( ) contracts . ExecutionClass {
return contracts . ExecutionClassDeterministic
}
func ( v * Validator ) ManifestMetadata ( ) map [ string ] any {
if v == nil || v . itemResolver == nil {
return nil
}
metadata := map [ string ] any { "policy" : policy }
seeded := v . itemResolver . Seeded ( )
if seeded . Bound ( ) {
metadata [ "item_registry_digest" ] = seeded . Digest ( )
metadata [ "item_count" ] = seeded . Count ( )
}
return metadata
}
func ( v * Validator ) CheckpointFingerprints ( ) [ ] pipeline . CheckpointFingerprint {
if v == nil || v . itemResolver == nil {
return nil
}
return [ ] pipeline . CheckpointFingerprint { { Name : "policy" , Value : policy } , { Name : "item_registry" , Value : v . itemResolver . Seeded ( ) . ProjectionDigest ( ) } }
}
func ( v * Validator ) Validate ( _ context . Context , req contracts . TypedValidationRequest [ dnd . ItemOccurrenceList ] ) ( contracts . ValidationResult , error ) {
if occurrenceshape . Validate ( req . Value ) != nil {
return contracts . ValidationResult { Approved : true } , nil
}
if v == nil || v . itemResolver == nil {
return contracts . ValidationResult { } , fmt . Errorf ( "item occurrence registry validator must not be nil" )
}
registry , err := v . itemResolver . Resolve ( req . References )
if err != nil {
return contracts . ValidationResult { } , fmt . Errorf ( "resolve item registry: %w" , err )
}
if ! registry . Bound ( ) {
2026-08-29 01:24:45 +00:00
var corrections diagnostics . Corrections
corrections . Add ( "registry" , "Use only contextual item names from the supplied item registry; omit an occurrence that cannot be matched unambiguously." , "" )
return rejection ( [ ] string { "item registry reference is required" } , corrections ) , nil
2026-08-05 19:55:34 +00:00
}
issues := make ( [ ] string , 0 )
2026-08-29 01:24:45 +00:00
var corrections diagnostics . Corrections
2026-08-05 19:55:34 +00:00
for index , occurrence := range req . Value . Occurrences {
2026-08-29 01:24:45 +00:00
record := fmt . Sprintf ( "Affected %s occurrence for item %s %s." , diagnostics . Quote ( string ( occurrence . Kind ) ) , diagnostics . Quote ( occurrence . Name ) , diagnostics . SourceRange ( occurrence . SourceRefs ) )
2026-08-05 19:55:34 +00:00
item , ok := registry . LookupID ( occurrence . ItemID )
if ! ok {
issues = append ( issues , fmt . Sprintf ( "occurrences[%d].item_id is not in the item registry: %s" , index , diagnostics . Quote ( occurrence . ItemID ) ) )
2026-08-29 01:24:45 +00:00
corrections . Add ( "registry" , "Use only contextual item names from the supplied item registry; omit an occurrence that cannot be matched unambiguously." , record )
2026-08-05 19:55:34 +00:00
continue
}
if occurrence . Name != item . Name {
issues = append ( issues , fmt . Sprintf ( "occurrences[%d] does not match registry item %s" , index , diagnostics . Quote ( occurrence . ItemID ) ) )
2026-08-29 01:24:45 +00:00
corrections . Add ( "canonical-name" , "Use the exact contextual item name supplied by the registry." , record + " Use registry name " + diagnostics . Quote ( item . Name ) + "." )
2026-08-05 19:55:34 +00:00
}
}
if len ( issues ) == 0 {
return contracts . ValidationResult { Approved : true } , nil
}
2026-08-29 01:24:45 +00:00
return rejection ( issues , corrections ) , nil
2026-08-05 19:55:34 +00:00
}
2026-08-29 01:24:45 +00:00
func rejection ( issues [ ] string , corrections diagnostics . Corrections ) contracts . ValidationResult {
return contracts . ValidationResult { Approved : false , ReasonCode : ReasonCode , Message : diagnostics . Aggregate ( "invalid item occurrence registry" , issues ) , CorrectionGuidance : corrections . Guidance ( "Correct every rejected item occurrence and return the complete replacement occurrence list" ) }
2026-08-05 19:55:34 +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 . ItemOccurrenceListKind , Spec ( ) , validateOptions , func ( request pipeline . BuildRequest ) ( contracts . TypedValidator [ dnd . ItemOccurrenceList ] , error ) {
options , err := DecodeOptions ( request . Options )
if err != nil {
return nil , err
}
return New ( options , request . References )
} )
}
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 }