From 45e5b381f554a962bf2889cb9cad740c38f9ebed Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Tue, 10 Nov 2026 20:18:47 +0000 Subject: [PATCH] Separate glossary context from prompt protection --- docs/internal/prompts.md | 6 ++++-- docs/roadmap/remediation.md | 2 ++ .../framework/proposal_generation/module_proposal.go | 11 +++++++++++ .../proposal_generation/module_proposal_test.go | 4 ++++ .../assets/prompts/modules.glossary.proposal.yaml | 7 +++++-- .../assets/prompts/modules.grammar.proposal.yaml | 8 +++++--- .../assets/prompts/modules.homophones.proposal.yaml | 8 +++++--- .../assets/prompts/modules.spoken_word.proposal.yaml | 9 ++++++--- internal/prompts/registry.go | 3 +++ internal/prompts/registry_test.go | 4 +--- 10 files changed, 46 insertions(+), 16 deletions(-) diff --git a/docs/internal/prompts.md b/docs/internal/prompts.md index f722dad..1811d9a 100644 --- a/docs/internal/prompts.md +++ b/docs/internal/prompts.md @@ -17,8 +17,10 @@ The operator's prompt definition controls which variables its template uses. For a standard request preview or validation, select a native PromptKit profile and supply the template variables used by the selected prompt. The shipped -proposal definitions use `TranscriptDescriptionBlock`, `GlossaryJSON`, and -`SectionJSON`; the validator definitions use `TranscriptDescriptionBlock` and +proposal definitions use `TranscriptDescriptionBlock`, `GlossaryJSON`, +`ProtectedTermsJSON`, and `SectionJSON`. Glossary entries provide context, +and the separate spelling list defines protection. Preview defaults the +spelling list to `[]` when omitted. The validator definitions use `TranscriptDescriptionBlock` and `PayloadJSON`. The latter is supplied candidate JSON and does not require a proposal inference call. An operator replacement may use different variables. See the [upstream CLI reference](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/f369d6a78253b6e6fcb1e023fb2e9c3af1d20ff0/docs/cli.md) diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 68e432b..4b88a9d 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -121,6 +121,8 @@ behavior and the recorded wider-root case. ## R3 — P2: Separate Glossary Context From Protection in Proposal Prompts +**Status:** Completed. Proposal requests carry separate context and protected spellings, with an empty preview default. + **Category** An unintended prompt restriction in Stage 11. diff --git a/internal/framework/proposal_generation/module_proposal.go b/internal/framework/proposal_generation/module_proposal.go index 0fea353..15f01c5 100644 --- a/internal/framework/proposal_generation/module_proposal.go +++ b/internal/framework/proposal_generation/module_proposal.go @@ -2,6 +2,7 @@ package proposal_generation import ( "context" + "encoding/json" "fmt" "strings" @@ -47,6 +48,16 @@ func ExecuteModuleProposal(ctx context.Context, req ModuleProposalRequest) (cont return contracts.ProposalResult{}, err } + protected := make([]string, 0, len(req.ProposalRequest.ProtectedTerms)) + for _, term := range req.ProposalRequest.ProtectedTerms { + protected = append(protected, term.Spelling) + } + payload, err := json.Marshal(protected) + if err != nil { + return contracts.ProposalResult{}, fmt.Errorf("marshal protected spellings: %w", err) + } + variables["ProtectedTermsJSON"] = string(payload) + promptMetadata, ok := prompts.LookupMetadata(req.PromptID) if !ok { return contracts.ProposalResult{}, fmt.Errorf("unknown prompt ID %q", req.PromptID) diff --git a/internal/framework/proposal_generation/module_proposal_test.go b/internal/framework/proposal_generation/module_proposal_test.go index 2742e80..a1b185b 100644 --- a/internal/framework/proposal_generation/module_proposal_test.go +++ b/internal/framework/proposal_generation/module_proposal_test.go @@ -40,6 +40,7 @@ func TestExecuteModuleProposalBuildsMessagesFromSectionAndDescription(t *testing Settings: cfg, WorkingTranscript: transcript, Glossary: glossary, + ProtectedTerms: []domain.Term{{Spelling: "Separate protection"}}, Section: §ion, }, RunSpec: contracts.ModuleRunSpec{ @@ -76,6 +77,9 @@ func TestExecuteModuleProposalBuildsMessagesFromSectionAndDescription(t *testing if len(client.calls) != 1 || client.calls[0].PromptID != prompts.PromptIDModuleGrammarProposal || client.calls[0].Context.SectionIndex == nil || *client.calls[0].Context.SectionIndex != 7 { t.Fatalf("unexpected stage name calls: %+v", client.calls) } + if got := client.calls[0].Variables["ProtectedTermsJSON"]; got != `["Separate protection"]` { + t.Fatalf("protected spellings = %s", got) + } if len(out.Proposals) != 1 || out.Proposals[0].CorrectedText != "the" { t.Fatalf("unexpected proposals: %+v", out) } diff --git a/internal/prompts/assets/prompts/modules.glossary.proposal.yaml b/internal/prompts/assets/prompts/modules.glossary.proposal.yaml index 49fe96f..11e3c03 100644 --- a/internal/prompts/assets/prompts/modules.glossary.proposal.yaml +++ b/internal/prompts/assets/prompts/modules.glossary.proposal.yaml @@ -17,7 +17,7 @@ collections: - Inappropriate example: correcting "Lyra" to "Jesters" should be omitted because those words are not similar in spoken English, even if "Jesters" appears in the glossary. - Do not replace one clear glossary term, character name, location, or ordinary word with a different glossary term unless it is a plausible mishearing. - Preserve correctly identified glossary names and aliases. A recognized glossary term may still be a mishearing of another term; correct that misidentification only when spoken similarity and surrounding context support it. - - Apart from that supported mishearing exception, do not replace, Anglicize, normalize, lowercase, or otherwise alter protected glossary names or aliases away from their glossary spelling. + - Preserve spellings listed under Protected spellings. Glossary context supplies correction targets and guidance. - Preserve canonical glossary capitalization for protected names and aliases, even if they look unusual. - If a segment includes categories, treat them as additional transcript context. - Plural forms of glossary names and aliases are allowed targets when spoken similarity and context support them, even if the plural is not explicitly listed in the glossary. @@ -28,9 +28,12 @@ collections: {{ .HardeningPolicy }} - {{ .TranscriptDescriptionBlock }}Glossary: + {{ .TranscriptDescriptionBlock }}Glossary context: {{ .GlossaryJSON }} + Protected spellings: + {{ .ProtectedTermsJSON }} + Transcript section: {{ .SectionJSON }} output: diff --git a/internal/prompts/assets/prompts/modules.grammar.proposal.yaml b/internal/prompts/assets/prompts/modules.grammar.proposal.yaml index 02da0b4..250ed93 100644 --- a/internal/prompts/assets/prompts/modules.grammar.proposal.yaml +++ b/internal/prompts/assets/prompts/modules.grammar.proposal.yaml @@ -18,8 +18,7 @@ collections: - Apart from the a/an article exception, do not make word substitutions, spelling fixes, homophone fixes, filler cleanup, repetition cleanup, paraphrases, or other semantic rewrites. - Do not change one written word into a different written word, except for capitalization changes to the same letters and the a/an article exception. - If a possible correction depends on changing a content word into a different word, omit it here rather than bundling it together with formatting cleanup. - - Treat glossary names and aliases as protected spellings and context. - - Do not replace, Anglicize, normalize, lowercase, or otherwise alter protected glossary names or aliases away from their glossary spelling. + - Do not replace, Anglicize, normalize, lowercase, or otherwise alter spellings listed under Protected spellings away from their glossary spelling. - Preserve canonical glossary capitalization for protected names and aliases, even if they look unusual. - If a segment includes categories, treat them as additional transcript context. - Choose an original_text span that appears exactly once in the current segment text. @@ -31,9 +30,12 @@ collections: {{ .HardeningPolicy }} - {{ .TranscriptDescriptionBlock }}Protected glossary/context: + {{ .TranscriptDescriptionBlock }}Glossary context: {{ .GlossaryJSON }} + Protected spellings: + {{ .ProtectedTermsJSON }} + Transcript section: {{ .SectionJSON }} output: diff --git a/internal/prompts/assets/prompts/modules.homophones.proposal.yaml b/internal/prompts/assets/prompts/modules.homophones.proposal.yaml index 3003979..3522186 100644 --- a/internal/prompts/assets/prompts/modules.homophones.proposal.yaml +++ b/internal/prompts/assets/prompts/modules.homophones.proposal.yaml @@ -16,9 +16,8 @@ collections: - Reject unrelated substitutions like changing "Lyra" to "Jesters". - Reject antonyms or reversals such as changing "visible" to "invisible". - Do not add or remove punctuation, alter capitalization only, normalize spacing, remove filler words, collapse repetitions, or make general readability edits. - - Treat glossary names and aliases as protected spellings and context. - You may correct toward glossary names, aliases, or their plural forms when the correction is acoustically plausible and supported by local context. - - Do not replace, Anglicize, normalize, lowercase, or otherwise alter protected glossary names or aliases that already appear correctly in the transcript. + - Do not replace, Anglicize, normalize, lowercase, or otherwise alter spellings listed under Protected spellings that already appear correctly in the transcript. - Preserve canonical glossary capitalization for protected names and aliases, even if they look unusual. - If a segment includes categories, treat them as additional transcript context. - Choose an original_text span that appears exactly once in the current segment text. @@ -28,9 +27,12 @@ collections: {{ .HardeningPolicy }} - {{ .TranscriptDescriptionBlock }}Protected glossary/context: + {{ .TranscriptDescriptionBlock }}Glossary context: {{ .GlossaryJSON }} + Protected spellings: + {{ .ProtectedTermsJSON }} + Transcript section: {{ .SectionJSON }} output: diff --git a/internal/prompts/assets/prompts/modules.spoken_word.proposal.yaml b/internal/prompts/assets/prompts/modules.spoken_word.proposal.yaml index e941b83..49ded6d 100644 --- a/internal/prompts/assets/prompts/modules.spoken_word.proposal.yaml +++ b/internal/prompts/assets/prompts/modules.spoken_word.proposal.yaml @@ -20,8 +20,8 @@ collections: - Do not paraphrase, summarize, reorder ideas, replace content with different wording, or make substantive semantic edits. - Do not change clear content words just because a different phrasing reads better. - Do not convert uncertain statements into certain statements. - - Treat glossary names and aliases as protected spellings and context. Accidental repetition of a protected name may be reduced when that identity remains in the segment; preserve substantive references and intentional emphasis. - - Do not replace, Anglicize, normalize, lowercase, or otherwise alter protected glossary names or aliases that already appear correctly in the transcript. + - Accidental repetition of a protected name may be reduced when that identity remains in the segment; preserve substantive references and intentional emphasis. + - Do not replace, Anglicize, normalize, lowercase, or otherwise alter spellings listed under Protected spellings that already appear correctly in the transcript. - Preserve canonical glossary capitalization for protected names and aliases, even if they look unusual. - If a segment includes categories, treat them as additional transcript context. - Choose an original_text span that appears exactly once in the current segment text. @@ -33,9 +33,12 @@ collections: {{ .HardeningPolicy }} - {{ .TranscriptDescriptionBlock }}Protected glossary/context: + {{ .TranscriptDescriptionBlock }}Glossary context: {{ .GlossaryJSON }} + Protected spellings: + {{ .ProtectedTermsJSON }} + Transcript section: {{ .SectionJSON }} output: diff --git a/internal/prompts/registry.go b/internal/prompts/registry.go index 835d6fd..c404747 100644 --- a/internal/prompts/registry.go +++ b/internal/prompts/registry.go @@ -93,6 +93,9 @@ func WithSharedVariables(values map[string]string) map[string]string { result[key] = value } } + if _, exists := result["ProtectedTermsJSON"]; !exists { + result["ProtectedTermsJSON"] = "[]" + } return result } diff --git a/internal/prompts/registry_test.go b/internal/prompts/registry_test.go index 239b8b2..9c36044 100644 --- a/internal/prompts/registry_test.go +++ b/internal/prompts/registry_test.go @@ -38,9 +38,7 @@ func TestEmbeddedDefinitionsPrepareThroughPromptKit(t *testing.T) { "SectionJSON": "{\"text\":\"fixture-section\"}", "PayloadJSON": "[{\"text\":\"fixture-correction\"}]", } - for key, value := range SharedVariables() { - vars[key] = value - } + vars = WithSharedVariables(vars) for _, tc := range []struct{ id, instruction, schema, payload string }{ {PromptIDModuleGlossaryProposal, "strongly supported by the glossary", "correction_set.v1.json", "fixture-section"}, {PromptIDModuleHomophonesProposal, "homophone correction assistant", "correction_set.v1.json", "fixture-section"},