89 lines
3.0 KiB
Go
89 lines
3.0 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"
|
|
SkipReasonEmptyResultingText ProposalSkipReason = "empty_resulting_segment"
|
|
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)
|
|
if strings.TrimSpace(corrected) == "" {
|
|
return SegmentPreviewResult{SkipReason: SkipReasonEmptyResultingText}
|
|
}
|
|
return SegmentPreviewResult{
|
|
Applicable: true,
|
|
CorrectedSegmentText: corrected,
|
|
ReplacementCount: 1,
|
|
}
|
|
|
|
case ReplacementPolicyReplaceAll:
|
|
corrected := strings.ReplaceAll(segment.Text, proposal.OriginalText, proposal.CorrectedText)
|
|
if strings.TrimSpace(corrected) == "" {
|
|
return SegmentPreviewResult{SkipReason: SkipReasonEmptyResultingText}
|
|
}
|
|
return SegmentPreviewResult{
|
|
Applicable: true,
|
|
CorrectedSegmentText: corrected,
|
|
ReplacementCount: matchCount,
|
|
}
|
|
}
|
|
|
|
return SegmentPreviewResult{SkipReason: SkipReasonInvalidProposal}
|
|
}
|