2026-05-10 23:12:46 +00:00
package cli
import (
2026-05-11 14:22:22 +00:00
"context"
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-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-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 {
return strings . HasSuffix ( filepath . Base ( os . Args [ 0 ] ) , ".test" )
}
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
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 ,
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-12 01:25:52 +00:00
Config : & inv . Config ,
Transcript : normalizedTranscript ,
Glossary : glossary ,
ModuleSpecs : moduleSpecs ,
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
}
outputBytes , err := schema . TranscriptToJSON ( 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 )
}
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-10 23:19:59 +00:00
cfg , err := config . LoadFromEnv ( )
if err != nil {
fmt . Fprintf ( stderr , "audita process: invalid environment configuration: %v\n" , err )
return 2
}
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-10 23:19:59 +00:00
overrides := config . CLIOverrides { }
2026-05-12 02:57:06 +00:00
explicitModules := false
2026-05-10 23:19:59 +00:00
fs . Visit ( func ( f * flag . Flag ) {
switch f . Name {
case "modules" :
2026-05-12 02:57:06 +00:00
explicitModules = true
2026-05-10 23:24:48 +00:00
overrides . ModulesCSV = pFlags . modules
2026-05-10 23:19:59 +00:00
case "llm-api-key" :
2026-05-10 23:24:48 +00:00
overrides . PrimaryLLMAPIKey = pFlags . llmAPIKey
2026-05-10 23:19:59 +00:00
case "validation-llm-api-key" :
2026-05-10 23:24:48 +00:00
overrides . ValidationLLMAPIKey = pFlags . validationLLMAPIKey
2026-05-10 23:19:59 +00:00
case "model" :
2026-05-10 23:24:48 +00:00
overrides . PrimaryModel = pFlags . model
2026-05-10 23:19:59 +00:00
case "validation-model" :
2026-05-10 23:24:48 +00:00
overrides . ValidationModel = pFlags . validationModel
2026-05-10 23:19:59 +00:00
case "base-url" :
2026-05-10 23:24:48 +00:00
overrides . PrimaryBaseURL = pFlags . baseURL
2026-05-10 23:19:59 +00:00
case "validation-base-url" :
2026-05-10 23:24:48 +00:00
overrides . ValidationBaseURL = pFlags . validationBaseURL
2026-05-10 23:19:59 +00:00
case "llm-timeout-seconds" :
2026-05-10 23:24:48 +00:00
overrides . PrimaryLLMTimeoutSeconds = pFlags . llmTimeoutSeconds
2026-05-12 21:17:35 +00:00
case "total-llm-concurrency" :
overrides . TotalLLMConcurrency = pFlags . totalLLMConcurrency
case "proposal-llm-concurrency" :
overrides . ProposalLLMConcurrency = pFlags . proposalLLMConcurrency
2026-05-12 12:59:46 -05:00
case "llm-concurrency" :
overrides . PrimaryLLMConcurrency = pFlags . llmConcurrency
2026-05-10 23:19:59 +00:00
case "validation-llm-timeout-seconds" :
2026-05-10 23:24:48 +00:00
overrides . ValidationLLMTimeoutSeconds = pFlags . validationLLMTimeoutSeconds
2026-05-10 23:19:59 +00:00
case "max-retries" :
2026-05-10 23:24:48 +00:00
overrides . MaxRetries = pFlags . maxRetries
2026-05-10 23:19:59 +00:00
case "validation-max-retries" :
2026-05-10 23:24:48 +00:00
overrides . ValidationMaxRetries = pFlags . validationMaxRetries
case "validation-llm-concurrency" :
overrides . ValidationLLMConcurrency = pFlags . validationLLMConcurrency
2026-05-10 23:19:59 +00:00
case "validation-max-prompt-tokens" :
2026-05-10 23:24:48 +00:00
overrides . ValidationMaxPromptTokens = pFlags . validationMaxPromptTokens
2026-05-10 23:19:59 +00:00
case "max-section-tokens" :
2026-05-10 23:24:48 +00:00
overrides . MaxSectionTokens = pFlags . maxSectionTokens
2026-05-10 23:19:59 +00:00
case "min-section-tokens" :
2026-05-10 23:24:48 +00:00
overrides . MinSectionTokens = pFlags . minSectionTokens
2026-05-10 23:19:59 +00:00
case "target-sections" :
2026-05-10 23:24:48 +00:00
overrides . TargetSections = pFlags . targetSections
2026-05-10 23:19:59 +00:00
case "glossary-confidence-threshold" :
2026-05-10 23:24:48 +00:00
overrides . GlossaryConfidenceThreshold = pFlags . glossaryConfidenceThreshold
2026-05-10 23:19:59 +00:00
case "grammar-confidence-threshold" :
2026-05-10 23:24:48 +00:00
overrides . GrammarConfidenceThreshold = pFlags . grammarConfidenceThreshold
2026-05-10 23:19:59 +00:00
case "homophones-confidence-threshold" :
2026-05-10 23:24:48 +00:00
overrides . HomophonesConfidenceThreshold = pFlags . homophonesConfidenceThreshold
2026-05-10 23:19:59 +00:00
case "spoken-word-confidence-threshold" :
2026-05-10 23:24:48 +00:00
overrides . SpokenWordConfidenceThreshold = pFlags . spokenWordConfidenceThreshold
2026-05-10 23:19:59 +00:00
case "normalize-max-segment-gap" :
2026-05-10 23:24:48 +00:00
overrides . NormalizeMaxSegmentGap = pFlags . normalizeMaxSegmentGap
2026-05-10 23:19:59 +00:00
case "normalize-ellipsis-gap" :
2026-05-10 23:24:48 +00:00
overrides . NormalizeEllipsisGap = pFlags . normalizeEllipsisGap
2026-05-10 23:19:59 +00:00
case "normalize-max-segment-duration" :
2026-05-10 23:24:48 +00:00
overrides . NormalizeMaxSegmentDuration = pFlags . normalizeMaxSegmentDuration
2026-05-10 23:19:59 +00:00
case "normalize-max-segment-tokens" :
2026-05-10 23:24:48 +00:00
overrides . NormalizeMaxSegmentTokens = pFlags . normalizeMaxSegmentTokens
2026-05-13 12:13:32 +00:00
case "transcript-description" :
overrides . TranscriptDescription = pFlags . transcriptDescription
2026-05-10 23:19:59 +00:00
case "work-dir" :
2026-05-10 23:24:48 +00:00
overrides . WorkDir = pFlags . workDir
2026-05-10 23:19:59 +00:00
case "work-dir-retention" :
2026-05-10 23:24:48 +00:00
overrides . WorkDirRetention = pFlags . workDirRetention
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 ,
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 {
errorPhase , errorMessage := extractErrorPhase ( runErr )
2026-05-11 14:22:22 +00:00
report := buildProcessReport ( "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-11 14:22:22 +00:00
report := buildProcessReport ( "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 14:22:22 +00:00
hasSkippedCorrections := false
if runOutput != nil {
for _ , mr := range runOutput . ModuleResults {
2026-05-12 00:26:27 +00:00
if len ( mr . SkippedChanges ) > 0 || len ( mr . ValidatorRejected ) > 0 {
2026-05-11 14:22:22 +00:00
hasSkippedCorrections = true
break
}
}
}
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-11 14:22:22 +00:00
HasSkippedCorrections : hasSkippedCorrections ,
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-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-11 14:22:22 +00:00
func buildProcessReport ( status string , inv processInvocation , runDir * diagnostics . RunDirectory , startedAt , completedAt time . Time , errorMessage string , errorPhase string , normalizationSummary * normalization . NormalizationSummary , chunkingSummary * chunking . Summary , runOutput * runner . RunOutput ) reporting . ProcessReport {
2026-05-10 23:30:46 +00:00
report := reporting . ProcessReport {
2026-05-12 15:19:50 -05:00
Phase : "default_pipeline" ,
2026-05-10 23:30:46 +00:00
Status : status ,
Operation : "process" ,
TranscriptPath : inv . TranscriptPath ,
GlossaryPath : inv . GlossaryPath ,
OutputPath : inv . OutputPath ,
Modules : append ( [ ] string ( nil ) , inv . Config . Modules ... ) ,
StartedAt : startedAt ,
CompletedAt : & completedAt ,
2026-05-11 00:16:50 +00:00
ErrorPhase : errorPhase ,
2026-05-10 23:30:46 +00:00
}
2026-05-11 13:59:46 +00:00
if runDir != nil {
diagnosticsDir := runDir . Path ( )
report . Diagnostics = & reporting . DiagnosticsMetadata {
DirectoryPath : diagnosticsDir ,
SourceTranscriptPath : filepath . Join ( diagnosticsDir , "source-transcript.json" ) ,
ParsedSourceTranscriptPath : filepath . Join ( diagnosticsDir , "source-transcript-parsed.json" ) ,
NormalizedTranscriptPath : filepath . Join ( diagnosticsDir , "normalized-transcript.json" ) ,
NormalizationSummaryPath : filepath . Join ( diagnosticsDir , "normalization-summary.json" ) ,
ChunkingSummaryPath : filepath . Join ( diagnosticsDir , "chunking-summary.json" ) ,
InvocationMetadataPath : filepath . Join ( diagnosticsDir , "invocation.json" ) ,
RedactedEffectiveConfigPath : filepath . Join ( diagnosticsDir , "effective-config.json" ) ,
}
if status == "failed" {
report . Diagnostics . ErrorLogPath = filepath . Join ( diagnosticsDir , "error.log" )
}
}
2026-05-10 23:30:46 +00:00
if errorMessage != "" {
report . ErrorMessage = errorMessage
}
2026-05-11 00:16:50 +00:00
if normalizationSummary != nil {
report . InputSegmentCount = & normalizationSummary . InputSegmentCount
report . NormalizedSegmentCount = & normalizationSummary . OutputSegmentCount
report . NormalizationMerges = & normalizationSummary . MergesPerformed
report . NormalizationIDReassignments = & normalizationSummary . IDsReassigned
report . NormalizationSkipped . DifferentSpeakers = & normalizationSummary . SkippedMerges . DifferentSpeakers
report . NormalizationSkipped . GapTooLarge = & normalizationSummary . SkippedMerges . GapTooLarge
report . NormalizationSkipped . DurationExceeded = & normalizationSummary . SkippedMerges . DurationExceeded
report . NormalizationSkipped . TokenLimitExceeded = & normalizationSummary . SkippedMerges . TokenLimitExceeded
}
2026-05-11 12:47:31 +00:00
if chunkingSummary != nil {
report . Chunking = & reporting . ChunkingSummary {
ChunkCount : chunkingSummary . ChunkCount ,
MinEstimatedTokens : chunkingSummary . MinEstimatedTokens ,
MaxEstimatedTokens : chunkingSummary . MaxEstimatedTokens ,
TotalEstimatedTokens : chunkingSummary . TotalEstimatedTokens ,
TargetSections : chunkingSummary . TargetSections ,
MaxSectionTokens : chunkingSummary . MaxSectionTokens ,
MinSectionTokens : chunkingSummary . MinSectionTokens ,
}
}
2026-05-11 14:22:22 +00:00
report . ModulesSummary , report . ModuleResults = buildModuleReporting ( runOutput )
2026-05-10 23:30:46 +00:00
return report
}
2026-05-11 14:22:22 +00:00
func buildModuleReporting ( runOutput * runner . RunOutput ) ( * reporting . ModulesSummary , [ ] reporting . ModuleReport ) {
if runOutput == nil || len ( runOutput . ModuleResults ) == 0 {
return nil , nil
}
moduleReports := make ( [ ] reporting . ModuleReport , 0 , len ( runOutput . ModuleResults ) )
summary := & reporting . ModulesSummary { ModuleCount : len ( runOutput . ModuleResults ) }
for _ , r := range runOutput . ModuleResults {
startedAt := r . StartedAt
completedAt := r . CompletedAt
moduleReports = append ( moduleReports , reporting . ModuleReport {
2026-05-12 00:26:27 +00:00
ModuleKey : r . ModuleKey ,
ModuleInstance : r . ModuleInstance ,
ReplacementPolicy : string ( r . ReplacementPolicy ) ,
Status : r . Status ,
ProposalCount : r . ProposalCount ,
ValidatorDecisions : mapValidatorDecisions ( r . ValidatorDecisions ) ,
ValidatorRejected : mapValidatorRejected ( r . ValidatorRejected ) ,
AppliedChanges : r . AppliedChanges ,
SkippedChanges : r . SkippedChanges ,
ErrorMessage : r . ErrorMessage ,
StartedAt : & startedAt ,
CompletedAt : & completedAt ,
2026-05-11 14:22:22 +00:00
} )
summary . TotalAppliedChanges += len ( r . AppliedChanges )
2026-05-12 00:26:27 +00:00
summary . TotalSkippedChanges += len ( r . SkippedChanges ) + len ( r . ValidatorRejected )
2026-05-11 14:22:22 +00:00
if r . Status == runner . ModuleStatusFailed && summary . FailedModuleInstance == "" {
summary . FailedModuleInstance = r . ModuleInstance
}
}
return summary , moduleReports
}
2026-05-12 00:26:27 +00:00
func mapValidatorDecisions ( in [ ] runner . ValidatorDecisionRecord ) [ ] reporting . ValidatorDecisionReport {
if len ( in ) == 0 {
return nil
}
out := make ( [ ] reporting . ValidatorDecisionReport , len ( in ) )
for i , d := range in {
out [ i ] = reporting . ValidatorDecisionReport {
2026-05-12 01:25:52 +00:00
ValidatorName : d . ValidatorName ,
ProposalIndex : d . ProposalIndex ,
Approved : d . Approved ,
ReasonCode : d . ReasonCode ,
Message : d . Message ,
DiagnosticArtifactPath : d . DiagnosticArtifactPath ,
2026-05-12 00:26:27 +00:00
}
}
return out
}
func mapValidatorRejected ( in [ ] runner . ValidatorRejectedChange ) [ ] reporting . ValidatorRejectedReport {
if len ( in ) == 0 {
return nil
}
out := make ( [ ] reporting . ValidatorRejectedReport , len ( in ) )
for i , d := range in {
out [ i ] = reporting . ValidatorRejectedReport {
ValidatorName : d . ValidatorName ,
ProposalIndex : d . ProposalIndex ,
ModuleKey : d . ModuleKey ,
ModuleInstance : d . ModuleInstance ,
TargetSegmentID : d . TargetSegmentID ,
OriginalText : d . OriginalText ,
CorrectedText : d . CorrectedText ,
ReasonCode : d . ReasonCode ,
Message : d . Message ,
}
}
return out
}
2026-05-10 23:24:48 +00:00
type processFlags struct {
glossaryPath * string
outputPath * string
reportJSONPath * string
modules * string
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 {
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" ) ,
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
}
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" )
fmt . Fprintln ( w )
fmt . Fprintln ( w , "Example:" )
fmt . Fprintln ( w , " audita process transcript.json --glossary glossary.yaml --output corrected.json" )
}
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" )
}