Share section lookups and replacement previews across validators

This commit is contained in:
2026-11-11 12:28:00 +00:00
parent 45b42a2689
commit 5d90478f70
7 changed files with 107 additions and 79 deletions

View File

@@ -3,11 +3,9 @@ package validators
import (
"context"
"fmt"
"math"
"strings"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/modulecatalog"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/schema"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/proposals"
)
@@ -60,21 +58,16 @@ 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
}
}
section := req.sectionLookup()
decisions := make([]Decision, 0, len(req.CandidateProposal))
for _, c := range req.CandidateProposal {
text, ok := byID[c.TargetSegmentID]
position, ok := section.Position(c.TargetSegmentID)
if !ok {
decisions = append(decisions, rejection(c.ProposalIndex, ReasonMissingTargetSegment, "target segment was not found"))
continue
}
if !strings.Contains(text, c.OriginalText) {
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
}
@@ -91,21 +84,11 @@ type NonEmptyResultingSegmentValidator struct{}
func (v NonEmptyResultingSegmentValidator) Name() string { return "non_empty_resulting_segment" }
func (v NonEmptyResultingSegmentValidator) 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
}
}
req.Section = req.sectionLookup()
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 := previewForEvidence(req, &segment, c.CorrectionProposal)
preview := req.Preview(c)
if preview.SkipReason == proposals.SkipReasonEmptyResultingText {
decisions = append(decisions, rejection(c.ProposalIndex, ReasonEmptyResultingText, "proposal would leave the segment empty"))
continue
@@ -149,13 +132,11 @@ func ValidateProtectedTerms(req Request) (Result, error) {
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.WorkingTranscript != nil {
for i := range req.WorkingTranscript.Segments {
segment := &req.WorkingTranscript.Segments[i]
if segment.ID != c.TargetSegmentID {
continue
}
preview := previewForEvidence(req, segment, c.CorrectionProposal)
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.
@@ -164,7 +145,6 @@ func ValidateProtectedTerms(req Request) (Result, error) {
reason = ""
}
}
break
}
}
if reason != "" {
@@ -178,10 +158,3 @@ func ValidateProtectedTerms(req Request) (Result, error) {
}
return Result{ValidatorName: "protected_terms", Decisions: decisions}, nil
}
func previewForEvidence(req Request, segment *schema.Segment, correction proposals.CorrectionProposal) proposals.SegmentPreviewResult {
if req.IndependentEvidence && (math.IsNaN(correction.Confidence) || correction.Confidence < 0 || correction.Confidence > 1) {
correction.Confidence = 0.5
}
return proposals.PreviewProposalForSegment(segment, correction, req.ReplacementPolicy)
}

View File

@@ -9,7 +9,6 @@ import (
"gitea.maximumdirect.net/eric/audita/v3/internal/core/chunking"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/config"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/schema"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/outcomes"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/responseschema"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/stagename"
@@ -243,30 +242,24 @@ func errPayload(err error) any {
func BuildLLMValidationRequest(validatorName string, validatorType LLMValidatorType, req Request) (LLMValidationRequest, []Decision) {
items := make([]LLMValidationItem, 0, len(req.CandidateProposal))
immediate := make([]Decision, 0)
var segments []schema.Segment
positions := make(map[int]int)
req.Section = req.sectionLookup()
segments := req.Section.segments
var glossary glossaryContext
if req.GlossaryView != nil {
glossary = req.GlossaryView.context
} else {
glossary = prepareGlossaryContext(req.Glossary)
}
if req.WorkingTranscript != nil {
segments = req.WorkingTranscript.Segments
for i, seg := range segments {
positions[seg.ID] = i
}
}
for _, p := range req.CandidateProposal {
position, ok := positions[p.TargetSegmentID]
position, ok := req.Section.Position(p.TargetSegmentID)
if !ok {
immediate = append(immediate, rejection(p.ProposalIndex, ReasonMissingTargetSegment, "target segment was not found"))
continue
}
seg := segments[position]
preview := previewForEvidence(req, &seg, p.CorrectionProposal)
preview := req.Preview(p)
if !preview.Applicable {
immediate = append(immediate, rejection(p.ProposalIndex, string(preview.SkipReason), "proposal is not previewable for LLM validation"))
continue

View File

@@ -41,6 +41,8 @@ type Request struct {
RepairAttempt *int `json:"repair_attempt,omitempty"`
SectionIndex *int `json:"section_index,omitempty"`
WorkingTranscript *schema.Transcript `json:"-"`
Section *SectionLookup `json:"-"`
Previews map[int]proposals.SegmentPreviewResult `json:"-"`
CandidateProposal []proposals.EnrichedCorrectionProposal `json:"candidate_proposals"`
ModuleKey string `json:"module_key"`
ModuleInstance string `json:"module_instance"`

View File

@@ -0,0 +1,61 @@
package validators
import (
"math"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/schema"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/proposals"
)
// SectionLookup indexes the immutable source section shared by validation
// and revalidation. Application checks its own, changing transcript.
type SectionLookup struct {
segments []schema.Segment
positions map[int]int
}
func NewSectionLookup(transcript *schema.Transcript) *SectionLookup {
section := &SectionLookup{}
if transcript != nil {
section.segments = transcript.Segments
section.positions = make(map[int]int, len(section.segments))
for i, segment := range section.segments {
section.positions[segment.ID] = i
}
}
return section
}
func (s *SectionLookup) Position(id int) (int, bool) {
position, ok := s.positions[id]
return position, ok
}
func (r Request) sectionLookup() *SectionLookup {
if r.Section != nil {
return r.Section
}
return NewSectionLookup(r.WorkingTranscript)
}
// Preview reuses the replacement preview for this proposal index within one
// validation pass. A revision starts a new pass with its own preview map.
func (r Request) Preview(candidate proposals.EnrichedCorrectionProposal) proposals.SegmentPreviewResult {
if preview, ok := r.Previews[candidate.ProposalIndex]; ok {
return preview
}
section := r.sectionLookup()
var segment *schema.Segment
if position, ok := section.Position(candidate.TargetSegmentID); ok {
segment = &section.segments[position]
}
correction := candidate.CorrectionProposal
if r.IndependentEvidence && (math.IsNaN(correction.Confidence) || correction.Confidence < 0 || correction.Confidence > 1) {
correction.Confidence = 0.5
}
preview := proposals.PreviewProposalForSegment(segment, correction, r.ReplacementPolicy)
if r.Previews != nil {
r.Previews[candidate.ProposalIndex] = preview
}
return preview
}