Files
audita/internal/framework/proposals/preview.go

82 lines
2.7 KiB
Go

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}
}