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 }