From 58c6ab2d54f497350167ce7ef408c4f8bb450668 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 16 May 2026 08:46:48 -0500 Subject: [PATCH] Updated audita configuration to reflect the new audita public CLI --- README.md | 27 +++ architecture.md | 25 +++ examples/pipeline.minimal.yml | 17 +- internal/adapters/audita/runner.go | 6 + internal/adapters/audita/subprocess.go | 188 +++++++++++++------- internal/adapters/audita/subprocess_test.go | 183 +++++++++++-------- internal/app/runner.go | 9 +- internal/config/config.go | 7 +- internal/config/load.go | 17 -- internal/config/load_validate_test.go | 113 ++++++++++-- internal/config/validate.go | 40 +++-- internal/stage/polish.go | 87 +++++---- internal/stage/polish_test.go | 37 +++- 13 files changed, 516 insertions(+), 240 deletions(-) diff --git a/README.md b/README.md index c0b84e8..8320e2a 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,33 @@ YAML decoding is strict (`KnownFields(true)`), so unknown fields fail fast. - `transcripts/normalized.json`: Seriatim-normalized transcript from the normalize stage - `transcripts/trimmed.json`: gameplay-only normalized polished transcript from trim stage +## Audita Configuration + +`pipeline.audita` configures the real Audita subprocess adapter used by `polish`. + +Required: + +- `binary` +- `timeout` +- `base_url` +- `model` + +Optional: + +- `llm_api_key_env` (when set, Narratio requires that env var and passes it to Audita as `AUDITA_LLM_API_KEY`) +- `modules` override list (when empty/omitted, Narratio does not pass `--modules`) +- `transcript_description` +- `config_path` +- `output_schema` (`bare-segments` or `audita-v1`) +- `work_dir_retention` (`always`, `auto`, or `never`) +- `total_llm_concurrency` (> 0 when provided) +- `proposal_llm_concurrency` (> 0 when provided) +- `validation_model` +- `validation_llm_concurrency` (> 0 when provided) +- `report` (defaults to `true`) + +Narratio passes only configured optional Audita flags. Omitted optional values are left to Audita runtime defaults. + ## Normalize Configuration `pipeline.normalize` is optional. When omitted, Narratio defaults to: diff --git a/architecture.md b/architecture.md index d30ac69..8e8a68a 100644 --- a/architecture.md +++ b/architecture.md @@ -116,6 +116,31 @@ Optional pipeline secrets directory: `pipeline.normalize` is optional. Existing pipelines without normalize config continue to work. +`pipeline.audita` drives the real Audita subprocess adapter for the `polish` stage. + +Audita required fields: + +- `binary` +- `timeout` +- `base_url` +- `model` + +Audita optional fields: + +- `llm_api_key_env` (enforced only when configured) +- `modules` override list (when omitted/empty, Narratio does not pass `--modules`) +- `transcript_description` +- `config_path` +- `output_schema` (`bare-segments` or `audita-v1`) +- `work_dir_retention` (`always`, `auto`, `never`) +- `total_llm_concurrency` (> 0 when provided) +- `proposal_llm_concurrency` (> 0 when provided) +- `validation_model` +- `validation_llm_concurrency` (> 0 when provided) +- `report` (default `true`) + +Narratio passes only configured optional Audita flags; omitted optional values defer to Audita defaults. + When `pipeline.normalize` is omitted, defaults are applied: - `output_path: transcripts/normalized.json` diff --git a/examples/pipeline.minimal.yml b/examples/pipeline.minimal.yml index 59926e0..94f869e 100644 --- a/examples/pipeline.minimal.yml +++ b/examples/pipeline.minimal.yml @@ -33,17 +33,16 @@ audita: binary: "audita" timeout: "3h" llm_api_key_env: "AUDITA_LLM_API_KEY" - modules: - - glossary - - homophones - - glossary - - spoken_word - - grammar - - homophones - - glossary + # Optional: pass only when overriding Audita's default module sequence. + modules: [] base_url: "https://openrouter.ai/api/v1" model: "openrouter/google/gemma-4-31b-it" - llm_concurrency: 1 + transcript_description: "" + config_path: "" + output_schema: "audita-v1" + work_dir_retention: "auto" + total_llm_concurrency: 1 + proposal_llm_concurrency: 1 validation_model: "" validation_llm_concurrency: 1 report: true diff --git a/internal/adapters/audita/runner.go b/internal/adapters/audita/runner.go index 1fa61d7..32d301f 100644 --- a/internal/adapters/audita/runner.go +++ b/internal/adapters/audita/runner.go @@ -24,6 +24,12 @@ type PolishRequest struct { Modules []string BaseURL string Model string + TranscriptDescription string + ConfigPath string + OutputSchema string + WorkDirRetention string + TotalLLMConcurrency *int + ProposalLLMConcurrency *int ValidationModel string ValidationLLMConcurrency *int StdoutLogPath string diff --git a/internal/adapters/audita/subprocess.go b/internal/adapters/audita/subprocess.go index e45e630..0cca016 100644 --- a/internal/adapters/audita/subprocess.go +++ b/internal/adapters/audita/subprocess.go @@ -21,7 +21,12 @@ type SubprocessRunnerConfig struct { Modules []string BaseURL string Model string - LLMConcurrency *int + TranscriptDescription string + ConfigPath string + OutputSchema string + WorkDirRetention string + TotalLLMConcurrency *int + ProposalLLMConcurrency *int ValidationModel string ValidationLLMConcurrency *int Report bool @@ -35,7 +40,12 @@ type SubprocessRunner struct { modules []string baseURL string model string - llmConcurrency *int + transcriptDescription string + configPath string + outputSchema string + workDirRetention string + totalLLMConcurrency *int + proposalLLMConcurrency *int validationModel string validationLLMConcurrency *int report bool @@ -49,7 +59,12 @@ func NewSubprocessRunnerFromConfigValues( modules []string, baseURL string, model string, - llmConcurrency *int, + transcriptDescription string, + configPath string, + outputSchema string, + workDirRetention string, + totalLLMConcurrency *int, + proposalLLMConcurrency *int, validationModel string, validationLLMConcurrency *int, report bool, @@ -68,7 +83,12 @@ func NewSubprocessRunnerFromConfigValues( Modules: modules, BaseURL: baseURL, Model: model, - LLMConcurrency: llmConcurrency, + TranscriptDescription: transcriptDescription, + ConfigPath: configPath, + OutputSchema: outputSchema, + WorkDirRetention: workDirRetention, + TotalLLMConcurrency: totalLLMConcurrency, + ProposalLLMConcurrency: proposalLLMConcurrency, ValidationModel: validationModel, ValidationLLMConcurrency: validationLLMConcurrency, Report: report, @@ -83,9 +103,6 @@ func NewSubprocessRunner(cfg SubprocessRunnerConfig) (*SubprocessRunner, error) if cfg.Timeout <= 0 { return nil, fmt.Errorf("audita timeout must be > 0") } - if len(cfg.Modules) == 0 { - return nil, fmt.Errorf("audita modules must include at least one module") - } for i, module := range cfg.Modules { if strings.TrimSpace(module) == "" { return nil, fmt.Errorf("audita module at index %d is empty", i) @@ -104,12 +121,25 @@ func NewSubprocessRunner(cfg SubprocessRunnerConfig) (*SubprocessRunner, error) if strings.TrimSpace(cfg.Model) == "" { return nil, fmt.Errorf("audita model is required") } - if cfg.LLMConcurrency != nil && *cfg.LLMConcurrency <= 0 { - return nil, fmt.Errorf("audita llm concurrency must be > 0 when provided") + if cfg.TotalLLMConcurrency != nil && *cfg.TotalLLMConcurrency <= 0 { + return nil, fmt.Errorf("audita total llm concurrency must be > 0 when provided") + } + if cfg.ProposalLLMConcurrency != nil && *cfg.ProposalLLMConcurrency <= 0 { + return nil, fmt.Errorf("audita proposal llm concurrency must be > 0 when provided") } if cfg.ValidationLLMConcurrency != nil && *cfg.ValidationLLMConcurrency <= 0 { return nil, fmt.Errorf("audita validation llm concurrency must be > 0 when provided") } + switch strings.TrimSpace(cfg.OutputSchema) { + case "", "bare-segments", "audita-v1": + default: + return nil, fmt.Errorf("audita output schema must be one of: bare-segments, audita-v1") + } + switch strings.TrimSpace(cfg.WorkDirRetention) { + case "", "always", "auto", "never": + default: + return nil, fmt.Errorf("audita work dir retention must be one of: always, auto, never") + } modules := make([]string, len(cfg.Modules)) for i, m := range cfg.Modules { @@ -123,7 +153,12 @@ func NewSubprocessRunner(cfg SubprocessRunnerConfig) (*SubprocessRunner, error) modules: modules, baseURL: strings.TrimSpace(cfg.BaseURL), model: strings.TrimSpace(cfg.Model), - llmConcurrency: cfg.LLMConcurrency, + transcriptDescription: strings.TrimSpace(cfg.TranscriptDescription), + configPath: strings.TrimSpace(cfg.ConfigPath), + outputSchema: strings.TrimSpace(cfg.OutputSchema), + workDirRetention: strings.TrimSpace(cfg.WorkDirRetention), + totalLLMConcurrency: cfg.TotalLLMConcurrency, + proposalLLMConcurrency: cfg.ProposalLLMConcurrency, validationModel: strings.TrimSpace(cfg.ValidationModel), validationLLMConcurrency: cfg.ValidationLLMConcurrency, report: cfg.Report, @@ -152,7 +187,7 @@ func (r *SubprocessRunner) Run(ctx context.Context, req PolishRequest) (PolishRe } reqModules := req.Modules - if len(reqModules) == 0 { + if reqModules == nil { reqModules = append([]string(nil), r.modules...) } args := r.buildArgs(req, reqModules) @@ -168,14 +203,9 @@ func (r *SubprocessRunner) Run(ctx context.Context, req PolishRequest) (PolishRe env["AUDITA_LLM_API_KEY"] = credential credentialPresent = true } - primaryConcurrencyViaEnv := false - if r.llmConcurrency != nil { - env["AUDITA_LLM_CONCURRENCY"] = strconv.Itoa(*r.llmConcurrency) - primaryConcurrencyViaEnv = true - } if req.GeneratedConfigPath != "" { - if err := r.writeInvocationConfig(req, args, reqModules, credentialPresent, primaryConcurrencyViaEnv); err != nil { + if err := r.writeInvocationConfig(req, args, reqModules, credentialPresent); err != nil { return PolishResult{}, fmt.Errorf("write audita invocation config %q: %w", req.GeneratedConfigPath, err) } } @@ -196,7 +226,7 @@ func (r *SubprocessRunner) Run(ctx context.Context, req PolishRequest) (PolishRe req.StderrLogPath, ) wrappedMessage = addSubprocessStreamHint(wrappedMessage, err) - return r.failureResult(req, reqModules, runRes, credentialPresent, primaryConcurrencyViaEnv), fmt.Errorf( + return r.failureResult(req, reqModules, runRes, credentialPresent), fmt.Errorf( "%s: %w", wrappedMessage, err, @@ -204,11 +234,11 @@ func (r *SubprocessRunner) Run(ctx context.Context, req PolishRequest) (PolishRe } if err := validateProcessedOutput(req.OutputProcessedPath); err != nil { - return r.failureResult(req, reqModules, runRes, credentialPresent, primaryConcurrencyViaEnv), fmt.Errorf("validate audita processed output %q: %w", req.OutputProcessedPath, err) + return r.failureResult(req, reqModules, runRes, credentialPresent), fmt.Errorf("validate audita processed output %q: %w", req.OutputProcessedPath, err) } if r.report { if err := validateJSONFile(req.ReportPath); err != nil { - return r.failureResult(req, reqModules, runRes, credentialPresent, primaryConcurrencyViaEnv), fmt.Errorf("validate audita report output %q: %w", req.ReportPath, err) + return r.failureResult(req, reqModules, runRes, credentialPresent), fmt.Errorf("validate audita report output %q: %w", req.ReportPath, err) } } @@ -223,21 +253,25 @@ func (r *SubprocessRunner) Run(ctx context.Context, req PolishRequest) (PolishRe Duration: runRes.Duration, InvokedBinary: r.binary, Metadata: map[string]any{ - "adapter": "audita_subprocess", - "modules": reqModules, - "base_url": r.baseURL, - "model": r.model, - "validation_model": r.validationModel, - "validation_llm_concurrency": r.validationLLMConcurrency, - "credential_env_var": r.llmAPIKeyEnv, - "credential_present": credentialPresent, - "primary_llm_concurrency_via_env": primaryConcurrencyViaEnv, - "primary_llm_concurrency_env_name": "AUDITA_LLM_CONCURRENCY", + "adapter": "audita_subprocess", + "modules": reqModules, + "base_url": r.baseURL, + "model": r.model, + "transcript_description": r.transcriptDescription, + "config_path": r.configPath, + "output_schema": r.outputSchema, + "work_dir_retention": r.workDirRetention, + "validation_model": r.validationModel, + "total_llm_concurrency": r.totalLLMConcurrency, + "proposal_llm_concurrency": r.proposalLLMConcurrency, + "validation_llm_concurrency": r.validationLLMConcurrency, + "credential_env_var": r.llmAPIKeyEnv, + "credential_present": credentialPresent, }, }, nil } -func (r *SubprocessRunner) failureResult(req PolishRequest, modules []string, runRes subprocess.RunResult, credentialPresent bool, primaryConcurrencyViaEnv bool) PolishResult { +func (r *SubprocessRunner) failureResult(req PolishRequest, modules []string, runRes subprocess.RunResult, credentialPresent bool) PolishResult { return PolishResult{ ProcessedTranscriptPath: req.OutputProcessedPath, ReportPath: req.ReportPath, @@ -249,16 +283,20 @@ func (r *SubprocessRunner) failureResult(req PolishRequest, modules []string, ru Duration: runRes.Duration, InvokedBinary: r.binary, Metadata: map[string]any{ - "adapter": "audita_subprocess", - "modules": modules, - "base_url": r.baseURL, - "model": r.model, - "validation_model": r.validationModel, - "validation_llm_concurrency": r.validationLLMConcurrency, - "credential_env_var": r.llmAPIKeyEnv, - "credential_present": credentialPresent, - "primary_llm_concurrency_via_env": primaryConcurrencyViaEnv, - "primary_llm_concurrency_env_name": "AUDITA_LLM_CONCURRENCY", + "adapter": "audita_subprocess", + "modules": modules, + "base_url": r.baseURL, + "model": r.model, + "transcript_description": r.transcriptDescription, + "config_path": r.configPath, + "output_schema": r.outputSchema, + "work_dir_retention": r.workDirRetention, + "validation_model": r.validationModel, + "total_llm_concurrency": r.totalLLMConcurrency, + "proposal_llm_concurrency": r.proposalLLMConcurrency, + "validation_llm_concurrency": r.validationLLMConcurrency, + "credential_env_var": r.llmAPIKeyEnv, + "credential_present": credentialPresent, }, } } @@ -269,14 +307,34 @@ func (r *SubprocessRunner) buildArgs(req PolishRequest, modules []string) []stri req.MergedTranscriptPath, "--glossary", req.GlossaryPath, "--output", req.OutputProcessedPath, - "--modules", strings.Join(modules, ","), "--base-url", r.baseURL, "--model", r.model, "--work-dir", req.WorkDir, } + if len(modules) > 0 { + args = append(args, "--modules", strings.Join(modules, ",")) + } if r.report { args = append(args, "--report-json", req.ReportPath) } + if r.transcriptDescription != "" { + args = append(args, "--transcript-description", r.transcriptDescription) + } + if r.configPath != "" { + args = append(args, "--config", r.configPath) + } + if r.outputSchema != "" { + args = append(args, "--output-schema", r.outputSchema) + } + if r.workDirRetention != "" { + args = append(args, "--work-dir-retention", r.workDirRetention) + } + if r.totalLLMConcurrency != nil { + args = append(args, "--total-llm-concurrency", strconv.Itoa(*r.totalLLMConcurrency)) + } + if r.proposalLLMConcurrency != nil { + args = append(args, "--proposal-llm-concurrency", strconv.Itoa(*r.proposalLLMConcurrency)) + } if r.validationModel != "" { args = append(args, "--validation-model", r.validationModel) } @@ -286,29 +344,31 @@ func (r *SubprocessRunner) buildArgs(req PolishRequest, modules []string) []stri return args } -func (r *SubprocessRunner) writeInvocationConfig(req PolishRequest, args []string, modules []string, credentialPresent bool, primaryConcurrencyViaEnv bool) error { +func (r *SubprocessRunner) writeInvocationConfig(req PolishRequest, args []string, modules []string, credentialPresent bool) error { payload := map[string]any{ - "schema": "audita.generated.v1", - "binary": r.binary, - "args": args, - "timeout": r.timeout.String(), - "modules": modules, - "base_url": r.baseURL, - "model": r.model, - "validation_model": r.validationModel, - "validation_llm_concurrency": r.validationLLMConcurrency, - "report_enabled": r.report, - "merged_transcript_path": req.MergedTranscriptPath, - "glossary_path": req.GlossaryPath, - "output_path": req.OutputProcessedPath, - "report_path": req.ReportPath, - "work_dir": req.WorkDir, - "credential_env_var": r.llmAPIKeyEnv, - "credential_present": credentialPresent, - "primary_llm_concurrency_via_env": primaryConcurrencyViaEnv, - } - if r.llmConcurrency != nil { - payload["llm_concurrency"] = *r.llmConcurrency + "schema": "audita.generated.v1", + "binary": r.binary, + "args": args, + "timeout": r.timeout.String(), + "modules": modules, + "base_url": r.baseURL, + "model": r.model, + "transcript_description": r.transcriptDescription, + "config_path": r.configPath, + "output_schema": r.outputSchema, + "work_dir_retention": r.workDirRetention, + "validation_model": r.validationModel, + "total_llm_concurrency": r.totalLLMConcurrency, + "proposal_llm_concurrency": r.proposalLLMConcurrency, + "validation_llm_concurrency": r.validationLLMConcurrency, + "report_enabled": r.report, + "merged_transcript_path": req.MergedTranscriptPath, + "glossary_path": req.GlossaryPath, + "output_path": req.OutputProcessedPath, + "report_path": req.ReportPath, + "work_dir": req.WorkDir, + "credential_env_var": r.llmAPIKeyEnv, + "credential_present": credentialPresent, } return subprocess.WriteYAMLAtomic(req.GeneratedConfigPath, payload, 0o644) } diff --git a/internal/adapters/audita/subprocess_test.go b/internal/adapters/audita/subprocess_test.go index 9ce7cc9..b648fe0 100644 --- a/internal/adapters/audita/subprocess_test.go +++ b/internal/adapters/audita/subprocess_test.go @@ -25,7 +25,8 @@ func TestSubprocessRunnerSuccessArgsEnvAndValidation(t *testing.T) { t.Setenv("AUDITA_HELPER_RECORD_PATH", recordPath) wrapper := writeAuditaHelperWrapper(t) - llmConcurrency := 1 + totalLLMConcurrency := 3 + proposalLLMConcurrency := 2 validationLLMConcurrency := 2 runner, err := NewSubprocessRunner(SubprocessRunnerConfig{ Binary: wrapper, @@ -34,7 +35,12 @@ func TestSubprocessRunnerSuccessArgsEnvAndValidation(t *testing.T) { Modules: []string{"glossary", "homophones", "glossary"}, BaseURL: "https://openrouter.ai/api/v1", Model: "openrouter/google/gemma-4-31b-it", - LLMConcurrency: &llmConcurrency, + TranscriptDescription: "Campaign Session 42", + ConfigPath: "/etc/audita/config.yml", + OutputSchema: "audita-v1", + WorkDirRetention: "auto", + TotalLLMConcurrency: &totalLLMConcurrency, + ProposalLLMConcurrency: &proposalLLMConcurrency, ValidationModel: "openrouter/google/gemma-4-31b-it", ValidationLLMConcurrency: &validationLLMConcurrency, Report: true, @@ -93,11 +99,17 @@ func TestSubprocessRunnerSuccessArgsEnvAndValidation(t *testing.T) { "process", req.MergedTranscriptPath, "--glossary", req.GlossaryPath, "--output", req.OutputProcessedPath, - "--modules", "glossary,homophones,glossary", "--base-url", "https://openrouter.ai/api/v1", "--model", "openrouter/google/gemma-4-31b-it", "--work-dir", req.WorkDir, + "--modules", "glossary,homophones,glossary", "--report-json", req.ReportPath, + "--transcript-description", "Campaign Session 42", + "--config", "/etc/audita/config.yml", + "--output-schema", "audita-v1", + "--work-dir-retention", "auto", + "--total-llm-concurrency", "3", + "--proposal-llm-concurrency", "2", "--validation-model", "openrouter/google/gemma-4-31b-it", "--validation-llm-concurrency", "2", } @@ -107,8 +119,8 @@ func TestSubprocessRunnerSuccessArgsEnvAndValidation(t *testing.T) { if rec.Env["AUDITA_LLM_API_KEY"] != "super-secret" { t.Fatalf("AUDITA_LLM_API_KEY = %q, want propagated secret", rec.Env["AUDITA_LLM_API_KEY"]) } - if rec.Env["AUDITA_LLM_CONCURRENCY"] != "1" { - t.Fatalf("AUDITA_LLM_CONCURRENCY = %q, want 1", rec.Env["AUDITA_LLM_CONCURRENCY"]) + if rec.Env["AUDITA_LLM_CONCURRENCY"] != "" { + t.Fatalf("AUDITA_LLM_CONCURRENCY = %q, want empty/omitted", rec.Env["AUDITA_LLM_CONCURRENCY"]) } cfgData, err := os.ReadFile(req.GeneratedConfigPath) @@ -124,16 +136,14 @@ func TestSubprocessRunnerMissingConfiguredCredentialFails(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("helper wrapper script uses /bin/sh") } - llmConcurrency := 1 runner := mustAuditaRunner(t, SubprocessRunnerConfig{ - Binary: writeAuditaHelperWrapper(t), - Timeout: mustParseAuditaDuration(t, "2s"), - LLMAPIKeyEnv: "MISSING_AUDITA_KEY", - Modules: []string{"glossary"}, - BaseURL: "https://openrouter.ai/api/v1", - Model: "openrouter/google/gemma-4-31b-it", - LLMConcurrency: &llmConcurrency, - Report: false, + Binary: writeAuditaHelperWrapper(t), + Timeout: mustParseAuditaDuration(t, "2s"), + LLMAPIKeyEnv: "MISSING_AUDITA_KEY", + Modules: []string{"glossary"}, + BaseURL: "https://openrouter.ai/api/v1", + Model: "openrouter/google/gemma-4-31b-it", + Report: false, }) req := auditaReqForTest(t, false) @@ -155,16 +165,14 @@ func TestSubprocessRunnerUnconfiguredCredentialEnvOmitsCredential(t *testing.T) recordPath := filepath.Join(t.TempDir(), "record.json") t.Setenv("AUDITA_HELPER_RECORD_PATH", recordPath) - llmConcurrency := 1 runner := mustAuditaRunner(t, SubprocessRunnerConfig{ - Binary: writeAuditaHelperWrapper(t), - Timeout: mustParseAuditaDuration(t, "2s"), - LLMAPIKeyEnv: "", - Modules: []string{"glossary"}, - BaseURL: "https://openrouter.ai/api/v1", - Model: "openrouter/google/gemma-4-31b-it", - LLMConcurrency: &llmConcurrency, - Report: false, + Binary: writeAuditaHelperWrapper(t), + Timeout: mustParseAuditaDuration(t, "2s"), + LLMAPIKeyEnv: "", + Modules: []string{"glossary"}, + BaseURL: "https://openrouter.ai/api/v1", + Model: "openrouter/google/gemma-4-31b-it", + Report: false, }) req := auditaReqForTest(t, false) @@ -211,6 +219,35 @@ func TestSubprocessRunnerInheritsParentEnvironment(t *testing.T) { } } +func TestSubprocessRunnerOmitsModulesFlagWhenNotConfigured(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("helper wrapper script uses /bin/sh") + } + t.Setenv("GO_WANT_AUDITA_HELPER", "1") + t.Setenv("AUDITA_HELPER_MODE", "success") + recordPath := filepath.Join(t.TempDir(), "record.json") + t.Setenv("AUDITA_HELPER_RECORD_PATH", recordPath) + + runner := mustAuditaRunner(t, SubprocessRunnerConfig{ + Binary: writeAuditaHelperWrapper(t), + Timeout: mustParseAuditaDuration(t, "2s"), + LLMAPIKeyEnv: "", + BaseURL: "https://openrouter.ai/api/v1", + Model: "openrouter/google/gemma-4-31b-it", + Report: false, + }) + req := auditaReqForTest(t, false) + if _, err := runner.Run(context.Background(), req); err != nil { + t.Fatalf("Run() error = %v", err) + } + rec := readAuditaHelperRecord(t, recordPath) + for i := 0; i < len(rec.Args); i++ { + if rec.Args[i] == "--modules" { + t.Fatalf("args contained --modules unexpectedly: %#v", rec.Args) + } + } +} + func TestSubprocessRunnerSubprocessFailure(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("helper wrapper script uses /bin/sh") @@ -220,16 +257,14 @@ func TestSubprocessRunnerSubprocessFailure(t *testing.T) { t.Setenv("OPENAI_KEY_SOURCE", "super-secret") t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) - llmConcurrency := 1 runner := mustAuditaRunner(t, SubprocessRunnerConfig{ - Binary: writeAuditaHelperWrapper(t), - Timeout: mustParseAuditaDuration(t, "2s"), - LLMAPIKeyEnv: "OPENAI_KEY_SOURCE", - Modules: []string{"glossary"}, - BaseURL: "https://openrouter.ai/api/v1", - Model: "openrouter/google/gemma-4-31b-it", - LLMConcurrency: &llmConcurrency, - Report: true, + Binary: writeAuditaHelperWrapper(t), + Timeout: mustParseAuditaDuration(t, "2s"), + LLMAPIKeyEnv: "OPENAI_KEY_SOURCE", + Modules: []string{"glossary"}, + BaseURL: "https://openrouter.ai/api/v1", + Model: "openrouter/google/gemma-4-31b-it", + Report: true, }) req := auditaReqForTest(t, true) _, err := runner.Run(context.Background(), req) @@ -256,16 +291,14 @@ func TestSubprocessRunnerSubprocessFailureAddsStderrDescriptorHint(t *testing.T) t.Setenv("OPENAI_KEY_SOURCE", "super-secret") t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) - llmConcurrency := 1 runner := mustAuditaRunner(t, SubprocessRunnerConfig{ - Binary: writeAuditaHelperWrapper(t), - Timeout: mustParseAuditaDuration(t, "2s"), - LLMAPIKeyEnv: "OPENAI_KEY_SOURCE", - Modules: []string{"glossary"}, - BaseURL: "https://openrouter.ai/api/v1", - Model: "openrouter/google/gemma-4-31b-it", - LLMConcurrency: &llmConcurrency, - Report: true, + Binary: writeAuditaHelperWrapper(t), + Timeout: mustParseAuditaDuration(t, "2s"), + LLMAPIKeyEnv: "OPENAI_KEY_SOURCE", + Modules: []string{"glossary"}, + BaseURL: "https://openrouter.ai/api/v1", + Model: "openrouter/google/gemma-4-31b-it", + Report: true, }) req := auditaReqForTest(t, true) _, err := runner.Run(context.Background(), req) @@ -286,16 +319,14 @@ func TestSubprocessRunnerMissingOutputFails(t *testing.T) { t.Setenv("OPENAI_KEY_SOURCE", "super-secret") t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) - llmConcurrency := 1 runner := mustAuditaRunner(t, SubprocessRunnerConfig{ - Binary: writeAuditaHelperWrapper(t), - Timeout: mustParseAuditaDuration(t, "2s"), - LLMAPIKeyEnv: "OPENAI_KEY_SOURCE", - Modules: []string{"glossary"}, - BaseURL: "https://openrouter.ai/api/v1", - Model: "openrouter/google/gemma-4-31b-it", - LLMConcurrency: &llmConcurrency, - Report: false, + Binary: writeAuditaHelperWrapper(t), + Timeout: mustParseAuditaDuration(t, "2s"), + LLMAPIKeyEnv: "OPENAI_KEY_SOURCE", + Modules: []string{"glossary"}, + BaseURL: "https://openrouter.ai/api/v1", + Model: "openrouter/google/gemma-4-31b-it", + Report: false, }) req := auditaReqForTest(t, false) _, err := runner.Run(context.Background(), req) @@ -316,16 +347,14 @@ func TestSubprocessRunnerInvalidOutputJSONFails(t *testing.T) { t.Setenv("OPENAI_KEY_SOURCE", "super-secret") t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) - llmConcurrency := 1 runner := mustAuditaRunner(t, SubprocessRunnerConfig{ - Binary: writeAuditaHelperWrapper(t), - Timeout: mustParseAuditaDuration(t, "2s"), - LLMAPIKeyEnv: "OPENAI_KEY_SOURCE", - Modules: []string{"glossary"}, - BaseURL: "https://openrouter.ai/api/v1", - Model: "openrouter/google/gemma-4-31b-it", - LLMConcurrency: &llmConcurrency, - Report: false, + Binary: writeAuditaHelperWrapper(t), + Timeout: mustParseAuditaDuration(t, "2s"), + LLMAPIKeyEnv: "OPENAI_KEY_SOURCE", + Modules: []string{"glossary"}, + BaseURL: "https://openrouter.ai/api/v1", + Model: "openrouter/google/gemma-4-31b-it", + Report: false, }) req := auditaReqForTest(t, false) _, err := runner.Run(context.Background(), req) @@ -346,16 +375,14 @@ func TestSubprocessRunnerSegmentsMissingFails(t *testing.T) { t.Setenv("OPENAI_KEY_SOURCE", "super-secret") t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) - llmConcurrency := 1 runner := mustAuditaRunner(t, SubprocessRunnerConfig{ - Binary: writeAuditaHelperWrapper(t), - Timeout: mustParseAuditaDuration(t, "2s"), - LLMAPIKeyEnv: "OPENAI_KEY_SOURCE", - Modules: []string{"glossary"}, - BaseURL: "https://openrouter.ai/api/v1", - Model: "openrouter/google/gemma-4-31b-it", - LLMConcurrency: &llmConcurrency, - Report: false, + Binary: writeAuditaHelperWrapper(t), + Timeout: mustParseAuditaDuration(t, "2s"), + LLMAPIKeyEnv: "OPENAI_KEY_SOURCE", + Modules: []string{"glossary"}, + BaseURL: "https://openrouter.ai/api/v1", + Model: "openrouter/google/gemma-4-31b-it", + Report: false, }) req := auditaReqForTest(t, false) _, err := runner.Run(context.Background(), req) @@ -376,16 +403,14 @@ func TestSubprocessRunnerInvalidReportJSONFails(t *testing.T) { t.Setenv("OPENAI_KEY_SOURCE", "super-secret") t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) - llmConcurrency := 1 runner := mustAuditaRunner(t, SubprocessRunnerConfig{ - Binary: writeAuditaHelperWrapper(t), - Timeout: mustParseAuditaDuration(t, "2s"), - LLMAPIKeyEnv: "OPENAI_KEY_SOURCE", - Modules: []string{"glossary"}, - BaseURL: "https://openrouter.ai/api/v1", - Model: "openrouter/google/gemma-4-31b-it", - LLMConcurrency: &llmConcurrency, - Report: true, + Binary: writeAuditaHelperWrapper(t), + Timeout: mustParseAuditaDuration(t, "2s"), + LLMAPIKeyEnv: "OPENAI_KEY_SOURCE", + Modules: []string{"glossary"}, + BaseURL: "https://openrouter.ai/api/v1", + Model: "openrouter/google/gemma-4-31b-it", + Report: true, }) req := auditaReqForTest(t, true) _, err := runner.Run(context.Background(), req) @@ -398,11 +423,11 @@ func TestSubprocessRunnerInvalidReportJSONFails(t *testing.T) { } func TestSubprocessRunnerConstructorValidation(t *testing.T) { - _, err := NewSubprocessRunnerFromConfigValues("", "3h", "AUDITA_LLM_API_KEY", []string{"glossary"}, "https://openrouter.ai/api/v1", "openrouter/google/gemma-4-31b-it", nil, "", nil, true) + _, err := NewSubprocessRunnerFromConfigValues("", "3h", "AUDITA_LLM_API_KEY", []string{"glossary"}, "https://openrouter.ai/api/v1", "openrouter/google/gemma-4-31b-it", "", "", "", "", nil, nil, "", nil, true) if err == nil { t.Fatal("expected binary validation error") } - _, err = NewSubprocessRunnerFromConfigValues("audita", "bad", "AUDITA_LLM_API_KEY", []string{"glossary"}, "https://openrouter.ai/api/v1", "openrouter/google/gemma-4-31b-it", nil, "", nil, true) + _, err = NewSubprocessRunnerFromConfigValues("audita", "bad", "AUDITA_LLM_API_KEY", []string{"glossary"}, "https://openrouter.ai/api/v1", "openrouter/google/gemma-4-31b-it", "", "", "", "", nil, nil, "", nil, true) if err == nil { t.Fatal("expected timeout parse error") } diff --git a/internal/app/runner.go b/internal/app/runner.go index 76ffe97..742319c 100644 --- a/internal/app/runner.go +++ b/internal/app/runner.go @@ -230,7 +230,7 @@ func buildDefaultAuditaRunner(cfg *config.Config) (audita.Runner, error) { } a := cfg.Pipeline.Audita - if strings.TrimSpace(a.Binary) == "" || strings.TrimSpace(a.Timeout) == "" || len(a.Modules) == 0 || strings.TrimSpace(a.BaseURL) == "" || strings.TrimSpace(a.Model) == "" { + if strings.TrimSpace(a.Binary) == "" || strings.TrimSpace(a.Timeout) == "" || strings.TrimSpace(a.BaseURL) == "" || strings.TrimSpace(a.Model) == "" { // Compatibility fallback for tests or internal call paths that bypass config validation/defaults. return &audita.NoopRunner{}, nil } @@ -247,7 +247,12 @@ func buildDefaultAuditaRunner(cfg *config.Config) (audita.Runner, error) { append([]string(nil), a.Modules...), a.BaseURL, a.Model, - a.LLMConcurrency, + a.TranscriptDescription, + a.ConfigPath, + a.OutputSchema, + a.WorkDirRetention, + a.TotalLLMConcurrency, + a.ProposalLLMConcurrency, a.ValidationModel, a.ValidationLLMConcurrency, report, diff --git a/internal/config/config.go b/internal/config/config.go index b6de178..88e9429 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -85,9 +85,14 @@ type AuditaConfig struct { Modules []string `yaml:"modules"` BaseURL string `yaml:"base_url"` Model string `yaml:"model"` - LLMConcurrency *int `yaml:"llm_concurrency"` + TotalLLMConcurrency *int `yaml:"total_llm_concurrency"` + ProposalLLMConcurrency *int `yaml:"proposal_llm_concurrency"` ValidationModel string `yaml:"validation_model"` ValidationLLMConcurrency *int `yaml:"validation_llm_concurrency"` + TranscriptDescription string `yaml:"transcript_description"` + ConfigPath string `yaml:"config_path"` + OutputSchema string `yaml:"output_schema"` + WorkDirRetention string `yaml:"work_dir_retention"` Report *bool `yaml:"report"` } diff --git a/internal/config/load.go b/internal/config/load.go index 00ffb34..dbf966b 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -143,29 +143,12 @@ func applyAuditaDefaults(cfg *AuditaConfig) { if cfg.Timeout == "" { cfg.Timeout = "3h" } - if cfg.Modules == nil { - cfg.Modules = []string{ - "glossary", - "homophones", - "glossary", - "spoken_word", - "grammar", - "homophones", - "glossary", - } - } if cfg.BaseURL == "" { cfg.BaseURL = "https://openrouter.ai/api/v1" } if cfg.Model == "" { cfg.Model = "openrouter/google/gemma-4-31b-it" } - if cfg.LLMConcurrency == nil { - cfg.LLMConcurrency = intPtr(1) - } - if cfg.ValidationLLMConcurrency == nil { - cfg.ValidationLLMConcurrency = intPtr(1) - } if cfg.Report == nil { cfg.Report = boolPtr(true) } diff --git a/internal/config/load_validate_test.go b/internal/config/load_validate_test.go index f6a0c7f..c9e60b5 100644 --- a/internal/config/load_validate_test.go +++ b/internal/config/load_validate_test.go @@ -453,7 +453,7 @@ inputs: wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.timeout must be a valid duration", }, { - name: "empty audita modules fails", + name: "empty audita modules is valid override", pipelineYAML: `workspace: root: /tmp/narratio whisperx: @@ -471,7 +471,6 @@ inputs: autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml `, - wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.modules must include at least one module", }, { name: "empty audita module item fails", @@ -541,7 +540,7 @@ inputs: wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.base_url must be a valid URL", }, { - name: "invalid audita llm_concurrency fails", + name: "legacy audita llm_concurrency field fails strict decode", pipelineYAML: `workspace: root: /tmp/narratio whisperx: @@ -550,7 +549,7 @@ seriatim: binary: seriatim audita: binary: audita - llm_concurrency: 0 + llm_concurrency: 1 `, sessionYAML: `session_id: 2026-05-03 inputs: @@ -559,7 +558,49 @@ inputs: autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml `, - wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.llm_concurrency must be > 0", + wantLoadErr: "strict decode failed", + }, + { + name: "invalid audita total_llm_concurrency fails", + pipelineYAML: `workspace: + root: /tmp/narratio +whisperx: + transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe +seriatim: + binary: seriatim +audita: + binary: audita + total_llm_concurrency: 0 +`, + sessionYAML: `session_id: 2026-05-03 +inputs: + audio_dir: ./audio + speakers_file: ./speakers.yml + autocorrect_file: ./autocorrect.yml + glossary_file: ./glossary.yml +`, + wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.total_llm_concurrency must be > 0", + }, + { + name: "invalid audita proposal_llm_concurrency fails", + pipelineYAML: `workspace: + root: /tmp/narratio +whisperx: + transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe +seriatim: + binary: seriatim +audita: + binary: audita + proposal_llm_concurrency: 0 +`, + sessionYAML: `session_id: 2026-05-03 +inputs: + audio_dir: ./audio + speakers_file: ./speakers.yml + autocorrect_file: ./autocorrect.yml + glossary_file: ./glossary.yml +`, + wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.proposal_llm_concurrency must be > 0", }, { name: "invalid audita validation_llm_concurrency fails", @@ -582,6 +623,48 @@ inputs: `, wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.validation_llm_concurrency must be > 0", }, + { + name: "invalid audita output_schema fails", + pipelineYAML: `workspace: + root: /tmp/narratio +whisperx: + transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe +seriatim: + binary: seriatim +audita: + binary: audita + output_schema: bad +`, + sessionYAML: `session_id: 2026-05-03 +inputs: + audio_dir: ./audio + speakers_file: ./speakers.yml + autocorrect_file: ./autocorrect.yml + glossary_file: ./glossary.yml +`, + wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.output_schema must be one of: bare-segments, audita-v1", + }, + { + name: "invalid audita work_dir_retention fails", + pipelineYAML: `workspace: + root: /tmp/narratio +whisperx: + transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe +seriatim: + binary: seriatim +audita: + binary: audita + work_dir_retention: sometimes +`, + sessionYAML: `session_id: 2026-05-03 +inputs: + audio_dir: ./audio + speakers_file: ./speakers.yml + autocorrect_file: ./autocorrect.yml + glossary_file: ./glossary.yml +`, + wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.work_dir_retention must be one of: always, auto, never", + }, } for _, tt := range tests { @@ -645,8 +728,8 @@ inputs: if cfg.Pipeline.Audita.LLMAPIKeyEnv != "" { t.Fatalf("audita.llm_api_key_env = %q, want empty by default", cfg.Pipeline.Audita.LLMAPIKeyEnv) } - if got := strings.Join(cfg.Pipeline.Audita.Modules, ","); got != "glossary,homophones,glossary,spoken_word,grammar,homophones,glossary" { - t.Fatalf("audita.modules = %q, want default sequence", got) + if cfg.Pipeline.Audita.Modules != nil { + t.Fatalf("audita.modules = %#v, want nil default (optional override)", cfg.Pipeline.Audita.Modules) } if cfg.Pipeline.Audita.BaseURL != "https://openrouter.ai/api/v1" { t.Fatalf("audita.base_url = %q, want %q", cfg.Pipeline.Audita.BaseURL, "https://openrouter.ai/api/v1") @@ -654,14 +737,17 @@ inputs: if cfg.Pipeline.Audita.Model != "openrouter/google/gemma-4-31b-it" { t.Fatalf("audita.model = %q, want %q", cfg.Pipeline.Audita.Model, "openrouter/google/gemma-4-31b-it") } - if cfg.Pipeline.Audita.LLMConcurrency == nil || *cfg.Pipeline.Audita.LLMConcurrency != 1 { - t.Fatalf("audita.llm_concurrency = %v, want 1", cfg.Pipeline.Audita.LLMConcurrency) - } if cfg.Pipeline.Audita.ValidationModel != "" { t.Fatalf("audita.validation_model = %q, want empty default", cfg.Pipeline.Audita.ValidationModel) } - if cfg.Pipeline.Audita.ValidationLLMConcurrency == nil || *cfg.Pipeline.Audita.ValidationLLMConcurrency != 1 { - t.Fatalf("audita.validation_llm_concurrency = %v, want 1", cfg.Pipeline.Audita.ValidationLLMConcurrency) + if cfg.Pipeline.Audita.TotalLLMConcurrency != nil { + t.Fatalf("audita.total_llm_concurrency = %v, want nil default", cfg.Pipeline.Audita.TotalLLMConcurrency) + } + if cfg.Pipeline.Audita.ProposalLLMConcurrency != nil { + t.Fatalf("audita.proposal_llm_concurrency = %v, want nil default", cfg.Pipeline.Audita.ProposalLLMConcurrency) + } + if cfg.Pipeline.Audita.ValidationLLMConcurrency != nil { + t.Fatalf("audita.validation_llm_concurrency = %v, want nil default", cfg.Pipeline.Audita.ValidationLLMConcurrency) } if cfg.Pipeline.Audita.Report == nil || *cfg.Pipeline.Audita.Report != true { t.Fatalf("audita.report = %v, want true", cfg.Pipeline.Audita.Report) @@ -725,7 +811,8 @@ func TestValidateMissingAudioSource(t *testing.T) { Modules: []string{"glossary", "homophones"}, BaseURL: "https://openrouter.ai/api/v1", Model: "openrouter/google/gemma-4-31b-it", - LLMConcurrency: intPtr(1), + TotalLLMConcurrency: intPtr(1), + ProposalLLMConcurrency: intPtr(1), ValidationModel: "", ValidationLLMConcurrency: intPtr(1), Report: boolPtr(true), diff --git a/internal/config/validate.go b/internal/config/validate.go index 0bcbdcd..b28f6c5 100644 --- a/internal/config/validate.go +++ b/internal/config/validate.go @@ -200,15 +200,12 @@ func validateAudita(cfg AuditaConfig) error { if err := validateDuration("pipeline.audita.timeout", cfg.Timeout); err != nil { return err } - if len(cfg.Modules) == 0 { - return fmt.Errorf("pipeline.audita.modules must include at least one module") - } - for i, mod := range cfg.Modules { - m := strings.TrimSpace(mod) - if m == "" { + for i, m := range cfg.Modules { + module := strings.TrimSpace(m) + if module == "" { return fmt.Errorf("pipeline.audita.modules[%d] must be non-empty", i) } - switch m { + switch module { case "glossary", "homophones", "spoken_word", "grammar": default: return fmt.Errorf("pipeline.audita.modules[%d] must be one of: glossary, homophones, spoken_word, grammar", i) @@ -226,18 +223,31 @@ func validateAudita(cfg AuditaConfig) error { if strings.TrimSpace(cfg.Model) == "" { return fmt.Errorf("pipeline.audita.model is required") } - if cfg.LLMConcurrency == nil { - return fmt.Errorf("pipeline.audita.llm_concurrency must be set (defaults should populate this)") + if cfg.TotalLLMConcurrency != nil && *cfg.TotalLLMConcurrency <= 0 { + return fmt.Errorf("pipeline.audita.total_llm_concurrency must be > 0") } - if *cfg.LLMConcurrency <= 0 { - return fmt.Errorf("pipeline.audita.llm_concurrency must be > 0") + if cfg.ProposalLLMConcurrency != nil && *cfg.ProposalLLMConcurrency <= 0 { + return fmt.Errorf("pipeline.audita.proposal_llm_concurrency must be > 0") } - if cfg.ValidationLLMConcurrency == nil { - return fmt.Errorf("pipeline.audita.validation_llm_concurrency must be set (defaults should populate this)") - } - if *cfg.ValidationLLMConcurrency <= 0 { + if cfg.ValidationLLMConcurrency != nil && *cfg.ValidationLLMConcurrency <= 0 { return fmt.Errorf("pipeline.audita.validation_llm_concurrency must be > 0") } + if strings.TrimSpace(cfg.TranscriptDescription) == "" && cfg.TranscriptDescription != "" { + return fmt.Errorf("pipeline.audita.transcript_description must be non-empty when provided") + } + if strings.TrimSpace(cfg.ConfigPath) == "" && cfg.ConfigPath != "" { + return fmt.Errorf("pipeline.audita.config_path must be non-empty when provided") + } + switch strings.TrimSpace(cfg.OutputSchema) { + case "", "bare-segments", "audita-v1": + default: + return fmt.Errorf("pipeline.audita.output_schema must be one of: bare-segments, audita-v1") + } + switch strings.TrimSpace(cfg.WorkDirRetention) { + case "", "always", "auto", "never": + default: + return fmt.Errorf("pipeline.audita.work_dir_retention must be one of: always, auto, never") + } return nil } diff --git a/internal/stage/polish.go b/internal/stage/polish.go index 60de37e..42e52a8 100644 --- a/internal/stage/polish.go +++ b/internal/stage/polish.go @@ -90,6 +90,12 @@ func (polishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St Modules: append([]string(nil), env.Config.Pipeline.Audita.Modules...), BaseURL: env.Config.Pipeline.Audita.BaseURL, Model: env.Config.Pipeline.Audita.Model, + TranscriptDescription: env.Config.Pipeline.Audita.TranscriptDescription, + ConfigPath: env.Config.Pipeline.Audita.ConfigPath, + OutputSchema: env.Config.Pipeline.Audita.OutputSchema, + WorkDirRetention: env.Config.Pipeline.Audita.WorkDirRetention, + TotalLLMConcurrency: env.Config.Pipeline.Audita.TotalLLMConcurrency, + ProposalLLMConcurrency: env.Config.Pipeline.Audita.ProposalLLMConcurrency, ValidationModel: env.Config.Pipeline.Audita.ValidationModel, ValidationLLMConcurrency: env.Config.Pipeline.Audita.ValidationLLMConcurrency, StdoutLogPath: stdoutPath, @@ -141,44 +147,52 @@ func (polishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St if env.Config.Pipeline.Audita.ValidationLLMConcurrency != nil { validationConcurrency = *env.Config.Pipeline.Audita.ValidationLLMConcurrency } - var llmConcurrency any - if env.Config.Pipeline.Audita.LLMConcurrency != nil { - llmConcurrency = *env.Config.Pipeline.Audita.LLMConcurrency + var totalLLMConcurrency any + if env.Config.Pipeline.Audita.TotalLLMConcurrency != nil { + totalLLMConcurrency = *env.Config.Pipeline.Audita.TotalLLMConcurrency + } + var proposalLLMConcurrency any + if env.Config.Pipeline.Audita.ProposalLLMConcurrency != nil { + proposalLLMConcurrency = *env.Config.Pipeline.Audita.ProposalLLMConcurrency } meta := map[string]any{ - "stage": "polish", - "merged_transcript_path": mergedPath, - "merged_transcript_source": source, - "glossary_path": glossaryPath, - "output_path": finalProcessedPath, - "report_path": finalReportPath, - "audita_work_dir": workDir, - "report_enabled": reportEnabled, - "modules": append([]string(nil), req.Modules...), - "base_url": req.BaseURL, - "model": req.Model, - "validation_model": req.ValidationModel, - "llm_concurrency": llmConcurrency, - "validation_llm_concurrency": validationConcurrency, - "llm_api_key_env": env.Config.Pipeline.Audita.LLMAPIKeyEnv, - "timeout": env.Config.Pipeline.Audita.Timeout, - "binary": env.Config.Pipeline.Audita.Binary, - "generated_config_path": generatedConfigPath, - "stdout_log_path": stdoutPath, - "stderr_log_path": stderrPath, - "adapter_duration_ms": res.Duration.Milliseconds(), - "adapter_exit_code": res.ExitCode, - "adapter_invoked_binary": res.InvokedBinary, - "adapter_processed_output_path": res.ProcessedTranscriptPath, - "adapter_report_path": res.ReportPath, - "adapter_generated_config_path": res.GeneratedConfigPath, - "adapter_work_dir": res.WorkDir, - "adapter_stdout_log_path": res.StdoutLogPath, - "adapter_stderr_log_path": res.StderrLogPath, - "credential_env_var": env.Config.Pipeline.Audita.LLMAPIKeyEnv, - "credential_present": false, - "primary_llm_concurrency_via_env": false, + "stage": "polish", + "merged_transcript_path": mergedPath, + "merged_transcript_source": source, + "glossary_path": glossaryPath, + "output_path": finalProcessedPath, + "report_path": finalReportPath, + "audita_work_dir": workDir, + "report_enabled": reportEnabled, + "modules": append([]string(nil), req.Modules...), + "base_url": req.BaseURL, + "model": req.Model, + "transcript_description": req.TranscriptDescription, + "config_path": req.ConfigPath, + "output_schema": req.OutputSchema, + "work_dir_retention": req.WorkDirRetention, + "validation_model": req.ValidationModel, + "total_llm_concurrency": totalLLMConcurrency, + "proposal_llm_concurrency": proposalLLMConcurrency, + "validation_llm_concurrency": validationConcurrency, + "llm_api_key_env": env.Config.Pipeline.Audita.LLMAPIKeyEnv, + "timeout": env.Config.Pipeline.Audita.Timeout, + "binary": env.Config.Pipeline.Audita.Binary, + "generated_config_path": generatedConfigPath, + "stdout_log_path": stdoutPath, + "stderr_log_path": stderrPath, + "adapter_duration_ms": res.Duration.Milliseconds(), + "adapter_exit_code": res.ExitCode, + "adapter_invoked_binary": res.InvokedBinary, + "adapter_processed_output_path": res.ProcessedTranscriptPath, + "adapter_report_path": res.ReportPath, + "adapter_generated_config_path": res.GeneratedConfigPath, + "adapter_work_dir": res.WorkDir, + "adapter_stdout_log_path": res.StdoutLogPath, + "adapter_stderr_log_path": res.StderrLogPath, + "credential_env_var": env.Config.Pipeline.Audita.LLMAPIKeyEnv, + "credential_present": false, } if res.Metadata != nil { meta["adapter_metadata"] = res.Metadata @@ -188,9 +202,6 @@ func (polishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St if value, ok := res.Metadata["credential_env_var"]; ok { meta["credential_env_var"] = value } - if value, ok := res.Metadata["primary_llm_concurrency_via_env"]; ok { - meta["primary_llm_concurrency_via_env"] = value - } } return &StageResult{ diff --git a/internal/stage/polish_test.go b/internal/stage/polish_test.go index efc2910..8f52d18 100644 --- a/internal/stage/polish_test.go +++ b/internal/stage/polish_test.go @@ -60,6 +60,24 @@ func TestPolishStagePolishesMergedTranscriptAndRecordsMetadata(t *testing.T) { if req.ValidationModel != "openrouter/google/gemma-4-31b-it" { t.Fatalf("validation model = %q", req.ValidationModel) } + if req.TranscriptDescription != "Campaign Session 42" { + t.Fatalf("transcript description = %q", req.TranscriptDescription) + } + if req.ConfigPath != "/etc/audita/config.yml" { + t.Fatalf("config path = %q", req.ConfigPath) + } + if req.OutputSchema != "audita-v1" { + t.Fatalf("output schema = %q", req.OutputSchema) + } + if req.WorkDirRetention != "auto" { + t.Fatalf("work dir retention = %q", req.WorkDirRetention) + } + if req.TotalLLMConcurrency == nil || *req.TotalLLMConcurrency != 3 { + t.Fatalf("total llm concurrency = %#v, want 3", req.TotalLLMConcurrency) + } + if req.ProposalLLMConcurrency == nil || *req.ProposalLLMConcurrency != 2 { + t.Fatalf("proposal llm concurrency = %#v, want 2", req.ProposalLLMConcurrency) + } if req.ValidationLLMConcurrency == nil || *req.ValidationLLMConcurrency != 2 { t.Fatalf("validation llm concurrency = %#v, want 2", req.ValidationLLMConcurrency) } @@ -89,6 +107,15 @@ func TestPolishStagePolishesMergedTranscriptAndRecordsMetadata(t *testing.T) { if result.Metadata["audita_work_dir"] != filepath.Join(paths.ArtifactsDir, "audita-work") { t.Fatalf("metadata audita_work_dir = %#v", result.Metadata["audita_work_dir"]) } + if result.Metadata["total_llm_concurrency"] != 3 { + t.Fatalf("metadata total_llm_concurrency = %#v, want 3", result.Metadata["total_llm_concurrency"]) + } + if result.Metadata["proposal_llm_concurrency"] != 2 { + t.Fatalf("metadata proposal_llm_concurrency = %#v, want 2", result.Metadata["proposal_llm_concurrency"]) + } + if result.Metadata["output_schema"] != "audita-v1" { + t.Fatalf("metadata output_schema = %#v, want audita-v1", result.Metadata["output_schema"]) + } } func TestPolishStageFallsBackToMergedTranscriptPath(t *testing.T) { @@ -221,7 +248,8 @@ func setupPolishEnv(t *testing.T) (*Env, *manifest.Manifest) { writeFile(t, pipelinePath, "workspace:\n root: "+workspace+"\n") report := true - llmConcurrency := 1 + totalLLMConcurrency := 3 + proposalLLMConcurrency := 2 validationLLMConcurrency := 2 cfg := &config.Config{ @@ -236,8 +264,13 @@ func setupPolishEnv(t *testing.T) (*Env, *manifest.Manifest) { Modules: []string{"glossary", "homophones", "grammar"}, BaseURL: "https://openrouter.ai/api/v1", Model: "openrouter/google/gemma-4-31b-it", + TranscriptDescription: "Campaign Session 42", + ConfigPath: "/etc/audita/config.yml", + OutputSchema: "audita-v1", + WorkDirRetention: "auto", ValidationModel: "openrouter/google/gemma-4-31b-it", - LLMConcurrency: &llmConcurrency, + TotalLLMConcurrency: &totalLLMConcurrency, + ProposalLLMConcurrency: &proposalLLMConcurrency, ValidationLLMConcurrency: &validationLLMConcurrency, Report: &report, },