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-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-11-10 16:08:53 +00:00
ck "go.maximumdirect.net/configkit"
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-11-10 16:08:53 +00:00
// Config contains application settings. Invocation paths are not settings.
2026-05-10 23:19:59 +00:00
type Config struct {
2026-11-10 16:08:53 +00:00
Version int ` yaml:"version" `
Glossaries [ ] string ` yaml:"glossaries" `
ProtectedTerms [ ] string ` yaml:"protected_terms" `
GlossaryCollections map [ string ] GlossaryCollection ` yaml:"glossary_collections" `
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" `
Reporting Reporting ` yaml:"reporting" `
Diagnostics Diagnostics ` yaml:"diagnostics" `
2026-05-10 23:19:59 +00:00
}
2026-11-10 16:08:53 +00:00
type GlossaryCollection struct {
Entries [ ] GlossaryEntry ` yaml:"entries" `
}
type GlossaryEntry struct {
ID string ` yaml:"id" `
Name string ` yaml:"name" `
Category string ` yaml:"category" `
Description string ` yaml:"description" `
Aliases [ ] string ` yaml:"aliases" `
Plural * string ` yaml:"plural" `
Extra map [ string ] any ` yaml:",inline" `
2026-05-10 23:19:59 +00:00
}
2026-09-24 01:33:03 +00:00
type LLM struct {
2026-11-10 16:08:53 +00:00
ProposalProfile * string ` yaml:"proposal_profile" `
ValidationProfile * string ` yaml:"validation_profile" `
2026-09-24 01:33:03 +00:00
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 {
2026-11-10 16:08:53 +00:00
Stages [ ] Stage ` yaml:"stages" `
}
type Stage struct {
Module string ` yaml:"module" `
ID * string ` yaml:"id" `
Glossaries [ ] string ` yaml:"glossaries" `
ProtectedTerms [ ] string ` yaml:"protected_terms" `
ConfidenceThreshold float64 ` yaml:"confidence_threshold" `
LLM LLM ` yaml:"llm" `
Validation Validation ` yaml:"validation" `
Context Context ` yaml:"context" `
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
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-11-10 16:08:53 +00:00
Version : 3 ,
2026-09-24 01:33:03 +00:00
LLM : LLM { TransientRetries : 2 , RepairAttempts : 2 , OperationTimeout : 15 * time . Minute } ,
2026-11-10 16:08:53 +00:00
Pipeline : Pipeline { Stages : [ ] Stage { { Module : "glossary" } , { Module : "homophones" } , { Module : "glossary" } , { Module : "spoken_word" } , { Module : "grammar" } } } ,
2026-09-24 01:33:03 +00:00
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-11-10 17:14:31 +00:00
const glossaryImports = ` config :
imports :
- path : glossary . yml
required : false
destination : [ glossary_collections ]
- dir : glossary
required : false
recursive : true
pattern : [ "*.yml" , "*.yaml" ]
destination : [ glossary_collections ]
`
2026-11-10 16:08:53 +00:00
// Load resolves the selected main source and preserves native source evidence.
func Load ( input ck . Input , overlay string , overrides ... ck . Override ) ( * ck . Result [ Config ] , error ) {
input . Imports = true
return ck . LoadLayered ( input , ck . LayeredOptions [ Config ] {
2026-09-24 01:33:03 +00:00
Overlay : overlay , Overrides : overrides ,
2026-11-10 17:14:31 +00:00
Underlays : [ ] ck . Underlay { { Inputs : [ ] ck . Input { { Source : ck . Bytes ( "Audita glossary imports" , [ ] byte ( glossaryImports ) ) , Imports : true , MainRelative : ck . MainRelative ( ) } } } } ,
2026-09-24 01:33:03 +00:00
Definition : ck . Definition [ Config ] {
2026-11-10 17:14:31 +00:00
Defaults : defaults ,
SequenceIdentities : [ ] ck . SequenceIdentity { ck . SequenceIdentityMatching ( ck . SequenceSelector { ck . SequenceLiteralKey ( "glossary_collections" ) , ck . SequenceAnyMappingKey ( ) , ck . SequenceLiteralKey ( "entries" ) } , "id" ) } ,
2026-09-24 01:33:03 +00:00
Version : & ck . VersionRule { Path : ck . Keys ( "version" ) , Check : func ( version ck . Version , v * ck . Validation ) error {
2026-11-10 16:08:53 +00:00
if version . Present && ( version . Null || version . Text != "3" ) {
v . Error ( ck . Keys ( "version" ) , "use application configuration version 3; see docs/migrations/v3.md" )
2026-09-24 01:33:03 +00:00
}
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-11-10 17:14:31 +00:00
for name , collection := range c . GlossaryCollections {
for i , entry := range collection . Entries {
path := ck . Keys ( "glossary_collections" , name , "entries" ) . Index ( i )
check ( entry . ID != "" , path . Key ( "id" ) , "entry id must not be empty" )
check ( entry . Name != "" , path . Key ( "name" ) , "entry name must not be empty" )
check ( entry . Category != "" , path . Key ( "category" ) , "entry category must not be empty" )
for index , alias := range entry . Aliases {
check ( alias != "" , path . Key ( "aliases" ) . Index ( index ) , "alias must not be empty" )
}
check ( entry . Plural == nil || * entry . Plural != "" , path . Key ( "plural" ) , "plural must not be empty" )
}
}
2026-09-24 01:33:03 +00:00
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" )
2026-11-10 16:08:53 +00:00
for i , stage := range c . Pipeline . Stages {
check ( modulecatalog . IsSupported ( stage . Module ) , ck . Keys ( "pipeline" , "stages" ) . Index ( i ) . Key ( "module" ) , "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
}