Refactor validators into built-in registry chains
This commit is contained in:
84
internal/validators/chains.go
Normal file
84
internal/validators/chains.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package validators
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/contracts"
|
||||
frameworkvalidators "gitea.maximumdirect.net/eric/audita/internal/framework/validators"
|
||||
)
|
||||
|
||||
var builtInChains = map[string][]string{
|
||||
"glossary": {
|
||||
KeyNoEffect,
|
||||
KeyOriginalTextPresence,
|
||||
KeyConfidenceThreshold,
|
||||
KeyProtectedTerms,
|
||||
KeyNonEmptyCorrectedText,
|
||||
KeySpokenFormPlausibility,
|
||||
KeyMeaningReversalReview,
|
||||
},
|
||||
"homophones": {
|
||||
KeyNoEffect,
|
||||
KeyOriginalTextPresence,
|
||||
KeyConfidenceThreshold,
|
||||
KeyProtectedTerms,
|
||||
KeyNonEmptyCorrectedText,
|
||||
KeySpokenFormPlausibility,
|
||||
KeyMeaningReversalReview,
|
||||
},
|
||||
"spoken_word": {
|
||||
KeyNoEffect,
|
||||
KeyOriginalTextPresence,
|
||||
KeyConfidenceThreshold,
|
||||
KeyProtectedTerms,
|
||||
KeyNonEmptyCorrectedText,
|
||||
KeySpokenWordReview,
|
||||
KeyMeaningReversalReview,
|
||||
},
|
||||
"grammar": {
|
||||
KeyNoEffect,
|
||||
KeyOriginalTextPresence,
|
||||
KeyConfidenceThreshold,
|
||||
KeyProtectedTerms,
|
||||
KeyNonEmptyCorrectedText,
|
||||
KeyGrammarReview,
|
||||
KeyMeaningReversalReview,
|
||||
},
|
||||
}
|
||||
|
||||
func BuiltInChainKeys(moduleKey string) ([]string, error) {
|
||||
keys, ok := builtInChains[moduleKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no built-in validator chain for module %q", moduleKey)
|
||||
}
|
||||
out := make([]string, len(keys))
|
||||
copy(out, keys)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ResolveBuiltInChain(moduleKey string, registry *Registry) ([]contracts.Validator, error) {
|
||||
keys, err := BuiltInChainKeys(moduleKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if registry == nil {
|
||||
registry = NewBuiltInRegistry()
|
||||
}
|
||||
|
||||
out := make([]contracts.Validator, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
if moduleKey == "glossary" && key == KeyProtectedTerms {
|
||||
// Glossary stages preserve current stricter protection semantics while
|
||||
// reporting the stable protected_terms key.
|
||||
v := frameworkvalidators.GlossaryStageProtectedGlossaryTermValidator{}
|
||||
out = append(out, v)
|
||||
continue
|
||||
}
|
||||
v, buildErr := registry.MustBuild(key)
|
||||
if buildErr != nil {
|
||||
return nil, buildErr
|
||||
}
|
||||
out = append(out, v)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
66
internal/validators/module_chains_test.go
Normal file
66
internal/validators/module_chains_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package validators_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/modules/glossary"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/modules/grammar"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/modules/homophones"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/modules/spoken_word"
|
||||
builtinvalidators "gitea.maximumdirect.net/eric/audita/internal/validators"
|
||||
)
|
||||
|
||||
func TestProductionModulesUseRegisteredValidatorKeys(t *testing.T) {
|
||||
registry := builtinvalidators.NewBuiltInRegistry()
|
||||
keys := map[string]struct{}{}
|
||||
for _, key := range registry.RegisteredKeys() {
|
||||
keys[key] = struct{}{}
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
build func() ([]contracts.Validator, error)
|
||||
}{
|
||||
{name: "glossary", build: func() ([]contracts.Validator, error) {
|
||||
m, err := glossary.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.Validators(), nil
|
||||
}},
|
||||
{name: "homophones", build: func() ([]contracts.Validator, error) {
|
||||
m, err := homophones.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.Validators(), nil
|
||||
}},
|
||||
{name: "spoken_word", build: func() ([]contracts.Validator, error) {
|
||||
m, err := spoken_word.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.Validators(), nil
|
||||
}},
|
||||
{name: "grammar", build: func() ([]contracts.Validator, error) {
|
||||
m, err := grammar.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.Validators(), nil
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
validators, err := tc.build()
|
||||
if err != nil {
|
||||
t.Fatalf("build module %s: %v", tc.name, err)
|
||||
}
|
||||
for _, v := range validators {
|
||||
if _, ok := keys[v.Name()]; !ok {
|
||||
t.Fatalf("module %s uses unregistered validator key %q", tc.name, v.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
104
internal/validators/registry.go
Normal file
104
internal/validators/registry.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package validators
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/contracts"
|
||||
frameworkvalidators "gitea.maximumdirect.net/eric/audita/internal/framework/validators"
|
||||
)
|
||||
|
||||
const (
|
||||
KeyConfidenceThreshold = "confidence_threshold"
|
||||
KeyOriginalTextPresence = "original_text_presence"
|
||||
KeyNonEmptyCorrectedText = "non_empty_corrected_text"
|
||||
KeyNoEffect = "no_effect"
|
||||
KeyProtectedTerms = "protected_terms"
|
||||
|
||||
KeySpokenFormPlausibility = "spoken_form_plausibility"
|
||||
KeyMeaningReversalReview = "meaning_reversal_review"
|
||||
KeyEditorialReview = "editorial_review"
|
||||
KeyGrammarReview = "grammar_review"
|
||||
KeySpokenWordReview = "spoken_word_review"
|
||||
)
|
||||
|
||||
type BuiltInValidatorDefinition struct {
|
||||
Key string
|
||||
Build func() (contracts.Validator, error)
|
||||
LLMBacked bool
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
definitions map[string]BuiltInValidatorDefinition
|
||||
}
|
||||
|
||||
func NewBuiltInRegistry() *Registry {
|
||||
defs := []BuiltInValidatorDefinition{
|
||||
{Key: KeyConfidenceThreshold, Build: func() (contracts.Validator, error) { return frameworkvalidators.ConfidenceThresholdValidator{}, nil }},
|
||||
{Key: KeyOriginalTextPresence, Build: func() (contracts.Validator, error) { return frameworkvalidators.OriginalTextPresenceValidator{}, nil }},
|
||||
{Key: KeyNonEmptyCorrectedText, Build: func() (contracts.Validator, error) { return frameworkvalidators.NonEmptyCorrectionValidator{}, nil }},
|
||||
{Key: KeyNoEffect, Build: func() (contracts.Validator, error) { return frameworkvalidators.NoEffectValidator{}, nil }},
|
||||
{Key: KeyProtectedTerms, Build: func() (contracts.Validator, error) { return frameworkvalidators.ProtectedGlossaryTermValidator{}, nil }},
|
||||
{Key: KeySpokenFormPlausibility, LLMBacked: true, Build: func() (contracts.Validator, error) {
|
||||
return frameworkvalidators.NewLLMBackedValidator(KeySpokenFormPlausibility, frameworkvalidators.LLMValidatorTypeSpokenFormPlausibility, "")
|
||||
}},
|
||||
{Key: KeyMeaningReversalReview, LLMBacked: true, Build: func() (contracts.Validator, error) {
|
||||
return frameworkvalidators.NewLLMBackedValidator(KeyMeaningReversalReview, frameworkvalidators.LLMValidatorTypeMeaningReversal, "")
|
||||
}},
|
||||
{Key: KeyEditorialReview, LLMBacked: true, Build: func() (contracts.Validator, error) {
|
||||
return frameworkvalidators.NewLLMBackedValidator(KeyEditorialReview, frameworkvalidators.LLMValidatorTypeEditorialReview, "")
|
||||
}},
|
||||
{Key: KeyGrammarReview, LLMBacked: true, Build: func() (contracts.Validator, error) {
|
||||
return frameworkvalidators.NewLLMBackedValidator(KeyGrammarReview, frameworkvalidators.LLMValidatorTypeGrammarReview, "")
|
||||
}},
|
||||
{Key: KeySpokenWordReview, LLMBacked: true, Build: func() (contracts.Validator, error) {
|
||||
return frameworkvalidators.NewLLMBackedValidator(KeySpokenWordReview, frameworkvalidators.LLMValidatorTypeSpokenWordReview, "")
|
||||
}},
|
||||
}
|
||||
|
||||
m := make(map[string]BuiltInValidatorDefinition, len(defs))
|
||||
for _, def := range defs {
|
||||
m[def.Key] = def
|
||||
}
|
||||
return &Registry{definitions: m}
|
||||
}
|
||||
|
||||
func (r *Registry) Lookup(key string) (BuiltInValidatorDefinition, bool) {
|
||||
if r == nil {
|
||||
return BuiltInValidatorDefinition{}, false
|
||||
}
|
||||
def, ok := r.definitions[strings.TrimSpace(key)]
|
||||
return def, ok
|
||||
}
|
||||
|
||||
func (r *Registry) MustBuild(key string) (contracts.Validator, error) {
|
||||
if r == nil {
|
||||
return nil, fmt.Errorf("validator registry is nil")
|
||||
}
|
||||
def, ok := r.Lookup(key)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown validator key %q", key)
|
||||
}
|
||||
v, err := def.Build()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build validator %q: %w", key, err)
|
||||
}
|
||||
if v == nil {
|
||||
return nil, fmt.Errorf("build validator %q: returned nil validator", key)
|
||||
}
|
||||
if v.Name() != def.Key {
|
||||
return nil, fmt.Errorf("validator key/name mismatch for %q: got %q", key, v.Name())
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (r *Registry) RegisteredKeys() []string {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
keys := make([]string, 0, len(r.definitions))
|
||||
for k := range r.definitions {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
79
internal/validators/registry_test.go
Normal file
79
internal/validators/registry_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package validators
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuiltInRegistryRegistersAllKeys(t *testing.T) {
|
||||
r := NewBuiltInRegistry()
|
||||
for _, key := range []string{
|
||||
KeyConfidenceThreshold,
|
||||
KeyOriginalTextPresence,
|
||||
KeyNonEmptyCorrectedText,
|
||||
KeyNoEffect,
|
||||
KeyProtectedTerms,
|
||||
KeySpokenFormPlausibility,
|
||||
KeyMeaningReversalReview,
|
||||
KeyEditorialReview,
|
||||
KeyGrammarReview,
|
||||
KeySpokenWordReview,
|
||||
} {
|
||||
def, ok := r.Lookup(key)
|
||||
if !ok {
|
||||
t.Fatalf("expected validator key %q to be registered", key)
|
||||
}
|
||||
v, err := def.Build()
|
||||
if err != nil {
|
||||
t.Fatalf("build validator %q: %v", key, err)
|
||||
}
|
||||
if v.Name() != key {
|
||||
t.Fatalf("expected validator name %q, got %q", key, v.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuiltInRegistryUnknownKeyFails(t *testing.T) {
|
||||
r := NewBuiltInRegistry()
|
||||
if _, err := r.MustBuild("missing"); err == nil {
|
||||
t.Fatalf("expected unknown validator key error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuiltInChainKeysResolveForProductionModules(t *testing.T) {
|
||||
for _, moduleKey := range []string{"glossary", "homophones", "spoken_word", "grammar"} {
|
||||
keys, err := BuiltInChainKeys(moduleKey)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve keys for %q: %v", moduleKey, err)
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
t.Fatalf("expected non-empty chain for %q", moduleKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveBuiltInChainUsesRegisteredKeys(t *testing.T) {
|
||||
r := NewBuiltInRegistry()
|
||||
for _, moduleKey := range []string{"glossary", "homophones", "spoken_word", "grammar"} {
|
||||
chain, err := ResolveBuiltInChain(moduleKey, r)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve chain for %q: %v", moduleKey, err)
|
||||
}
|
||||
if len(chain) == 0 {
|
||||
t.Fatalf("expected non-empty chain for %q", moduleKey)
|
||||
}
|
||||
for _, v := range chain {
|
||||
if v == nil {
|
||||
t.Fatalf("nil validator in %q chain", moduleKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuiltInChainUnknownModuleFails(t *testing.T) {
|
||||
if _, err := BuiltInChainKeys("unknown"); err == nil {
|
||||
t.Fatalf("expected unknown module chain failure")
|
||||
}
|
||||
if _, err := ResolveBuiltInChain("unknown", NewBuiltInRegistry()); err == nil {
|
||||
t.Fatalf("expected unknown module chain failure")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user