Centralize validator classification and malformed output handling
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/audita/internal/framework/runner"
|
"gitea.maximumdirect.net/eric/audita/internal/framework/runner"
|
||||||
|
validatormetadata "gitea.maximumdirect.net/eric/audita/internal/validators/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -48,12 +49,6 @@ func buildCorrectionLedger(runDirPath string, runOutput *runner.RunOutput) []cor
|
|||||||
}
|
}
|
||||||
|
|
||||||
entries := make([]correctionLedgerEntry, 0)
|
entries := make([]correctionLedgerEntry, 0)
|
||||||
llmBacked := map[string]bool{
|
|
||||||
"spoken_form_plausibility": true,
|
|
||||||
"meaning_reversal_review": true,
|
|
||||||
"editorial_review": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, module := range runOutput.ModuleResults {
|
for _, module := range runOutput.ModuleResults {
|
||||||
decisionsByProposal := make(map[int][]runner.ValidatorDecisionRecord)
|
decisionsByProposal := make(map[int][]runner.ValidatorDecisionRecord)
|
||||||
for _, decision := range module.ValidatorDecisions {
|
for _, decision := range module.ValidatorDecisions {
|
||||||
@@ -72,8 +67,8 @@ func buildCorrectionLedger(runDirPath string, runOutput *runner.RunOutput) []cor
|
|||||||
AppliedCorrectedText: change.CorrectedText,
|
AppliedCorrectedText: change.CorrectedText,
|
||||||
ReplacementPolicy: string(module.ReplacementPolicy),
|
ReplacementPolicy: string(module.ReplacementPolicy),
|
||||||
Disposition: correctionDispositionApplied,
|
Disposition: correctionDispositionApplied,
|
||||||
DeterministicValidatorResults: filterLedgerDecisions(decisionsByProposal[change.ProposalIndex], false, llmBacked),
|
DeterministicValidatorResults: filterLedgerDecisions(decisionsByProposal[change.ProposalIndex], false),
|
||||||
LLMValidatorResults: filterLedgerDecisions(decisionsByProposal[change.ProposalIndex], true, llmBacked),
|
LLMValidatorResults: filterLedgerDecisions(decisionsByProposal[change.ProposalIndex], true),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for _, change := range module.SkippedChanges {
|
for _, change := range module.SkippedChanges {
|
||||||
@@ -89,8 +84,8 @@ func buildCorrectionLedger(runDirPath string, runOutput *runner.RunOutput) []cor
|
|||||||
Disposition: correctionDispositionSkipped,
|
Disposition: correctionDispositionSkipped,
|
||||||
DispositionReasonCode: string(change.SkipReason),
|
DispositionReasonCode: string(change.SkipReason),
|
||||||
DispositionMessage: change.Message,
|
DispositionMessage: change.Message,
|
||||||
DeterministicValidatorResults: filterLedgerDecisions(decisionsByProposal[change.ProposalIndex], false, llmBacked),
|
DeterministicValidatorResults: filterLedgerDecisions(decisionsByProposal[change.ProposalIndex], false),
|
||||||
LLMValidatorResults: filterLedgerDecisions(decisionsByProposal[change.ProposalIndex], true, llmBacked),
|
LLMValidatorResults: filterLedgerDecisions(decisionsByProposal[change.ProposalIndex], true),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for _, rejection := range module.ValidatorRejected {
|
for _, rejection := range module.ValidatorRejected {
|
||||||
@@ -106,8 +101,8 @@ func buildCorrectionLedger(runDirPath string, runOutput *runner.RunOutput) []cor
|
|||||||
Disposition: correctionDispositionRejected,
|
Disposition: correctionDispositionRejected,
|
||||||
DispositionReasonCode: rejection.ReasonCode,
|
DispositionReasonCode: rejection.ReasonCode,
|
||||||
DispositionMessage: rejection.Message,
|
DispositionMessage: rejection.Message,
|
||||||
DeterministicValidatorResults: filterLedgerDecisions(decisionsByProposal[rejection.ProposalIndex], false, llmBacked),
|
DeterministicValidatorResults: filterLedgerDecisions(decisionsByProposal[rejection.ProposalIndex], false),
|
||||||
LLMValidatorResults: filterLedgerDecisions(decisionsByProposal[rejection.ProposalIndex], true, llmBacked),
|
LLMValidatorResults: filterLedgerDecisions(decisionsByProposal[rejection.ProposalIndex], true),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if module.Status == runner.ModuleStatusFailed {
|
if module.Status == runner.ModuleStatusFailed {
|
||||||
@@ -135,13 +130,14 @@ func buildCorrectionLedger(runDirPath string, runOutput *runner.RunOutput) []cor
|
|||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterLedgerDecisions(in []runner.ValidatorDecisionRecord, wantLLM bool, llmBacked map[string]bool) []ledgerValidatorDecisionRecord {
|
func filterLedgerDecisions(in []runner.ValidatorDecisionRecord, wantLLM bool) []ledgerValidatorDecisionRecord {
|
||||||
if len(in) == 0 {
|
if len(in) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
out := make([]ledgerValidatorDecisionRecord, 0, len(in))
|
out := make([]ledgerValidatorDecisionRecord, 0, len(in))
|
||||||
for _, decision := range in {
|
for _, decision := range in {
|
||||||
if llmBacked[decision.ValidatorName] != wantLLM {
|
isLLMBacked := validatormetadata.ClassForKey(decision.ValidatorName) == validatormetadata.ExecutionClassLLMBacked
|
||||||
|
if isLLMBacked != wantLLM {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out = append(out, ledgerValidatorDecisionRecord{
|
out = append(out, ledgerValidatorDecisionRecord{
|
||||||
|
|||||||
46
internal/cli/review_artifacts_test.go
Normal file
46
internal/cli/review_artifacts_test.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
|
||||||
|
"gitea.maximumdirect.net/eric/audita/internal/framework/runner"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildCorrectionLedgerClassifiesValidatorDecisionsFromCanonicalMetadata(t *testing.T) {
|
||||||
|
output := &runner.RunOutput{
|
||||||
|
ModuleResults: []runner.ModuleResult{
|
||||||
|
{
|
||||||
|
ModuleKey: "glossary",
|
||||||
|
ModuleInstance: "glossary",
|
||||||
|
ReplacementPolicy: proposals.ReplacementPolicyReplaceAll,
|
||||||
|
ValidatorDecisions: []runner.ValidatorDecisionRecord{
|
||||||
|
{ValidatorName: "proposal_shape", ProposalIndex: 3, Approved: true, ReasonCode: "approved"},
|
||||||
|
{ValidatorName: "spoken_form_plausibility", ProposalIndex: 3, Approved: true, ReasonCode: "approved"},
|
||||||
|
},
|
||||||
|
AppliedChanges: []proposals.AppliedChange{
|
||||||
|
{
|
||||||
|
ProposalIndex: 3,
|
||||||
|
ModuleKey: "glossary",
|
||||||
|
ModuleInstance: "glossary",
|
||||||
|
TargetSegmentID: 1,
|
||||||
|
OriginalText: "gestures",
|
||||||
|
CorrectedText: "Jesters",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ledger := buildCorrectionLedger("/tmp/audita-run-id", output)
|
||||||
|
if len(ledger) != 1 {
|
||||||
|
t.Fatalf("expected one ledger entry, got %d", len(ledger))
|
||||||
|
}
|
||||||
|
entry := ledger[0]
|
||||||
|
if len(entry.DeterministicValidatorResults) != 1 || entry.DeterministicValidatorResults[0].ValidatorKey != "proposal_shape" {
|
||||||
|
t.Fatalf("unexpected deterministic decision split: %+v", entry.DeterministicValidatorResults)
|
||||||
|
}
|
||||||
|
if len(entry.LLMValidatorResults) != 1 || entry.LLMValidatorResults[0].ValidatorKey != "spoken_form_plausibility" {
|
||||||
|
t.Fatalf("unexpected llm-backed decision split: %+v", entry.LLMValidatorResults)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
|
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
|
||||||
"gitea.maximumdirect.net/eric/audita/internal/framework/responseschema"
|
"gitea.maximumdirect.net/eric/audita/internal/framework/responseschema"
|
||||||
"gitea.maximumdirect.net/eric/audita/internal/framework/stagename"
|
"gitea.maximumdirect.net/eric/audita/internal/framework/stagename"
|
||||||
|
"gitea.maximumdirect.net/eric/audita/internal/framework/structuredoutput"
|
||||||
stagewarnings "gitea.maximumdirect.net/eric/audita/internal/framework/warnings"
|
stagewarnings "gitea.maximumdirect.net/eric/audita/internal/framework/warnings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -159,7 +160,7 @@ func GenerateCandidates(ctx context.Context, req Request) (Result, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if callErr != nil {
|
if callErr != nil {
|
||||||
if isMalformedStructuredOutputError(callErr) {
|
if structuredoutput.IsMalformedError(callErr) {
|
||||||
return Result{
|
return Result{
|
||||||
Warnings: []stagewarnings.StageWarning{newMalformedProposalWarning(req.Section, artifacts, callErr)},
|
Warnings: []stagewarnings.StageWarning{newMalformedProposalWarning(req.Section, artifacts, callErr)},
|
||||||
Artifacts: artifacts,
|
Artifacts: artifacts,
|
||||||
@@ -230,27 +231,6 @@ func errPayload(err error) any {
|
|||||||
return map[string]any{"error": err.Error()}
|
return map[string]any{"error": err.Error()}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isMalformedStructuredOutputError(err error) bool {
|
|
||||||
if err == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
msg := err.Error()
|
|
||||||
for _, marker := range []string{
|
|
||||||
"malformed structured output",
|
|
||||||
"decode structured output:",
|
|
||||||
"decode provider response envelope:",
|
|
||||||
"provider response missing choices",
|
|
||||||
"provider response missing assistant message content",
|
|
||||||
"provider response assistant message content is empty",
|
|
||||||
"provider response assistant message content is not valid JSON",
|
|
||||||
} {
|
|
||||||
if strings.Contains(msg, marker) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMalformedProposalWarning(section *contracts.SectionMetadata, artifacts InteractionArtifacts, err error) stagewarnings.StageWarning {
|
func newMalformedProposalWarning(section *contracts.SectionMetadata, artifacts InteractionArtifacts, err error) stagewarnings.StageWarning {
|
||||||
warning := stagewarnings.StageWarning{
|
warning := stagewarnings.StageWarning{
|
||||||
Scope: stagewarnings.ScopeProposalGeneration,
|
Scope: stagewarnings.ScopeProposalGeneration,
|
||||||
|
|||||||
@@ -272,6 +272,23 @@ func TestGenerateCandidatesMalformedStructuredOutputReturnsWarning(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGenerateCandidatesProviderMalformedEnvelopeReturnsWarning(t *testing.T) {
|
||||||
|
client := &fakeStructuredClient{err: errors.New("provider response missing choices")}
|
||||||
|
req := defaultRequest(t)
|
||||||
|
req.LLMClient = client
|
||||||
|
|
||||||
|
result, err := GenerateCandidates(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected malformed provider envelope to downgrade to warning, got %v", err)
|
||||||
|
}
|
||||||
|
if len(result.Corrections) != 0 || len(result.Enriched) != 0 {
|
||||||
|
t.Fatalf("expected no proposals on malformed response, got %+v", result)
|
||||||
|
}
|
||||||
|
if len(result.Warnings) != 1 || result.Warnings[0].ReasonCode != "proposal_response_malformed" {
|
||||||
|
t.Fatalf("unexpected warnings: %+v", result.Warnings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGenerateCandidatesDeterministicIndexAssignment(t *testing.T) {
|
func TestGenerateCandidatesDeterministicIndexAssignment(t *testing.T) {
|
||||||
baseResponse := StructuredCorrectionSet{
|
baseResponse := StructuredCorrectionSet{
|
||||||
Corrections: []StructuredCorrectionProposal{
|
Corrections: []StructuredCorrectionProposal{
|
||||||
|
|||||||
28
internal/framework/structuredoutput/malformed.go
Normal file
28
internal/framework/structuredoutput/malformed.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package structuredoutput
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
var malformedMarkers = []string{
|
||||||
|
"malformed structured output",
|
||||||
|
"decode structured output:",
|
||||||
|
"decode provider response envelope:",
|
||||||
|
"provider response missing choices",
|
||||||
|
"provider response missing assistant message content",
|
||||||
|
"provider response assistant message content is empty",
|
||||||
|
"provider response assistant message content is not valid JSON",
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsMalformedError reports whether err matches provider malformed
|
||||||
|
// structured-output failure markers that should be downgraded.
|
||||||
|
func IsMalformedError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
msg := err.Error()
|
||||||
|
for _, marker := range malformedMarkers {
|
||||||
|
if strings.Contains(msg, marker) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
32
internal/framework/structuredoutput/malformed_test.go
Normal file
32
internal/framework/structuredoutput/malformed_test.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package structuredoutput
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsMalformedError(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "nil", err: nil, want: false},
|
||||||
|
{name: "generic", err: errors.New("network timeout"), want: false},
|
||||||
|
{name: "malformed", err: errors.New("malformed structured output"), want: true},
|
||||||
|
{name: "decode structured", err: errors.New("decode structured output: unexpected end of JSON input"), want: true},
|
||||||
|
{name: "missing choices", err: errors.New("provider response missing choices"), want: true},
|
||||||
|
{name: "missing content", err: errors.New("provider response missing assistant message content"), want: true},
|
||||||
|
{name: "empty content", err: errors.New("provider response assistant message content is empty"), want: true},
|
||||||
|
{name: "invalid content json", err: errors.New("provider response assistant message content is not valid JSON"), want: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got := IsMalformedError(tc.err)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Fatalf("IsMalformedError(%v): got=%v want=%v", tc.err, got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
|
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
|
||||||
"gitea.maximumdirect.net/eric/audita/internal/framework/responseschema"
|
"gitea.maximumdirect.net/eric/audita/internal/framework/responseschema"
|
||||||
"gitea.maximumdirect.net/eric/audita/internal/framework/stagename"
|
"gitea.maximumdirect.net/eric/audita/internal/framework/stagename"
|
||||||
|
"gitea.maximumdirect.net/eric/audita/internal/framework/structuredoutput"
|
||||||
stagewarnings "gitea.maximumdirect.net/eric/audita/internal/framework/warnings"
|
stagewarnings "gitea.maximumdirect.net/eric/audita/internal/framework/warnings"
|
||||||
"gitea.maximumdirect.net/eric/audita/internal/prompts"
|
"gitea.maximumdirect.net/eric/audita/internal/prompts"
|
||||||
)
|
)
|
||||||
@@ -144,7 +145,7 @@ func (v *LLMBackedValidator) Validate(ctx context.Context, req Request) (Result,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if isMalformedStructuredOutputError(err) {
|
if structuredoutput.IsMalformedError(err) {
|
||||||
llmDecisions = append(llmDecisions, rejectBatch(batch.Items, ReasonValidatorMalformed, fmt.Sprintf("validator response malformed: %s", strings.TrimSpace(err.Error())))...)
|
llmDecisions = append(llmDecisions, rejectBatch(batch.Items, ReasonValidatorMalformed, fmt.Sprintf("validator response malformed: %s", strings.TrimSpace(err.Error())))...)
|
||||||
warnings = append(warnings, newValidatorWarning(v.name, batch.BatchIndex, ReasonValidatorMalformed, err.Error(), artifacts))
|
warnings = append(warnings, newValidatorWarning(v.name, batch.BatchIndex, ReasonValidatorMalformed, err.Error(), artifacts))
|
||||||
continue
|
continue
|
||||||
@@ -387,24 +388,3 @@ func diagnosticArtifactPath(artifacts InteractionArtifacts) string {
|
|||||||
}
|
}
|
||||||
return artifacts.ResponsePayloadPath
|
return artifacts.ResponsePayloadPath
|
||||||
}
|
}
|
||||||
|
|
||||||
func isMalformedStructuredOutputError(err error) bool {
|
|
||||||
if err == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
msg := err.Error()
|
|
||||||
for _, marker := range []string{
|
|
||||||
"malformed structured output",
|
|
||||||
"decode structured output:",
|
|
||||||
"decode provider response envelope:",
|
|
||||||
"provider response missing choices",
|
|
||||||
"provider response missing assistant message content",
|
|
||||||
"provider response assistant message content is empty",
|
|
||||||
"provider response assistant message content is not valid JSON",
|
|
||||||
} {
|
|
||||||
if strings.Contains(msg, marker) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -265,6 +265,23 @@ func TestLLMBackedValidatorMalformedOutputRejectsBatch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLLMBackedValidatorProviderMalformedEnvelopeRejectsBatch(t *testing.T) {
|
||||||
|
client := &fakeStructuredLLMClient{err: errors.New("provider response assistant message content is empty")}
|
||||||
|
v, _ := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility, "test-model")
|
||||||
|
req := makeReq([]proposals.EnrichedCorrectionProposal{mk(0, "gestures", "Jesters")})
|
||||||
|
req.LLMClient = client
|
||||||
|
res, err := v.Validate(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected malformed provider envelope downgrade, got %v", err)
|
||||||
|
}
|
||||||
|
if len(res.Decisions) != 1 || res.Decisions[0].Approved || res.Decisions[0].ReasonCode != ReasonValidatorMalformed {
|
||||||
|
t.Fatalf("unexpected decisions: %+v", res.Decisions)
|
||||||
|
}
|
||||||
|
if len(res.Warnings) != 1 || res.Warnings[0].ReasonCode != ReasonValidatorMalformed {
|
||||||
|
t.Fatalf("expected malformed warning, got %+v", res.Warnings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLLMBackedValidatorMissingDecisionRejectsBatch(t *testing.T) {
|
func TestLLMBackedValidatorMissingDecisionRejectsBatch(t *testing.T) {
|
||||||
client := &fakeStructuredLLMClient{responses: []LLMValidationResponse{{Validations: []LLMValidationDecision{}}}}
|
client := &fakeStructuredLLMClient{responses: []LLMValidationResponse{{Validations: []LLMValidationDecision{}}}}
|
||||||
v, _ := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility, "test-model")
|
v, _ := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility, "test-model")
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package metadata
|
package metadata
|
||||||
|
|
||||||
import "gitea.maximumdirect.net/eric/audita/internal/framework/contracts"
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/audita/internal/framework/contracts"
|
||||||
|
)
|
||||||
|
|
||||||
type ExecutionClass string
|
type ExecutionClass string
|
||||||
|
|
||||||
@@ -9,6 +13,31 @@ const (
|
|||||||
ExecutionClassLLMBacked ExecutionClass = "llm_backed"
|
ExecutionClassLLMBacked ExecutionClass = "llm_backed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
KeyProposalShape = "proposal_shape"
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
var executionClassByKey = map[string]ExecutionClass{
|
||||||
|
KeyProposalShape: ExecutionClassDeterministic,
|
||||||
|
KeyConfidenceThreshold: ExecutionClassDeterministic,
|
||||||
|
KeyOriginalTextPresence: ExecutionClassDeterministic,
|
||||||
|
KeyNonEmptyCorrectedText: ExecutionClassDeterministic,
|
||||||
|
KeyNoEffect: ExecutionClassDeterministic,
|
||||||
|
KeyProtectedTerms: ExecutionClassDeterministic,
|
||||||
|
KeySpokenFormPlausibility: ExecutionClassLLMBacked,
|
||||||
|
KeyMeaningReversalReview: ExecutionClassLLMBacked,
|
||||||
|
KeyEditorialReview: ExecutionClassLLMBacked,
|
||||||
|
}
|
||||||
|
|
||||||
type ClassifiedValidator interface {
|
type ClassifiedValidator interface {
|
||||||
contracts.Validator
|
contracts.Validator
|
||||||
ExecutionClass() ExecutionClass
|
ExecutionClass() ExecutionClass
|
||||||
@@ -18,9 +47,11 @@ func ClassOf(v contracts.Validator) ExecutionClass {
|
|||||||
if v == nil {
|
if v == nil {
|
||||||
return ExecutionClassDeterministic
|
return ExecutionClassDeterministic
|
||||||
}
|
}
|
||||||
|
|
||||||
|
classFromKey := ClassForKey(v.Name())
|
||||||
classified, ok := v.(ClassifiedValidator)
|
classified, ok := v.(ClassifiedValidator)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ExecutionClassDeterministic
|
return classFromKey
|
||||||
}
|
}
|
||||||
switch classified.ExecutionClass() {
|
switch classified.ExecutionClass() {
|
||||||
case ExecutionClassLLMBacked:
|
case ExecutionClassLLMBacked:
|
||||||
@@ -28,8 +59,16 @@ func ClassOf(v contracts.Validator) ExecutionClass {
|
|||||||
case ExecutionClassDeterministic:
|
case ExecutionClassDeterministic:
|
||||||
return ExecutionClassDeterministic
|
return ExecutionClassDeterministic
|
||||||
default:
|
default:
|
||||||
|
return classFromKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClassForKey(key string) ExecutionClass {
|
||||||
|
class, ok := executionClassByKey[strings.TrimSpace(key)]
|
||||||
|
if !ok {
|
||||||
return ExecutionClassDeterministic
|
return ExecutionClassDeterministic
|
||||||
}
|
}
|
||||||
|
return class
|
||||||
}
|
}
|
||||||
|
|
||||||
func Wrap(v contracts.Validator, class ExecutionClass) contracts.Validator {
|
func Wrap(v contracts.Validator, class ExecutionClass) contracts.Validator {
|
||||||
|
|||||||
@@ -16,6 +16,16 @@ func (u unclassifiedValidator) Validate(_ context.Context, _ contracts.Validatio
|
|||||||
return frameworkvalidators.Result{ValidatorName: u.Name(), Decisions: nil}, nil
|
return frameworkvalidators.Result{ValidatorName: u.Name(), Decisions: nil}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type namedUnclassifiedValidator struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n namedUnclassifiedValidator) Name() string { return n.name }
|
||||||
|
|
||||||
|
func (n namedUnclassifiedValidator) Validate(_ context.Context, _ contracts.ValidationRequest) (frameworkvalidators.Result, error) {
|
||||||
|
return frameworkvalidators.Result{ValidatorName: n.Name(), Decisions: nil}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestClassOfDefaultsToDeterministic(t *testing.T) {
|
func TestClassOfDefaultsToDeterministic(t *testing.T) {
|
||||||
if got := ClassOf(unclassifiedValidator{}); got != ExecutionClassDeterministic {
|
if got := ClassOf(unclassifiedValidator{}); got != ExecutionClassDeterministic {
|
||||||
t.Fatalf("expected deterministic default class, got %q", got)
|
t.Fatalf("expected deterministic default class, got %q", got)
|
||||||
@@ -28,3 +38,22 @@ func TestWrapExposesExecutionClass(t *testing.T) {
|
|||||||
t.Fatalf("expected llm_backed class, got %q", got)
|
t.Fatalf("expected llm_backed class, got %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClassForKey(t *testing.T) {
|
||||||
|
if got := ClassForKey(KeyProposalShape); got != ExecutionClassDeterministic {
|
||||||
|
t.Fatalf("expected deterministic class for %q, got %q", KeyProposalShape, got)
|
||||||
|
}
|
||||||
|
if got := ClassForKey(KeySpokenFormPlausibility); got != ExecutionClassLLMBacked {
|
||||||
|
t.Fatalf("expected llm_backed class for %q, got %q", KeySpokenFormPlausibility, got)
|
||||||
|
}
|
||||||
|
if got := ClassForKey("unknown"); got != ExecutionClassDeterministic {
|
||||||
|
t.Fatalf("expected deterministic fallback for unknown key, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClassOfFallsBackToStableValidatorKey(t *testing.T) {
|
||||||
|
v := namedUnclassifiedValidator{name: KeyEditorialReview}
|
||||||
|
if got := ClassOf(v); got != ExecutionClassLLMBacked {
|
||||||
|
t.Fatalf("expected llm_backed fallback by key, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/audita/internal/validators/confidence_threshold"
|
"gitea.maximumdirect.net/eric/audita/internal/validators/confidence_threshold"
|
||||||
"gitea.maximumdirect.net/eric/audita/internal/validators/editorial_review"
|
"gitea.maximumdirect.net/eric/audita/internal/validators/editorial_review"
|
||||||
"gitea.maximumdirect.net/eric/audita/internal/validators/meaning_reversal_review"
|
"gitea.maximumdirect.net/eric/audita/internal/validators/meaning_reversal_review"
|
||||||
|
validatormetadata "gitea.maximumdirect.net/eric/audita/internal/validators/metadata"
|
||||||
"gitea.maximumdirect.net/eric/audita/internal/validators/no_effect"
|
"gitea.maximumdirect.net/eric/audita/internal/validators/no_effect"
|
||||||
"gitea.maximumdirect.net/eric/audita/internal/validators/non_empty_corrected_text"
|
"gitea.maximumdirect.net/eric/audita/internal/validators/non_empty_corrected_text"
|
||||||
"gitea.maximumdirect.net/eric/audita/internal/validators/original_text_presence"
|
"gitea.maximumdirect.net/eric/audita/internal/validators/original_text_presence"
|
||||||
@@ -17,22 +18,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
KeyProposalShape = "proposal_shape"
|
KeyProposalShape = validatormetadata.KeyProposalShape
|
||||||
KeyConfidenceThreshold = "confidence_threshold"
|
KeyConfidenceThreshold = validatormetadata.KeyConfidenceThreshold
|
||||||
KeyOriginalTextPresence = "original_text_presence"
|
KeyOriginalTextPresence = validatormetadata.KeyOriginalTextPresence
|
||||||
KeyNonEmptyCorrectedText = "non_empty_corrected_text"
|
KeyNonEmptyCorrectedText = validatormetadata.KeyNonEmptyCorrectedText
|
||||||
KeyNoEffect = "no_effect"
|
KeyNoEffect = validatormetadata.KeyNoEffect
|
||||||
KeyProtectedTerms = "protected_terms"
|
KeyProtectedTerms = validatormetadata.KeyProtectedTerms
|
||||||
|
|
||||||
KeySpokenFormPlausibility = "spoken_form_plausibility"
|
KeySpokenFormPlausibility = validatormetadata.KeySpokenFormPlausibility
|
||||||
KeyMeaningReversalReview = "meaning_reversal_review"
|
KeyMeaningReversalReview = validatormetadata.KeyMeaningReversalReview
|
||||||
KeyEditorialReview = "editorial_review"
|
KeyEditorialReview = validatormetadata.KeyEditorialReview
|
||||||
)
|
)
|
||||||
|
|
||||||
type BuiltInValidatorDefinition struct {
|
type BuiltInValidatorDefinition struct {
|
||||||
Key string
|
Key string
|
||||||
Build func() (contracts.Validator, error)
|
Build func() (contracts.Validator, error)
|
||||||
LLMBacked bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Registry struct {
|
type Registry struct {
|
||||||
@@ -47,9 +47,9 @@ func NewBuiltInRegistry() *Registry {
|
|||||||
{Key: KeyNonEmptyCorrectedText, Build: non_empty_corrected_text.New},
|
{Key: KeyNonEmptyCorrectedText, Build: non_empty_corrected_text.New},
|
||||||
{Key: KeyNoEffect, Build: no_effect.New},
|
{Key: KeyNoEffect, Build: no_effect.New},
|
||||||
{Key: KeyProtectedTerms, Build: protected_terms.New},
|
{Key: KeyProtectedTerms, Build: protected_terms.New},
|
||||||
{Key: KeySpokenFormPlausibility, LLMBacked: true, Build: spoken_form_plausibility.New},
|
{Key: KeySpokenFormPlausibility, Build: spoken_form_plausibility.New},
|
||||||
{Key: KeyMeaningReversalReview, LLMBacked: true, Build: meaning_reversal_review.New},
|
{Key: KeyMeaningReversalReview, Build: meaning_reversal_review.New},
|
||||||
{Key: KeyEditorialReview, LLMBacked: true, Build: editorial_review.New},
|
{Key: KeyEditorialReview, Build: editorial_review.New},
|
||||||
}
|
}
|
||||||
|
|
||||||
m := make(map[string]BuiltInValidatorDefinition, len(defs))
|
m := make(map[string]BuiltInValidatorDefinition, len(defs))
|
||||||
|
|||||||
@@ -82,11 +82,6 @@ func TestBuiltInValidatorPackagesConstruct(t *testing.T) {
|
|||||||
|
|
||||||
func TestRegistryBuildsClassifiedValidators(t *testing.T) {
|
func TestRegistryBuildsClassifiedValidators(t *testing.T) {
|
||||||
r := NewBuiltInRegistry()
|
r := NewBuiltInRegistry()
|
||||||
llmKeys := map[string]bool{
|
|
||||||
KeySpokenFormPlausibility: true,
|
|
||||||
KeyMeaningReversalReview: true,
|
|
||||||
KeyEditorialReview: true,
|
|
||||||
}
|
|
||||||
for _, key := range r.RegisteredKeys() {
|
for _, key := range r.RegisteredKeys() {
|
||||||
v, err := r.MustBuild(key)
|
v, err := r.MustBuild(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -95,16 +90,28 @@ func TestRegistryBuildsClassifiedValidators(t *testing.T) {
|
|||||||
if _, ok := v.(validatormetadata.ClassifiedValidator); !ok {
|
if _, ok := v.(validatormetadata.ClassifiedValidator); !ok {
|
||||||
t.Fatalf("expected built validator %q to expose execution classification metadata", key)
|
t.Fatalf("expected built validator %q to expose execution classification metadata", key)
|
||||||
}
|
}
|
||||||
want := validatormetadata.ExecutionClassDeterministic
|
want := validatormetadata.ClassForKey(key)
|
||||||
if llmKeys[key] {
|
|
||||||
want = validatormetadata.ExecutionClassLLMBacked
|
|
||||||
}
|
|
||||||
if got := validatormetadata.ClassOf(v); got != want {
|
if got := validatormetadata.ClassOf(v); got != want {
|
||||||
t.Fatalf("expected class %q for %q, got %q", want, key, got)
|
t.Fatalf("expected class %q for %q, got %q", want, key, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExecutionClassResolvableByStableKeyAndByValidatorInstance(t *testing.T) {
|
||||||
|
r := NewBuiltInRegistry()
|
||||||
|
for _, key := range r.RegisteredKeys() {
|
||||||
|
v, err := r.MustBuild(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("must build %q: %v", key, err)
|
||||||
|
}
|
||||||
|
fromKey := validatormetadata.ClassForKey(key)
|
||||||
|
fromInstance := validatormetadata.ClassOf(v)
|
||||||
|
if fromInstance != fromKey {
|
||||||
|
t.Fatalf("class mismatch for %q: key=%q instance=%q", key, fromKey, fromInstance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRegistryProtectedTermsUsesNonGlossaryStageBehavior(t *testing.T) {
|
func TestRegistryProtectedTermsUsesNonGlossaryStageBehavior(t *testing.T) {
|
||||||
r := NewBuiltInRegistry()
|
r := NewBuiltInRegistry()
|
||||||
v, err := r.MustBuild(KeyProtectedTerms)
|
v, err := r.MustBuild(KeyProtectedTerms)
|
||||||
|
|||||||
Reference in New Issue
Block a user