Derive independent glossary context and protection

This commit is contained in:
2026-11-10 17:25:08 +00:00
parent d42e2095d7
commit 89de2a7014
23 changed files with 364 additions and 173 deletions

View File

@@ -43,6 +43,7 @@ func ApplyProposals(
transcript *schema.Transcript,
proposals []EnrichedCorrectionProposal,
policy ReplacementPolicy,
allowsChange func(before, after string) bool,
) ApplyResult {
working := cloneTranscript(transcript)
ordered := sortProposalsByIndex(proposals)
@@ -68,6 +69,11 @@ func ApplyProposals(
continue
}
if allowsChange != nil && !allowsChange(working.Segments[segmentIdx].Text, preview.CorrectedSegmentText) {
skipped = append(skipped, newSkippedChange(proposal, SkipReasonProtectedTerm))
continue
}
working.Segments[segmentIdx].Text = preview.CorrectedSegmentText
applied = append(applied, AppliedChange{
ProposalIndex: proposal.ProposalIndex,
@@ -149,6 +155,8 @@ func skipReasonMessage(reason ProposalSkipReason) string {
return "original_text and corrected_text are identical"
case SkipReasonEmptyResultingText:
return "proposal would leave the segment empty"
case SkipReasonProtectedTerm:
return "proposal may alter protected glossary terminology"
case SkipReasonInvalidProposal:
return "proposal failed structural or policy validation"
default:

View File

@@ -38,7 +38,7 @@ func TestApplyProposalsFixtureTranscriptGoldens(t *testing.T) {
t.Fatalf("failed to parse fixture policy: %v", err)
}
result := ApplyProposals(&fixture.Transcript, fixture.Proposals, policy)
result := ApplyProposals(&fixture.Transcript, fixture.Proposals, policy, nil)
assertJSONSemanticallyEqualFromGolden(t, result.Transcript, name+".transcript.golden.json")
})
}
@@ -62,7 +62,7 @@ func TestApplyProposalsFixtureSkipCoverage(t *testing.T) {
t.Fatalf("failed to parse fixture policy: %v", err)
}
result := ApplyProposals(&fixture.Transcript, fixture.Proposals, policy)
result := ApplyProposals(&fixture.Transcript, fixture.Proposals, policy, nil)
if len(result.Skipped) == 0 {
t.Fatal("expected at least one skipped change")
}
@@ -81,7 +81,7 @@ func TestApplyProposalsFixtureMixedScenarioRecordGoldens(t *testing.T) {
t.Fatalf("failed to parse fixture policy: %v", err)
}
result := ApplyProposals(&fixture.Transcript, fixture.Proposals, policy)
result := ApplyProposals(&fixture.Transcript, fixture.Proposals, policy, nil)
payload := recordsPayload{
AppliedChanges: result.Applied,
SkippedChanges: result.Skipped,

View File

@@ -21,7 +21,7 @@ func TestApplyProposalsApplyingOneProposal(t *testing.T) {
},
}
result := ApplyProposals(transcript, proposals, ReplacementPolicyRequireUnique)
result := ApplyProposals(transcript, proposals, ReplacementPolicyRequireUnique, nil)
if got := result.Transcript.Segments[0].Text; got != "Jesters" {
t.Fatalf("expected corrected text Jesters, got %q", got)
@@ -57,7 +57,7 @@ func TestApplyProposalsApplyingMultipleProposalsInStableOrder(t *testing.T) {
},
}
result := ApplyProposals(transcript, proposals, ReplacementPolicyReplaceAll)
result := ApplyProposals(transcript, proposals, ReplacementPolicyReplaceAll, nil)
if got := result.Transcript.Segments[0].Text; got != "X X" {
t.Fatalf("expected final text X X, got %q", got)
@@ -93,7 +93,7 @@ func TestApplyProposalsStaleProposalSkippedAfterEarlierChange(t *testing.T) {
},
}
result := ApplyProposals(transcript, proposals, ReplacementPolicyRequireUnique)
result := ApplyProposals(transcript, proposals, ReplacementPolicyRequireUnique, nil)
if got := result.Transcript.Segments[0].Text; got != "foo-bar" {
t.Fatalf("expected final text foo-bar, got %q", got)
@@ -120,7 +120,7 @@ func TestApplyProposalsRechecksBlankResultAfterEarlierDeletion(t *testing.T) {
t.Fatalf("proposal must be initially applicable: %+v", preview)
}
}
result := ApplyProposals(transcript, proposed, ReplacementPolicyRequireUnique)
result := ApplyProposals(transcript, proposed, ReplacementPolicyRequireUnique, nil)
if got := result.Transcript.Segments[0].Text; got != " b" {
t.Fatalf("expected literal surviving text, got %q", got)
}
@@ -143,7 +143,7 @@ func TestApplyProposalsMissingSegmentSkip(t *testing.T) {
},
}
result := ApplyProposals(transcript, proposals, ReplacementPolicyRequireUnique)
result := ApplyProposals(transcript, proposals, ReplacementPolicyRequireUnique, nil)
if len(result.Skipped) != 1 {
t.Fatalf("expected 1 skipped change, got %d", len(result.Skipped))
@@ -167,7 +167,7 @@ func TestApplyProposalsRequireUniqueAmbiguousSkip(t *testing.T) {
},
}
result := ApplyProposals(transcript, proposals, ReplacementPolicyRequireUnique)
result := ApplyProposals(transcript, proposals, ReplacementPolicyRequireUnique, nil)
if len(result.Skipped) != 1 {
t.Fatalf("expected 1 skipped change, got %d", len(result.Skipped))
@@ -191,7 +191,7 @@ func TestApplyProposalsReplaceAllMultipleReplacements(t *testing.T) {
},
}
result := ApplyProposals(transcript, proposals, ReplacementPolicyReplaceAll)
result := ApplyProposals(transcript, proposals, ReplacementPolicyReplaceAll, nil)
if len(result.Applied) != 1 {
t.Fatalf("expected 1 applied change, got %d", len(result.Applied))
@@ -220,7 +220,7 @@ func TestApplyProposalsNoMutationOfInputTranscript(t *testing.T) {
},
}
_ = ApplyProposals(transcript, proposals, ReplacementPolicyRequireUnique)
_ = ApplyProposals(transcript, proposals, ReplacementPolicyRequireUnique, nil)
if !reflect.DeepEqual(transcript, before) {
t.Fatalf("input transcript mutated: before=%+v after=%+v", before, transcript)
@@ -249,7 +249,7 @@ func TestApplyProposalsSegmentMetadataPreservation(t *testing.T) {
},
}
result := ApplyProposals(transcript, proposals, ReplacementPolicyRequireUnique)
result := ApplyProposals(transcript, proposals, ReplacementPolicyRequireUnique, nil)
seg := result.Transcript.Segments[0]
if seg.Speaker != "A" || seg.Start != 12.5 || seg.End != 15.0 {
@@ -283,7 +283,7 @@ func TestApplyProposalsAppliedAndSkippedRecords(t *testing.T) {
},
}
result := ApplyProposals(transcript, proposals, ReplacementPolicyRequireUnique)
result := ApplyProposals(transcript, proposals, ReplacementPolicyRequireUnique, nil)
if len(result.Applied) != 1 || len(result.Skipped) != 1 {
t.Fatalf("expected 1 applied and 1 skipped, got applied=%d skipped=%d", len(result.Applied), len(result.Skipped))

View File

@@ -17,6 +17,7 @@ const (
SkipReasonNoEffect ProposalSkipReason = "no_effect"
SkipReasonEmptyResultingText ProposalSkipReason = "empty_resulting_segment"
SkipReasonInvalidProposal ProposalSkipReason = "invalid_proposal"
SkipReasonProtectedTerm ProposalSkipReason = "protected_glossary_term"
)
// SegmentPreviewResult is a deterministic preview of applying one proposal to