2026-05-10 23:19:59 +00:00
package config
import (
"fmt"
"strings"
2026-09-24 01:33:03 +00:00
"time"
2026-05-23 17:39:13 +00:00
2026-09-24 01:33:03 +00:00
ck "gitea.maximumdirect.net/eric/ConfigKit"
2026-10-11 01:40:38 +00:00
"gitea.maximumdirect.net/eric/audita/v3/internal/core/modulecatalog"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/outputschema"
2026-05-10 23:19:59 +00:00
)
const (
2026-09-24 17:27:11 +00:00
DefaultModulesCSV = "glossary,homophones,glossary,spoken_word,grammar"
DefaultTranscriptDescriptionMaxChars = 500
DefaultValidationMaxPromptTokens = 2048
2026-05-10 23:19:59 +00:00
)
2026-09-24 01:33:03 +00:00
// Config is the version-2 application configuration. Invocation paths are not settings.
2026-05-10 23:19:59 +00:00
type Config struct {
2026-09-24 01:33:03 +00:00
Version int ` yaml:"version" `
ProfilesDir string ` yaml:"profiles_dir" `
Backends map [ string ] Backend ` yaml:"backends" `
LLM LLM ` yaml:"llm" `
Pipeline Pipeline ` yaml:"pipeline" `
Output Output ` yaml:"output" `
Validation Validation ` yaml:"validation" `
Chunking Chunking ` yaml:"chunking" `
Normalization Normalization ` yaml:"normalization" `
Thresholds Thresholds ` yaml:"thresholds" `
Context Context ` yaml:"context" `
2026-09-24 17:27:11 +00:00
Reporting Reporting ` yaml:"reporting" `
2026-09-24 01:33:03 +00:00
Diagnostics Diagnostics ` yaml:"diagnostics" `
2026-05-10 23:19:59 +00:00
}
2026-09-24 01:33:03 +00:00
type Backend struct {
Endpoint string ` yaml:"endpoint" `
Concurrency int ` yaml:"concurrency" `
QueueCapacity int ` yaml:"queue_capacity" `
Credential * ck . Secret ` yaml:"credential" `
2026-05-10 23:19:59 +00:00
}
2026-09-24 01:33:03 +00:00
type LLM struct {
ProposalProfile string ` yaml:"proposal_profile" `
ValidationProfile string ` yaml:"validation_profile" `
TransientRetries int ` yaml:"transient_retries" `
RepairAttempts int ` yaml:"repair_attempts" `
OperationTimeout time . Duration ` yaml:"operation_timeout" `
2026-05-10 23:19:59 +00:00
}
2026-09-24 01:33:03 +00:00
type Pipeline struct {
Modules [ ] string ` yaml:"modules" `
2026-05-10 23:19:59 +00:00
}
2026-09-24 01:33:03 +00:00
type Output struct {
Schema string ` yaml:"schema" `
}
type Validation struct {
MaxPromptTokens int ` yaml:"max_prompt_tokens" `
}
type Chunking struct {
TargetSections * int ` yaml:"target_sections" `
MaxSectionTokens int ` yaml:"max_section_tokens" `
MinSectionTokens int ` yaml:"min_section_tokens" `
}
type Normalization struct {
MaxSegmentGap float64 ` yaml:"max_segment_gap" `
EllipsisGap float64 ` yaml:"ellipsis_gap" `
MaxSegmentDuration float64 ` yaml:"max_segment_duration" `
MaxSegmentTokens int ` yaml:"max_segment_tokens" `
}
type Thresholds struct {
Glossary float64 ` yaml:"glossary" `
Homophones float64 ` yaml:"homophones" `
SpokenWord float64 ` yaml:"spoken_word" `
Grammar float64 ` yaml:"grammar" `
}
type Context struct {
Description string ` yaml:"description" `
}
type Diagnostics struct {
2026-09-24 17:27:11 +00:00
Debug bool ` yaml:"debug" `
WorkDir string ` yaml:"work_dir" `
}
type Reporting struct {
Enabled bool ` yaml:"enabled" `
StdoutDir string ` yaml:"stdout_dir" `
2026-05-10 23:19:59 +00:00
}
2026-09-24 01:33:03 +00:00
func defaults ( ) Config {
2026-05-10 23:19:59 +00:00
return Config {
2026-09-24 01:33:03 +00:00
ProfilesDir : "profiles" ,
LLM : LLM { TransientRetries : 2 , RepairAttempts : 2 , OperationTimeout : 15 * time . Minute } ,
Pipeline : Pipeline { Modules : [ ] string { "glossary" , "homophones" , "glossary" , "spoken_word" , "grammar" } } ,
Output : Output { Schema : "bare-segments" } ,
2026-09-24 02:07:17 +00:00
Validation : Validation { MaxPromptTokens : DefaultValidationMaxPromptTokens } ,
2026-09-24 01:33:03 +00:00
Chunking : Chunking { MaxSectionTokens : 8192 , MinSectionTokens : 2048 } ,
Normalization : Normalization { MaxSegmentGap : 4 , EllipsisGap : 3.5 , MaxSegmentDuration : 60 , MaxSegmentTokens : 2048 } ,
Thresholds : Thresholds { Glossary : .8 , Homophones : .8 , SpokenWord : .8 , Grammar : .8 } ,
2026-09-24 17:27:11 +00:00
Reporting : Reporting { Enabled : true , StdoutDir : "." } ,
Diagnostics : Diagnostics { WorkDir : "/tmp/audita" } ,
2026-05-10 23:19:59 +00:00
}
}
2026-09-24 01:33:03 +00:00
// Load resolves the selected main file, optional overlay, and explicit setting overrides.
// Its result retains safe effective output and provenance for CLI composition.
func Load ( source ck . Source , overlay string , overrides ... ck . Override ) ( * ck . Result [ Config ] , error ) {
return ck . Load ( source , ck . Options [ Config ] {
Overlay : overlay , Overrides : overrides ,
Definition : ck . Definition [ Config ] {
Defaults : defaults ,
Version : & ck . VersionRule { Path : ck . Keys ( "version" ) , Check : func ( version ck . Version , v * ck . Validation ) error {
if ! version . Present || version . Null || version . Text != "2" {
v . Error ( ck . Keys ( "version" ) , "version 2 is required; see docs/migrations/v2.md" )
}
return nil
} } ,
Resolve : func ( c * Config , r * ck . Resolution ) error {
ck . Default ( r , ck . Keys ( "llm" , "validation_profile" ) , & c . LLM . ValidationProfile , c . LLM . ProposalProfile , "validation uses the final proposal profile when omitted" , ck . Keys ( "llm" , "proposal_profile" ) )
for id , backend := range c . Backends {
changed := false
if ! r . IsExplicit ( ck . Keys ( "backends" , id , "concurrency" ) ) {
backend . Concurrency = 1
changed = true
}
if ! r . IsExplicit ( ck . Keys ( "backends" , id , "queue_capacity" ) ) {
backend . QueueCapacity = 1024
changed = true
}
if changed {
c . Backends [ id ] = backend
r . Record ( ck . Keys ( "backends" , id ) , ck . ChangeDerivedDefault , "finite backend defaults" )
}
}
return nil
} ,
Validate : validate ,
} ,
} )
2026-05-10 23:19:59 +00:00
}
2026-09-24 01:33:03 +00:00
func validate ( c * Config , v * ck . Validation ) {
check := func ( ok bool , path ck . Path , message string ) {
if ! ok {
v . Error ( path , message )
}
2026-05-10 23:19:59 +00:00
}
2026-09-24 01:33:03 +00:00
check ( strings . TrimSpace ( c . ProfilesDir ) != "" , ck . Keys ( "profiles_dir" ) , "profile directory is required" )
check ( strings . TrimSpace ( c . LLM . ProposalProfile ) != "" , ck . Keys ( "llm" , "proposal_profile" ) , "proposal profile is required" )
check ( strings . TrimSpace ( c . LLM . ValidationProfile ) != "" , ck . Keys ( "llm" , "validation_profile" ) , "validation profile is required" )
check ( c . LLM . TransientRetries >= 0 , ck . Keys ( "llm" , "transient_retries" ) , "retry budget must be non-negative" )
check ( c . LLM . RepairAttempts >= 0 && c . LLM . RepairAttempts <= 3 , ck . Keys ( "llm" , "repair_attempts" ) , "repair attempts must be between zero and three" )
check ( c . LLM . OperationTimeout > 0 , ck . Keys ( "llm" , "operation_timeout" ) , "operation timeout must be positive" )
check ( len ( c . Backends ) > 0 , ck . Keys ( "backends" ) , "at least one backend is required" )
for id , b := range c . Backends {
p := ck . Keys ( "backends" , id )
check ( strings . TrimSpace ( b . Endpoint ) != "" , p . Key ( "endpoint" ) , "backend endpoint is required" )
check ( b . Concurrency > 0 , p . Key ( "concurrency" ) , "backend concurrency must be positive" )
check ( b . QueueCapacity >= 0 , p . Key ( "queue_capacity" ) , "queue capacity must be non-negative" )
2026-05-10 23:19:59 +00:00
}
2026-09-24 01:33:03 +00:00
for i , module := range c . Pipeline . Modules {
check ( modulecatalog . IsSupported ( module ) , ck . Keys ( "pipeline" , "modules" ) . Index ( i ) , "unsupported module key" )
2026-05-10 23:19:59 +00:00
}
2026-09-24 01:33:03 +00:00
check ( outputschema . IsSupported ( c . Output . Schema ) , ck . Keys ( "output" , "schema" ) , "unsupported output schema" )
check ( c . Validation . MaxPromptTokens > 0 , ck . Keys ( "validation" , "max_prompt_tokens" ) , "validation prompt budget must be positive" )
check ( c . Chunking . MaxSectionTokens > 0 , ck . Keys ( "chunking" , "max_section_tokens" ) , "maximum section tokens must be positive" )
check ( c . Chunking . MinSectionTokens > 0 && c . Chunking . MinSectionTokens <= c . Chunking . MaxSectionTokens , ck . Keys ( "chunking" , "min_section_tokens" ) , "minimum section tokens must be positive and no greater than maximum" )
check ( c . Chunking . TargetSections == nil || * c . Chunking . TargetSections > 0 , ck . Keys ( "chunking" , "target_sections" ) , "target sections must be positive when set" )
check ( c . Normalization . MaxSegmentGap >= 0 , ck . Keys ( "normalization" , "max_segment_gap" ) , "maximum segment gap must be non-negative" )
check ( c . Normalization . EllipsisGap >= 0 , ck . Keys ( "normalization" , "ellipsis_gap" ) , "ellipsis gap must be non-negative" )
check ( c . Normalization . MaxSegmentDuration > 0 , ck . Keys ( "normalization" , "max_segment_duration" ) , "maximum segment duration must be positive" )
check ( c . Normalization . MaxSegmentTokens > 0 , ck . Keys ( "normalization" , "max_segment_tokens" ) , "maximum segment tokens must be positive" )
for name , threshold := range map [ string ] float64 { "glossary" : c . Thresholds . Glossary , "homophones" : c . Thresholds . Homophones , "spoken_word" : c . Thresholds . SpokenWord , "grammar" : c . Thresholds . Grammar } {
check ( threshold >= 0 && threshold <= 1 , ck . Keys ( "thresholds" , name ) , "confidence threshold must be between zero and one" )
2026-05-12 21:17:35 +00:00
}
2026-09-24 01:33:03 +00:00
check ( len ( strings . TrimSpace ( c . Context . Description ) ) <= DefaultTranscriptDescriptionMaxChars , ck . Keys ( "context" , "description" ) , fmt . Sprintf ( "description must be at most %d bytes after trimming" , DefaultTranscriptDescriptionMaxChars ) )
2026-05-12 21:17:35 +00:00
}
2026-09-24 01:33:03 +00:00
// ParseModulesCSV is used only for an explicitly supplied --modules override.
func ParseModulesCSV ( raw string ) ( [ ] string , error ) {
if raw == "" {
return [ ] string { } , nil
2026-05-12 21:17:35 +00:00
}
2026-09-24 01:33:03 +00:00
parts := strings . Split ( raw , "," )
for i := range parts {
parts [ i ] = strings . TrimSpace ( parts [ i ] )
if parts [ i ] == "" {
return nil , fmt . Errorf ( "modules list contains an empty value" )
}
2026-05-12 21:17:35 +00:00
}
2026-09-24 01:33:03 +00:00
return parts , nil
2026-05-12 21:17:35 +00:00
}