159 lines
5.9 KiB
Go
159 lines
5.9 KiB
Go
package validators
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
|
|
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
|
|
)
|
|
|
|
type ProposalShapeValidator struct{}
|
|
|
|
func (v ProposalShapeValidator) Name() string { return "proposal_shape" }
|
|
|
|
func (v ProposalShapeValidator) Validate(_ context.Context, req Request) (Result, error) {
|
|
decisions := make([]Decision, 0, len(req.CandidateProposal))
|
|
for _, c := range req.CandidateProposal {
|
|
switch {
|
|
case c.TargetSegmentID <= 0:
|
|
decisions = append(decisions, rejection(c.ProposalIndex, ReasonInvalidTargetSegment, "proposal target segment id must be positive"))
|
|
case strings.TrimSpace(c.OriginalText) == "":
|
|
decisions = append(decisions, rejection(c.ProposalIndex, ReasonEmptyOriginalText, "proposal original_text must not be empty"))
|
|
case c.Confidence < 0.0 || c.Confidence > 1.0:
|
|
decisions = append(decisions, rejection(c.ProposalIndex, ReasonInvalidConfidence, "proposal confidence must be between 0.0 and 1.0"))
|
|
default:
|
|
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
|
|
}
|
|
|
|
type ConfidenceThresholdValidator struct{}
|
|
|
|
func (v ConfidenceThresholdValidator) Name() string { return "confidence_threshold" }
|
|
|
|
func (v ConfidenceThresholdValidator) Validate(_ context.Context, req Request) (Result, error) {
|
|
threshold := confidenceThresholdForModule(req.ModuleKey, req.Config)
|
|
decisions := make([]Decision, 0, len(req.CandidateProposal))
|
|
for _, c := range req.CandidateProposal {
|
|
if c.Confidence < threshold {
|
|
decisions = append(decisions, rejection(c.ProposalIndex, ReasonLowConfidence, fmt.Sprintf("confidence %.4f below threshold %.4f", c.Confidence, threshold)))
|
|
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
|
|
}
|
|
|
|
type OriginalTextPresenceValidator struct{}
|
|
|
|
func (v OriginalTextPresenceValidator) Name() string { return "original_text_presence" }
|
|
|
|
func (v OriginalTextPresenceValidator) Validate(_ context.Context, req Request) (Result, error) {
|
|
byID := make(map[int]string)
|
|
if req.WorkingTranscript != nil {
|
|
for _, seg := range req.WorkingTranscript.Segments {
|
|
byID[seg.ID] = seg.Text
|
|
}
|
|
}
|
|
|
|
decisions := make([]Decision, 0, len(req.CandidateProposal))
|
|
for _, c := range req.CandidateProposal {
|
|
text, ok := byID[c.TargetSegmentID]
|
|
if !ok {
|
|
decisions = append(decisions, rejection(c.ProposalIndex, ReasonMissingTargetSegment, "target segment was not found"))
|
|
continue
|
|
}
|
|
if !strings.Contains(text, c.OriginalText) {
|
|
decisions = append(decisions, rejection(c.ProposalIndex, ReasonMissingOriginalText, "original_text was not found in current segment text"))
|
|
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
|
|
}
|
|
|
|
type NonEmptyCorrectionValidator struct{}
|
|
|
|
func (v NonEmptyCorrectionValidator) Name() string { return "non_empty_corrected_text" }
|
|
|
|
func (v NonEmptyCorrectionValidator) Validate(_ context.Context, req Request) (Result, error) {
|
|
segmentsByID := make(map[int]schema.Segment)
|
|
if req.WorkingTranscript != nil {
|
|
for _, seg := range req.WorkingTranscript.Segments {
|
|
segmentsByID[seg.ID] = seg
|
|
}
|
|
}
|
|
|
|
decisions := make([]Decision, 0, len(req.CandidateProposal))
|
|
for _, c := range req.CandidateProposal {
|
|
segment, ok := segmentsByID[c.TargetSegmentID]
|
|
if !ok {
|
|
decisions = append(decisions, approval(c.ProposalIndex))
|
|
continue
|
|
}
|
|
preview := proposals.PreviewProposalForSegment(&segment, c.CorrectionProposal, req.ReplacementPolicy)
|
|
if preview.SkipReason == proposals.SkipReasonEmptyResultingText {
|
|
decisions = append(decisions, rejection(c.ProposalIndex, ReasonEmptyResultingText, "proposal would leave the segment empty"))
|
|
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
|
|
}
|
|
|
|
type NoEffectValidator struct{}
|
|
|
|
func (v NoEffectValidator) Name() string { return "no_effect" }
|
|
|
|
func (v NoEffectValidator) Validate(_ context.Context, req Request) (Result, error) {
|
|
decisions := make([]Decision, 0, len(req.CandidateProposal))
|
|
for _, c := range req.CandidateProposal {
|
|
if c.OriginalText == c.CorrectedText {
|
|
decisions = append(decisions, rejection(c.ProposalIndex, ReasonNoEffect, "original_text and corrected_text are identical"))
|
|
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
|
|
}
|
|
|
|
func ValidateProtectedTerms(req Request, glossaryStage bool) (Result, error) {
|
|
vocab := NewProtectedVocabulary(req.Glossary)
|
|
decisions := make([]Decision, 0, len(req.CandidateProposal))
|
|
for _, c := range req.CandidateProposal {
|
|
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
|
|
}
|
|
decisions = append(decisions, approval(c.ProposalIndex))
|
|
}
|
|
if err := EnforceDecisionCardinality(req.CandidateProposal, decisions); err != nil {
|
|
return Result{}, err
|
|
}
|
|
return Result{ValidatorName: "protected_terms", Decisions: decisions}, nil
|
|
}
|