package proposals import ( "strings" "gitea.maximumdirect.net/eric/audita/internal/core/schema" ) // ProposalSkipReason is a stable, report-friendly reason code for // unapplied/unsafe proposals. type ProposalSkipReason string const ( SkipReasonMissingSegment ProposalSkipReason = "missing_segment" SkipReasonMissingOriginalText ProposalSkipReason = "missing_original_text" SkipReasonAmbiguousOriginal ProposalSkipReason = "ambiguous_original_text" SkipReasonNoEffect ProposalSkipReason = "no_effect" SkipReasonInvalidProposal ProposalSkipReason = "invalid_proposal" ) // SegmentPreviewResult is a deterministic preview of applying one proposal to // one transcript segment. type SegmentPreviewResult struct { Applicable bool `json:"applicable"` CorrectedSegmentText string `json:"corrected_segment_text,omitempty"` ReplacementCount int `json:"replacement_count"` SkipReason ProposalSkipReason `json:"skip_reason,omitempty"` } // PreviewProposalForSegment previews applying a proposal to a single segment // under the given replacement policy without mutating the input segment. func PreviewProposalForSegment(segment *schema.Segment, proposal CorrectionProposal, policy ReplacementPolicy) SegmentPreviewResult { if segment == nil { return SegmentPreviewResult{SkipReason: SkipReasonMissingSegment} } if err := proposal.Validate(); err != nil { return SegmentPreviewResult{SkipReason: SkipReasonInvalidProposal} } if !policy.IsValid() { return SegmentPreviewResult{SkipReason: SkipReasonInvalidProposal} } if proposal.TargetSegmentID != segment.ID { return SegmentPreviewResult{SkipReason: SkipReasonMissingSegment} } matchCount := strings.Count(segment.Text, proposal.OriginalText) if matchCount == 0 { return SegmentPreviewResult{SkipReason: SkipReasonMissingOriginalText} } if proposal.OriginalText == proposal.CorrectedText { return SegmentPreviewResult{SkipReason: SkipReasonNoEffect} } switch policy { case ReplacementPolicyRequireUnique: if matchCount != 1 { return SegmentPreviewResult{SkipReason: SkipReasonAmbiguousOriginal} } corrected := strings.Replace(segment.Text, proposal.OriginalText, proposal.CorrectedText, 1) return SegmentPreviewResult{ Applicable: true, CorrectedSegmentText: corrected, ReplacementCount: 1, } case ReplacementPolicyReplaceAll: corrected := strings.ReplaceAll(segment.Text, proposal.OriginalText, proposal.CorrectedText) return SegmentPreviewResult{ Applicable: true, CorrectedSegmentText: corrected, ReplacementCount: matchCount, } } return SegmentPreviewResult{SkipReason: SkipReasonInvalidProposal} }