2026-05-10 23:12:46 +00:00
package cli
import (
2026-05-11 14:22:22 +00:00
"context"
2026-05-13 12:36:20 +00:00
"encoding/json"
2026-05-10 23:12:46 +00:00
"errors"
"flag"
"fmt"
"io"
2026-05-12 12:19:50 +00:00
"os"
2026-05-11 13:59:46 +00:00
"path/filepath"
2026-05-10 23:19:59 +00:00
"strings"
2026-05-10 23:30:46 +00:00
"time"
2026-05-10 23:19:59 +00:00
2026-05-11 12:47:31 +00:00
"gitea.maximumdirect.net/eric/audita/internal/core/chunking"
2026-05-10 23:19:59 +00:00
"gitea.maximumdirect.net/eric/audita/internal/core/config"
2026-05-11 00:19:28 +00:00
"gitea.maximumdirect.net/eric/audita/internal/core/diagnostics"
2026-05-11 00:41:29 +00:00
coreio "gitea.maximumdirect.net/eric/audita/internal/core/io"
2026-05-11 00:13:26 +00:00
"gitea.maximumdirect.net/eric/audita/internal/core/normalization"
2026-05-13 12:59:10 +00:00
"gitea.maximumdirect.net/eric/audita/internal/core/outputschema"
2026-05-10 23:30:46 +00:00
"gitea.maximumdirect.net/eric/audita/internal/core/reporting"
2026-05-11 00:01:42 +00:00
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
2026-05-11 14:22:22 +00:00
"gitea.maximumdirect.net/eric/audita/internal/framework/contracts"
2026-05-12 02:57:06 +00:00
"gitea.maximumdirect.net/eric/audita/internal/framework/llm"
"gitea.maximumdirect.net/eric/audita/internal/framework/modules"
2026-05-23 18:27:30 -05:00
"gitea.maximumdirect.net/eric/audita/internal/framework/processreport"
2026-05-12 12:19:50 +00:00
"gitea.maximumdirect.net/eric/audita/internal/framework/proposal_generation"
2026-05-11 14:22:22 +00:00
"gitea.maximumdirect.net/eric/audita/internal/framework/runner"
2026-05-12 12:19:50 +00:00
"gitea.maximumdirect.net/eric/audita/internal/framework/validators"
2026-05-10 23:12:46 +00:00
)
2026-05-12 12:19:50 +00:00
type noOpStructuredLLMClient struct { }
func ( c noOpStructuredLLMClient ) CompleteStructured ( ctx context . Context , req contracts . StructuredCompletionRequest , out any ) ( contracts . StructuredCompletionResponse , error ) {
_ = ctx
_ = req
switch target := out . ( type ) {
case * validators . LLMValidationResponse :
* target = validators . LLMValidationResponse { Validations : [ ] validators . LLMValidationDecision { } }
case * proposal_generation . StructuredCorrectionSet :
* target = proposal_generation . StructuredCorrectionSet { Corrections : nil }
}
return contracts . StructuredCompletionResponse { } , nil
}
func shouldUseNoOpLLMClientForTests ( ) bool {
2026-05-14 00:28:28 +00:00
return strings . HasSuffix ( filepath . Base ( os . Args [ 0 ] ) , ".test" ) || os . Getenv ( "GO_WANT_HELPER_PROCESS" ) == "1"
2026-05-12 12:19:50 +00:00
}
2026-05-10 23:24:48 +00:00
type processInvocation struct {
2026-05-12 02:57:06 +00:00
TranscriptPath string
GlossaryPath string
OutputPath string
ReportJSONPath string
Config config . Config
2026-05-13 12:36:20 +00:00
ConfigPath string
ConfigSource string
2026-05-13 12:59:10 +00:00
ConfigVersion * int
2026-05-12 02:57:06 +00:00
ExplicitModules bool
2026-05-10 23:24:48 +00:00
}
2026-05-11 14:22:22 +00:00
var processModuleFactory runner . ModuleFactory
2026-05-12 02:25:33 +00:00
var processProposalLLMClient contracts . StructuredLLMClient
var processProposalLLMScheduler runner . ValidationScheduler
2026-05-12 01:25:52 +00:00
var processValidationLLMClient contracts . StructuredLLMClient
var processValidationLLMScheduler runner . ValidationScheduler
2026-05-12 13:32:35 +00:00
var processRunnerContext = func ( ) ( context . Context , context . CancelFunc ) {
return context . Background ( ) , func ( ) { }
}
2026-05-11 14:22:22 +00:00
var processRunner = func ( inv processInvocation , stdout io . Writer ) ( * normalization . NormalizationSummary , * chunking . Summary , * runner . RunOutput , * diagnostics . RunDirectory , error ) {
2026-05-11 00:19:28 +00:00
runDir , err := diagnostics . NewRunDirectory ( inv . Config . WorkDir , string ( inv . Config . WorkDirRetention ) )
if err != nil {
2026-05-11 14:22:22 +00:00
return nil , nil , nil , nil , fmt . Errorf ( "run_dir_creation: %w" , err )
2026-05-11 00:19:28 +00:00
}
2026-05-11 14:22:22 +00:00
fail := func ( phase string , err error , runOutput * runner . RunOutput ) ( * normalization . NormalizationSummary , * chunking . Summary , * runner . RunOutput , * diagnostics . RunDirectory , error ) {
2026-05-11 00:41:29 +00:00
_ = runDir . WriteErrorLog ( fmt . Sprintf ( "%s: %v" , phase , err ) )
2026-05-11 14:22:22 +00:00
return nil , nil , runOutput , runDir , fmt . Errorf ( "%s: %w" , phase , err )
2026-05-10 23:27:54 +00:00
}
2026-05-11 00:01:42 +00:00
2026-05-11 13:58:13 +00:00
if err := runDir . WriteInvocationMetadata ( diagnostics . InvocationMetadata {
2026-05-13 12:13:32 +00:00
Operation : "process" ,
TranscriptPath : inv . TranscriptPath ,
GlossaryPath : inv . GlossaryPath ,
OutputPath : inv . OutputPath ,
ReportJSONPath : inv . ReportJSONPath ,
2026-05-13 12:36:20 +00:00
ConfigPath : inv . ConfigPath ,
ConfigSource : inv . ConfigSource ,
2026-05-13 12:59:10 +00:00
ConfigVersion : inv . ConfigVersion ,
2026-05-13 12:13:32 +00:00
TranscriptDescription : inv . Config . TranscriptDescription ,
Modules : append ( [ ] string ( nil ) , inv . Config . Modules ... ) ,
2026-05-11 13:58:13 +00:00
} ) ; err != nil {
_ = runDir . WriteErrorLog ( fmt . Sprintf ( "invocation_metadata: %v" , err ) )
}
if err := runDir . WriteEffectiveConfig ( inv . Config ) ; err != nil {
_ = runDir . WriteErrorLog ( fmt . Sprintf ( "effective_config: %v" , err ) )
}
2026-05-11 00:41:29 +00:00
transcriptBytes , err := coreio . ReadRequiredFile ( inv . TranscriptPath , "transcript" )
2026-05-11 00:01:42 +00:00
if err != nil {
2026-05-11 14:22:22 +00:00
return fail ( "transcript_read" , err , nil )
2026-05-10 23:27:54 +00:00
}
2026-05-11 00:01:42 +00:00
2026-05-11 00:41:29 +00:00
glossaryBytes , err := coreio . ReadRequiredFile ( inv . GlossaryPath , "glossary" )
2026-05-11 00:01:42 +00:00
if err != nil {
2026-05-11 14:22:22 +00:00
return fail ( "glossary_read" , err , nil )
2026-05-11 00:19:28 +00:00
}
2026-05-11 00:41:29 +00:00
sourceTranscript , err := schema . ParseSourceTranscriptJSON ( transcriptBytes )
2026-05-11 00:01:42 +00:00
if err != nil {
2026-05-11 14:22:22 +00:00
return fail ( "transcript_schema" , err , nil )
2026-05-11 00:01:42 +00:00
}
2026-05-11 14:22:22 +00:00
glossary , err := schema . ParseGlossaryYAML ( glossaryBytes )
if err != nil {
return fail ( "glossary_schema" , err , nil )
2026-05-11 00:01:42 +00:00
}
2026-05-11 00:41:29 +00:00
if err := runDir . WriteSourceTranscript ( sourceTranscript , transcriptBytes ) ; err != nil {
_ = runDir . WriteErrorLog ( fmt . Sprintf ( "source_artifact: %v" , err ) )
2026-05-11 00:01:42 +00:00
}
2026-05-11 00:41:29 +00:00
canonical := sourceToCanonicalTranscript ( sourceTranscript )
2026-05-11 00:13:26 +00:00
normalizer := normalization . NewNormalizer ( normalization . NormalizationConfig {
MaxSegmentGap : inv . Config . Normalization . MaxSegmentGap ,
EllipsisGap : inv . Config . Normalization . EllipsisGap ,
MaxSegmentDuration : inv . Config . Normalization . MaxSegmentDuration ,
MaxSegmentTokens : inv . Config . Normalization . MaxSegmentTokens ,
} )
2026-05-11 12:47:31 +00:00
normalizedTranscript , normSummary := normalizer . Normalize ( canonical )
2026-05-11 00:13:26 +00:00
2026-05-11 00:19:28 +00:00
if err := runDir . WriteNormalizedTranscript ( normalizedTranscript ) ; err != nil {
_ = runDir . WriteErrorLog ( fmt . Sprintf ( "normalized_artifact: %v" , err ) )
}
2026-05-11 12:47:31 +00:00
if err := runDir . WriteNormalizationSummary ( normSummary ) ; err != nil {
2026-05-11 00:19:28 +00:00
_ = runDir . WriteErrorLog ( fmt . Sprintf ( "normalization_summary: %v" , err ) )
}
2026-05-11 12:47:31 +00:00
// Compute chunks after normalization
chunker := chunking . NewChunker ( chunking . ChunkingConfig {
MaxSectionTokens : inv . Config . MaxSectionTokens ,
MinSectionTokens : inv . Config . MinSectionTokens ,
TargetSections : inv . Config . TargetSections ,
} )
sections , chunkErr := chunker . ChunkTranscript ( normalizedTranscript )
if chunkErr != nil {
2026-05-11 14:22:22 +00:00
return fail ( "chunking" , chunkErr , nil )
2026-05-11 12:47:31 +00:00
}
chunkConfig := chunking . ChunkingConfig {
MaxSectionTokens : inv . Config . MaxSectionTokens ,
MinSectionTokens : inv . Config . MinSectionTokens ,
TargetSections : inv . Config . TargetSections ,
}
chunkSummary := chunking . ComputeSummary ( sections , chunkConfig )
chunkDetailedSummary := chunking . ComputeDetailedSummary ( sections , chunkConfig )
if err := runDir . WriteChunkingSummary ( & chunkDetailedSummary ) ; err != nil {
_ = runDir . WriteErrorLog ( fmt . Sprintf ( "chunking_summary: %v" , err ) )
}
2026-05-11 14:22:22 +00:00
workingTranscript := normalizedTranscript
var runOutput * runner . RunOutput
2026-05-12 02:57:06 +00:00
moduleFactory := processModuleFactory
2026-05-12 12:19:50 +00:00
if moduleFactory == nil {
2026-05-12 02:57:06 +00:00
moduleFactory = modules . NewFactory ( modules . Dependencies {
Config : & inv . Config ,
Glossary : glossary ,
DiagnosticsDir : runDir . Path ( ) ,
} )
}
if moduleFactory != nil {
proposalLLMClient := processProposalLLMClient
validationLLMClient := processValidationLLMClient
proposalScheduler := processProposalLLMScheduler
validationScheduler := processValidationLLMScheduler
if processModuleFactory == nil {
// Production runtime path: construct clients/schedulers from config.
if proposalLLMClient == nil {
2026-05-12 12:19:50 +00:00
if shouldUseNoOpLLMClientForTests ( ) {
proposalLLMClient = noOpStructuredLLMClient { }
} else {
primaryCfg := llm . ResolvePrimaryConfig ( inv . Config )
2026-05-13 02:10:24 +00:00
client , clientErr := llm . NewOpenAICompatibleClient ( primaryCfg . ToOpenAICompatibleClientConfig ( nil ) )
2026-05-12 12:19:50 +00:00
if clientErr != nil {
return fail ( "runner_setup" , clientErr , nil )
}
proposalLLMClient = client
2026-05-12 02:57:06 +00:00
}
}
if validationLLMClient == nil {
2026-05-12 12:19:50 +00:00
if shouldUseNoOpLLMClientForTests ( ) {
validationLLMClient = noOpStructuredLLMClient { }
} else {
validationCfg := llm . ResolveValidationConfig ( inv . Config )
2026-05-13 02:10:24 +00:00
client , clientErr := llm . NewOpenAICompatibleClient ( validationCfg . ToOpenAICompatibleClientConfig ( nil ) )
2026-05-12 12:19:50 +00:00
if clientErr != nil {
return fail ( "runner_setup" , clientErr , nil )
}
validationLLMClient = client
2026-05-12 02:57:06 +00:00
}
}
2026-05-12 12:59:46 -05:00
globalScheduler := proposalScheduler
if globalScheduler == nil {
2026-05-12 21:17:35 +00:00
s , sErr := llm . NewScheduler ( inv . Config . TotalLLMConcurrency )
2026-05-12 02:57:06 +00:00
if sErr != nil {
return fail ( "runner_setup" , sErr , nil )
}
2026-05-12 12:59:46 -05:00
globalScheduler = s
}
if proposalScheduler == nil {
proposalScheduler = globalScheduler
2026-05-12 21:17:35 +00:00
if inv . Config . EffectiveProposalLLMConcurrency ( ) < inv . Config . TotalLLMConcurrency {
s , sErr := llm . NewScheduler ( inv . Config . EffectiveProposalLLMConcurrency ( ) )
if sErr != nil {
return fail ( "runner_setup" , sErr , nil )
}
proposalScheduler = composeSchedulers ( globalScheduler , s )
}
2026-05-12 02:57:06 +00:00
}
if validationScheduler == nil {
2026-05-12 12:59:46 -05:00
validationScheduler = globalScheduler
2026-05-12 21:17:35 +00:00
if inv . Config . ValidationLLMConcurrency != nil && inv . Config . EffectiveValidationLLMConcurrency ( ) < inv . Config . TotalLLMConcurrency {
s , sErr := llm . NewScheduler ( inv . Config . EffectiveValidationLLMConcurrency ( ) )
2026-05-12 12:59:46 -05:00
if sErr != nil {
return fail ( "runner_setup" , sErr , nil )
}
validationScheduler = composeSchedulers ( globalScheduler , s )
2026-05-12 02:57:06 +00:00
}
}
}
2026-05-11 14:22:22 +00:00
moduleSpecs , err := contracts . ResolveModuleRunSpecs ( inv . Config . Modules )
if err != nil {
return fail ( "runner_setup" , err , nil )
}
2026-05-12 13:32:35 +00:00
runCtx , cancelRun := processRunnerContext ( )
defer cancelRun ( )
runnerResult , runErr := runner . New ( moduleFactory ) . Run ( runCtx , runner . RunInput {
2026-05-13 19:03:37 +00:00
Config : & inv . Config ,
Transcript : normalizedTranscript ,
Glossary : glossary ,
ModuleSpecs : moduleSpecs ,
EffectiveConcurrency : runner . EffectiveConcurrencyLimits {
TotalLLM : inv . Config . TotalLLMConcurrency ,
ProposalLLM : inv . Config . EffectiveProposalLLMConcurrency ( ) ,
ValidationLLM : inv . Config . EffectiveValidationLLMConcurrency ( ) ,
} ,
2026-05-12 02:57:06 +00:00
ProposalLLMClient : proposalLLMClient ,
ProposalLLMScheduler : proposalScheduler ,
2026-05-12 02:25:33 +00:00
ProposalDiagnosticsDir : runDir . Path ( ) ,
2026-05-12 02:57:06 +00:00
ValidationLLMClient : validationLLMClient ,
ValidationLLMScheduler : validationScheduler ,
2026-05-12 01:25:52 +00:00
ValidationDiagnosticsDir : runDir . Path ( ) ,
2026-05-11 14:22:22 +00:00
} )
runOutput = & runnerResult
if runErr != nil {
return fail ( "runner_execution" , runErr , runOutput )
}
workingTranscript = runnerResult . FinalTranscript
}
2026-05-13 12:59:10 +00:00
encoderDef , err := outputschema . Resolve ( inv . Config . OutputSchema )
if err != nil {
return fail ( "output_schema" , err , runOutput )
}
outputBytes , err := encoderDef . Encoder ( workingTranscript )
2026-05-11 00:01:42 +00:00
if err != nil {
2026-05-11 14:22:22 +00:00
return fail ( "serialization" , err , runOutput )
2026-05-10 23:27:54 +00:00
}
if strings . TrimSpace ( inv . OutputPath ) != "" {
2026-05-11 00:41:29 +00:00
if err := coreio . WriteFile ( inv . OutputPath , outputBytes ) ; err != nil {
2026-05-11 14:22:22 +00:00
return fail ( "output_write" , err , runOutput )
2026-05-10 23:27:54 +00:00
}
2026-05-11 14:22:22 +00:00
return normSummary , & chunkSummary , runOutput , runDir , nil
2026-05-10 23:27:54 +00:00
}
2026-05-11 00:01:42 +00:00
if _ , err := stdout . Write ( outputBytes ) ; err != nil {
2026-05-11 14:22:22 +00:00
return fail ( "stdout_write" , err , runOutput )
2026-05-11 00:41:29 +00:00
}
2026-05-11 14:22:22 +00:00
return normSummary , & chunkSummary , runOutput , runDir , nil
2026-05-11 00:41:29 +00:00
}
func sourceToCanonicalTranscript ( source * schema . SourceTranscript ) * schema . Transcript {
segments := make ( [ ] schema . Segment , len ( source . Segments ) )
for i , s := range source . Segments {
id := i + 1
if s . ID != nil {
id = * s . ID
}
segments [ i ] = schema . Segment {
ID : id ,
Speaker : s . Speaker ,
Start : s . Start ,
End : s . End ,
Text : s . Text ,
Categories : s . Categories ,
}
2026-05-10 23:27:54 +00:00
}
2026-05-11 00:41:29 +00:00
return & schema . Transcript { Segments : segments }
2026-05-10 23:24:48 +00:00
}
2026-05-12 12:59:46 -05:00
type chainedScheduler struct {
schedulers [ ] runner . ValidationScheduler
}
func ( s chainedScheduler ) Run ( ctx context . Context , fn func ( context . Context ) error ) error {
if len ( s . schedulers ) == 0 {
return fn ( ctx )
}
run := fn
for i := len ( s . schedulers ) - 1 ; i >= 0 ; i -- {
scheduler := s . schedulers [ i ]
next := run
run = func ( callCtx context . Context ) error {
return scheduler . Run ( callCtx , next )
}
}
return run ( ctx )
}
func composeSchedulers ( schedulers ... runner . ValidationScheduler ) runner . ValidationScheduler {
filtered := make ( [ ] runner . ValidationScheduler , 0 , len ( schedulers ) )
for _ , scheduler := range schedulers {
if scheduler != nil {
filtered = append ( filtered , scheduler )
}
}
switch len ( filtered ) {
case 0 :
return nil
case 1 :
return filtered [ 0 ]
default :
return chainedScheduler { schedulers : filtered }
}
}
2026-05-10 23:12:46 +00:00
// Run executes the Audita CLI with the provided arguments and streams.
func Run ( args [ ] string , stdout , stderr io . Writer ) int {
if len ( args ) == 0 {
writeRootUsage ( stdout )
return 0
}
if isHelpCommand ( args ) {
writeRootUsage ( stdout )
return 0
}
if args [ 0 ] == "process" {
return runProcess ( args [ 1 : ] , stdout , stderr )
}
2026-05-13 12:36:20 +00:00
if args [ 0 ] == "config" {
return runConfig ( args [ 1 : ] , stdout , stderr )
}
2026-05-10 23:12:46 +00:00
fmt . Fprintf ( stderr , "audita: unknown command %q\n\n" , args [ 0 ] )
writeRootUsage ( stderr )
return 2
}
func runProcess ( args [ ] string , stdout , stderr io . Writer ) int {
2026-05-10 23:30:46 +00:00
startedAt := time . Now ( ) . UTC ( )
2026-05-13 12:36:20 +00:00
configPathOverride , configPathOverrideSet , err := findConfigPathOverride ( args )
2026-05-10 23:19:59 +00:00
if err != nil {
2026-05-13 12:36:20 +00:00
fmt . Fprintf ( stderr , "audita process: invalid CLI configuration: %v\n" , err )
return 2
}
2026-05-23 17:43:27 +00:00
effectiveConfig , err := config . LoadEffectiveConfig ( configPathOverride , configPathOverrideSet )
2026-05-13 12:36:20 +00:00
if err != nil {
2026-05-23 17:43:27 +00:00
var effectiveConfigErr * config . EffectiveConfigError
if errors . As ( err , & effectiveConfigErr ) {
switch effectiveConfigErr . Kind {
case config . EffectiveConfigErrorLoadFile , config . EffectiveConfigErrorApplyFile :
fmt . Fprintf ( stderr , "audita process: invalid config file: %v\n" , effectiveConfigErr )
case config . EffectiveConfigErrorApplyEnv :
fmt . Fprintf ( stderr , "audita process: invalid environment configuration: %v\n" , effectiveConfigErr )
default :
fmt . Fprintf ( stderr , "audita process: %v\n" , effectiveConfigErr )
}
} else {
fmt . Fprintf ( stderr , "audita process: %v\n" , err )
2026-05-13 12:36:20 +00:00
}
2026-05-10 23:19:59 +00:00
return 2
}
2026-05-23 17:43:27 +00:00
cfg := effectiveConfig . Config
configPath := effectiveConfig . ConfigPath
configSource := effectiveConfig . ConfigSource
configVersion := effectiveConfig . ConfigVersion
2026-05-10 23:24:48 +00:00
fs , pFlags := newProcessFlagSet ( cfg , stderr )
2026-05-10 23:12:46 +00:00
2026-05-10 23:24:48 +00:00
if isHelpCommand ( args ) || hasHelpFlag ( args ) {
writeProcessUsage ( stdout , fs )
return 0
2026-05-10 23:19:59 +00:00
}
2026-05-10 23:24:48 +00:00
parseArgs := args
transcriptFromFront := ""
if len ( args ) > 0 && ! strings . HasPrefix ( args [ 0 ] , "-" ) {
transcriptFromFront = args [ 0 ]
parseArgs = args [ 1 : ]
2026-05-10 23:19:59 +00:00
}
2026-05-10 23:24:48 +00:00
if err := fs . Parse ( parseArgs ) ; err != nil {
2026-05-10 23:12:46 +00:00
if errors . Is ( err , flag . ErrHelp ) {
2026-05-10 23:24:48 +00:00
writeProcessUsage ( stdout , fs )
2026-05-10 23:12:46 +00:00
return 0
}
return 2
}
2026-05-23 18:33:12 -05:00
overrides , explicitModules := processCLIOverrides ( fs , pFlags )
2026-05-10 23:19:59 +00:00
if err := cfg . ApplyCLIOverrides ( overrides ) ; err != nil {
fmt . Fprintf ( stderr , "audita process: invalid CLI configuration: %v\n" , err )
return 2
}
2026-05-10 23:12:46 +00:00
remaining := fs . Args ( )
2026-05-10 23:24:48 +00:00
positional := make ( [ ] string , 0 , len ( remaining ) + 1 )
if transcriptFromFront != "" {
positional = append ( positional , transcriptFromFront )
}
positional = append ( positional , remaining ... )
if len ( positional ) != 1 {
2026-05-10 23:12:46 +00:00
fmt . Fprintln ( stderr , "audita process: expected exactly 1 transcript JSON path argument" )
return 2
}
2026-05-10 23:24:48 +00:00
if strings . TrimSpace ( * pFlags . glossaryPath ) == "" {
fmt . Fprintln ( stderr , "audita process: --glossary is required" )
return 2
}
inv := processInvocation {
2026-05-12 02:57:06 +00:00
TranscriptPath : positional [ 0 ] ,
GlossaryPath : * pFlags . glossaryPath ,
OutputPath : * pFlags . outputPath ,
ReportJSONPath : * pFlags . reportJSONPath ,
Config : cfg ,
2026-05-13 12:36:20 +00:00
ConfigPath : configPath ,
ConfigSource : configSource ,
2026-05-13 12:59:10 +00:00
ConfigVersion : configVersion ,
2026-05-12 02:57:06 +00:00
ExplicitModules : explicitModules ,
2026-05-10 23:24:48 +00:00
}
2026-05-10 23:12:46 +00:00
2026-05-11 14:22:22 +00:00
normSummary , chunkSummary , runOutput , runDir , runErr := processRunner ( inv , stdout )
2026-05-11 00:41:29 +00:00
completedAt := time . Now ( ) . UTC ( )
if runErr != nil {
2026-05-13 19:03:37 +00:00
if runDir != nil && runOutput != nil {
if runOutput . Utilization != nil {
2026-05-23 17:32:30 +00:00
_ = runDir . WriteJSONArtifact ( diagnostics . ArtifactUtilizationSummary , runOutput . Utilization )
2026-05-13 19:03:37 +00:00
}
2026-05-23 18:27:30 -05:00
_ = runDir . WriteJSONArtifact ( diagnostics . ArtifactCorrectionLedger , processreport . BuildCorrectionLedger ( processreport . CorrectionLedgerInput {
RunDirectoryPath : runDir . Path ( ) ,
RunOutput : runOutput ,
} ) )
2026-05-13 19:03:37 +00:00
}
2026-05-11 00:41:29 +00:00
errorPhase , errorMessage := extractErrorPhase ( runErr )
2026-05-23 18:27:30 -05:00
report := processreport . Build ( processReportInput ( "failed" , inv , runDir , startedAt , completedAt , errorMessage , errorPhase , nil , nil , runOutput ) )
2026-05-11 00:41:29 +00:00
2026-05-10 23:30:46 +00:00
if strings . TrimSpace ( inv . ReportJSONPath ) != "" {
2026-05-11 00:41:29 +00:00
if err := reporting . WriteProcessReport ( inv . ReportJSONPath , report ) ; err != nil {
fmt . Fprintf ( stderr , "audita process: %v\n" , err )
2026-05-10 23:30:46 +00:00
}
}
2026-05-11 00:41:29 +00:00
if runDir != nil {
_ = runDir . WriteReport ( report )
2026-05-11 14:01:56 +00:00
_ = runDir . ApplyRetention ( diagnostics . RetentionDecisionInput {
RunSucceeded : false ,
} )
2026-05-11 00:16:50 +00:00
}
2026-05-11 00:41:29 +00:00
fmt . Fprintf ( stderr , "audita process: %v\n" , runErr )
2026-05-12 13:32:35 +00:00
if runDir != nil {
fmt . Fprintf ( stderr , "audita process: diagnostics: %s\n" , runDir . Path ( ) )
}
2026-05-11 00:16:50 +00:00
return 1
}
2026-05-13 19:03:37 +00:00
if runDir != nil && runOutput != nil {
if runOutput . Utilization != nil {
2026-05-23 17:32:30 +00:00
_ = runDir . WriteJSONArtifact ( diagnostics . ArtifactUtilizationSummary , runOutput . Utilization )
2026-05-13 19:03:37 +00:00
}
2026-05-23 18:27:30 -05:00
_ = runDir . WriteJSONArtifact ( diagnostics . ArtifactCorrectionLedger , processreport . BuildCorrectionLedger ( processreport . CorrectionLedgerInput {
RunDirectoryPath : runDir . Path ( ) ,
RunOutput : runOutput ,
} ) )
2026-05-13 19:03:37 +00:00
}
2026-05-23 18:27:30 -05:00
report := processreport . Build ( processReportInput ( "success" , inv , runDir , startedAt , completedAt , "" , "" , normSummary , chunkSummary , runOutput ) )
2026-05-11 00:41:29 +00:00
2026-05-11 00:16:50 +00:00
if strings . TrimSpace ( inv . ReportJSONPath ) != "" {
if err := reporting . WriteProcessReport ( inv . ReportJSONPath , report ) ; err != nil {
2026-05-11 00:41:29 +00:00
if runDir != nil {
_ = runDir . WriteErrorLog ( fmt . Sprintf ( "report_write: %v" , err ) )
2026-05-11 14:01:56 +00:00
_ = runDir . ApplyRetention ( diagnostics . RetentionDecisionInput {
RunSucceeded : false ,
} )
2026-05-11 00:41:29 +00:00
}
2026-05-11 00:16:50 +00:00
fmt . Fprintf ( stderr , "audita process: %v\n" , err )
return 1
}
}
2026-05-10 23:24:48 +00:00
2026-05-11 00:41:29 +00:00
if runDir != nil {
_ = runDir . WriteReport ( report )
2026-05-11 14:01:56 +00:00
if err := runDir . ApplyRetention ( diagnostics . RetentionDecisionInput {
RunSucceeded : true ,
2026-05-23 18:27:30 -05:00
HasSkippedCorrections : processreport . HasSkippedCorrections ( runOutput ) ,
2026-05-11 14:01:56 +00:00
} ) ; err != nil {
2026-05-11 00:41:29 +00:00
fmt . Fprintf ( stderr , "audita process: failed to apply work-dir retention: %v\n" , err )
2026-05-10 23:30:46 +00:00
return 1
}
}
2026-05-10 23:24:48 +00:00
return 0
}
2026-05-13 12:36:20 +00:00
func runConfig ( args [ ] string , stdout , stderr io . Writer ) int {
if len ( args ) == 0 || isHelpCommand ( args ) || hasHelpFlag ( args ) {
writeConfigUsage ( stdout )
return 0
}
switch args [ 0 ] {
case "validate" :
return runConfigValidate ( args [ 1 : ] , stdout , stderr )
case "print-effective" :
return runConfigPrintEffective ( args [ 1 : ] , stdout , stderr )
default :
fmt . Fprintf ( stderr , "audita config: unknown command %q\n\n" , args [ 0 ] )
writeConfigUsage ( stderr )
return 2
}
}
func runConfigValidate ( args [ ] string , stdout , stderr io . Writer ) int {
fs := flag . NewFlagSet ( "config validate" , flag . ContinueOnError )
fs . SetOutput ( stderr )
configPath := fs . String ( "config" , "" , "Path to versioned YAML config file" )
if err := fs . Parse ( args ) ; err != nil {
if errors . Is ( err , flag . ErrHelp ) {
writeConfigValidateUsage ( stdout )
return 0
}
return 2
}
if strings . TrimSpace ( * configPath ) == "" {
fmt . Fprintln ( stderr , "audita config validate: --config is required" )
return 2
}
if len ( fs . Args ( ) ) != 0 {
fmt . Fprintln ( stderr , "audita config validate: unexpected positional arguments" )
return 2
}
fileCfg , err := config . LoadFileConfig ( strings . TrimSpace ( * configPath ) )
if err != nil {
fmt . Fprintf ( stderr , "audita config validate: %v\n" , err )
return 2
}
cfg := config . Default ( )
if err := cfg . ApplyFileConfig ( fileCfg ) ; err != nil {
fmt . Fprintf ( stderr , "audita config validate: %v\n" , err )
return 2
}
2026-05-23 17:39:13 +00:00
if err := cfg . Validate ( ) ; err != nil {
fmt . Fprintf ( stderr , "audita config validate: %v\n" , err )
return 2
}
2026-05-13 12:36:20 +00:00
fmt . Fprintln ( stdout , "config is valid" )
return 0
}
func runConfigPrintEffective ( args [ ] string , stdout , stderr io . Writer ) int {
fs := flag . NewFlagSet ( "config print-effective" , flag . ContinueOnError )
fs . SetOutput ( stderr )
configPath := fs . String ( "config" , "" , "Path to versioned YAML config file" )
if err := fs . Parse ( args ) ; err != nil {
if errors . Is ( err , flag . ErrHelp ) {
writeConfigPrintEffectiveUsage ( stdout )
return 0
}
return 2
}
if len ( fs . Args ( ) ) != 0 {
fmt . Fprintln ( stderr , "audita config print-effective: unexpected positional arguments" )
return 2
}
configPathValue := strings . TrimSpace ( * configPath )
configPathSet := configPathValue != ""
2026-05-23 17:43:27 +00:00
effectiveConfig , err := config . LoadEffectiveConfig ( configPathValue , configPathSet )
2026-05-13 12:36:20 +00:00
if err != nil {
fmt . Fprintf ( stderr , "audita config print-effective: %v\n" , err )
return 2
}
2026-05-23 17:43:27 +00:00
cfg := effectiveConfig . Config
2026-05-13 12:36:20 +00:00
redacted := cfg . Redacted ( )
out , err := json . MarshalIndent ( redacted , "" , " " )
if err != nil {
fmt . Fprintf ( stderr , "audita config print-effective: %v\n" , err )
return 1
}
out = append ( out , '\n' )
if _ , err := stdout . Write ( out ) ; err != nil {
fmt . Fprintf ( stderr , "audita config print-effective: %v\n" , err )
return 1
}
return 0
}
2026-05-11 00:41:29 +00:00
func extractErrorPhase ( err error ) ( phase string , message string ) {
msg := err . Error ( )
if strings . Contains ( msg , ": " ) {
parts := strings . SplitN ( msg , ": " , 2 )
if len ( parts ) == 2 {
return parts [ 0 ] , parts [ 1 ]
}
}
return "" , msg
}
2026-05-23 18:27:30 -05:00
func processReportInput ( status string , inv processInvocation , runDir * diagnostics . RunDirectory , startedAt , completedAt time . Time , errorMessage string , errorPhase string , normalizationSummary * normalization . NormalizationSummary , chunkingSummary * chunking . Summary , runOutput * runner . RunOutput ) processreport . BuildInput {
runDirectoryPath := ""
2026-05-11 13:59:46 +00:00
if runDir != nil {
2026-05-23 18:27:30 -05:00
runDirectoryPath = runDir . Path ( )
}
return processreport . BuildInput {
Status : status ,
TranscriptPath : inv . TranscriptPath ,
GlossaryPath : inv . GlossaryPath ,
OutputPath : inv . OutputPath ,
Modules : inv . Config . Modules ,
OutputSchema : inv . Config . OutputSchema ,
ConfigVersion : inv . ConfigVersion ,
StartedAt : startedAt ,
CompletedAt : completedAt ,
ErrorMessage : errorMessage ,
ErrorPhase : errorPhase ,
RunDirectoryPath : runDirectoryPath ,
NormalizationSummary : normalizationSummary ,
ChunkingSummary : chunkingSummary ,
RunOutput : runOutput ,
2026-05-12 00:26:27 +00:00
}
}
2026-05-10 23:24:48 +00:00
type processFlags struct {
2026-05-13 12:36:20 +00:00
configPath * string
2026-05-10 23:24:48 +00:00
glossaryPath * string
outputPath * string
reportJSONPath * string
modules * string
2026-05-13 12:59:10 +00:00
outputSchema * string
2026-05-10 23:24:48 +00:00
llmAPIKey * string
validationLLMAPIKey * string
model * string
validationModel * string
baseURL * string
validationBaseURL * string
llmTimeoutSeconds * int
2026-05-12 21:17:35 +00:00
totalLLMConcurrency * int
proposalLLMConcurrency * int
2026-05-12 12:59:46 -05:00
llmConcurrency * int
2026-05-10 23:24:48 +00:00
validationLLMTimeoutSeconds * int
validationMaxPromptTokens * int
targetSections * int
maxRetries * int
validationMaxRetries * int
validationLLMConcurrency * int
maxSectionTokens * int
minSectionTokens * int
glossaryConfidenceThreshold * float64
grammarConfidenceThreshold * float64
homophonesConfidenceThreshold * float64
spokenWordConfidenceThreshold * float64
normalizeMaxSegmentGap * float64
normalizeEllipsisGap * float64
normalizeMaxSegmentDuration * float64
normalizeMaxSegmentTokens * int
2026-05-13 12:13:32 +00:00
transcriptDescription * string
2026-05-10 23:24:48 +00:00
workDir * string
workDirRetention * string
}
func newProcessFlagSet ( cfg config . Config , stderr io . Writer ) ( * flag . FlagSet , processFlags ) {
fs := flag . NewFlagSet ( "process" , flag . ContinueOnError )
fs . SetOutput ( stderr )
validationTimeoutSecondsDefault := cfg . PrimaryLLM . TimeoutSeconds
if cfg . ValidationLLM . TimeoutSeconds != nil {
validationTimeoutSecondsDefault = * cfg . ValidationLLM . TimeoutSeconds
}
validationMaxRetriesDefault := cfg . PrimaryLLM . MaxRetries
if cfg . ValidationLLM . MaxRetries != nil {
validationMaxRetriesDefault = * cfg . ValidationLLM . MaxRetries
}
2026-05-12 21:17:35 +00:00
validationLLMConcurrencyDefault := cfg . TotalLLMConcurrency
if cfg . ValidationLLMConcurrency != nil {
validationLLMConcurrencyDefault = * cfg . ValidationLLMConcurrency
2026-05-10 23:24:48 +00:00
}
targetSectionsDefault := 0
if cfg . TargetSections != nil {
targetSectionsDefault = * cfg . TargetSections
}
pFlags := processFlags {
2026-05-13 12:36:20 +00:00
configPath : fs . String ( "config" , "" , "Path to versioned YAML config file" ) ,
2026-05-10 23:24:48 +00:00
glossaryPath : fs . String ( "glossary" , "" , "Path to glossary YAML file" ) ,
outputPath : fs . String ( "output" , "" , "Path to corrected transcript JSON output file" ) ,
reportJSONPath : fs . String ( "report-json" , "" , "Path to machine-readable report JSON output file" ) ,
modules : fs . String ( "modules" , strings . Join ( cfg . Modules , "," ) , "Comma-separated module sequence override" ) ,
2026-05-13 12:59:10 +00:00
outputSchema : fs . String ( "output-schema" , cfg . OutputSchema , "Output schema: bare-segments|audita-v1" ) ,
2026-05-10 23:24:48 +00:00
llmAPIKey : fs . String ( "llm-api-key" , cfg . PrimaryLLM . APIKey , "Primary LLM API key" ) ,
validationLLMAPIKey : fs . String ( "validation-llm-api-key" , cfg . ValidationLLM . APIKey , "Validation LLM API key" ) ,
model : fs . String ( "model" , cfg . PrimaryLLM . Model , "Primary LLM model name" ) ,
validationModel : fs . String ( "validation-model" , cfg . ValidationLLM . Model , "Validation LLM model name" ) ,
baseURL : fs . String ( "base-url" , cfg . PrimaryLLM . BaseURL , "Primary OpenAI-compatible base URL" ) ,
validationBaseURL : fs . String ( "validation-base-url" , cfg . ValidationLLM . BaseURL , "Validation OpenAI-compatible base URL" ) ,
llmTimeoutSeconds : fs . Int ( "llm-timeout-seconds" , cfg . PrimaryLLM . TimeoutSeconds , "Primary LLM timeout in seconds" ) ,
2026-05-12 21:17:35 +00:00
totalLLMConcurrency : fs . Int ( "total-llm-concurrency" , cfg . TotalLLMConcurrency , "Total concurrent LLM calls across proposal and validation" ) ,
proposalLLMConcurrency : fs . Int ( "proposal-llm-concurrency" , cfg . EffectiveProposalLLMConcurrency ( ) , "Concurrent proposal-generation LLM calls" ) ,
llmConcurrency : fs . Int ( "llm-concurrency" , cfg . TotalLLMConcurrency , "Alias for --total-llm-concurrency" ) ,
2026-05-10 23:24:48 +00:00
validationLLMTimeoutSeconds : fs . Int ( "validation-llm-timeout-seconds" , validationTimeoutSecondsDefault , "Validation LLM timeout in seconds" ) ,
validationMaxPromptTokens : fs . Int ( "validation-max-prompt-tokens" , cfg . ValidationMaxPromptTokens , "Validation max prompt tokens" ) ,
targetSections : fs . Int ( "target-sections" , targetSectionsDefault , "Target number of transcript sections" ) ,
maxRetries : fs . Int ( "max-retries" , cfg . PrimaryLLM . MaxRetries , "Maximum structured-output retries" ) ,
validationMaxRetries : fs . Int ( "validation-max-retries" , validationMaxRetriesDefault , "Validation structured-output retries" ) ,
2026-05-12 21:17:35 +00:00
validationLLMConcurrency : fs . Int ( "validation-llm-concurrency" , validationLLMConcurrencyDefault , "Concurrent validation LLM calls (inherits total when unset)" ) ,
2026-05-10 23:24:48 +00:00
maxSectionTokens : fs . Int ( "max-section-tokens" , cfg . MaxSectionTokens , "Maximum section tokens" ) ,
minSectionTokens : fs . Int ( "min-section-tokens" , cfg . MinSectionTokens , "Minimum section tokens" ) ,
glossaryConfidenceThreshold : fs . Float64 ( "glossary-confidence-threshold" , cfg . Thresholds . Glossary , "Glossary confidence threshold" ) ,
grammarConfidenceThreshold : fs . Float64 ( "grammar-confidence-threshold" , cfg . Thresholds . Grammar , "Grammar confidence threshold" ) ,
homophonesConfidenceThreshold : fs . Float64 ( "homophones-confidence-threshold" , cfg . Thresholds . Homophones , "Homophones confidence threshold" ) ,
spokenWordConfidenceThreshold : fs . Float64 ( "spoken-word-confidence-threshold" , cfg . Thresholds . SpokenWord , "Spoken-word confidence threshold" ) ,
normalizeMaxSegmentGap : fs . Float64 ( "normalize-max-segment-gap" , cfg . Normalization . MaxSegmentGap , "Maximum same-speaker merge gap" ) ,
normalizeEllipsisGap : fs . Float64 ( "normalize-ellipsis-gap" , cfg . Normalization . EllipsisGap , "Gap threshold for ellipsis insertion" ) ,
normalizeMaxSegmentDuration : fs . Float64 ( "normalize-max-segment-duration" , cfg . Normalization . MaxSegmentDuration , "Maximum merged segment duration" ) ,
normalizeMaxSegmentTokens : fs . Int ( "normalize-max-segment-tokens" , cfg . Normalization . MaxSegmentTokens , "Maximum merged segment token estimate" ) ,
2026-05-13 12:13:32 +00:00
transcriptDescription : fs . String ( "transcript-description" , cfg . TranscriptDescription , "Brief background context for LLM prompts; does not override transcript content" ) ,
2026-05-10 23:24:48 +00:00
workDir : fs . String ( "work-dir" , cfg . WorkDir , "Per-run work directory" ) ,
workDirRetention : fs . String ( "work-dir-retention" , string ( cfg . WorkDirRetention ) , "Work-dir retention policy: auto|always|never" ) ,
}
return fs , pFlags
2026-05-10 23:12:46 +00:00
}
2026-05-13 12:36:20 +00:00
func findConfigPathOverride ( args [ ] string ) ( path string , set bool , err error ) {
for i := 0 ; i < len ( args ) ; i ++ {
arg := strings . TrimSpace ( args [ i ] )
if arg == "" {
continue
}
if arg == "--config" {
if i + 1 >= len ( args ) {
return "" , false , fmt . Errorf ( "--config requires a path" )
}
return strings . TrimSpace ( args [ i + 1 ] ) , true , nil
}
if strings . HasPrefix ( arg , "--config=" ) {
return strings . TrimSpace ( strings . TrimPrefix ( arg , "--config=" ) ) , true , nil
}
}
return "" , false , nil
}
2026-05-10 23:12:46 +00:00
func isHelpCommand ( args [ ] string ) bool {
if len ( args ) == 0 {
return false
}
if len ( args ) == 1 {
switch args [ 0 ] {
case "help" , "-h" , "--help" :
return true
}
}
if len ( args ) == 2 && args [ 0 ] == "help" {
switch args [ 1 ] {
case "process" :
return true
}
}
return false
}
func hasHelpFlag ( args [ ] string ) bool {
for _ , arg := range args {
if arg == "-h" || arg == "--help" {
return true
}
}
return false
}
func writeRootUsage ( w io . Writer ) {
2026-05-11 13:35:15 +00:00
fmt . Fprintln ( w , "Audita is a transcript processing CLI." )
2026-05-10 23:12:46 +00:00
fmt . Fprintln ( w )
fmt . Fprintln ( w , "Usage:" )
fmt . Fprintln ( w , " audita <command> [options]" )
fmt . Fprintln ( w )
fmt . Fprintln ( w , "Commands:" )
fmt . Fprintln ( w , " process Process a transcript JSON file" )
2026-05-13 12:36:20 +00:00
fmt . Fprintln ( w , " config Validate and inspect config" )
2026-05-10 23:12:46 +00:00
fmt . Fprintln ( w )
fmt . Fprintln ( w , "Example:" )
fmt . Fprintln ( w , " audita process transcript.json --glossary glossary.yaml --output corrected.json" )
}
2026-05-13 12:36:20 +00:00
func writeConfigUsage ( w io . Writer ) {
fmt . Fprintln ( w , "Validate and inspect Audita config." )
fmt . Fprintln ( w )
fmt . Fprintln ( w , "Usage:" )
fmt . Fprintln ( w , " audita config <command> [flags]" )
fmt . Fprintln ( w )
fmt . Fprintln ( w , "Commands:" )
fmt . Fprintln ( w , " validate Validate a versioned YAML config file" )
fmt . Fprintln ( w , " print-effective Print redacted effective config JSON (defaults + config file + env)" )
}
func writeConfigValidateUsage ( w io . Writer ) {
fmt . Fprintln ( w , "Validate a versioned YAML config file." )
fmt . Fprintln ( w )
fmt . Fprintln ( w , "Usage:" )
fmt . Fprintln ( w , " audita config validate --config <path>" )
}
func writeConfigPrintEffectiveUsage ( w io . Writer ) {
fmt . Fprintln ( w , "Print redacted effective config JSON." )
fmt . Fprintln ( w )
fmt . Fprintln ( w , "Usage:" )
fmt . Fprintln ( w , " audita config print-effective [--config <path>]" )
}
2026-05-10 23:24:48 +00:00
func writeProcessUsage ( w io . Writer , fs * flag . FlagSet ) {
2026-05-10 23:12:46 +00:00
fmt . Fprintln ( w , "Process a transcript JSON file." )
fmt . Fprintln ( w )
fmt . Fprintln ( w , "Usage:" )
fmt . Fprintln ( w , " audita process <transcript.json> [flags]" )
fmt . Fprintln ( w )
fmt . Fprintln ( w , "Flags:" )
2026-05-10 23:24:48 +00:00
fs . VisitAll ( func ( f * flag . Flag ) {
fmt . Fprintf ( w , " --%s\n" , f . Name )
} )
2026-05-10 23:12:46 +00:00
fmt . Fprintln ( w )
fmt . Fprintln ( w , "Example:" )
fmt . Fprintln ( w , " audita process transcript.json --glossary glossary.yaml --output corrected.json" )
}