Reuse prepared glossary data across stage sections

This commit is contained in:
2026-11-10 20:42:28 +00:00
parent 8ad27e8368
commit c16d61e091
21 changed files with 174 additions and 142 deletions

View File

@@ -37,9 +37,9 @@ Shared proposal-generation plumbing is centralized in:
Module packages own:
- prompt selection (`internal/prompts` prompt IDs).
`proposal_generation.BuildProposalVariables` encodes the glossary, selected
transcript section, and trimmed description once for all four modules. PromptKit
renders their embedded definitions from those variables and validates the
Each stage prepares the encoded glossary context and protected spellings once.
Proposal generation adds the selected transcript section and trimmed description
for each request. PromptKit renders the selected definition and validates the
correction-set schema before Audita maps proposals. Domain validators still
check confidence, applicability, protected glossary terms, and replacement
policy. Model-facing segment IDs are one-based positions within the proposal

View File

@@ -120,10 +120,11 @@ is covered by [report assembly](diagnostics-reporting.md).
## Supplemental Validation Context
Request construction prepares one segment-position lookup and a matcher from the
selected context entries for all candidates. Context selection remains candidate-specific,
including the proposed replacement text. The independent protection view is used
by the deterministic guard and application check.
Each stage prepares a context matcher and indexes its selected glossary entries
by identity. Request construction uses a segment-position lookup and returns
matched entries in selection order for each candidate, including matches in the
replacement text. The deterministic guard and application check share the
stage's separate protection matcher.
Each candidate includes the immediately preceding and following source segments
available within its fixed chunk, labeled `preceding` or `following` with speaker,

View File

@@ -562,6 +562,8 @@ are preserved. Readiness keeps the same late-stage diagnostics.
## R16 — P2: Reuse Prepared Glossary Views and Index Validation Context
**Status:** Completed. Each stage prepares shared protection and context matchers and serializes its proposal glossary variables once. Context gathers matched entry positions in selection order. Tests cover shared identities, ordering, exemptions, and request content.
**Category**
Per-section rebuilds and per-candidate glossary scans in Stages 11 and 12.

View File

@@ -10,6 +10,7 @@ import (
"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/outcomes"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/promptcontext"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/proposals"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/structuredoutput"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/validators"
@@ -73,13 +74,14 @@ type ExecutionContext struct {
StageIndex *int `json:"stage_index,omitempty"`
StageID *string `json:"stage_id,omitempty"`
// ReportDiagnosticError may be called concurrently; it must not affect processing.
ReportDiagnosticError func(error) `json:"-"`
Settings config.Stage `json:"-"`
WorkingTranscript *schema.Transcript `json:"-"`
Glossary []glossary.Entry `json:"-"`
ProtectedTerms []glossary.Term `json:"-"`
Section *SectionMetadata `json:"section,omitempty"`
DiagnosticsDir string `json:"diagnostics_dir,omitempty"`
ReportDiagnosticError func(error) `json:"-"`
Settings config.Stage `json:"-"`
WorkingTranscript *schema.Transcript `json:"-"`
Glossary []glossary.Entry `json:"-"`
ProtectedTerms []glossary.Term `json:"-"`
GlossaryVariables *promptcontext.GlossaryVariables `json:"-"`
Section *SectionMetadata `json:"section,omitempty"`
DiagnosticsDir string `json:"diagnostics_dir,omitempty"`
}
// ProposalRequest is the input to module proposal generation.

View File

@@ -0,0 +1,32 @@
package promptcontext
import (
"encoding/json"
"fmt"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/glossary"
)
// GlossaryVariables contains the glossary inputs shared by a stage's proposals.
type GlossaryVariables struct {
ContextJSON string
ProtectionJSON string
}
func PrepareGlossaryVariables(entries []glossary.Entry, terms []glossary.Term) (GlossaryVariables, error) {
contextJSON, err := json.MarshalIndent(entries, "", " ")
if err != nil {
return GlossaryVariables{}, fmt.Errorf("marshal glossary prompt context: %w", err)
}
protected := make([]string, 0, len(terms))
for _, term := range terms {
if !term.Exempt {
protected = append(protected, term.Spelling)
}
}
protectionJSON, err := json.Marshal(protected)
if err != nil {
return GlossaryVariables{}, fmt.Errorf("marshal protected spellings: %w", err)
}
return GlossaryVariables{ContextJSON: string(contextJSON), ProtectionJSON: string(protectionJSON)}, nil
}

View File

@@ -14,6 +14,7 @@ import (
"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/llm"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/promptcontext"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/proposals"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/validators"
"gitea.maximumdirect.net/eric/audita/v3/internal/prompts"
@@ -60,7 +61,11 @@ func TestRepairedProposalStillFacesDomainChecks(t *testing.T) {
t.Fatal(err)
}
transcript := &schema.Transcript{Segments: []schema.Segment{{ID: 1, Speaker: "A", Start: 0, End: 1, Text: "gestures"}}}
vars, err := BuildProposalVariables(transcript, nil, 0, "")
prepared, err := promptcontext.PrepareGlossaryVariables(nil, nil)
if err != nil {
t.Fatal(err)
}
vars, err := buildProposalVariables(transcript, prepared, 0, "")
if err != nil {
t.Fatal(err)
}

View File

@@ -6,6 +6,7 @@ import (
"gitea.maximumdirect.net/eric/audita/v3/internal/core/glossary"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/schema"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/promptcontext"
)
func TestProposalContextPreservesDistinctEntries(t *testing.T) {
@@ -13,7 +14,11 @@ func TestProposalContextPreservesDistinctEntries(t *testing.T) {
{Collection: "spells", ID: "shield", Name: "Shield", Category: "spell", Description: "A spell"},
{Collection: "items", ID: "shield", Name: "Shield", Category: "item", Description: "An item"},
}
variables, err := BuildProposalVariables(&schema.Transcript{}, entries, 0, "")
prepared, err := promptcontext.PrepareGlossaryVariables(entries, nil)
if err != nil {
t.Fatal(err)
}
variables, err := buildProposalVariables(&schema.Transcript{}, prepared, 0, "")
if err != nil {
t.Fatal(err)
}

View File

@@ -2,31 +2,22 @@ package proposal_generation
import (
"context"
"encoding/json"
"fmt"
"strings"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/glossary"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/schema"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/contracts"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/promptcontext"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/stagename"
"gitea.maximumdirect.net/eric/audita/v3/internal/prompts"
)
type ProposalVariableBuilder func(transcript *schema.Transcript, entries []glossary.Entry, sectionIndex int, transcriptDescription string) (map[string]string, error)
type ModuleProposalRequest struct {
ProposalRequest contracts.ProposalRequest
PromptID string
BuildVariables ProposalVariableBuilder
}
// ExecuteModuleProposal runs shared proposal generation plumbing for one
// module, leaving only module-specific prompt message building at call sites.
// ExecuteModuleProposal prepares and runs a module's selected prompt.
func ExecuteModuleProposal(ctx context.Context, req ModuleProposalRequest) (contracts.ProposalResult, error) {
if req.BuildVariables == nil {
return contracts.ProposalResult{}, fmt.Errorf("proposal variable builder is required")
}
if strings.TrimSpace(req.PromptID) == "" {
return contracts.ProposalResult{}, fmt.Errorf("prompt ID must not be empty")
}
@@ -38,9 +29,17 @@ func ExecuteModuleProposal(ctx context.Context, req ModuleProposalRequest) (cont
transcriptDescription := req.ProposalRequest.Settings.Context.Description
variables, err := req.BuildVariables(
glossary := req.ProposalRequest.GlossaryVariables
if glossary == nil {
prepared, err := promptcontext.PrepareGlossaryVariables(req.ProposalRequest.Glossary, req.ProposalRequest.ProtectedTerms)
if err != nil {
return contracts.ProposalResult{}, err
}
glossary = &prepared
}
variables, err := buildProposalVariables(
req.ProposalRequest.WorkingTranscript,
req.ProposalRequest.Glossary,
*glossary,
sectionIndex,
transcriptDescription,
)
@@ -48,18 +47,6 @@ 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 {
if !term.Exempt {
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)

View File

@@ -4,7 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"reflect"
"strings"
"testing"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/config"
@@ -29,11 +29,6 @@ func TestExecuteModuleProposalBuildsMessagesFromSectionAndDescription(t *testing
transcript := &schema.Transcript{Segments: []schema.Segment{{ID: 1, Speaker: "A", Start: 0, End: 1, Text: "teh"}}}
glossary := []domain.Entry{{ID: "x", Name: "X"}}
var gotSectionIndex int
var gotDescription string
var gotTranscript *schema.Transcript
var gotGlossary []domain.Entry
out, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{
ProposalRequest: contracts.ProposalRequest{
ExecutionContext: contracts.ExecutionContext{
@@ -51,32 +46,20 @@ func TestExecuteModuleProposalBuildsMessagesFromSectionAndDescription(t *testing
LLMClient: client,
},
PromptID: prompts.PromptIDModuleGrammarProposal,
BuildVariables: func(inTranscript *schema.Transcript, inGlossary []domain.Entry, sectionIndex int, transcriptDescription string) (map[string]string, error) {
gotSectionIndex = sectionIndex
gotDescription = transcriptDescription
gotTranscript = inTranscript
gotGlossary = inGlossary
return map[string]string{"SectionJSON": "[]"}, nil
},
})
if err != nil {
t.Fatalf("ExecuteModuleProposal error: %v", err)
}
if gotSectionIndex != 7 {
t.Fatalf("section index: got=%d want=%d", gotSectionIndex, 7)
}
if gotDescription != cfg.Context.Description {
t.Fatalf("transcript description: got=%q want=%q", gotDescription, cfg.Context.Description)
}
if gotTranscript != transcript {
t.Fatalf("expected shared transcript pointer")
}
if !reflect.DeepEqual(gotGlossary, glossary) {
t.Fatalf("expected glossary entries")
}
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)
}
variables := client.calls[0].Variables
if !strings.Contains(variables["SectionJSON"], `"section_index": 7`) ||
!strings.Contains(variables["SectionJSON"], "teh") ||
!strings.Contains(variables["TranscriptDescriptionBlock"], cfg.Context.Description) ||
!strings.Contains(variables["GlossaryJSON"], `"name": "X"`) {
t.Fatalf("missing section, glossary, or description: %+v", variables)
}
if got := client.calls[0].Variables["ProtectedTermsJSON"]; got != `["Separate protection"]` {
t.Fatalf("protected spellings = %s", got)
}
@@ -86,40 +69,10 @@ func TestExecuteModuleProposalBuildsMessagesFromSectionAndDescription(t *testing
}
func TestExecuteModuleProposalValidatesInputs(t *testing.T) {
if _, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{}); err == nil {
t.Fatalf("expected missing message builder error")
}
_, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{
BuildVariables: func(transcript *schema.Transcript, glossary []domain.Entry, sectionIndex int, transcriptDescription string) (map[string]string, error) {
return nil, nil
},
})
if err == nil {
t.Fatalf("expected empty prompt ID error")
}
_, err = ExecuteModuleProposal(context.Background(), ModuleProposalRequest{
PromptID: "missing.prompt.id",
BuildVariables: func(transcript *schema.Transcript, glossary []domain.Entry, sectionIndex int, transcriptDescription string) (map[string]string, error) {
return map[string]string{"SectionJSON": "[]"}, nil
},
})
if err == nil {
t.Fatalf("expected unknown prompt ID error")
}
}
func TestExecuteModuleProposalPropagatesBuilderError(t *testing.T) {
wantErr := errors.New("builder failed")
_, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{
PromptID: prompts.PromptIDModuleGlossaryProposal,
BuildVariables: func(transcript *schema.Transcript, glossary []domain.Entry, sectionIndex int, transcriptDescription string) (map[string]string, error) {
return nil, wantErr
},
})
if !errors.Is(err, wantErr) {
t.Fatalf("expected builder error, got %v", err)
for _, promptID := range []string{"", "missing.prompt.id"} {
if _, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{PromptID: promptID}); err == nil {
t.Fatalf("invalid prompt ID accepted: %q", promptID)
}
}
}
@@ -134,9 +87,6 @@ func TestExecuteModuleProposalPreservesFatalOperation(t *testing.T) {
LLMClient: client,
},
PromptID: prompts.PromptIDModuleGrammarProposal,
BuildVariables: func(*schema.Transcript, []domain.Entry, int, string) (map[string]string, error) {
return map[string]string{"SectionJSON": "[]"}, nil
},
})
if !errors.Is(err, cause) || len(result.Operations) != 1 || result.Operations[0].Reference.ModuleInstance != "grammar_2" || result.Operations[0].Reference.SectionIndex != 4 {
t.Fatalf("fatal operation lost: %+v, %v", result, err)
@@ -157,7 +107,7 @@ func TestModuleProposalMapsLocalReferencesWithoutGuessing(t *testing.T) {
RunSpec: contracts.ModuleRunSpec{ModuleKey: "grammar", InstanceName: "grammar", ReplacementPolicy: proposals.ReplacementPolicyRequireUnique},
LLMClient: client,
},
PromptID: prompts.PromptIDModuleGrammarProposal, BuildVariables: BuildProposalVariables,
PromptID: prompts.PromptIDModuleGrammarProposal,
})
if err != nil {
t.Fatal(err)

View File

@@ -1,28 +1,21 @@
package proposal_generation
import (
"encoding/json"
"fmt"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/glossary"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/schema"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/promptcontext"
)
// BuildProposalVariables preserves the domain inputs while PromptKit renders
// the selected embedded proposal definition.
func BuildProposalVariables(transcript *schema.Transcript, entries []glossary.Entry, sectionIndex int, transcriptDescription string) (map[string]string, error) {
glossaryJSON, err := json.MarshalIndent(entries, "", " ")
if err != nil {
return nil, fmt.Errorf("marshal glossary prompt context: %w", err)
}
func buildProposalVariables(transcript *schema.Transcript, glossary promptcontext.GlossaryVariables, sectionIndex int, transcriptDescription string) (map[string]string, error) {
sectionJSON, err := promptcontext.MarshalTranscriptSectionJSON(transcript, sectionIndex)
if err != nil {
return nil, fmt.Errorf("marshal transcript prompt context: %w", err)
}
return map[string]string{
"TranscriptDescriptionBlock": promptcontext.TranscriptDescriptionBlock(transcriptDescription),
"GlossaryJSON": string(glossaryJSON),
"GlossaryJSON": glossary.ContextJSON,
"ProtectedTermsJSON": glossary.ProtectionJSON,
"SectionJSON": string(sectionJSON),
}, nil
}

View File

@@ -6,19 +6,24 @@ import (
domain "gitea.maximumdirect.net/eric/audita/v3/internal/core/glossary"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/schema"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/promptcontext"
)
func TestBuildProposalVariablesPreservesSectionGlossaryAndDescription(t *testing.T) {
transcript := &schema.Transcript{Segments: []schema.Segment{{ID: 1, Speaker: "A", Start: 0, End: 1, Text: "gestures"}}}
glossary := []domain.Entry{{ID: "jesters", Name: "Jesters"}}
variables, err := BuildProposalVariables(transcript, glossary, 0, " Fantasy names ")
prepared, err := promptcontext.PrepareGlossaryVariables(glossary, nil)
if err != nil {
t.Fatal(err)
}
variables, err := buildProposalVariables(transcript, prepared, 0, " Fantasy names ")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(variables["SectionJSON"], `"section_index": 0`) || !strings.Contains(variables["SectionJSON"], "gestures") || !strings.Contains(variables["GlossaryJSON"], "Jesters") || !strings.Contains(variables["TranscriptDescriptionBlock"], "Fantasy names") {
t.Fatalf("encoded variables incomplete: %+v", variables)
}
empty, err := BuildProposalVariables(transcript, glossary, 0, " ")
empty, err := buildProposalVariables(transcript, prepared, 0, " ")
if err != nil {
t.Fatal(err)
}

View File

@@ -15,6 +15,7 @@ import (
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/contracts"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/llm"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/outcomes"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/promptcontext"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/proposals"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/validators"
validatormetadata "gitea.maximumdirect.net/eric/audita/v3/internal/validators/metadata"
@@ -163,6 +164,11 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (RunOutput, error) {
}
policy := module.ReplacementPolicy()
glossaryVariables, err := promptcontext.PrepareGlossaryVariables(spec.Glossary, spec.ProtectedTerms)
if err != nil {
return snapshot(), fmt.Errorf("module %q glossary preparation: %w", spec.InstanceName, err)
}
glossaryView := validators.PrepareGlossaryView(spec.Glossary, spec.ProtectedTerms)
pipelineResult, pipelineErr := runModulePipeline(ctx, collectSectionProposalsInput{
Module: module,
Spec: spec,
@@ -172,6 +178,8 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (RunOutput, error) {
DiagnosticsDir: input.ProposalDiagnosticsDir,
ValidationClient: spec.ValidationClient,
ValidationDiagnosticsDir: input.ValidationDiagnosticsDir,
GlossaryVariables: &glossaryVariables,
GlossaryView: glossaryView,
ReportDiagnosticError: input.ReportDiagnosticError,
})
if input.CaptureCandidates {
@@ -198,7 +206,7 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (RunOutput, error) {
return snapshot(), fmt.Errorf("module %q failed: %w", spec.InstanceName, pipelineErr)
}
updated, applied, skipped, appliedBySection, applyErr := applyWithinSections(sections, pipelineResult.Approved, policy, validators.NewProtectedVocabulary(spec.ProtectedTerms), spec.ModuleKey == modulecatalog.KeySpokenWord)
updated, applied, skipped, appliedBySection, applyErr := applyWithinSections(sections, pipelineResult.Approved, policy, glossaryView.Protection, spec.ModuleKey == modulecatalog.KeySpokenWord)
if applyErr != nil {
return snapshot(), fmt.Errorf("module %q application failed: %w", spec.InstanceName, applyErr)
}
@@ -286,6 +294,8 @@ type collectSectionProposalsInput struct {
DiagnosticsDir string
ValidationClient contracts.StructuredLLMClient
ValidationDiagnosticsDir string
GlossaryVariables *promptcontext.GlossaryVariables
GlossaryView *validators.GlossaryView
}
type sectionProposalResult struct {
@@ -343,6 +353,7 @@ func collectSectionProposals(ctx context.Context, input collectSectionProposalsI
WorkingTranscript: transcriptFromSection(section),
Glossary: input.Spec.Glossary,
ProtectedTerms: input.Spec.ProtectedTerms,
GlossaryVariables: input.GlossaryVariables,
Section: &meta,
DiagnosticsDir: input.DiagnosticsDir,
ReportDiagnosticError: input.ReportDiagnosticError,
@@ -488,6 +499,7 @@ func runModulePipeline(ctx context.Context, input collectSectionProposalsInput)
SectionEnriched: enriched,
ValidationLLMClient: input.ValidationClient,
DiagnosticsDir: input.ValidationDiagnosticsDir,
GlossaryView: input.GlossaryView,
ReportDiagnosticError: input.ReportDiagnosticError,
})
setErr(err)
@@ -571,6 +583,7 @@ type validateSectionCandidatesInput struct {
SectionEnriched []proposals.EnrichedCorrectionProposal
ValidationLLMClient contracts.StructuredLLMClient
DiagnosticsDir string
GlossaryView *validators.GlossaryView
}
type validateSectionCandidatesResult struct {
@@ -618,6 +631,7 @@ func validateSectionCandidates(ctx context.Context, input validateSectionCandida
ReplacementPolicy: input.Policy,
Glossary: input.Spec.Glossary,
ProtectedTerms: input.Spec.ProtectedTerms,
GlossaryView: input.GlossaryView,
Settings: input.Spec.Settings,
LLMClient: input.ValidationLLMClient,
DiagnosticsWriter: diagnosticsWriter,

View File

@@ -137,7 +137,12 @@ func (v NoEffectValidator) Validate(_ context.Context, req Request) (Result, err
}
func ValidateProtectedTerms(req Request) (Result, error) {
vocab := NewProtectedVocabulary(req.ProtectedTerms)
var vocab ProtectedVocabulary
if req.GlossaryView != nil {
vocab = req.GlossaryView.Protection
} else {
vocab = NewProtectedVocabulary(req.ProtectedTerms)
}
decisions := make([]Decision, 0, len(req.CandidateProposal))
glossaryStage := req.ModuleKey == modulecatalog.KeyGlossary
for _, c := range req.CandidateProposal {

View File

@@ -1,15 +1,43 @@
package validators
import (
"slices"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/chunking"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/glossary"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/schema"
)
// GlossaryView holds the read-only matching data shared by a stage's validators
// and apply-time checks.
type GlossaryView struct {
Protection ProtectedVocabulary
context glossaryContext
}
func PrepareGlossaryView(entries []glossary.Entry, protected []glossary.Term) *GlossaryView {
return &GlossaryView{Protection: NewProtectedVocabulary(protected), context: prepareGlossaryContext(entries)}
}
type glossaryContext struct {
entries []glossary.Entry
positions map[glossary.Identity][]int
vocabulary ProtectedVocabulary
}
func prepareGlossaryContext(entries []glossary.Entry) glossaryContext {
positions := make(map[glossary.Identity][]int, len(entries))
for i, entry := range entries {
identity := glossary.Identity{Collection: entry.Collection, ID: entry.ID}
positions[identity] = append(positions[identity], i)
}
return glossaryContext{entries: entries, positions: positions, vocabulary: NewProtectedVocabulary(glossary.Vocabulary(entries, nil))}
}
// validationContext supplies adjacent source segments within the current chunk
// and glossary definitions mentioned in the candidate or that local context.
// These fields travel in each item so batching accounts for their size.
func validationContext(segments []schema.Segment, position int, selected []glossary.Entry, vocabulary ProtectedVocabulary, corrected string) ([]NeighboringSegment, []glossary.Entry) {
func (view glossaryContext) validationContext(segments []schema.Segment, position int, corrected string) ([]NeighboringSegment, []glossary.Entry) {
var neighbors []NeighboringSegment
source := segments[position].Text
if position > 0 {
@@ -23,19 +51,20 @@ func validationContext(segments []schema.Segment, position int, selected []gloss
for _, neighbor := range neighbors {
texts = append(texts, neighbor.Text)
}
mentioned := make(map[glossary.Identity]struct{})
for _, text := range texts {
for _, occurrence := range view.vocabulary.occurrences(text) {
mentioned[occurrence.identity] = struct{}{}
}
}
var matched []int
for identity := range mentioned {
matched = append(matched, view.positions[identity]...)
}
slices.Sort(matched)
var entries []glossary.Entry
if selected != nil {
mentioned := make(map[glossary.Identity]bool)
for _, text := range texts {
for identity := range vocabulary.occurrencesByIdentity(text) {
mentioned[identity] = true
}
}
for _, entry := range selected {
if mentioned[glossary.Identity{Collection: entry.Collection, ID: entry.ID}] {
entries = append(entries, entry)
}
}
for _, index := range matched {
entries = append(entries, view.entries[index])
}
return neighbors, entries
}

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/glossary"
"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/proposals"
@@ -254,7 +253,12 @@ func BuildLLMValidationRequest(validatorName string, validatorType LLMValidatorT
immediate := make([]Decision, 0)
var segments []schema.Segment
positions := make(map[int]int)
vocabulary := NewProtectedVocabulary(glossary.Vocabulary(req.Glossary, nil))
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 {
@@ -276,7 +280,7 @@ func BuildLLMValidationRequest(validatorName string, validatorType LLMValidatorT
continue
}
neighbors, glossaryEntries := validationContext(segments, position, req.Glossary, vocabulary, preview.CorrectedSegmentText)
neighbors, glossaryEntries := glossary.validationContext(segments, position, preview.CorrectedSegmentText)
items = append(items, LLMValidationItem{
CorrectionIndex: p.ProposalIndex,
SegmentID: p.TargetSegmentID,

View File

@@ -45,6 +45,7 @@ type Request struct {
ReplacementPolicy proposals.ReplacementPolicy `json:"replacement_policy"`
Glossary []glossary.Entry `json:"-"`
ProtectedTerms []glossary.Term `json:"-"`
GlossaryView *GlossaryView `json:"-"`
Settings config.Stage `json:"-"`
LLMClient StructuredLLMClient `json:"-"`
DiagnosticsWriter InteractionDiagnosticsWriter `json:"-"`

View File

@@ -110,13 +110,14 @@ func TestExemptGlossarySpellingKeepsLongestMatchBoundary(t *testing.T) {
func TestValidationContextIncludesEachSharedDefinition(t *testing.T) {
entries := []glossary.Entry{
{Collection: "items", ID: "sword", Name: "Sword", Category: "item"},
{Collection: "spells", ID: "shield", Name: "Shield", Category: "spell"},
{Collection: "items", ID: "shield", Name: "Shield", Category: "item"},
{Collection: "items", ID: "sword", Name: "Sword", Category: "item"},
{Collection: "items", ID: "bow", Name: "Bow", Category: "item"},
}
segments := []schema.Segment{{ID: 1, Text: "Shield"}}
_, selected := validationContext(segments, 0, entries, NewProtectedVocabulary(glossary.Vocabulary(entries, nil)), "Barrier")
if len(selected) != 2 || selected[0].Category != "spell" || selected[1].ID != "shield" || selected[1].Collection != "items" {
segments := []schema.Segment{{ID: 1, Text: "Shield Shield"}}
_, selected := prepareGlossaryContext(entries).validationContext(segments, 0, "Sword")
if len(selected) != 3 || selected[0].ID != "sword" || selected[1].Category != "spell" || selected[2].ID != "shield" || selected[2].Collection != "items" {
t.Fatalf("selected=%+v", selected)
}
}

View File

@@ -39,6 +39,5 @@ func (m *Module) Propose(ctx context.Context, req contracts.ProposalRequest) (co
return proposal_generation.ExecuteModuleProposal(ctx, proposal_generation.ModuleProposalRequest{
ProposalRequest: req,
PromptID: prompts.PromptIDModuleGlossaryProposal,
BuildVariables: proposal_generation.BuildProposalVariables,
})
}

View File

@@ -39,6 +39,5 @@ func (m *Module) Propose(ctx context.Context, req contracts.ProposalRequest) (co
return proposal_generation.ExecuteModuleProposal(ctx, proposal_generation.ModuleProposalRequest{
ProposalRequest: req,
PromptID: prompts.PromptIDModuleGrammarProposal,
BuildVariables: proposal_generation.BuildProposalVariables,
})
}

View File

@@ -39,6 +39,5 @@ func (m *Module) Propose(ctx context.Context, req contracts.ProposalRequest) (co
return proposal_generation.ExecuteModuleProposal(ctx, proposal_generation.ModuleProposalRequest{
ProposalRequest: req,
PromptID: prompts.PromptIDModuleHomophonesProposal,
BuildVariables: proposal_generation.BuildProposalVariables,
})
}

View File

@@ -39,6 +39,5 @@ func (m *Module) Propose(ctx context.Context, req contracts.ProposalRequest) (co
return proposal_generation.ExecuteModuleProposal(ctx, proposal_generation.ModuleProposalRequest{
ProposalRequest: req,
PromptID: prompts.PromptIDModuleSpokenWordProposal,
BuildVariables: proposal_generation.BuildProposalVariables,
})
}