164 lines
5.1 KiB
Go
164 lines
5.1 KiB
Go
package runner
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/eric/audita/internal/core/config"
|
|
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
|
|
"gitea.maximumdirect.net/eric/audita/internal/framework/contracts"
|
|
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
|
|
)
|
|
|
|
const (
|
|
ModuleStatusSuccess = "success"
|
|
ModuleStatusFailed = "failed"
|
|
)
|
|
|
|
// ModuleFactory resolves one module instance for one run spec.
|
|
type ModuleFactory interface {
|
|
ModuleForSpec(spec contracts.ModuleRunSpec) (contracts.TranscriptModule, error)
|
|
}
|
|
|
|
// Runner executes module instances sequentially against a working transcript.
|
|
type Runner struct {
|
|
factory ModuleFactory
|
|
}
|
|
|
|
// ModuleResult captures deterministic per-module execution output.
|
|
type ModuleResult struct {
|
|
ModuleKey string `json:"module_key"`
|
|
ModuleInstance string `json:"module_instance"`
|
|
ReplacementPolicy proposals.ReplacementPolicy `json:"replacement_policy"`
|
|
Status string `json:"status"`
|
|
ProposalCount int `json:"proposal_count"`
|
|
AppliedChanges []proposals.AppliedChange `json:"applied_changes,omitempty"`
|
|
SkippedChanges []proposals.SkippedChange `json:"skipped_changes,omitempty"`
|
|
ErrorMessage string `json:"error_message,omitempty"`
|
|
StartedAt time.Time `json:"started_at"`
|
|
CompletedAt time.Time `json:"completed_at"`
|
|
}
|
|
|
|
// RunInput is the deterministic runner input.
|
|
type RunInput struct {
|
|
Config *config.Config
|
|
Transcript *schema.Transcript
|
|
Glossary *schema.Glossary
|
|
ModuleSpecs []contracts.ModuleRunSpec
|
|
}
|
|
|
|
// RunOutput is the deterministic runner output.
|
|
type RunOutput struct {
|
|
FinalTranscript *schema.Transcript `json:"-"`
|
|
ModuleResults []ModuleResult `json:"module_results"`
|
|
}
|
|
|
|
func New(factory ModuleFactory) *Runner {
|
|
return &Runner{factory: factory}
|
|
}
|
|
|
|
func (r *Runner) Run(ctx context.Context, input RunInput) (RunOutput, error) {
|
|
if r == nil || r.factory == nil {
|
|
return RunOutput{}, fmt.Errorf("runner module factory is required")
|
|
}
|
|
|
|
working := cloneTranscript(input.Transcript)
|
|
results := make([]ModuleResult, 0, len(input.ModuleSpecs))
|
|
|
|
for _, spec := range input.ModuleSpecs {
|
|
startedAt := time.Now().UTC()
|
|
module, err := r.factory.ModuleForSpec(spec)
|
|
if err != nil {
|
|
failed := ModuleResult{
|
|
ModuleKey: spec.ModuleKey,
|
|
ModuleInstance: spec.InstanceName,
|
|
Status: ModuleStatusFailed,
|
|
ErrorMessage: err.Error(),
|
|
StartedAt: startedAt,
|
|
CompletedAt: time.Now().UTC(),
|
|
}
|
|
results = append(results, failed)
|
|
return RunOutput{FinalTranscript: working, ModuleResults: results}, fmt.Errorf("module %q setup failed: %w", spec.InstanceName, err)
|
|
}
|
|
|
|
policy := module.ReplacementPolicy()
|
|
proposed, err := module.Propose(ctx, contracts.ProposalRequest{
|
|
ExecutionContext: contracts.ExecutionContext{
|
|
Config: input.Config,
|
|
WorkingTranscript: working,
|
|
Glossary: input.Glossary,
|
|
},
|
|
RunSpec: contracts.ModuleRunSpec{
|
|
ModuleKey: spec.ModuleKey,
|
|
InstanceName: spec.InstanceName,
|
|
ReplacementPolicy: policy,
|
|
},
|
|
})
|
|
if err != nil {
|
|
failed := ModuleResult{
|
|
ModuleKey: spec.ModuleKey,
|
|
ModuleInstance: spec.InstanceName,
|
|
ReplacementPolicy: policy,
|
|
Status: ModuleStatusFailed,
|
|
ErrorMessage: err.Error(),
|
|
StartedAt: startedAt,
|
|
CompletedAt: time.Now().UTC(),
|
|
}
|
|
results = append(results, failed)
|
|
return RunOutput{FinalTranscript: working, ModuleResults: results}, fmt.Errorf("module %q failed: %w", spec.InstanceName, err)
|
|
}
|
|
|
|
enriched := make([]proposals.EnrichedCorrectionProposal, 0, len(proposed))
|
|
for i, p := range proposed {
|
|
enriched = append(enriched, proposals.EnrichedCorrectionProposal{
|
|
CorrectionProposal: p,
|
|
ProposalMetadata: proposals.ProposalMetadata{
|
|
ProposalIndex: i,
|
|
ModuleKey: spec.ModuleKey,
|
|
ModuleInstance: spec.InstanceName,
|
|
},
|
|
})
|
|
}
|
|
|
|
applyResult := proposals.ApplyProposals(working, enriched, policy)
|
|
working = applyResult.Transcript
|
|
|
|
results = append(results, ModuleResult{
|
|
ModuleKey: spec.ModuleKey,
|
|
ModuleInstance: spec.InstanceName,
|
|
ReplacementPolicy: policy,
|
|
Status: ModuleStatusSuccess,
|
|
ProposalCount: len(enriched),
|
|
AppliedChanges: applyResult.Applied,
|
|
SkippedChanges: applyResult.Skipped,
|
|
StartedAt: startedAt,
|
|
CompletedAt: time.Now().UTC(),
|
|
})
|
|
}
|
|
|
|
return RunOutput{FinalTranscript: working, ModuleResults: results}, nil
|
|
}
|
|
|
|
func cloneTranscript(t *schema.Transcript) *schema.Transcript {
|
|
if t == nil {
|
|
return &schema.Transcript{}
|
|
}
|
|
segments := make([]schema.Segment, len(t.Segments))
|
|
for i, s := range t.Segments {
|
|
var categories []string
|
|
if s.Categories != nil {
|
|
categories = append([]string(nil), s.Categories...)
|
|
}
|
|
segments[i] = schema.Segment{
|
|
ID: s.ID,
|
|
Speaker: s.Speaker,
|
|
Start: s.Start,
|
|
End: s.End,
|
|
Text: s.Text,
|
|
Categories: categories,
|
|
}
|
|
}
|
|
return &schema.Transcript{Segments: segments}
|
|
}
|