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

@@ -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)
}
}