Unify CLI configuration selection and overrides
This commit is contained in:
@@ -2,9 +2,6 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -12,6 +9,7 @@ import (
|
||||
"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"
|
||||
@@ -53,6 +51,7 @@ type processInvocation struct {
|
||||
OutputPath string
|
||||
ReportJSONPath string
|
||||
Config config.Config
|
||||
ConfigResult *ck.Result[config.Config]
|
||||
ConfigPath string
|
||||
ConfigSource string
|
||||
ConfigVersion *int
|
||||
@@ -349,6 +348,16 @@ func Run(args []string, stdout, stderr io.Writer) int {
|
||||
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)
|
||||
@@ -369,89 +378,38 @@ func Run(args []string, stdout, stderr io.Writer) int {
|
||||
|
||||
func runProcess(args []string, stdout, stderr io.Writer) int {
|
||||
startedAt := time.Now().UTC()
|
||||
|
||||
configPathOverride, configPathOverrideSet, err := findConfigPathOverride(args)
|
||||
input, help, err := parseConfigInput("process", args, stderr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "audita process: invalid CLI configuration: %v\n", err)
|
||||
fmt.Fprintf(stderr, "audita process: %s\n", configInputError(err))
|
||||
return 2
|
||||
}
|
||||
effectiveConfig, err := config.LoadEffectiveConfig(configPathOverride, configPathOverrideSet)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
cfg := effectiveConfig.Config
|
||||
configPath := effectiveConfig.ConfigPath
|
||||
configSource := effectiveConfig.ConfigSource
|
||||
configVersion := effectiveConfig.ConfigVersion
|
||||
|
||||
fs, pFlags := newProcessFlagSet(cfg, stderr)
|
||||
|
||||
if isHelpCommand(args) || hasHelpFlag(args) {
|
||||
writeProcessUsage(stdout, fs)
|
||||
if help {
|
||||
writeProcessUsage(stdout)
|
||||
return 0
|
||||
}
|
||||
|
||||
parseArgs := args
|
||||
transcriptFromFront := ""
|
||||
if len(args) > 0 && !strings.HasPrefix(args[0], "-") {
|
||||
transcriptFromFront = args[0]
|
||||
parseArgs = args[1:]
|
||||
selected, err := loadConfigInput(input, "process")
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "audita process: %s\n", configInputError(err))
|
||||
return 2
|
||||
}
|
||||
|
||||
if err := fs.Parse(parseArgs); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
writeProcessUsage(stdout, fs)
|
||||
return 0
|
||||
version := selected.Result.Config.Version
|
||||
explicitModules := false
|
||||
for _, override := range input.Overrides {
|
||||
if override.Path.String() == ck.Keys("pipeline", "modules").String() {
|
||||
explicitModules = true
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
overrides, explicitModules := processCLIOverrides(fs, pFlags)
|
||||
|
||||
if err := cfg.ApplyCLIOverrides(overrides); err != nil {
|
||||
fmt.Fprintf(stderr, "audita process: invalid CLI configuration: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
|
||||
remaining := fs.Args()
|
||||
positional := make([]string, 0, len(remaining)+1)
|
||||
if transcriptFromFront != "" {
|
||||
positional = append(positional, transcriptFromFront)
|
||||
}
|
||||
positional = append(positional, remaining...)
|
||||
|
||||
if len(positional) != 1 {
|
||||
fmt.Fprintln(stderr, "audita process: expected exactly 1 transcript JSON path argument")
|
||||
return 2
|
||||
}
|
||||
if strings.TrimSpace(*pFlags.glossaryPath) == "" {
|
||||
fmt.Fprintln(stderr, "audita process: --glossary is required")
|
||||
return 2
|
||||
}
|
||||
|
||||
inv := processInvocation{
|
||||
TranscriptPath: positional[0],
|
||||
GlossaryPath: *pFlags.glossaryPath,
|
||||
OutputPath: *pFlags.outputPath,
|
||||
ReportJSONPath: *pFlags.reportJSONPath,
|
||||
Config: cfg,
|
||||
ConfigPath: configPath,
|
||||
ConfigSource: configSource,
|
||||
ConfigVersion: configVersion,
|
||||
TranscriptPath: input.TranscriptPath,
|
||||
GlossaryPath: input.GlossaryPath,
|
||||
OutputPath: input.OutputPath,
|
||||
ReportJSONPath: input.ReportPath,
|
||||
Config: selected.Result.Config,
|
||||
ConfigResult: selected.Result,
|
||||
ConfigPath: selected.Path,
|
||||
ConfigSource: selected.Source,
|
||||
ConfigVersion: &version,
|
||||
ExplicitModules: explicitModules,
|
||||
}
|
||||
|
||||
@@ -531,7 +489,7 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
|
||||
}
|
||||
|
||||
func runConfig(args []string, stdout, stderr io.Writer) int {
|
||||
if len(args) == 0 || isHelpCommand(args) || hasHelpFlag(args) {
|
||||
if len(args) == 0 || isHelpCommand(args) || args[0] == "--help" || args[0] == "-h" {
|
||||
writeConfigUsage(stdout)
|
||||
return 0
|
||||
}
|
||||
@@ -549,38 +507,17 @@ func runConfig(args []string, stdout, stderr io.Writer) int {
|
||||
}
|
||||
|
||||
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))
|
||||
input, help, err := parseConfigInput("validate", args, stderr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "audita config validate: %v\n", err)
|
||||
fmt.Fprintf(stderr, "audita config validate: %s\n", configInputError(err))
|
||||
return 2
|
||||
}
|
||||
cfg := config.Default()
|
||||
if err := cfg.ApplyFileConfig(fileCfg); err != nil {
|
||||
fmt.Fprintf(stderr, "audita config validate: %v\n", err)
|
||||
return 2
|
||||
if help {
|
||||
writeConfigValidateUsage(stdout)
|
||||
return 0
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
fmt.Fprintf(stderr, "audita config validate: %v\n", err)
|
||||
if _, err := loadConfigInput(input, "validate"); err != nil {
|
||||
fmt.Fprintf(stderr, "audita config validate: %s\n", configInputError(err))
|
||||
return 2
|
||||
}
|
||||
fmt.Fprintln(stdout, "config is valid")
|
||||
@@ -588,39 +525,21 @@ func runConfigValidate(args []string, stdout, stderr io.Writer) int {
|
||||
}
|
||||
|
||||
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 != ""
|
||||
effectiveConfig, err := config.LoadEffectiveConfig(configPathValue, configPathSet)
|
||||
input, help, err := parseConfigInput("print-effective", args, stderr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "audita config print-effective: %v\n", err)
|
||||
fmt.Fprintf(stderr, "audita config print-effective: %s\n", configInputError(err))
|
||||
return 2
|
||||
}
|
||||
|
||||
cfg := effectiveConfig.Config
|
||||
|
||||
redacted := cfg.Redacted()
|
||||
out, err := json.MarshalIndent(redacted, "", " ")
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "audita config print-effective: %v\n", err)
|
||||
return 1
|
||||
if help {
|
||||
writeConfigPrintEffectiveUsage(stdout)
|
||||
return 0
|
||||
}
|
||||
out = append(out, '\n')
|
||||
if _, err := stdout.Write(out); err != nil {
|
||||
selected, err := loadConfigInput(input, "print-effective")
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "audita config print-effective: %s\n", configInputError(err))
|
||||
return 2
|
||||
}
|
||||
if err := selected.Result.WriteEffective(stdout); err != nil {
|
||||
fmt.Fprintf(stderr, "audita config print-effective: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
@@ -662,128 +581,6 @@ func processReportInput(status string, inv processInvocation, runDir *diagnostic
|
||||
}
|
||||
}
|
||||
|
||||
type processFlags struct {
|
||||
configPath *string
|
||||
glossaryPath *string
|
||||
outputPath *string
|
||||
reportJSONPath *string
|
||||
modules *string
|
||||
outputSchema *string
|
||||
llmAPIKey *string
|
||||
validationLLMAPIKey *string
|
||||
model *string
|
||||
validationModel *string
|
||||
baseURL *string
|
||||
validationBaseURL *string
|
||||
llmTimeoutSeconds *int
|
||||
totalLLMConcurrency *int
|
||||
proposalLLMConcurrency *int
|
||||
llmConcurrency *int
|
||||
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
|
||||
transcriptDescription *string
|
||||
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
|
||||
}
|
||||
|
||||
validationLLMConcurrencyDefault := cfg.TotalLLMConcurrency
|
||||
if cfg.ValidationLLMConcurrency != nil {
|
||||
validationLLMConcurrencyDefault = *cfg.ValidationLLMConcurrency
|
||||
}
|
||||
|
||||
targetSectionsDefault := 0
|
||||
if cfg.TargetSections != nil {
|
||||
targetSectionsDefault = *cfg.TargetSections
|
||||
}
|
||||
|
||||
pFlags := processFlags{
|
||||
configPath: fs.String("config", "", "Path to versioned YAML config file"),
|
||||
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"),
|
||||
outputSchema: fs.String("output-schema", cfg.OutputSchema, "Output schema: bare-segments|audita-v1"),
|
||||
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"),
|
||||
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"),
|
||||
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"),
|
||||
validationLLMConcurrency: fs.Int("validation-llm-concurrency", validationLLMConcurrencyDefault, "Concurrent validation LLM calls (inherits total when unset)"),
|
||||
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"),
|
||||
transcriptDescription: fs.String("transcript-description", cfg.TranscriptDescription, "Brief background context for LLM prompts; does not override transcript content"),
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func isHelpCommand(args []string) bool {
|
||||
if len(args) == 0 {
|
||||
return false
|
||||
@@ -803,15 +600,6 @@ func isHelpCommand(args []string) bool {
|
||||
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) {
|
||||
fmt.Fprintln(w, "Audita is a transcript processing CLI.")
|
||||
fmt.Fprintln(w)
|
||||
@@ -834,33 +622,40 @@ func writeConfigUsage(w io.Writer) {
|
||||
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)")
|
||||
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 <path>")
|
||||
fmt.Fprintln(w, " audita config validate --config <path> [--overlay <name>] [setting flags]")
|
||||
}
|
||||
|
||||
func writeConfigPrintEffectiveUsage(w io.Writer) {
|
||||
fmt.Fprintln(w, "Print redacted effective config JSON.")
|
||||
fmt.Fprintln(w, "Print redacted effective settings YAML.")
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintln(w, "Usage:")
|
||||
fmt.Fprintln(w, " audita config print-effective [--config <path>]")
|
||||
fmt.Fprintln(w, " audita config print-effective [--config <path>] [--overlay <name>] [setting flags]")
|
||||
}
|
||||
|
||||
func writeProcessUsage(w io.Writer, fs *flag.FlagSet) {
|
||||
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 <transcript.json> [flags]")
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintln(w, "Flags:")
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
fmt.Fprintf(w, " --%s\n", f.Name)
|
||||
})
|
||||
for _, name := range []string{
|
||||
"config", "overlay", "glossary", "output", "report-json",
|
||||
"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", "work-dir-retention",
|
||||
} {
|
||||
fmt.Fprintf(w, " --%s\n", name)
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintln(w, "Example:")
|
||||
fmt.Fprintln(w, " audita process transcript.json --glossary glossary.yaml --output corrected.json")
|
||||
|
||||
Reference in New Issue
Block a user