From 0b01c3a83d45444a6503e9b04e9f94bec895a18c Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 23 May 2026 18:49:44 -0500 Subject: [PATCH] Implemented the config source setter cleanup identified during the code audit --- docs/policy/documentation.md | 356 ++++++++++++++++++++++++++ internal/core/config/apply_helpers.go | 171 +++++++++++++ internal/core/config/config_test.go | 247 ++++++++++++++++++ internal/core/config/env.go | 133 +++++----- internal/core/config/file_config.go | 114 ++++----- internal/core/config/flags.go | 143 ++++------- 6 files changed, 933 insertions(+), 231 deletions(-) create mode 100644 docs/policy/documentation.md create mode 100644 internal/core/config/apply_helpers.go diff --git a/docs/policy/documentation.md b/docs/policy/documentation.md new file mode 100644 index 0000000..d9ed7fa --- /dev/null +++ b/docs/policy/documentation.md @@ -0,0 +1,356 @@ +# Go Project Documentation Policy + +## Purpose + +Project documentation must help four audiences: + +1. users who need to run the application; +2. administrators/operators who need to configure and operate it; +3. developers who need to understand and change it safely; +4. LLM coding agents that need clear scope, boundaries, and invariants. + +Docs should be accurate, concise, task-oriented, and organized by audience. Prefer links to canonical docs over repetition. + +## Core Rules + +### 1. Keep docs concise + +Each document should cover a defined scope and only the essentials for that scope. + +Avoid: +- long background explanations; +- repeated reference material; +- implementation detail in user-facing docs; +- aspirational language outside roadmap docs; +- verbose examples where one minimal example is clearer. + +### 2. Document only implemented behavior outside roadmap files + +Unimplemented, planned, aspirational, experimental, or future work may be described only under: + +- `docs/roadmap/` + +No other documentation file, including `README.md`, should describe code, features, modules, stages, commands, config fields, or behaviors that do not currently exist. + +If a feature is partial, non-roadmap docs may describe only the implemented portion and its current boundary. + +### 3. Use canonical homes + +Each type of information should have one canonical location. + +Canonical homes: + +- project purpose and quickstart: `README.md` +- development principles: `docs/policy/architecture.md` +- configuration reference: `docs/config.md` +- CLI reference: `docs/cli.md` +- operations and recovery: `docs/operations.md` +- troubleshooting: `docs/troubleshooting.md` +- implemented internals: `docs/internal/` +- future work: `docs/roadmap/` +- contributor workflow: `docs/policy/development.md` +- copyable examples: `examples/` + +Other files should summarize briefly and link to the canonical source. + +### 4. Keep examples real + +Examples should be valid, maintained, and free of secrets. + +Where practical: +- example configs should load successfully; +- example commands should match real CLI syntax; +- important examples should be covered by tests. + +## Documentation Profiles + +All projects require: + +- `README.md` +- `docs/policy/architecture.md` + +Additional docs depend on the project. + +### Small library + +Recommended: +- `docs/policy/development.md`, if contributor conventions are non-obvious + +### Simple CLI + +Required: +- `docs/cli.md` + +Recommended: +- `docs/policy/development.md` + +### Config-driven CLI + +Required: +- `docs/cli.md` +- `docs/config.md` + +Recommended: +- `examples/` +- `docs/policy/development.md` + +### Stateful or operator-facing application + +Required: +- `docs/cli.md`, if CLI-based +- `docs/config.md`, if config-driven +- `docs/operations.md` + +Recommended: +- `docs/troubleshooting.md` +- `examples/` +- `docs/policy/development.md` + +### Modular, staged, service-oriented, or orchestration application + +Required: +- `docs/cli.md`, if CLI-based +- `docs/config.md`, if config-driven +- `docs/operations.md` +- `docs/internal/` +- `docs/policy/development.md` + +Recommended: +- `docs/troubleshooting.md` +- validated examples under `examples/` + +## Required Documents + +### README.md + +**Audience:** users, administrators, operators + +The README is the outward-facing project orientation page. + +It should include, in order: + +1. concise description; +2. elevator pitch; +3. shortest useful command or usage example; +4. links to targeted docs. + +The README should be short. It is not a manual. + +The “shortest useful command” means the simplest command that performs the project’s core use case. (It does not mean `app --help`.) + +### docs/policy/architecture.md + +**Audience:** developers, LLM coding agents + +`docs/policy/architecture.md` is required for every project. + +It is an inward-facing development policy document. It should describe how the project is intended to be built and changed. + +It should include: + +- project shape; +- core design principles; +- package and boundary philosophy; +- state/persistence philosophy, if applicable; +- external integration philosophy, if applicable; +- error-handling and logging principles; +- testing expectations; +- documentation expectations; +- architectural invariants; +- explicit non-goals, if useful. + +For small projects, this file may be brief. It may simply state that the project is intentionally narrow, monolithic, and dependency-light. + +### docs/policy/development.md + +**Audience:** developers, LLM coding agents + +Required for projects maintained by humans and LLM coding agents. + +It should include: + +- repository layout; +- build/test commands; +- coding conventions; +- dependency policy; +- how to add config fields; +- how to add CLI flags; +- how to add stages/modules/adapters, if applicable; +- how to update examples; +- documentation update expectations. + +### docs/config.md + +**Audience:** administrators, operators, advanced users + +Required for applications with configuration files. + +It should include, in order: + +1. config file locations and discovery precedence; +2. minimal working config; +3. production-oriented config; +4. full configuration reference; +5. secrets handling, if applicable; +6. links to maintained examples. + +The full configuration reference should be canonical. + +### docs/cli.md + +**Audience:** users, administrators, operators + +Required for CLI applications. + +It should include, in order: + +1. shortest useful command; +2. command overview; +3. complete flag reference; +4. common workflows; +5. diagnostic or recovery commands, if applicable. + +Explain when commands are useful, not just their syntax. + +### docs/operations.md + +**Audience:** administrators, operators + +Required for applications that maintain state, support resume behavior, run multiple stages, write durable artifacts, use remote storage, or require recovery procedures. + +It should cover: + +- normal workflow; +- filesystem layout; +- remote storage layout, if applicable; +- logs and manifests; +- resume/retry behavior; +- cleanup behavior; +- archive/backup behavior; +- safe recovery procedures; +- operational caveats. + +### docs/troubleshooting.md + +**Audience:** administrators, operators + +Recommended once recurring failure modes exist. + +Each entry should include: + +- symptom; +- likely cause; +- diagnostic command or inspection step; +- safe fix; +- relevant links. + +### docs/internal/ + +**Audience:** developers, LLM coding agents + +Required for modular, staged, service-oriented, or orchestration projects. + +This directory describes implemented internal components. It is not the roadmap. + +Use one file per major component where useful. + +Each component doc should include: + +1. purpose; +2. inputs and outputs; +3. boundaries; +4. config fields used; +5. external adapters used; +6. state or manifest behavior, if applicable; +7. skip/resume behavior, if applicable; +8. failure behavior; +9. tests to inspect before changing; +10. architectural invariants. + +### docs/roadmap/ + +**Audience:** maintainers, developers, LLM coding agents + +This is the only place for planned, future, aspirational, experimental, or unimplemented work. + +Roadmap docs should clearly distinguish: + +- proposed work; +- accepted plans; +- deferred ideas; +- rejected ideas; +- implementation prompts or task breakdowns, if useful. + +Roadmap docs should not be confused with current behavior. + +### docs/integrations/ + +**Audience:** developers, LLM coding agents + +Required for projects that depend on external CLIs, APIs, services, protocols, or file formats where the integration contract is important to maintain. + +This directory contains concise, versioned reference notes for external integration contracts. It should document only the parts of the external system that this project actually uses. + +Use one file per integration where useful. + +## Examples Directory + +Projects with non-trivial configuration or workflows should include `examples/`. + +Useful examples include: + +- minimal working config; +- production-oriented config; +- full annotated config; +- local development config; +- remote/object-storage config; +- minimal session/input file. + +Examples should be valid, maintained, tested when practical, and linked from relevant docs. + +## Security and Privacy + +Docs and examples must not include: + +- real API keys; +- tokens; +- passwords; +- private keys; +- private environment dumps; +- sensitive user data; +- raw private transcripts; +- private infrastructure details unless intentionally public. + +Document secret-handling mechanisms, not actual secret values. + +## Maintenance Rules + +When docs change, verify the affected behavior. + +Where practical: + +- load example config files in tests; +- test CLI examples or command parser behavior; +- validate documented flags against real flags; +- remove stale references; +- update links after renames; +- keep roadmap content out of non-roadmap docs. + +If documentation and code disagree, fix the documentation and/or open a roadmap item; do not leave aspirational behavior in current-behavior docs. + +Documentation is complete only when it matches the current code. + +## Documentation Change Checklist + +Before merging documentation changes, verify: + +- README is concise and orientation-focused. +- `docs/policy/architecture.md` describes development principles. +- Future work appears only under `docs/roadmap/`. +- User-facing docs avoid unnecessary internals. +- Developer-facing docs preserve boundaries and invariants. +- Config examples match the schema. +- CLI examples match real commands and flags. +- Defaults appear in the canonical config reference. +- No secrets or private data are included. +- Links are accurate. diff --git a/internal/core/config/apply_helpers.go b/internal/core/config/apply_helpers.go new file mode 100644 index 0000000..fd222ae --- /dev/null +++ b/internal/core/config/apply_helpers.go @@ -0,0 +1,171 @@ +package config + +import "strings" + +type llmTargetPatch struct { + apiKey *string + model *string + baseURL *string + timeoutSeconds *int + maxRetries *int +} + +type concurrencyPatch struct { + totalLLM *int + legacyTotalLLM *int + proposalLLM *int + validationLLM *int + inheritProposal bool + allowLegacyAlias bool +} + +type chunkingPatch struct { + targetSections *int + maxSectionTokens *int + minSectionTokens *int +} + +type thresholdsPatch struct { + glossary *float64 + grammar *float64 + homophones *float64 + spokenWord *float64 +} + +type normalizationPatch struct { + maxSegmentGap *float64 + ellipsisGap *float64 + maxSegmentDuration *float64 + maxSegmentTokens *int +} + +type contextPatch struct { + transcriptDescription *string +} + +type diagnosticsPatch struct { + workDir *string + workDirRetention *string +} + +func (c *Config) applyPrimaryLLMTargetPatch(patch llmTargetPatch) { + if patch.apiKey != nil { + c.PrimaryLLM.APIKey = *patch.apiKey + } + if patch.model != nil { + c.PrimaryLLM.Model = *patch.model + } + if patch.baseURL != nil { + c.PrimaryLLM.BaseURL = *patch.baseURL + } + if patch.timeoutSeconds != nil { + c.PrimaryLLM.TimeoutSeconds = *patch.timeoutSeconds + } + if patch.maxRetries != nil { + c.PrimaryLLM.MaxRetries = *patch.maxRetries + } +} + +func (c *Config) applyValidationLLMTargetPatch(patch llmTargetPatch) { + if patch.apiKey != nil { + c.ValidationLLM.APIKey = *patch.apiKey + } + if patch.model != nil { + c.ValidationLLM.Model = *patch.model + } + if patch.baseURL != nil { + c.ValidationLLM.BaseURL = *patch.baseURL + } + if patch.timeoutSeconds != nil { + value := *patch.timeoutSeconds + c.ValidationLLM.TimeoutSeconds = &value + } + if patch.maxRetries != nil { + value := *patch.maxRetries + c.ValidationLLM.MaxRetries = &value + } +} + +func (c *Config) applyConcurrencyPatch(patch concurrencyPatch) { + totalSet := false + if patch.totalLLM != nil { + c.TotalLLMConcurrency = *patch.totalLLM + totalSet = true + } + if patch.allowLegacyAlias && patch.legacyTotalLLM != nil && !totalSet { + c.TotalLLMConcurrency = *patch.legacyTotalLLM + totalSet = true + } + + proposalSet := false + if patch.proposalLLM != nil { + c.ProposalLLMConcurrency = *patch.proposalLLM + proposalSet = true + } + if patch.inheritProposal && totalSet && !proposalSet { + c.ProposalLLMConcurrency = c.TotalLLMConcurrency + } + + if patch.validationLLM != nil { + value := *patch.validationLLM + c.ValidationLLMConcurrency = &value + } +} + +func (c *Config) applyChunkingPatch(patch chunkingPatch) { + if patch.targetSections != nil { + value := *patch.targetSections + c.TargetSections = &value + } + if patch.maxSectionTokens != nil { + c.MaxSectionTokens = *patch.maxSectionTokens + } + if patch.minSectionTokens != nil { + c.MinSectionTokens = *patch.minSectionTokens + } +} + +func (c *Config) applyThresholdsPatch(patch thresholdsPatch) { + if patch.glossary != nil { + c.Thresholds.Glossary = *patch.glossary + } + if patch.grammar != nil { + c.Thresholds.Grammar = *patch.grammar + } + if patch.homophones != nil { + c.Thresholds.Homophones = *patch.homophones + } + if patch.spokenWord != nil { + c.Thresholds.SpokenWord = *patch.spokenWord + } +} + +func (c *Config) applyNormalizationPatch(patch normalizationPatch) { + if patch.maxSegmentGap != nil { + c.Normalization.MaxSegmentGap = *patch.maxSegmentGap + } + if patch.ellipsisGap != nil { + c.Normalization.EllipsisGap = *patch.ellipsisGap + } + if patch.maxSegmentDuration != nil { + c.Normalization.MaxSegmentDuration = *patch.maxSegmentDuration + } + if patch.maxSegmentTokens != nil { + c.Normalization.MaxSegmentTokens = *patch.maxSegmentTokens + } +} + +func (c *Config) applyContextPatch(patch contextPatch) { + if patch.transcriptDescription != nil { + c.TranscriptDescription = strings.TrimSpace(*patch.transcriptDescription) + } +} + +func (c *Config) applyDiagnosticsPatch(patch diagnosticsPatch) { + if patch.workDir != nil { + c.WorkDir = *patch.workDir + } + if patch.workDirRetention != nil { + c.WorkDirRetention = WorkDirRetention(*patch.workDirRetention) + } +} diff --git a/internal/core/config/config_test.go b/internal/core/config/config_test.go index 94a8e48..d2289e8 100644 --- a/internal/core/config/config_test.go +++ b/internal/core/config/config_test.go @@ -239,6 +239,186 @@ func TestApplyCLIOverridesTrimsTranscriptDescription(t *testing.T) { } } +func TestConfigSourcesApplySharedEffectiveFieldsConsistently(t *testing.T) { + fileCfg := mustParseFileConfigYAML(t, ` +version: 1 +output: + schema: " audita-v1 " +llm: + proposal: + base_url: https://proposal.example.test/v1 + model: provider/proposal + timeout: 101 + max_retries: 5 + validation: + base_url: https://validation.example.test/v1 + model: provider/validation + timeout: 202 + max_retries: 6 +chunking: + target_sections: 7 + max_section_tokens: 9000 + min_section_tokens: 1000 +thresholds: + glossary: 0.91 + grammar: 0.92 + homophones: 0.93 + spoken_word: 0.94 +normalization: + max_segment_gap: 1.2 + ellipsis_gap: 2.3 + max_segment_duration: 45.6 + max_segment_tokens: 321 +context: + description: " shared context " +diagnostics: + work_dir: /tmp/audita-shared + retention: always +`) + + tests := []struct { + name string + apply func(*Config) error + }{ + { + name: "file", + apply: func(cfg *Config) error { + return cfg.applyFileConfigWithLookup(fileCfg, mapLookup(map[string]string{})) + }, + }, + { + name: "env", + apply: func(cfg *Config) error { + return cfg.applyEnvOverrides(mapLookup(map[string]string{ + "AUDITA_MODEL": "provider/proposal", + "AUDITA_BASE_URL": "https://proposal.example.test/v1", + "AUDITA_LLM_TIMEOUT_SECONDS": "101", + "AUDITA_MAX_RETRIES": "5", + "AUDITA_VALIDATION_MODEL": "provider/validation", + "AUDITA_VALIDATION_BASE_URL": "https://validation.example.test/v1", + "AUDITA_VALIDATION_LLM_TIMEOUT_SECONDS": "202", + "AUDITA_VALIDATION_MAX_RETRIES": "6", + "AUDITA_TARGET_SECTIONS": "7", + "AUDITA_MAX_SECTION_TOKENS": "9000", + "AUDITA_MIN_SECTION_TOKENS": "1000", + "AUDITA_GLOSSARY_CONFIDENCE_THRESHOLD": "0.91", + "AUDITA_GRAMMAR_CONFIDENCE_THRESHOLD": "0.92", + "AUDITA_HOMOPHONES_CONFIDENCE_THRESHOLD": "0.93", + "AUDITA_SPOKEN_WORD_CONFIDENCE_THRESHOLD": "0.94", + "AUDITA_NORMALIZE_MAX_SEGMENT_GAP": "1.2", + "AUDITA_NORMALIZE_ELLIPSIS_GAP": "2.3", + "AUDITA_NORMALIZE_MAX_SEGMENT_DURATION": "45.6", + "AUDITA_NORMALIZE_MAX_SEGMENT_TOKENS": "321", + "AUDITA_WORK_DIR": "/tmp/audita-shared", + "AUDITA_WORK_DIR_RETENTION": "always", + })) + }, + }, + { + name: "cli", + apply: func(cfg *Config) error { + outputSchema := " audita-v1 " + proposalModel := "provider/proposal" + proposalBaseURL := "https://proposal.example.test/v1" + proposalTimeout := 101 + proposalMaxRetries := 5 + validationModel := "provider/validation" + validationBaseURL := "https://validation.example.test/v1" + validationTimeout := 202 + validationMaxRetries := 6 + targetSections := 7 + maxSectionTokens := 9000 + minSectionTokens := 1000 + glossaryThreshold := 0.91 + grammarThreshold := 0.92 + homophonesThreshold := 0.93 + spokenWordThreshold := 0.94 + normalizeMaxSegmentGap := 1.2 + normalizeEllipsisGap := 2.3 + normalizeMaxSegmentDuration := 45.6 + normalizeMaxSegmentTokens := 321 + description := " shared context " + workDir := "/tmp/audita-shared" + workDirRetention := "always" + return cfg.ApplyCLIOverrides(CLIOverrides{ + OutputSchema: &outputSchema, + PrimaryModel: &proposalModel, + PrimaryBaseURL: &proposalBaseURL, + PrimaryLLMTimeoutSeconds: &proposalTimeout, + MaxRetries: &proposalMaxRetries, + ValidationModel: &validationModel, + ValidationBaseURL: &validationBaseURL, + ValidationLLMTimeoutSeconds: &validationTimeout, + ValidationMaxRetries: &validationMaxRetries, + TargetSections: &targetSections, + MaxSectionTokens: &maxSectionTokens, + MinSectionTokens: &minSectionTokens, + GlossaryConfidenceThreshold: &glossaryThreshold, + GrammarConfidenceThreshold: &grammarThreshold, + HomophonesConfidenceThreshold: &homophonesThreshold, + SpokenWordConfidenceThreshold: &spokenWordThreshold, + NormalizeMaxSegmentGap: &normalizeMaxSegmentGap, + NormalizeEllipsisGap: &normalizeEllipsisGap, + NormalizeMaxSegmentDuration: &normalizeMaxSegmentDuration, + NormalizeMaxSegmentTokens: &normalizeMaxSegmentTokens, + TranscriptDescription: &description, + WorkDir: &workDir, + WorkDirRetention: &workDirRetention, + }) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cfg := Default() + if err := tc.apply(&cfg); err != nil { + t.Fatalf("apply config source: %v", err) + } + assertSharedEffectiveFields(t, cfg, sharedEffectiveFieldOptions{ + wantOutputSchemaOverride: tc.name != "env", + wantTranscriptDescriptionPatch: tc.name != "env", + }) + }) + } +} + +func TestCLIAPIKeyOverrideIsDirectValue(t *testing.T) { + cfg := Default() + apiKey := "NOT_AN_ENV_VAR_NAME" + validationAPIKey := "also direct" + + if err := cfg.ApplyCLIOverrides(CLIOverrides{PrimaryLLMAPIKey: &apiKey, ValidationLLMAPIKey: &validationAPIKey}); err != nil { + t.Fatalf("ApplyCLIOverrides failed: %v", err) + } + if cfg.PrimaryLLM.APIKey != apiKey { + t.Fatalf("expected direct primary api key, got %q", cfg.PrimaryLLM.APIKey) + } + if cfg.ValidationLLM.APIKey != validationAPIKey { + t.Fatalf("expected direct validation api key, got %q", cfg.ValidationLLM.APIKey) + } +} + +func TestApplyFileConfigTotalConcurrencyDoesNotChangeProposalWhenProposalUnset(t *testing.T) { + fileCfg := mustParseFileConfigYAML(t, ` +version: 1 +concurrency: + total_llm: 4 +`) + cfg := Default() + + if err := cfg.applyFileConfigWithLookup(fileCfg, mapLookup(map[string]string{})); err != nil { + t.Fatalf("applyFileConfigWithLookup failed: %v", err) + } + + if cfg.TotalLLMConcurrency != 4 { + t.Fatalf("expected file total concurrency 4, got %d", cfg.TotalLLMConcurrency) + } + if cfg.ProposalLLMConcurrency != DefaultLLMConcurrency { + t.Fatalf("expected file config to preserve proposal concurrency when unset, got %d", cfg.ProposalLLMConcurrency) + } +} + func TestValidationRejectsOverlyLongTranscriptDescription(t *testing.T) { cfg := Default() cfg.TranscriptDescription = strings.Repeat("a", DefaultTranscriptDescriptionMaxChars+1) @@ -456,3 +636,70 @@ func mapLookup(values map[string]string) func(string) (string, bool) { return value, ok } } + +func mustParseFileConfigYAML(t *testing.T, raw string) FileConfig { + t.Helper() + fileCfg, err := ParseFileConfigYAML([]byte(raw)) + if err != nil { + t.Fatalf("ParseFileConfigYAML failed: %v", err) + } + return fileCfg +} + +type sharedEffectiveFieldOptions struct { + wantOutputSchemaOverride bool + wantTranscriptDescriptionPatch bool +} + +func assertSharedEffectiveFields(t *testing.T, cfg Config, opts sharedEffectiveFieldOptions) { + t.Helper() + wantOutputSchema := DefaultOutputSchema + if opts.wantOutputSchemaOverride { + wantOutputSchema = "audita-v1" + } + if cfg.OutputSchema != wantOutputSchema { + t.Fatalf("unexpected output schema: %q", cfg.OutputSchema) + } + if cfg.PrimaryLLM.Model != "provider/proposal" || + cfg.PrimaryLLM.BaseURL != "https://proposal.example.test/v1" || + cfg.PrimaryLLM.TimeoutSeconds != 101 || + cfg.PrimaryLLM.MaxRetries != 5 { + t.Fatalf("unexpected primary llm config: %+v", cfg.PrimaryLLM) + } + if cfg.ValidationLLM.Model != "provider/validation" || + cfg.ValidationLLM.BaseURL != "https://validation.example.test/v1" || + cfg.ValidationLLM.TimeoutSeconds == nil || + *cfg.ValidationLLM.TimeoutSeconds != 202 || + cfg.ValidationLLM.MaxRetries == nil || + *cfg.ValidationLLM.MaxRetries != 6 { + t.Fatalf("unexpected validation llm config: %+v", cfg.ValidationLLM) + } + if cfg.TargetSections == nil || *cfg.TargetSections != 7 || + cfg.MaxSectionTokens != 9000 || + cfg.MinSectionTokens != 1000 { + t.Fatalf("unexpected chunking config: target=%v max=%d min=%d", cfg.TargetSections, cfg.MaxSectionTokens, cfg.MinSectionTokens) + } + if cfg.Thresholds.Glossary != 0.91 || + cfg.Thresholds.Grammar != 0.92 || + cfg.Thresholds.Homophones != 0.93 || + cfg.Thresholds.SpokenWord != 0.94 { + t.Fatalf("unexpected thresholds: %+v", cfg.Thresholds) + } + if cfg.Normalization.MaxSegmentGap != 1.2 || + cfg.Normalization.EllipsisGap != 2.3 || + cfg.Normalization.MaxSegmentDuration != 45.6 || + cfg.Normalization.MaxSegmentTokens != 321 { + t.Fatalf("unexpected normalization: %+v", cfg.Normalization) + } + wantDescription := "" + if opts.wantTranscriptDescriptionPatch { + wantDescription = "shared context" + } + if cfg.TranscriptDescription != wantDescription { + t.Fatalf("unexpected transcript description: %q", cfg.TranscriptDescription) + } + if cfg.WorkDir != "/tmp/audita-shared" || + cfg.WorkDirRetention != WorkDirRetentionAlways { + t.Fatalf("unexpected diagnostics config: work_dir=%q retention=%q", cfg.WorkDir, cfg.WorkDirRetention) + } +} diff --git a/internal/core/config/env.go b/internal/core/config/env.go index d924a01..64f07ff 100644 --- a/internal/core/config/env.go +++ b/internal/core/config/env.go @@ -7,8 +7,8 @@ import ( ) const ( - DefaultConfigPath = "/etc/audita/config.yml" - DefaultConfigPathUsrLocal = "/usr/local/etc/audita/config.yml" + DefaultConfigPath = "/etc/audita/config.yml" + DefaultConfigPathUsrLocal = "/usr/local/etc/audita/config.yml" ) var DefaultConfigSearchPaths = []string{ @@ -50,100 +50,93 @@ func (c *Config) applyEnvOverrides(lookup func(string) (string, bool)) error { cfg.Modules = modules } + primaryLLM := llmTargetPatch{} if raw, ok := lookup("AUDITA_LLM_API_KEY"); ok { - cfg.PrimaryLLM.APIKey = raw + primaryLLM.apiKey = &raw } else if raw, ok := lookup("OPENROUTER_API_KEY"); ok { - cfg.PrimaryLLM.APIKey = raw + primaryLLM.apiKey = &raw } - - if raw, ok := lookup("AUDITA_VALIDATION_LLM_API_KEY"); ok { - cfg.ValidationLLM.APIKey = raw - } - if raw, ok := lookup("AUDITA_MODEL"); ok { - cfg.PrimaryLLM.Model = raw + primaryLLM.model = &raw } - if raw, ok := lookup("AUDITA_VALIDATION_MODEL"); ok { - cfg.ValidationLLM.Model = raw - } - if raw, ok := lookup("AUDITA_BASE_URL"); ok { - cfg.PrimaryLLM.BaseURL = raw + primaryLLM.baseURL = &raw } - if raw, ok := lookup("AUDITA_VALIDATION_BASE_URL"); ok { - cfg.ValidationLLM.BaseURL = raw - } - if raw, ok := lookup("AUDITA_LLM_TIMEOUT_SECONDS"); ok { value, err := parseInt(raw) if err != nil { return fmt.Errorf("AUDITA_LLM_TIMEOUT_SECONDS: %w", err) } - cfg.PrimaryLLM.TimeoutSeconds = value + primaryLLM.timeoutSeconds = &value } - - if raw, ok := lookup("AUDITA_VALIDATION_LLM_TIMEOUT_SECONDS"); ok { - value, err := parseInt(raw) - if err != nil { - return fmt.Errorf("AUDITA_VALIDATION_LLM_TIMEOUT_SECONDS: %w", err) - } - cfg.ValidationLLM.TimeoutSeconds = &value - } - if raw, ok := lookup("AUDITA_MAX_RETRIES"); ok { value, err := parseInt(raw) if err != nil { return fmt.Errorf("AUDITA_MAX_RETRIES: %w", err) } - cfg.PrimaryLLM.MaxRetries = value + primaryLLM.maxRetries = &value + } + cfg.applyPrimaryLLMTargetPatch(primaryLLM) + + validationLLM := llmTargetPatch{} + if raw, ok := lookup("AUDITA_VALIDATION_LLM_API_KEY"); ok { + validationLLM.apiKey = &raw + } + if raw, ok := lookup("AUDITA_VALIDATION_MODEL"); ok { + validationLLM.model = &raw + } + if raw, ok := lookup("AUDITA_VALIDATION_BASE_URL"); ok { + validationLLM.baseURL = &raw + } + if raw, ok := lookup("AUDITA_VALIDATION_LLM_TIMEOUT_SECONDS"); ok { + value, err := parseInt(raw) + if err != nil { + return fmt.Errorf("AUDITA_VALIDATION_LLM_TIMEOUT_SECONDS: %w", err) + } + validationLLM.timeoutSeconds = &value + } + if raw, ok := lookup("AUDITA_VALIDATION_MAX_RETRIES"); ok { + value, err := parseInt(raw) + if err != nil { + return fmt.Errorf("AUDITA_VALIDATION_MAX_RETRIES: %w", err) + } + validationLLM.maxRetries = &value + } + cfg.applyValidationLLMTargetPatch(validationLLM) + + concurrency := concurrencyPatch{ + inheritProposal: true, + allowLegacyAlias: true, } - totalConcurrencySet := false if raw, ok := lookup("AUDITA_TOTAL_LLM_CONCURRENCY"); ok { value, err := parseInt(raw) if err != nil { return fmt.Errorf("AUDITA_TOTAL_LLM_CONCURRENCY: %w", err) } - cfg.TotalLLMConcurrency = value - totalConcurrencySet = true + concurrency.totalLLM = &value } if raw, ok := lookup("AUDITA_LLM_CONCURRENCY"); ok { value, err := parseInt(raw) if err != nil { return fmt.Errorf("AUDITA_LLM_CONCURRENCY: %w", err) } - if !totalConcurrencySet { - cfg.TotalLLMConcurrency = value - totalConcurrencySet = true - } + concurrency.legacyTotalLLM = &value } - - proposalConcurrencySet := false if raw, ok := lookup("AUDITA_PROPOSAL_LLM_CONCURRENCY"); ok { value, err := parseInt(raw) if err != nil { return fmt.Errorf("AUDITA_PROPOSAL_LLM_CONCURRENCY: %w", err) } - cfg.ProposalLLMConcurrency = value - proposalConcurrencySet = true - } - if totalConcurrencySet && !proposalConcurrencySet { - cfg.ProposalLLMConcurrency = cfg.TotalLLMConcurrency - } - - if raw, ok := lookup("AUDITA_VALIDATION_MAX_RETRIES"); ok { - value, err := parseInt(raw) - if err != nil { - return fmt.Errorf("AUDITA_VALIDATION_MAX_RETRIES: %w", err) - } - cfg.ValidationLLM.MaxRetries = &value + concurrency.proposalLLM = &value } if raw, ok := lookup("AUDITA_VALIDATION_LLM_CONCURRENCY"); ok { value, err := parseInt(raw) if err != nil { return fmt.Errorf("AUDITA_VALIDATION_LLM_CONCURRENCY: %w", err) } - cfg.ValidationLLMConcurrency = &value + concurrency.validationLLM = &value } + cfg.applyConcurrencyPatch(concurrency) if raw, ok := lookup("AUDITA_VALIDATION_MAX_PROMPT_TOKENS"); ok { value, err := parseInt(raw) @@ -153,12 +146,13 @@ func (c *Config) applyEnvOverrides(lookup func(string) (string, bool)) error { cfg.ValidationMaxPromptTokens = value } + chunking := chunkingPatch{} if raw, ok := lookup("AUDITA_MAX_SECTION_TOKENS"); ok { value, err := parseInt(raw) if err != nil { return fmt.Errorf("AUDITA_MAX_SECTION_TOKENS: %w", err) } - cfg.MaxSectionTokens = value + chunking.maxSectionTokens = &value } if raw, ok := lookup("AUDITA_MIN_SECTION_TOKENS"); ok { @@ -166,7 +160,7 @@ func (c *Config) applyEnvOverrides(lookup func(string) (string, bool)) error { if err != nil { return fmt.Errorf("AUDITA_MIN_SECTION_TOKENS: %w", err) } - cfg.MinSectionTokens = value + chunking.minSectionTokens = &value } if raw, ok := lookup("AUDITA_TARGET_SECTIONS"); ok { @@ -174,73 +168,80 @@ func (c *Config) applyEnvOverrides(lookup func(string) (string, bool)) error { if err != nil { return fmt.Errorf("AUDITA_TARGET_SECTIONS: %w", err) } - cfg.TargetSections = &value + chunking.targetSections = &value } + cfg.applyChunkingPatch(chunking) + thresholds := thresholdsPatch{} if raw, ok := lookup("AUDITA_GLOSSARY_CONFIDENCE_THRESHOLD"); ok { value, err := parseFloat(raw) if err != nil { return fmt.Errorf("AUDITA_GLOSSARY_CONFIDENCE_THRESHOLD: %w", err) } - cfg.Thresholds.Glossary = value + thresholds.glossary = &value } if raw, ok := lookup("AUDITA_GRAMMAR_CONFIDENCE_THRESHOLD"); ok { value, err := parseFloat(raw) if err != nil { return fmt.Errorf("AUDITA_GRAMMAR_CONFIDENCE_THRESHOLD: %w", err) } - cfg.Thresholds.Grammar = value + thresholds.grammar = &value } if raw, ok := lookup("AUDITA_HOMOPHONES_CONFIDENCE_THRESHOLD"); ok { value, err := parseFloat(raw) if err != nil { return fmt.Errorf("AUDITA_HOMOPHONES_CONFIDENCE_THRESHOLD: %w", err) } - cfg.Thresholds.Homophones = value + thresholds.homophones = &value } if raw, ok := lookup("AUDITA_SPOKEN_WORD_CONFIDENCE_THRESHOLD"); ok { value, err := parseFloat(raw) if err != nil { return fmt.Errorf("AUDITA_SPOKEN_WORD_CONFIDENCE_THRESHOLD: %w", err) } - cfg.Thresholds.SpokenWord = value + thresholds.spokenWord = &value } + cfg.applyThresholdsPatch(thresholds) + normalization := normalizationPatch{} if raw, ok := lookup("AUDITA_NORMALIZE_MAX_SEGMENT_GAP"); ok { value, err := parseFloat(raw) if err != nil { return fmt.Errorf("AUDITA_NORMALIZE_MAX_SEGMENT_GAP: %w", err) } - cfg.Normalization.MaxSegmentGap = value + normalization.maxSegmentGap = &value } if raw, ok := lookup("AUDITA_NORMALIZE_ELLIPSIS_GAP"); ok { value, err := parseFloat(raw) if err != nil { return fmt.Errorf("AUDITA_NORMALIZE_ELLIPSIS_GAP: %w", err) } - cfg.Normalization.EllipsisGap = value + normalization.ellipsisGap = &value } if raw, ok := lookup("AUDITA_NORMALIZE_MAX_SEGMENT_DURATION"); ok { value, err := parseFloat(raw) if err != nil { return fmt.Errorf("AUDITA_NORMALIZE_MAX_SEGMENT_DURATION: %w", err) } - cfg.Normalization.MaxSegmentDuration = value + normalization.maxSegmentDuration = &value } if raw, ok := lookup("AUDITA_NORMALIZE_MAX_SEGMENT_TOKENS"); ok { value, err := parseInt(raw) if err != nil { return fmt.Errorf("AUDITA_NORMALIZE_MAX_SEGMENT_TOKENS: %w", err) } - cfg.Normalization.MaxSegmentTokens = value + normalization.maxSegmentTokens = &value } + cfg.applyNormalizationPatch(normalization) + diagnostics := diagnosticsPatch{} if raw, ok := lookup("AUDITA_WORK_DIR"); ok { - cfg.WorkDir = raw + diagnostics.workDir = &raw } if raw, ok := lookup("AUDITA_WORK_DIR_RETENTION"); ok { - cfg.WorkDirRetention = WorkDirRetention(raw) + diagnostics.workDirRetention = &raw } + cfg.applyDiagnosticsPatch(diagnostics) cfg.syncLegacyConcurrencyAliases() diff --git a/internal/core/config/file_config.go b/internal/core/config/file_config.go index 7cf8e6c..5e6ffed 100644 --- a/internal/core/config/file_config.go +++ b/internal/core/config/file_config.go @@ -205,118 +205,98 @@ func (c *Config) applyFileConfigWithLookup(fileCfg FileConfig, lookup func(strin if fileCfg.LLM != nil { if fileCfg.LLM.Proposal != nil { - if fileCfg.LLM.Proposal.BaseURL != nil { - c.PrimaryLLM.BaseURL = *fileCfg.LLM.Proposal.BaseURL - } - if fileCfg.LLM.Proposal.Model != nil { - c.PrimaryLLM.Model = *fileCfg.LLM.Proposal.Model + patch := llmTargetPatch{ + model: fileCfg.LLM.Proposal.Model, + baseURL: fileCfg.LLM.Proposal.BaseURL, + maxRetries: fileCfg.LLM.Proposal.MaxRetries, } if fileCfg.LLM.Proposal.Timeout != nil { - c.PrimaryLLM.TimeoutSeconds = fileCfg.LLM.Proposal.Timeout.Seconds() - } - if fileCfg.LLM.Proposal.MaxRetries != nil { - c.PrimaryLLM.MaxRetries = *fileCfg.LLM.Proposal.MaxRetries + timeoutSeconds := fileCfg.LLM.Proposal.Timeout.Seconds() + patch.timeoutSeconds = &timeoutSeconds } if fileCfg.LLM.Proposal.APIKeyEnv != nil { apiKey, err := resolveAPIKeyEnv(*fileCfg.LLM.Proposal.APIKeyEnv, lookup) if err != nil { return fmt.Errorf("llm.proposal.api_key_env: %w", err) } - c.PrimaryLLM.APIKey = apiKey + patch.apiKey = &apiKey } + c.applyPrimaryLLMTargetPatch(patch) } if fileCfg.LLM.Validation != nil { - if fileCfg.LLM.Validation.BaseURL != nil { - c.ValidationLLM.BaseURL = *fileCfg.LLM.Validation.BaseURL - } - if fileCfg.LLM.Validation.Model != nil { - c.ValidationLLM.Model = *fileCfg.LLM.Validation.Model + patch := llmTargetPatch{ + model: fileCfg.LLM.Validation.Model, + baseURL: fileCfg.LLM.Validation.BaseURL, + maxRetries: fileCfg.LLM.Validation.MaxRetries, } if fileCfg.LLM.Validation.Timeout != nil { - v := fileCfg.LLM.Validation.Timeout.Seconds() - c.ValidationLLM.TimeoutSeconds = &v - } - if fileCfg.LLM.Validation.MaxRetries != nil { - v := *fileCfg.LLM.Validation.MaxRetries - c.ValidationLLM.MaxRetries = &v + timeoutSeconds := fileCfg.LLM.Validation.Timeout.Seconds() + patch.timeoutSeconds = &timeoutSeconds } if fileCfg.LLM.Validation.APIKeyEnv != nil { apiKey, err := resolveAPIKeyEnv(*fileCfg.LLM.Validation.APIKeyEnv, lookup) if err != nil { return fmt.Errorf("llm.validation.api_key_env: %w", err) } - c.ValidationLLM.APIKey = apiKey + patch.apiKey = &apiKey } + c.applyValidationLLMTargetPatch(patch) } } if fileCfg.Concurrency != nil { - if fileCfg.Concurrency.TotalLLM != nil { - c.TotalLLMConcurrency = *fileCfg.Concurrency.TotalLLM - } - if fileCfg.Concurrency.ProposalLLM != nil { - c.ProposalLLMConcurrency = *fileCfg.Concurrency.ProposalLLM - } - if fileCfg.Concurrency.ValidationLLM != nil { - v := *fileCfg.Concurrency.ValidationLLM - c.ValidationLLMConcurrency = &v - } + c.applyConcurrencyPatch(concurrencyPatch{ + totalLLM: fileCfg.Concurrency.TotalLLM, + proposalLLM: fileCfg.Concurrency.ProposalLLM, + validationLLM: fileCfg.Concurrency.ValidationLLM, + }) } if fileCfg.Chunking != nil { - if fileCfg.Chunking.TargetSections != nil { - v := *fileCfg.Chunking.TargetSections - c.TargetSections = &v - } - if fileCfg.Chunking.MaxSectionTokens != nil { - c.MaxSectionTokens = *fileCfg.Chunking.MaxSectionTokens - } - if fileCfg.Chunking.MinSectionTokens != nil { - c.MinSectionTokens = *fileCfg.Chunking.MinSectionTokens - } + c.applyChunkingPatch(chunkingPatch{ + targetSections: fileCfg.Chunking.TargetSections, + maxSectionTokens: fileCfg.Chunking.MaxSectionTokens, + minSectionTokens: fileCfg.Chunking.MinSectionTokens, + }) } if fileCfg.Normalization != nil { + patch := normalizationPatch{ + maxSegmentTokens: fileCfg.Normalization.MaxSegmentTokens, + } if fileCfg.Normalization.MaxSegmentGap != nil { - c.Normalization.MaxSegmentGap = fileCfg.Normalization.MaxSegmentGap.Seconds() + maxSegmentGap := fileCfg.Normalization.MaxSegmentGap.Seconds() + patch.maxSegmentGap = &maxSegmentGap } if fileCfg.Normalization.EllipsisGap != nil { - c.Normalization.EllipsisGap = fileCfg.Normalization.EllipsisGap.Seconds() + ellipsisGap := fileCfg.Normalization.EllipsisGap.Seconds() + patch.ellipsisGap = &ellipsisGap } if fileCfg.Normalization.MaxSegmentDuration != nil { - c.Normalization.MaxSegmentDuration = fileCfg.Normalization.MaxSegmentDuration.Seconds() - } - if fileCfg.Normalization.MaxSegmentTokens != nil { - c.Normalization.MaxSegmentTokens = *fileCfg.Normalization.MaxSegmentTokens + maxSegmentDuration := fileCfg.Normalization.MaxSegmentDuration.Seconds() + patch.maxSegmentDuration = &maxSegmentDuration } + c.applyNormalizationPatch(patch) } if fileCfg.Thresholds != nil { - if fileCfg.Thresholds.Glossary != nil { - c.Thresholds.Glossary = *fileCfg.Thresholds.Glossary - } - if fileCfg.Thresholds.Homophones != nil { - c.Thresholds.Homophones = *fileCfg.Thresholds.Homophones - } - if fileCfg.Thresholds.SpokenWord != nil { - c.Thresholds.SpokenWord = *fileCfg.Thresholds.SpokenWord - } - if fileCfg.Thresholds.Grammar != nil { - c.Thresholds.Grammar = *fileCfg.Thresholds.Grammar - } + c.applyThresholdsPatch(thresholdsPatch{ + glossary: fileCfg.Thresholds.Glossary, + grammar: fileCfg.Thresholds.Grammar, + homophones: fileCfg.Thresholds.Homophones, + spokenWord: fileCfg.Thresholds.SpokenWord, + }) } if fileCfg.Context != nil && fileCfg.Context.Description != nil { - c.TranscriptDescription = strings.TrimSpace(*fileCfg.Context.Description) + c.applyContextPatch(contextPatch{transcriptDescription: fileCfg.Context.Description}) } if fileCfg.Diagnostics != nil { - if fileCfg.Diagnostics.WorkDir != nil { - c.WorkDir = *fileCfg.Diagnostics.WorkDir - } - if fileCfg.Diagnostics.Retention != nil { - c.WorkDirRetention = WorkDirRetention(*fileCfg.Diagnostics.Retention) - } + c.applyDiagnosticsPatch(diagnosticsPatch{ + workDir: fileCfg.Diagnostics.WorkDir, + workDirRetention: fileCfg.Diagnostics.Retention, + }) } c.syncLegacyConcurrencyAliases() diff --git a/internal/core/config/flags.go b/internal/core/config/flags.go index 81259f3..52e99ee 100644 --- a/internal/core/config/flags.go +++ b/internal/core/config/flags.go @@ -51,107 +51,54 @@ func (c *Config) ApplyCLIOverrides(overrides CLIOverrides) error { c.OutputSchema = strings.TrimSpace(*overrides.OutputSchema) } - if overrides.PrimaryLLMAPIKey != nil { - c.PrimaryLLM.APIKey = *overrides.PrimaryLLMAPIKey - } - if overrides.ValidationLLMAPIKey != nil { - c.ValidationLLM.APIKey = *overrides.ValidationLLMAPIKey - } - if overrides.PrimaryModel != nil { - c.PrimaryLLM.Model = *overrides.PrimaryModel - } - if overrides.ValidationModel != nil { - c.ValidationLLM.Model = *overrides.ValidationModel - } - if overrides.PrimaryBaseURL != nil { - c.PrimaryLLM.BaseURL = *overrides.PrimaryBaseURL - } - if overrides.ValidationBaseURL != nil { - c.ValidationLLM.BaseURL = *overrides.ValidationBaseURL - } - if overrides.PrimaryLLMTimeoutSeconds != nil { - c.PrimaryLLM.TimeoutSeconds = *overrides.PrimaryLLMTimeoutSeconds - } - totalConcurrencySet := false - if overrides.TotalLLMConcurrency != nil { - c.TotalLLMConcurrency = *overrides.TotalLLMConcurrency - totalConcurrencySet = true - } - // Backward-compatible alias: --llm-concurrency maps to total concurrency - // only when --total-llm-concurrency is not set in the same CLI invocation. - if overrides.PrimaryLLMConcurrency != nil && !totalConcurrencySet { - c.TotalLLMConcurrency = *overrides.PrimaryLLMConcurrency - totalConcurrencySet = true - } - proposalConcurrencySet := false - if overrides.ProposalLLMConcurrency != nil { - c.ProposalLLMConcurrency = *overrides.ProposalLLMConcurrency - proposalConcurrencySet = true - } - if totalConcurrencySet && !proposalConcurrencySet { - c.ProposalLLMConcurrency = c.TotalLLMConcurrency - } - if overrides.ValidationLLMTimeoutSeconds != nil { - value := *overrides.ValidationLLMTimeoutSeconds - c.ValidationLLM.TimeoutSeconds = &value - } - if overrides.MaxRetries != nil { - c.PrimaryLLM.MaxRetries = *overrides.MaxRetries - } - if overrides.ValidationMaxRetries != nil { - value := *overrides.ValidationMaxRetries - c.ValidationLLM.MaxRetries = &value - } - if overrides.ValidationLLMConcurrency != nil { - value := *overrides.ValidationLLMConcurrency - c.ValidationLLMConcurrency = &value - } + c.applyPrimaryLLMTargetPatch(llmTargetPatch{ + apiKey: overrides.PrimaryLLMAPIKey, + model: overrides.PrimaryModel, + baseURL: overrides.PrimaryBaseURL, + timeoutSeconds: overrides.PrimaryLLMTimeoutSeconds, + maxRetries: overrides.MaxRetries, + }) + c.applyValidationLLMTargetPatch(llmTargetPatch{ + apiKey: overrides.ValidationLLMAPIKey, + model: overrides.ValidationModel, + baseURL: overrides.ValidationBaseURL, + timeoutSeconds: overrides.ValidationLLMTimeoutSeconds, + maxRetries: overrides.ValidationMaxRetries, + }) + c.applyConcurrencyPatch(concurrencyPatch{ + totalLLM: overrides.TotalLLMConcurrency, + legacyTotalLLM: overrides.PrimaryLLMConcurrency, + proposalLLM: overrides.ProposalLLMConcurrency, + validationLLM: overrides.ValidationLLMConcurrency, + inheritProposal: true, + allowLegacyAlias: true, + }) + if overrides.ValidationMaxPromptTokens != nil { c.ValidationMaxPromptTokens = *overrides.ValidationMaxPromptTokens } - if overrides.MaxSectionTokens != nil { - c.MaxSectionTokens = *overrides.MaxSectionTokens - } - if overrides.MinSectionTokens != nil { - c.MinSectionTokens = *overrides.MinSectionTokens - } - if overrides.TargetSections != nil { - value := *overrides.TargetSections - c.TargetSections = &value - } - if overrides.GlossaryConfidenceThreshold != nil { - c.Thresholds.Glossary = *overrides.GlossaryConfidenceThreshold - } - if overrides.GrammarConfidenceThreshold != nil { - c.Thresholds.Grammar = *overrides.GrammarConfidenceThreshold - } - if overrides.HomophonesConfidenceThreshold != nil { - c.Thresholds.Homophones = *overrides.HomophonesConfidenceThreshold - } - if overrides.SpokenWordConfidenceThreshold != nil { - c.Thresholds.SpokenWord = *overrides.SpokenWordConfidenceThreshold - } - if overrides.NormalizeMaxSegmentGap != nil { - c.Normalization.MaxSegmentGap = *overrides.NormalizeMaxSegmentGap - } - if overrides.NormalizeEllipsisGap != nil { - c.Normalization.EllipsisGap = *overrides.NormalizeEllipsisGap - } - if overrides.NormalizeMaxSegmentDuration != nil { - c.Normalization.MaxSegmentDuration = *overrides.NormalizeMaxSegmentDuration - } - if overrides.NormalizeMaxSegmentTokens != nil { - c.Normalization.MaxSegmentTokens = *overrides.NormalizeMaxSegmentTokens - } - if overrides.TranscriptDescription != nil { - c.TranscriptDescription = strings.TrimSpace(*overrides.TranscriptDescription) - } - if overrides.WorkDir != nil { - c.WorkDir = *overrides.WorkDir - } - if overrides.WorkDirRetention != nil { - c.WorkDirRetention = WorkDirRetention(*overrides.WorkDirRetention) - } + c.applyChunkingPatch(chunkingPatch{ + targetSections: overrides.TargetSections, + maxSectionTokens: overrides.MaxSectionTokens, + minSectionTokens: overrides.MinSectionTokens, + }) + c.applyThresholdsPatch(thresholdsPatch{ + glossary: overrides.GlossaryConfidenceThreshold, + grammar: overrides.GrammarConfidenceThreshold, + homophones: overrides.HomophonesConfidenceThreshold, + spokenWord: overrides.SpokenWordConfidenceThreshold, + }) + c.applyNormalizationPatch(normalizationPatch{ + maxSegmentGap: overrides.NormalizeMaxSegmentGap, + ellipsisGap: overrides.NormalizeEllipsisGap, + maxSegmentDuration: overrides.NormalizeMaxSegmentDuration, + maxSegmentTokens: overrides.NormalizeMaxSegmentTokens, + }) + c.applyContextPatch(contextPatch{transcriptDescription: overrides.TranscriptDescription}) + c.applyDiagnosticsPatch(diagnosticsPatch{ + workDir: overrides.WorkDir, + workDirRetention: overrides.WorkDirRetention, + }) c.syncLegacyConcurrencyAliases()