Files
notarius/internal/modules/extract/dnd/spells/validator.go

169 lines
4.9 KiB
Go

package spells
import (
"context"
"encoding/json"
"fmt"
"strings"
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/validate"
)
const (
shapeValidatorName = "dnd/spells/shape"
sourceRefValidatorName = "dnd/spells/source_refs"
reasonInvalidPayload = "invalid_payload"
reasonMissingRequiredField = "missing_required_field"
reasonMissingSourceRef = "missing_source_ref"
reasonInvalidSourceRef = "invalid_source_ref"
reasonSpellNotNearSource = "spell_not_near_source"
)
var _ contracts.Validator = ShapeValidator{}
var _ contracts.Validator = SourceRefValidator{}
type ShapeValidator struct{}
func (validator ShapeValidator) Name() string {
return shapeValidatorName
}
func (validator ShapeValidator) Validate(ctx context.Context, req contracts.ValidationRequest) (contracts.ValidationResult, error) {
decisions := make([]contracts.ValidationDecision, 0, len(req.Candidates))
for _, candidate := range req.Candidates {
decisions = append(decisions, validateShape(candidate))
}
return contracts.ValidationResult{
ValidatorName: validator.Name(),
Decisions: decisions,
}, nil
}
type SourceRefValidator struct{}
func (validator SourceRefValidator) Name() string {
return sourceRefValidatorName
}
func (validator SourceRefValidator) Validate(ctx context.Context, req contracts.ValidationRequest) (contracts.ValidationResult, error) {
if req.Source == nil {
return contracts.ValidationResult{}, fmt.Errorf("dnd spells source refs validator: source must not be nil")
}
decisions := make([]contracts.ValidationDecision, 0, len(req.Candidates))
var warnings []contracts.Warning
for _, candidate := range req.Candidates {
decisions = append(decisions, validateSourceRefs(req.Source, candidate))
warnings = append(warnings, sourceRelatednessWarnings(req.Source, candidate)...)
}
return contracts.ValidationResult{
ValidatorName: validator.Name(),
Decisions: decisions,
Warnings: warnings,
}, nil
}
func validateShape(candidate artifacts.ArtifactCandidate) contracts.ValidationDecision {
var payload SpellCast
if err := json.Unmarshal(candidate.Payload, &payload); err != nil {
return validate.Rejected(candidate.Index, reasonInvalidPayload, fmt.Sprintf("invalid spell cast payload: %v", err))
}
for _, field := range requiredSpellCastFields(payload) {
if strings.TrimSpace(field.value) == "" {
return validate.Rejected(candidate.Index, reasonMissingRequiredField, fmt.Sprintf("missing required field %q", field.name))
}
}
return validate.Approved(candidate.Index)
}
func validateSourceRefs(doc *source.SourceDocument, candidate artifacts.ArtifactCandidate) contracts.ValidationDecision {
if len(candidate.SourceRefs) == 0 {
return validate.Rejected(candidate.Index, reasonMissingSourceRef, "spell cast candidate must include at least one source ref")
}
for _, ref := range candidate.SourceRefs {
if err := source.ValidateRef(doc, ref); err != nil {
return validate.Rejected(candidate.Index, reasonInvalidSourceRef, err.Error())
}
}
return validate.Approved(candidate.Index)
}
func sourceRelatednessWarnings(doc *source.SourceDocument, candidate artifacts.ArtifactCandidate) []contracts.Warning {
if doc == nil || len(candidate.SourceRefs) == 0 {
return nil
}
var payload SpellCast
if err := json.Unmarshal(candidate.Payload, &payload); err != nil {
return nil
}
spell := strings.TrimSpace(payload.Spell)
if spell == "" {
return nil
}
needle := strings.ToLower(spell)
for _, ref := range candidate.SourceRefs {
text, ok := sourceRefText(doc, ref)
if !ok {
continue
}
if strings.Contains(strings.ToLower(text), needle) {
return nil
}
}
return []contracts.Warning{
{
Scope: fmt.Sprintf("candidate.%d", candidate.Index),
ReasonCode: reasonSpellNotNearSource,
Message: fmt.Sprintf("spell %q was not found in the cited source text", spell),
},
}
}
func sourceRefText(doc *source.SourceDocument, ref source.SourceRef) (string, bool) {
if err := source.ValidateRef(doc, ref); err != nil {
return "", false
}
start := -1
end := -1
for i, unit := range doc.Units {
if unit.ID == ref.StartUnitID {
start = i
}
if unit.ID == ref.EndUnitID {
end = i
}
}
if start < 0 || end < start {
return "", false
}
var b strings.Builder
for i := start; i <= end; i++ {
if b.Len() > 0 {
b.WriteString("\n")
}
b.WriteString(doc.Units[i].Text)
}
return b.String(), true
}
func requiredSpellCastFields(payload SpellCast) []struct {
name string
value string
} {
return []struct {
name string
value string
}{
{name: "caster", value: payload.Caster},
{name: "spell", value: payload.Spell},
{name: "effect", value: payload.Effect},
{name: "narrative_description", value: payload.NarrativeDescription},
}
}