Keep match boundaries for exempt glossary names
This commit is contained in:
@@ -50,7 +50,9 @@ func ExecuteModuleProposal(ctx context.Context, req ModuleProposalRequest) (cont
|
||||
|
||||
protected := make([]string, 0, len(req.ProposalRequest.ProtectedTerms))
|
||||
for _, term := range req.ProposalRequest.ProtectedTerms {
|
||||
protected = append(protected, term.Spelling)
|
||||
if !term.Exempt {
|
||||
protected = append(protected, term.Spelling)
|
||||
}
|
||||
}
|
||||
payload, err := json.Marshal(protected)
|
||||
if err != nil {
|
||||
|
||||
@@ -40,7 +40,7 @@ func TestExecuteModuleProposalBuildsMessagesFromSectionAndDescription(t *testing
|
||||
Settings: cfg,
|
||||
WorkingTranscript: transcript,
|
||||
Glossary: glossary,
|
||||
ProtectedTerms: []domain.Term{{Spelling: "Separate protection"}},
|
||||
ProtectedTerms: []domain.Term{{Spelling: "Separate protection"}, {Spelling: "Exempt boundary", Exempt: true}},
|
||||
Section: §ion,
|
||||
},
|
||||
RunSpec: contracts.ModuleRunSpec{
|
||||
|
||||
@@ -139,17 +139,24 @@ func (v NoEffectValidator) Validate(_ context.Context, req Request) (Result, err
|
||||
func ValidateProtectedTerms(req Request) (Result, error) {
|
||||
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)
|
||||
if reason != "" && req.ModuleKey == modulecatalog.KeySpokenWord && req.WorkingTranscript != nil {
|
||||
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 := proposals.PreviewProposalForSegment(segment, c.CorrectionProposal, req.ReplacementPolicy)
|
||||
if preview.Applicable && vocab.preservesRepeatedIdentities(segment.Text, preview.CorrectedSegmentText) {
|
||||
reason = ""
|
||||
if preview.Applicable {
|
||||
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 = ""
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -30,13 +30,15 @@ func NewProtectedVocabulary(terms []glossary.Term) ProtectedVocabulary {
|
||||
termsByFolded := make(map[string]protectedTermDef, len(terms))
|
||||
alternatives := make([]string, 0, len(terms))
|
||||
for _, term := range terms {
|
||||
termsByFolded[strings.ToLower(term.Spelling)] = protectedTermDef{identities: term.Identities, canonical: term.Spelling}
|
||||
if !term.Exempt {
|
||||
termsByFolded[strings.ToLower(term.Spelling)] = protectedTermDef{identities: term.Identities, canonical: term.Spelling}
|
||||
}
|
||||
alternatives = append(alternatives, regexp.QuoteMeta(term.Spelling))
|
||||
}
|
||||
sort.SliceStable(alternatives, func(i, j int) bool { return len(alternatives[i]) > len(alternatives[j]) })
|
||||
if len(alternatives) == 0 {
|
||||
return ProtectedVocabulary{termsByFolded: termsByFolded}
|
||||
if len(termsByFolded) == 0 {
|
||||
return ProtectedVocabulary{}
|
||||
}
|
||||
sort.SliceStable(alternatives, func(i, j int) bool { return len(alternatives[i]) > len(alternatives[j]) })
|
||||
pattern := regexp.MustCompile(`(?i)\b(?:` + strings.Join(alternatives, "|") + `)\b`)
|
||||
return ProtectedVocabulary{termsByFolded: termsByFolded, pattern: pattern}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,53 @@ func TestApplicationChecksCurrentTextWithSelectedProtection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExemptGlossarySpellingKeepsLongestMatchBoundary(t *testing.T) {
|
||||
active := []glossary.Entry{
|
||||
{ID: "greater-mage", Name: "Greater Mage Armor"},
|
||||
{ID: "greater-magus", Name: "Greater Magus Armor"},
|
||||
}
|
||||
protected := []glossary.Entry{
|
||||
active[0], active[1],
|
||||
{ID: "short", Name: "Mage Armor"},
|
||||
{ID: "long", Name: "Superior Greater Mage Armor"},
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
name, module, before, original, replacement string
|
||||
exempt, allowed bool
|
||||
}{
|
||||
{"full active name", "glossary", "Cast Greater Mage Armor.", "Greater Mage Armor", "Greater Magus Armor", true, true},
|
||||
{"span within active name", "glossary", "Cast Greater Mage Armor.", "Mage Armor", "Magus Armor", true, true},
|
||||
{"standalone protected name", "glossary", "Cast Mage Armor.", "Mage Armor", "Magus Armor", true, false},
|
||||
{"longer protected name", "glossary", "Cast Superior Greater Mage Armor.", "Greater Mage Armor", "Greater Magus Armor", true, false},
|
||||
{"same name in a grammar pass", "grammar", "Cast Greater Mage Armor.", "Greater Mage Armor", "Greater Magus Armor", false, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var exclusions []glossary.Entry
|
||||
if tc.exempt {
|
||||
exclusions = active
|
||||
}
|
||||
terms := glossary.Vocabulary(protected, exclusions)
|
||||
transcript := &schema.Transcript{Segments: []schema.Segment{{ID: 1, Text: tc.before}}}
|
||||
candidate := proposals.EnrichedCorrectionProposal{CorrectionProposal: proposals.CorrectionProposal{
|
||||
TargetSegmentID: 1, OriginalText: tc.original, CorrectedText: tc.replacement, Confidence: 1,
|
||||
}}
|
||||
policy := proposals.ReplacementPolicyRequireUnique
|
||||
decision, err := ValidateProtectedTerms(Request{ModuleKey: tc.module, WorkingTranscript: transcript, ReplacementPolicy: policy,
|
||||
ProtectedTerms: terms, CandidateProposal: []proposals.EnrichedCorrectionProposal{candidate}})
|
||||
if err != nil || len(decision.Decisions) != 1 || decision.Decisions[0].Approved != tc.allowed {
|
||||
t.Fatalf("decision=%+v err=%v", decision, err)
|
||||
}
|
||||
vocabulary := NewProtectedVocabulary(terms)
|
||||
result := proposals.ApplyProposals(transcript, []proposals.EnrichedCorrectionProposal{candidate}, policy, func(before, after string) bool {
|
||||
return vocabulary.AllowsChange(before, after, false)
|
||||
})
|
||||
if (len(result.Applied) == 1) != tc.allowed {
|
||||
t.Fatalf("apply-time protection disagrees with validation: %+v", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidationContextIncludesEachSharedDefinition(t *testing.T) {
|
||||
entries := []glossary.Entry{
|
||||
{Collection: "spells", ID: "shield", Name: "Shield", Category: "spell"},
|
||||
|
||||
Reference in New Issue
Block a user