package cli import ( "bytes" "context" "fmt" "io" "strings" "time" ck "gitea.maximumdirect.net/eric/ConfigKit" "gitea.maximumdirect.net/eric/audita/internal/core/chunking" "gitea.maximumdirect.net/eric/audita/internal/core/config" "gitea.maximumdirect.net/eric/audita/internal/core/diagnostics" coreio "gitea.maximumdirect.net/eric/audita/internal/core/io" "gitea.maximumdirect.net/eric/audita/internal/core/normalization" "gitea.maximumdirect.net/eric/audita/internal/core/outputschema" "gitea.maximumdirect.net/eric/audita/internal/core/schema" "gitea.maximumdirect.net/eric/audita/internal/framework/contracts" "gitea.maximumdirect.net/eric/audita/internal/framework/llm" "gitea.maximumdirect.net/eric/audita/internal/framework/modules" "gitea.maximumdirect.net/eric/audita/internal/framework/processreport" "gitea.maximumdirect.net/eric/audita/internal/framework/runner" ) type processInvocation struct { TranscriptPath string GlossaryPath string OutputPath string ReportDir string Config config.Config ConfigResult *ck.Result[config.Config] LLMRuntime *llm.Runtime ConfigPath string ConfigSource string Overlay string ConfigVersion *int } var processRunner = func(ctx context.Context, inv processInvocation, stdout io.Writer) (*normalization.NormalizationSummary, *chunking.Summary, *runner.RunOutput, *diagnostics.RunDirectory, error) { runDir, err := diagnostics.NewRunDirectory(inv.Config.Diagnostics.WorkDir, "always", llm.ConfiguredSecrets(&inv.Config)...) if err != nil { return nil, nil, nil, nil, fmt.Errorf("run_dir_creation: %w", err) } fail := func(phase string, err error, runOutput *runner.RunOutput) (*normalization.NormalizationSummary, *chunking.Summary, *runner.RunOutput, *diagnostics.RunDirectory, error) { _ = runDir.WriteErrorLog(fmt.Sprintf("%s: %s", phase, llm.RedactError(err, inv.Config))) return nil, nil, runOutput, runDir, fmt.Errorf("%s: %w", phase, err) } if err := runDir.WriteInvocationMetadata(diagnostics.InvocationMetadata{ Operation: "process", TranscriptPath: inv.TranscriptPath, GlossaryPath: inv.GlossaryPath, OutputPath: inv.OutputPath, ConfigPath: inv.ConfigPath, ConfigSource: inv.ConfigSource, Overlay: inv.Overlay, ConfigVersion: inv.ConfigVersion, TranscriptDescription: inv.Config.Context.Description, Modules: append([]string(nil), inv.Config.Pipeline.Modules...), }); err != nil { _ = runDir.WriteErrorLog(fmt.Sprintf("invocation_metadata: %v", err)) } if inv.ConfigResult != nil { var effective bytes.Buffer if err := inv.ConfigResult.WriteEffective(&effective); err == nil { err = runDir.WriteEffectiveConfig(effective.Bytes()) } if err != nil { _ = runDir.WriteErrorLog(fmt.Sprintf("effective_config: %v", err)) } if origins, err := llm.ExplanationYAML(inv.ConfigResult); err == nil { _ = runDir.WriteYAMLArtifact(diagnostics.ArtifactConfigOrigins, origins) } } if inv.LLMRuntime != nil { if profiles, err := inv.LLMRuntime.SelectedProfilesYAML(); err == nil { _ = runDir.WriteYAMLArtifact(diagnostics.ArtifactSelectedProfiles, profiles) } } transcriptBytes, err := coreio.ReadRequiredFile(inv.TranscriptPath, "transcript") if err != nil { return fail("transcript_read", err, nil) } glossaryBytes, err := coreio.ReadRequiredFile(inv.GlossaryPath, "glossary") if err != nil { return fail("glossary_read", err, nil) } sourceTranscript, err := schema.ParseSourceTranscriptJSON(transcriptBytes) if err != nil { return fail("transcript_schema", err, nil) } glossary, err := schema.ParseGlossaryYAML(glossaryBytes) if err != nil { return fail("glossary_schema", err, nil) } if err := runDir.WriteSourceTranscript(sourceTranscript, transcriptBytes); err != nil { _ = runDir.WriteErrorLog(fmt.Sprintf("source_artifact: %v", err)) } canonical := sourceToCanonicalTranscript(sourceTranscript) 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, }) normalizedTranscript, normSummary := normalizer.Normalize(canonical) if err := runDir.WriteNormalizedTranscript(normalizedTranscript); err != nil { _ = runDir.WriteErrorLog(fmt.Sprintf("normalized_artifact: %v", err)) } if err := runDir.WriteNormalizationSummary(normSummary); err != nil { _ = runDir.WriteErrorLog(fmt.Sprintf("normalization_summary: %v", err)) } // Compute chunks after normalization chunker := chunking.NewChunker(chunking.ChunkingConfig{ MaxSectionTokens: inv.Config.Chunking.MaxSectionTokens, MinSectionTokens: inv.Config.Chunking.MinSectionTokens, TargetSections: inv.Config.Chunking.TargetSections, }) sections, chunkErr := chunker.ChunkTranscript(normalizedTranscript) if chunkErr != nil { return fail("chunking", chunkErr, nil) } chunkConfig := chunking.ChunkingConfig{ MaxSectionTokens: inv.Config.Chunking.MaxSectionTokens, MinSectionTokens: inv.Config.Chunking.MinSectionTokens, TargetSections: inv.Config.Chunking.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)) } workingTranscript := normalizedTranscript var runOutput *runner.RunOutput observations := llm.NewObservationCollector() moduleFactory := modules.NewFactory(modules.Dependencies{Config: &inv.Config, Glossary: glossary, DiagnosticsDir: runDir.Path()}) if inv.LLMRuntime == nil { return fail("runner_setup", fmt.Errorf("LLM runtime is required"), nil) } proposalLLMClient, err := inv.LLMRuntime.NewRoleClient("proposal", observations.Hooks()) if err != nil { return fail("runner_setup", err, nil) } validationLLMClient, err := inv.LLMRuntime.NewRoleClient("validation", observations.Hooks()) if err != nil { return fail("runner_setup", err, nil) } moduleSpecs, err := contracts.ResolveModuleRunSpecs(inv.Config.Pipeline.Modules) if err != nil { return fail("runner_setup", err, nil) } runnerResult, runErr := runner.New(moduleFactory).Run(ctx, runner.RunInput{ Config: &inv.Config, Transcript: normalizedTranscript, Glossary: glossary, ModuleSpecs: moduleSpecs, ProposalLLMClient: proposalLLMClient, ProposalDiagnosticsDir: runDir.Path(), ValidationLLMClient: validationLLMClient, ValidationDiagnosticsDir: runDir.Path(), }) runnerResult.Observations = observations.Snapshot() runnerResult.ObservationsAvailable = true runnerResult.LLMSummary = llm.Summarize(runnerResult.Observations) runOutput = &runnerResult if runErr != nil { return fail("runner_execution", runErr, runOutput) } workingTranscript = runnerResult.FinalTranscript encoderDef, err := outputschema.Resolve(inv.Config.Output.Schema) if err != nil { return fail("output_schema", err, runOutput) } outputBytes, err := encoderDef.Encoder(workingTranscript) if err != nil { return fail("serialization", err, runOutput) } if strings.TrimSpace(inv.OutputPath) != "" { if err := coreio.WriteFile(inv.OutputPath, outputBytes); err != nil { return fail("output_write", err, runOutput) } return normSummary, &chunkSummary, runOutput, runDir, nil } if _, err := stdout.Write(outputBytes); err != nil { return fail("stdout_write", err, runOutput) } return normSummary, &chunkSummary, runOutput, runDir, nil } 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, } } return &schema.Transcript{Segments: segments} } // Run executes the Audita CLI with the provided arguments and streams. func Run(ctx context.Context, args []string, stdout, stderr io.Writer) int { if len(args) == 0 { writeRootUsage(stdout) return 0 } if len(args) == 2 && args[0] == "help" { switch args[1] { case "process": writeProcessUsage(stdout) return 0 case "config": writeConfigUsage(stdout) return 0 } } if isHelpCommand(args) { writeRootUsage(stdout) return 0 } if args[0] == "process" { return runProcess(ctx, args[1:], stdout, stderr) } if args[0] == "config" { return runConfig(ctx, args[1:], stdout, stderr) } fmt.Fprintf(stderr, "audita: unknown command %q\n\n", args[0]) writeRootUsage(stderr) return 2 } func runProcess(ctx context.Context, args []string, stdout, stderr io.Writer) int { startedAt := time.Now().UTC() input, help, err := parseConfigInput("process", args, stderr) if err != nil { fmt.Fprintf(stderr, "audita process: %s\n", configInputError(err)) return 2 } if help { writeProcessUsage(stdout) return 0 } selected, err := loadConfigInput(input, "process") if err != nil { fmt.Fprintf(stderr, "audita process: %s\n", configInputError(err)) return 2 } reportDir, err := selectedReportDir(selected.Result.Config, input.OutputPath) if err != nil { fmt.Fprintf(stderr, "audita process: %s\n", configInputError(err)) return 2 } if selected.Result.Config.Diagnostics.Debug && strings.TrimSpace(selected.Result.Config.Diagnostics.WorkDir) == "" { fmt.Fprintln(stderr, "audita process: debug work directory is required") return 2 } llmRuntime, err := llm.NewRuntime(ctx, selected.Result.Config, selected.Path, nil) if err != nil { fmt.Fprintf(stderr, "audita process: %s\n", llm.RedactError(err, selected.Result.Config)) if llm.IsAssetError(err) { return 1 } return 2 } version := selected.Result.Config.Version inv := processInvocation{ TranscriptPath: input.TranscriptPath, GlossaryPath: input.GlossaryPath, OutputPath: input.OutputPath, ReportDir: reportDir, Config: selected.Result.Config, ConfigResult: selected.Result, LLMRuntime: llmRuntime, ConfigPath: selected.Path, ConfigSource: selected.Source, Overlay: input.Overlay, ConfigVersion: &version, } normSummary, chunkSummary, runOutput, runDir, runErr := processRunner(ctx, inv, stdout) completedAt := time.Now().UTC() if runErr != nil { if runDir != nil && runOutput != nil { writeExecutionArtifacts(runDir, runOutput, inv.Config) _ = runDir.WriteJSONArtifact(diagnostics.ArtifactCorrectionLedger, processreport.BuildCorrectionLedger(processreport.CorrectionLedgerInput{ RunDirectoryPath: runDir.Path(), RunOutput: runOutput, })) } errorPhase, errorMessage := extractErrorPhase(runErr) errorMessage = llm.RedactError(fmt.Errorf("%s", errorMessage), inv.Config) report := processreport.Build(processReportInput("failed", inv, runDir, startedAt, completedAt, errorMessage, errorPhase, nil, nil, runOutput)) if runDir != nil { _ = runDir.WriteReport(report) } fmt.Fprintf(stderr, "audita process: %s\n", llm.RedactError(runErr, inv.Config)) if runDir != nil { fmt.Fprintf(stderr, "audita process: diagnostics: %s\n", runDir.Path()) } return 1 } if runDir != nil && runOutput != nil { writeExecutionArtifacts(runDir, runOutput, inv.Config) _ = runDir.WriteJSONArtifact(diagnostics.ArtifactCorrectionLedger, processreport.BuildCorrectionLedger(processreport.CorrectionLedgerInput{ RunDirectoryPath: runDir.Path(), RunOutput: runOutput, })) } report := processreport.Build(processReportInput("success", inv, runDir, startedAt, completedAt, "", "", normSummary, chunkSummary, runOutput)) if runDir != nil { _ = runDir.WriteReport(report) } return 0 } func writeExecutionArtifacts(runDir *diagnostics.RunDirectory, output *runner.RunOutput, cfg config.Config) { if !output.ObservationsAvailable { return } _ = runDir.WriteJSONArtifact(diagnostics.ArtifactLLMExecution, processreport.BuildLLMExecution(output.Observations)) _ = runDir.WriteJSONArtifact(diagnostics.ArtifactUtilizationSummary, processreport.BuildUtilization(output, cfg.Backends)) } func runConfig(ctx context.Context, args []string, stdout, stderr io.Writer) int { if len(args) == 0 || isHelpCommand(args) || args[0] == "--help" || args[0] == "-h" { writeConfigUsage(stdout) return 0 } switch args[0] { case "validate": return runConfigValidate(ctx, args[1:], stdout, stderr) case "print-effective": return runConfigPrintEffective(ctx, args[1:], stdout, stderr) default: fmt.Fprintf(stderr, "audita config: unknown command %q\n\n", args[0]) writeConfigUsage(stderr) return 2 } } func runConfigValidate(ctx context.Context, args []string, stdout, stderr io.Writer) int { input, help, err := parseConfigInput("validate", args, stderr) if err != nil { fmt.Fprintf(stderr, "audita config validate: %s\n", configInputError(err)) return 2 } if help { writeConfigValidateUsage(stdout) return 0 } selected, err := loadConfigInput(input, "validate") if err != nil { fmt.Fprintf(stderr, "audita config validate: %s\n", configInputError(err)) return 2 } if _, err := llm.NewRuntime(ctx, selected.Result.Config, selected.Path, nil); err != nil { fmt.Fprintf(stderr, "audita config validate: %s\n", llm.RedactError(err, selected.Result.Config)) if llm.IsAssetError(err) { return 1 } return 2 } fmt.Fprintln(stdout, "config is valid") return 0 } func runConfigPrintEffective(ctx context.Context, args []string, stdout, stderr io.Writer) int { input, help, err := parseConfigInput("print-effective", args, stderr) if err != nil { fmt.Fprintf(stderr, "audita config print-effective: %s\n", configInputError(err)) return 2 } if help { writeConfigPrintEffectiveUsage(stdout) return 0 } selected, err := loadConfigInput(input, "print-effective") if err != nil { fmt.Fprintf(stderr, "audita config print-effective: %s\n", configInputError(err)) return 2 } llmRuntime, err := llm.NewRuntime(ctx, selected.Result.Config, selected.Path, nil) if err != nil { fmt.Fprintf(stderr, "audita config print-effective: %s\n", llm.RedactError(err, selected.Result.Config)) if llm.IsAssetError(err) { return 1 } return 2 } if input.View == "settings" { err = selected.Result.WriteEffective(stdout) } else { var output []byte if input.View == "profiles" { output, err = llmRuntime.SelectedProfilesYAML() } else { output, err = llm.ExplanationYAML(selected.Result) } if err == nil { _, err = stdout.Write(output) } } if err != nil { fmt.Fprintf(stderr, "audita config print-effective: %v\n", err) return 1 } return 0 } 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 } 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 := "" if runDir != nil { runDirectoryPath = runDir.Path() } return processreport.BuildInput{ Status: status, TranscriptPath: inv.TranscriptPath, GlossaryPath: inv.GlossaryPath, OutputPath: inv.OutputPath, Modules: inv.Config.Pipeline.Modules, OutputSchema: inv.Config.Output.Schema, ConfigVersion: inv.ConfigVersion, StartedAt: startedAt, CompletedAt: completedAt, ErrorMessage: errorMessage, ErrorPhase: errorPhase, RunDirectoryPath: runDirectoryPath, NormalizationSummary: normalizationSummary, ChunkingSummary: chunkingSummary, RunOutput: runOutput, Backends: inv.Config.Backends, } } 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 writeRootUsage(w io.Writer) { fmt.Fprintln(w, "Audita is a transcript processing CLI.") fmt.Fprintln(w) fmt.Fprintln(w, "Usage:") fmt.Fprintln(w, " audita [options]") fmt.Fprintln(w) fmt.Fprintln(w, "Commands:") fmt.Fprintln(w, " process Process a transcript JSON file") fmt.Fprintln(w, " config Validate and inspect config") fmt.Fprintln(w) fmt.Fprintln(w, "Example:") fmt.Fprintln(w, " audita process transcript.json --glossary glossary.yaml --output corrected.json") } 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 [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 settings YAML") } 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 [--overlay ] [setting flags]") } func writeConfigPrintEffectiveUsage(w io.Writer) { fmt.Fprintln(w, "Print redacted effective settings YAML.") fmt.Fprintln(w) fmt.Fprintln(w, "Usage:") fmt.Fprintln(w, " audita config print-effective [--config ] [--overlay ] [setting flags]") } func writeProcessUsage(w io.Writer) { fmt.Fprintln(w, "Process a transcript JSON file.") fmt.Fprintln(w) fmt.Fprintln(w, "Usage:") fmt.Fprintln(w, " audita process [flags]") fmt.Fprintln(w) fmt.Fprintln(w, "Flags:") for _, name := range []string{ "config", "overlay", "glossary", "output", "proposal-profile", "validation-profile", "modules", "output-schema", "validation-max-prompt-tokens", "target-sections", "max-section-tokens", "min-section-tokens", "normalize-max-segment-gap", "normalize-ellipsis-gap", "normalize-max-segment-duration", "normalize-max-segment-tokens", "glossary-confidence-threshold", "homophones-confidence-threshold", "spoken-word-confidence-threshold", "grammar-confidence-threshold", "transcript-description", "work-dir", } { fmt.Fprintf(w, " --%s\n", name) } fmt.Fprintln(w, " --reporting[=true|false] Write the three-file report bundle (default true)") fmt.Fprintln(w, " --report-dir DIR Report directory for stdout output only; ignored with --output or --reporting=false") fmt.Fprintln(w, " --debug[=true|false] Persist detailed diagnostics (default false)") fmt.Fprintln(w) fmt.Fprintln(w, "Example:") fmt.Fprintln(w, " audita process transcript.json --glossary glossary.yaml --output corrected.json") }