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