Add versioned Audita config support

This commit is contained in:
2026-05-13 12:36:20 +00:00
parent ebbd2c8a63
commit 9a77a0cd0b
11 changed files with 1539 additions and 32 deletions

View File

@@ -2,6 +2,7 @@ package cli
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
@@ -50,6 +51,8 @@ type processInvocation struct {
OutputPath string
ReportJSONPath string
Config config.Config
ConfigPath string
ConfigSource string
ExplicitModules bool
}
@@ -79,6 +82,8 @@ var processRunner = func(inv processInvocation, stdout io.Writer) (*normalizatio
GlossaryPath: inv.GlossaryPath,
OutputPath: inv.OutputPath,
ReportJSONPath: inv.ReportJSONPath,
ConfigPath: inv.ConfigPath,
ConfigSource: inv.ConfigSource,
TranscriptDescription: inv.Config.TranscriptDescription,
Modules: append([]string(nil), inv.Config.Modules...),
}); err != nil {
@@ -340,6 +345,9 @@ func Run(args []string, stdout, stderr io.Writer) int {
if args[0] == "process" {
return runProcess(args[1:], stdout, stderr)
}
if args[0] == "config" {
return runConfig(args[1:], stdout, stderr)
}
fmt.Fprintf(stderr, "audita: unknown command %q\n\n", args[0])
writeRootUsage(stderr)
@@ -349,8 +357,30 @@ func Run(args []string, stdout, stderr io.Writer) int {
func runProcess(args []string, stdout, stderr io.Writer) int {
startedAt := time.Now().UTC()
cfg, err := config.LoadFromEnv()
configPathOverride, configPathOverrideSet, err := findConfigPathOverride(args)
if err != nil {
fmt.Fprintf(stderr, "audita process: invalid CLI configuration: %v\n", err)
return 2
}
configPath, configSource, err := resolveConfigPath(configPathOverride, configPathOverrideSet, os.LookupEnv)
if err != nil {
fmt.Fprintf(stderr, "audita process: %v\n", err)
return 2
}
cfg := config.Default()
if configPath != "" {
fileCfg, fileErr := config.LoadFileConfig(configPath)
if fileErr != nil {
fmt.Fprintf(stderr, "audita process: invalid config file: %v\n", fileErr)
return 2
}
if applyErr := cfg.ApplyFileConfig(fileCfg); applyErr != nil {
fmt.Fprintf(stderr, "audita process: invalid config file: %v\n", applyErr)
return 2
}
}
if err := cfg.ApplyEnvOverrides(); err != nil {
fmt.Fprintf(stderr, "audita process: invalid environment configuration: %v\n", err)
return 2
}
@@ -472,6 +502,8 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
OutputPath: *pFlags.outputPath,
ReportJSONPath: *pFlags.reportJSONPath,
Config: cfg,
ConfigPath: configPath,
ConfigSource: configSource,
ExplicitModules: explicitModules,
}
@@ -541,6 +573,114 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
return 0
}
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
}
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 != ""
path, _, err := resolveConfigPath(configPathValue, configPathSet, os.LookupEnv)
if err != nil {
fmt.Fprintf(stderr, "audita config print-effective: %v\n", err)
return 2
}
cfg := config.Default()
if path != "" {
fileCfg, fileErr := config.LoadFileConfig(path)
if fileErr != nil {
fmt.Fprintf(stderr, "audita config print-effective: %v\n", fileErr)
return 2
}
if applyErr := cfg.ApplyFileConfig(fileCfg); applyErr != nil {
fmt.Fprintf(stderr, "audita config print-effective: %v\n", applyErr)
return 2
}
}
if err := cfg.ApplyEnvOverrides(); err != nil {
fmt.Fprintf(stderr, "audita config print-effective: %v\n", err)
return 2
}
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
}
func extractErrorPhase(err error) (phase string, message string) {
msg := err.Error()
if strings.Contains(msg, ": ") {
@@ -683,6 +823,7 @@ func mapValidatorRejected(in []runner.ValidatorRejectedChange) []reporting.Valid
}
type processFlags struct {
configPath *string
glossaryPath *string
outputPath *string
reportJSONPath *string
@@ -743,6 +884,7 @@ func newProcessFlagSet(cfg config.Config, stderr io.Writer) (*flag.FlagSet, proc
}
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"),
@@ -781,6 +923,63 @@ func newProcessFlagSet(cfg config.Config, stderr io.Writer) (*flag.FlagSet, proc
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 resolveConfigPath(cliConfigPath string, cliConfigPathSet bool, lookup func(string) (string, bool)) (path string, source string, err error) {
if cliConfigPathSet {
path = strings.TrimSpace(cliConfigPath)
if path == "" {
return "", "", fmt.Errorf("--config requires a non-empty path")
}
if _, statErr := os.Stat(path); statErr != nil {
if os.IsNotExist(statErr) {
return "", "", fmt.Errorf("config file not found: %s", path)
}
return "", "", fmt.Errorf("cannot access config file %s: %w", path, statErr)
}
return path, "flag", nil
}
if raw, ok := lookup("AUDITA_CONFIG"); ok {
path = strings.TrimSpace(raw)
if path == "" {
return "", "", fmt.Errorf("AUDITA_CONFIG must not be empty")
}
if _, statErr := os.Stat(path); statErr != nil {
if os.IsNotExist(statErr) {
return "", "", fmt.Errorf("config file not found: %s", path)
}
return "", "", fmt.Errorf("cannot access config file %s: %w", path, statErr)
}
return path, "env", nil
}
defaultPath := config.DefaultConfigPath
if _, statErr := os.Stat(defaultPath); statErr == nil {
return defaultPath, "default", nil
} else if !os.IsNotExist(statErr) {
return "", "", fmt.Errorf("cannot access config file %s: %w", defaultPath, statErr)
}
return "", "", nil
}
func isHelpCommand(args []string) bool {
if len(args) == 0 {
return false
@@ -817,11 +1016,37 @@ func writeRootUsage(w io.Writer) {
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 <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>]")
}
func writeProcessUsage(w io.Writer, fs *flag.FlagSet) {
fmt.Fprintln(w, "Process a transcript JSON file.")
fmt.Fprintln(w)