package validators import ( "context" "fmt" "strings" "gitea.maximumdirect.net/eric/audita/v3/internal/core/modulecatalog" "gitea.maximumdirect.net/eric/audita/v3/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 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 := req.Settings.ConfidenceThreshold 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) { section := req.sectionLookup() decisions := make([]Decision, 0, len(req.CandidateProposal)) for _, c := range req.CandidateProposal { position, ok := section.Position(c.TargetSegmentID) if !ok { decisions = append(decisions, rejection(c.ProposalIndex, ReasonMissingTargetSegment, "target segment was not found")) continue } if !strings.Contains(section.segments[position].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 NonEmptyResultingSegmentValidator struct{} func (v NonEmptyResultingSegmentValidator) Name() string { return "non_empty_resulting_segment" } func (v NonEmptyResultingSegmentValidator) Validate(_ context.Context, req Request) (Result, error) { req.Section = req.sectionLookup() decisions := make([]Decision, 0, len(req.CandidateProposal)) for _, c := range req.CandidateProposal { preview := req.Preview(c) 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) (Result, error) { var vocab ProtectedVocabulary if req.GlossaryView != nil { vocab = req.GlossaryView.Protection } else { vocab = NewProtectedVocabulary(req.ProtectedTerms) } decisions := make([]Decision, 0, len(req.CandidateProposal)) glossaryStage := req.ModuleKey == modulecatalog.KeyGlossary for _, c := range req.CandidateProposal { reason := vocab.violationReason(c.OriginalText, c.CorrectedText) repetitionCleanup := reason != "" && req.ModuleKey == modulecatalog.KeySpokenWord if vocab.pattern != nil && (glossaryStage || repetitionCleanup) { req.Section = req.sectionLookup() if position, ok := req.Section.Position(c.TargetSegmentID); ok { segment := &req.Section.segments[position] preview := req.Preview(c) if preview.Applicable || (req.IndependentEvidence && preview.SkipReason == proposals.SkipReasonEmptyResultingText) { if glossaryStage { // A correction span may sit inside a longer exempt name. reason = vocab.violationReason(segment.Text, preview.CorrectedSegmentText) } else if vocab.preservesRepeatedIdentities(segment.Text, preview.CorrectedSegmentText) { reason = "" } } } } 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 }