Refactor validators into package-owned components

This commit is contained in:
2026-05-14 00:28:28 +00:00
parent 3b160cf05b
commit 52ffe42e73
26 changed files with 775 additions and 98 deletions

View File

@@ -95,23 +95,16 @@ func (v NoEffectValidator) Validate(_ context.Context, req Request) (Result, err
return Result{ValidatorName: v.Name(), Decisions: decisions}, nil
}
type ProtectedGlossaryTermValidator struct{}
func (v ProtectedGlossaryTermValidator) Name() string { return "protected_terms" }
func (v ProtectedGlossaryTermValidator) Validate(_ context.Context, req Request) (Result, error) {
if req.ModuleKey == "glossary" {
decisions := make([]Decision, 0, len(req.CandidateProposal))
for _, c := range req.CandidateProposal {
decisions = append(decisions, approval(c.ProposalIndex))
}
return Result{ValidatorName: v.Name(), Decisions: decisions}, nil
}
func ValidateProtectedTerms(req Request, glossaryStage bool) (Result, error) {
vocab := NewProtectedVocabulary(req.Glossary)
decisions := make([]Decision, 0, len(req.CandidateProposal))
for _, c := range req.CandidateProposal {
reason := vocab.violationReason(c.OriginalText, c.CorrectedText)
var reason string
if glossaryStage {
reason = vocab.glossaryStageViolationReason(c.OriginalText, c.CorrectedText)
} else {
reason = vocab.violationReason(c.OriginalText, c.CorrectedText)
}
if reason != "" {
decisions = append(decisions, rejection(c.ProposalIndex, ReasonProtectedGlossaryTerm, reason))
continue
@@ -121,28 +114,5 @@ func (v ProtectedGlossaryTermValidator) Validate(_ context.Context, req Request)
if err := EnforceDecisionCardinality(req.CandidateProposal, decisions); err != nil {
return Result{}, err
}
return Result{ValidatorName: v.Name(), Decisions: decisions}, nil
}
type GlossaryStageProtectedGlossaryTermValidator struct{}
func (v GlossaryStageProtectedGlossaryTermValidator) Name() string {
return "protected_terms"
}
func (v GlossaryStageProtectedGlossaryTermValidator) Validate(_ context.Context, req Request) (Result, error) {
vocab := NewProtectedVocabulary(req.Glossary)
decisions := make([]Decision, 0, len(req.CandidateProposal))
for _, c := range req.CandidateProposal {
reason := vocab.glossaryStageViolationReason(c.OriginalText, c.CorrectedText)
if reason != "" {
decisions = append(decisions, rejection(c.ProposalIndex, ReasonProtectedGlossaryTerm, reason))
continue
}
decisions = append(decisions, approval(c.ProposalIndex))
}
if err := EnforceDecisionCardinality(req.CandidateProposal, decisions); err != nil {
return Result{}, err
}
return Result{ValidatorName: v.Name(), Decisions: decisions}, nil
return Result{ValidatorName: "protected_terms", Decisions: decisions}, nil
}

View File

@@ -1,7 +1,6 @@
package validators
import (
"context"
"reflect"
"testing"
@@ -31,7 +30,7 @@ func TestExtractProtectedTermsEmptyGlossary(t *testing.T) {
}
}
func TestProtectedGlossaryTermValidatorAppliesToNonGlossaryModule(t *testing.T) {
func TestValidateProtectedTermsAppliesToNonGlossaryModule(t *testing.T) {
req := Request{
ModuleKey: "grammar",
Glossary: &schema.Glossary{
@@ -49,7 +48,7 @@ func TestProtectedGlossaryTermValidatorAppliesToNonGlossaryModule(t *testing.T)
},
},
}
res, err := (ProtectedGlossaryTermValidator{}).Validate(context.Background(), req)
res, err := ValidateProtectedTerms(req, false)
if err != nil {
t.Fatalf("validator error: %v", err)
}
@@ -58,7 +57,7 @@ func TestProtectedGlossaryTermValidatorAppliesToNonGlossaryModule(t *testing.T)
}
}
func TestGlossaryStageProtectedGlossaryTermValidatorAllowsProtectedTermSwap(t *testing.T) {
func TestValidateProtectedTermsGlossaryStageAllowsProtectedTermSwap(t *testing.T) {
req := Request{
ModuleKey: "glossary",
Glossary: &schema.Glossary{
@@ -76,7 +75,7 @@ func TestGlossaryStageProtectedGlossaryTermValidatorAllowsProtectedTermSwap(t *t
},
},
}
res, err := (GlossaryStageProtectedGlossaryTermValidator{}).Validate(context.Background(), req)
res, err := ValidateProtectedTerms(req, true)
if err != nil {
t.Fatalf("validator error: %v", err)
}

View File

@@ -123,7 +123,7 @@ func TestNoEffectValidator(t *testing.T) {
}
}
func TestProtectedGlossaryTermValidator(t *testing.T) {
func TestValidateProtectedTerms(t *testing.T) {
glossary := &schema.Glossary{Entries: []schema.GlossaryEntry{{Name: "OpenAI", Aliases: []string{"Open AI"}, Plural: "OpenAIs", Category: "brand", Summary: "brand"}}}
req := Request{
Glossary: glossary,
@@ -133,7 +133,7 @@ func TestProtectedGlossaryTermValidator(t *testing.T) {
mkCandidate(1, 1, "teh", "the", 0.9),
},
}
res, err := (ProtectedGlossaryTermValidator{}).Validate(context.Background(), req)
res, err := ValidateProtectedTerms(req, false)
if err != nil {
t.Fatalf("Validate error: %v", err)
}
@@ -145,24 +145,6 @@ func TestProtectedGlossaryTermValidator(t *testing.T) {
}
}
func TestProtectedGlossaryTermValidatorAllowsGlossaryModule(t *testing.T) {
glossary := &schema.Glossary{Entries: []schema.GlossaryEntry{{Name: "OpenAI", Category: "brand", Summary: "brand"}}}
req := Request{
Glossary: glossary,
ModuleKey: "glossary",
CandidateProposal: []proposals.EnrichedCorrectionProposal{
mkCandidate(0, 1, "OpenAI", "Open A I", 0.9),
},
}
res, err := (ProtectedGlossaryTermValidator{}).Validate(context.Background(), req)
if err != nil {
t.Fatalf("Validate error: %v", err)
}
if !res.Decisions[0].Approved {
t.Fatalf("expected glossary module approval, got %+v", res.Decisions[0])
}
}
func TestStableReasonCodes(t *testing.T) {
codes := []string{
ReasonApproved,