Add reporting and debug settings with context-aware CLI selection

This commit is contained in:
2026-09-24 17:27:11 +00:00
parent b98604e5ca
commit 63fcfc487d
8 changed files with 168 additions and 72 deletions

View File

@@ -1,11 +1,20 @@
package main package main
import ( import (
"context"
"os" "os"
"os/signal"
"syscall"
"gitea.maximumdirect.net/eric/audita/internal/cli" "gitea.maximumdirect.net/eric/audita/internal/cli"
) )
func main() { func main() {
os.Exit(cli.Run(os.Args[1:], os.Stdout, os.Stderr)) os.Exit(run())
}
func run() int {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
return cli.Run(ctx, os.Args[1:], os.Stdout, os.Stderr)
} }

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
@@ -23,7 +24,7 @@ func TestProcessSubprocessHelper(t *testing.T) {
} }
for i, arg := range os.Args { for i, arg := range os.Args {
if arg == "--" { if arg == "--" {
os.Exit(cli.Run(os.Args[i+1:], os.Stdout, os.Stderr)) os.Exit(cli.Run(context.Background(), os.Args[i+1:], os.Stdout, os.Stderr))
} }
} }
os.Exit(2) os.Exit(2)

View File

@@ -589,6 +589,16 @@ focused runner/runtime race tests when those packages are testable.
## Stage 7 — Add Reporting and Debug Settings and CLI Selection ## Stage 7 — Add Reporting and Debug Settings and CLI Selection
**Completed 2026-09-24.** Version-2 settings now resolve reporting and debug
independently, with typed visited boolean overrides and destination selection
only for enabled reports. The old JSON-report and retention flags/settings were
removed; CLI entry points now carry a context from interrupt/SIGTERM handling.
ConfigKit tests and focused CLI parser tests cover defaults, overlay false
overrides, bare booleans, destination selection, and removed flags. `go test
./internal/core/config`, `go test internal/cli/config_input.go
internal/cli/config_input_test.go`, and `git diff --check` passed. Whole CLI tests
remain unavailable until Stage 8 replaces the report builder's warning fields.
**Work:** Implement A in `core/config/config.go`, `cli/config_input.go`, invocation **Work:** Implement A in `core/config/config.go`, `cli/config_input.go`, invocation
state, and usage text. Add boolean flags with visited-value overrides and update state, and usage text. Add boolean flags with visited-value overrides and update
safe effective-setting/provenance output. Remove the old external JSON report flag safe effective-setting/provenance output. Remove the old external JSON report flag

View File

@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
@@ -23,7 +24,6 @@ type configInput struct {
TranscriptPath string TranscriptPath string
GlossaryPath string GlossaryPath string
OutputPath string OutputPath string
ReportPath string
Overrides []ck.Override Overrides []ck.Override
} }
@@ -61,12 +61,16 @@ func parseConfigInput(command string, args []string, output io.Writer) (configIn
"spoken-word-confidence-threshold": ck.Keys("thresholds", "spoken_word"), "spoken-word-confidence-threshold": ck.Keys("thresholds", "spoken_word"),
"grammar-confidence-threshold": ck.Keys("thresholds", "grammar"), "grammar-confidence-threshold": ck.Keys("thresholds", "grammar"),
"transcript-description": ck.Keys("context", "description"), "transcript-description": ck.Keys("context", "description"),
"reporting": ck.Keys("reporting", "enabled"),
"report-dir": ck.Keys("reporting", "stdout_dir"),
"debug": ck.Keys("diagnostics", "debug"),
"work-dir": ck.Keys("diagnostics", "work_dir"), "work-dir": ck.Keys("diagnostics", "work_dir"),
"work-dir-retention": ck.Keys("diagnostics", "retention"),
"modules": ck.Keys("pipeline", "modules"), "modules": ck.Keys("pipeline", "modules"),
} }
for name := range settingPaths { for name := range settingPaths {
switch name { switch name {
case "reporting", "debug":
fs.Bool(name, false, "Setting override")
case "validation-max-prompt-tokens", "target-sections", "max-section-tokens", "min-section-tokens", "normalize-max-segment-tokens": case "validation-max-prompt-tokens", "target-sections", "max-section-tokens", "min-section-tokens", "normalize-max-segment-tokens":
fs.Int(name, 0, "Setting override") fs.Int(name, 0, "Setting override")
case "normalize-max-segment-gap", "normalize-ellipsis-gap", "normalize-max-segment-duration", "glossary-confidence-threshold", "homophones-confidence-threshold", "spoken-word-confidence-threshold", "grammar-confidence-threshold": case "normalize-max-segment-gap", "normalize-ellipsis-gap", "normalize-max-segment-duration", "glossary-confidence-threshold", "homophones-confidence-threshold", "spoken-word-confidence-threshold", "grammar-confidence-threshold":
@@ -78,7 +82,6 @@ func parseConfigInput(command string, args []string, output io.Writer) (configIn
if command == "process" { if command == "process" {
fs.String("glossary", "", "Glossary YAML file") fs.String("glossary", "", "Glossary YAML file")
fs.String("output", "", "Corrected transcript output file") fs.String("output", "", "Corrected transcript output file")
fs.String("report-json", "", "Process report output file")
} }
parseArgs := args parseArgs := args
front := "" front := ""
@@ -102,7 +105,6 @@ func parseConfigInput(command string, args []string, output io.Writer) (configIn
input.TranscriptPath = positions[0] input.TranscriptPath = positions[0]
input.GlossaryPath = fs.Lookup("glossary").Value.String() input.GlossaryPath = fs.Lookup("glossary").Value.String()
input.OutputPath = fs.Lookup("output").Value.String() input.OutputPath = fs.Lookup("output").Value.String()
input.ReportPath = fs.Lookup("report-json").Value.String()
if strings.TrimSpace(input.GlossaryPath) == "" { if strings.TrimSpace(input.GlossaryPath) == "" {
return configInput{}, false, fmt.Errorf("--glossary is required") return configInput{}, false, fmt.Errorf("--glossary is required")
} }
@@ -128,6 +130,8 @@ func parseConfigInput(command string, args []string, output io.Writer) (configIn
} }
var value any = f.Value.String() var value any = f.Value.String()
switch f.Name { switch f.Name {
case "reporting", "debug":
value, _ = strconv.ParseBool(f.Value.String())
case "modules": case "modules":
modules, err := config.ParseModulesCSV(f.Value.String()) modules, err := config.ParseModulesCSV(f.Value.String())
if err != nil { if err != nil {
@@ -203,3 +207,16 @@ func configInputError(err error) string {
} }
return strings.Join(messages, "; ") return strings.Join(messages, "; ")
} }
func selectedReportDir(cfg config.Config, outputPath string) (string, error) {
if !cfg.Reporting.Enabled {
return "", nil
}
if strings.TrimSpace(outputPath) != "" {
return filepath.Dir(outputPath), nil
}
if strings.TrimSpace(cfg.Reporting.StdoutDir) == "" {
return "", fmt.Errorf("report directory is required when reporting is enabled and transcript output is stdout")
}
return cfg.Reporting.StdoutDir, nil
}

View File

@@ -9,6 +9,7 @@ import (
"testing" "testing"
ck "gitea.maximumdirect.net/eric/ConfigKit" ck "gitea.maximumdirect.net/eric/ConfigKit"
"gitea.maximumdirect.net/eric/audita/internal/core/config"
) )
func TestSharedConfigInput(t *testing.T) { func TestSharedConfigInput(t *testing.T) {
@@ -43,7 +44,7 @@ func TestSharedConfigInput(t *testing.T) {
func TestConfigInputRejectsRemovedAndProcessOnlyFlags(t *testing.T) { func TestConfigInputRejectsRemovedAndProcessOnlyFlags(t *testing.T) {
for _, command := range []string{"process", "validate", "print-effective"} { for _, command := range []string{"process", "validate", "print-effective"} {
for _, flag := range []string{"--model", "--llm-api-key", "--max-retries", "--llm-concurrency"} { for _, flag := range []string{"--model", "--llm-api-key", "--max-retries", "--llm-concurrency", "--report-json", "--work-dir-retention"} {
args := []string{flag, "value"} args := []string{flag, "value"}
if command == "process" { if command == "process" {
args = append([]string{"transcript.json", "--glossary", "glossary.yml"}, args...) args = append([]string{"transcript.json", "--glossary", "glossary.yml"}, args...)
@@ -169,3 +170,81 @@ func TestSharedLoadingUsesSelectedFileOverlayAndVisitedOverrides(t *testing.T) {
t.Fatal("unmodified invalid lower-precedence section relation accepted") t.Fatal("unmodified invalid lower-precedence section relation accepted")
} }
} }
func TestReportingAndDebugOverridesRemainIndependent(t *testing.T) {
dir := t.TempDir()
if err := os.Mkdir(filepath.Join(dir, "overlays"), 0700); err != nil {
t.Fatal(err)
}
main := filepath.Join(dir, "config.yml")
base := "version: 2\nbackends:\n local:\n endpoint: https://local.invalid/v1\nllm:\n proposal_profile: base\n"
if err := os.WriteFile(main, []byte(base), 0600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "overlays", "testing.yml"), []byte("reporting:\n enabled: true\n stdout_dir: reports\ndiagnostics:\n debug: true\n"), 0600); err != nil {
t.Fatal(err)
}
defaultInput, _, err := parseConfigInput("validate", []string{"--config", main}, io.Discard)
if err != nil {
t.Fatal(err)
}
defaults, err := loadConfigInput(defaultInput, "validate")
if err != nil {
t.Fatal(err)
}
if !defaults.Result.Config.Reporting.Enabled || defaults.Result.Config.Reporting.StdoutDir != "." || defaults.Result.Config.Diagnostics.Debug {
t.Fatalf("unexpected defaults: %+v", defaults.Result.Config)
}
for _, command := range []string{"process", "validate", "print-effective"} {
args := []string{"--config", main, "--overlay", "testing", "--reporting=false", "--debug=false"}
if command == "process" {
args = append([]string{"input.json", "--glossary", "glossary.yml"}, args...)
}
input, _, err := parseConfigInput(command, args, io.Discard)
if err != nil {
t.Fatal(err)
}
if len(input.Overrides) != 2 || input.Overrides[0].Value != false || input.Overrides[1].Value != false {
t.Fatalf("boolean overrides must remain typed: %+v", input.Overrides)
}
selected, err := loadConfigInput(input, command)
if err != nil {
t.Fatal(err)
}
if selected.Result.Config.Reporting.Enabled || selected.Result.Config.Diagnostics.Debug || selected.Result.Config.Reporting.StdoutDir != "reports" {
t.Fatalf("%s did not honor explicit false: %+v", command, selected.Result.Config)
}
}
}
func TestReportDirectorySelectionOnlyUsesActiveDestination(t *testing.T) {
cfg := config.Config{Reporting: config.Reporting{Enabled: true, StdoutDir: "reports"}}
if got, err := selectedReportDir(cfg, ""); err != nil || got != "reports" {
t.Fatalf("stdout destination: %q %v", got, err)
}
cfg.Reporting.StdoutDir = ""
if got, err := selectedReportDir(cfg, filepath.Join("out", "corrected.json")); err != nil || got != "out" {
t.Fatalf("file destination ignored stdout setting: %q %v", got, err)
}
if _, err := selectedReportDir(cfg, ""); err == nil {
t.Fatal("enabled stdout reporting accepted blank destination")
}
cfg.Reporting.Enabled = false
if got, err := selectedReportDir(cfg, ""); err != nil || got != "" {
t.Fatalf("disabled reporting probed destination: %q %v", got, err)
}
}
func TestBareBooleanFlagsAndReportDirectoryOverride(t *testing.T) {
input, _, err := parseConfigInput("process", []string{"input.json", "--glossary", "glossary.yml", "--reporting", "--debug", "--report-dir", "reports"}, io.Discard)
if err != nil {
t.Fatal(err)
}
values := map[string]any{}
for _, override := range input.Overrides {
values[override.Path.String()] = override.Value
}
if values[ck.Keys("reporting", "enabled").String()] != true || values[ck.Keys("diagnostics", "debug").String()] != true || values[ck.Keys("reporting", "stdout_dir").String()] != "reports" {
t.Fatalf("incorrect visited overrides: %+v", values)
}
}

View File

@@ -15,7 +15,6 @@ import (
coreio "gitea.maximumdirect.net/eric/audita/internal/core/io" coreio "gitea.maximumdirect.net/eric/audita/internal/core/io"
"gitea.maximumdirect.net/eric/audita/internal/core/normalization" "gitea.maximumdirect.net/eric/audita/internal/core/normalization"
"gitea.maximumdirect.net/eric/audita/internal/core/outputschema" "gitea.maximumdirect.net/eric/audita/internal/core/outputschema"
"gitea.maximumdirect.net/eric/audita/internal/core/reporting"
"gitea.maximumdirect.net/eric/audita/internal/core/schema" "gitea.maximumdirect.net/eric/audita/internal/core/schema"
"gitea.maximumdirect.net/eric/audita/internal/framework/contracts" "gitea.maximumdirect.net/eric/audita/internal/framework/contracts"
"gitea.maximumdirect.net/eric/audita/internal/framework/llm" "gitea.maximumdirect.net/eric/audita/internal/framework/llm"
@@ -28,7 +27,7 @@ type processInvocation struct {
TranscriptPath string TranscriptPath string
GlossaryPath string GlossaryPath string
OutputPath string OutputPath string
ReportJSONPath string ReportDir string
Config config.Config Config config.Config
ConfigResult *ck.Result[config.Config] ConfigResult *ck.Result[config.Config]
LLMRuntime *llm.Runtime LLMRuntime *llm.Runtime
@@ -38,8 +37,8 @@ type processInvocation struct {
ConfigVersion *int ConfigVersion *int
} }
var processRunner = func(inv processInvocation, stdout io.Writer) (*normalization.NormalizationSummary, *chunking.Summary, *runner.RunOutput, *diagnostics.RunDirectory, error) { 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, string(inv.Config.Diagnostics.Retention), llm.ConfiguredSecrets(&inv.Config)...) runDir, err := diagnostics.NewRunDirectory(inv.Config.Diagnostics.WorkDir, "always", llm.ConfiguredSecrets(&inv.Config)...)
if err != nil { if err != nil {
return nil, nil, nil, nil, fmt.Errorf("run_dir_creation: %w", err) return nil, nil, nil, nil, fmt.Errorf("run_dir_creation: %w", err)
} }
@@ -54,7 +53,6 @@ var processRunner = func(inv processInvocation, stdout io.Writer) (*normalizatio
TranscriptPath: inv.TranscriptPath, TranscriptPath: inv.TranscriptPath,
GlossaryPath: inv.GlossaryPath, GlossaryPath: inv.GlossaryPath,
OutputPath: inv.OutputPath, OutputPath: inv.OutputPath,
ReportJSONPath: inv.ReportJSONPath,
ConfigPath: inv.ConfigPath, ConfigPath: inv.ConfigPath,
ConfigSource: inv.ConfigSource, ConfigSource: inv.ConfigSource,
Overlay: inv.Overlay, Overlay: inv.Overlay,
@@ -168,7 +166,7 @@ var processRunner = func(inv processInvocation, stdout io.Writer) (*normalizatio
return fail("runner_setup", err, nil) return fail("runner_setup", err, nil)
} }
runnerResult, runErr := runner.New(moduleFactory).Run(context.Background(), runner.RunInput{ runnerResult, runErr := runner.New(moduleFactory).Run(ctx, runner.RunInput{
Config: &inv.Config, Config: &inv.Config,
Transcript: normalizedTranscript, Transcript: normalizedTranscript,
Glossary: glossary, Glossary: glossary,
@@ -231,7 +229,7 @@ func sourceToCanonicalTranscript(source *schema.SourceTranscript) *schema.Transc
} }
// Run executes the Audita CLI with the provided arguments and streams. // Run executes the Audita CLI with the provided arguments and streams.
func Run(args []string, stdout, stderr io.Writer) int { func Run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
if len(args) == 0 { if len(args) == 0 {
writeRootUsage(stdout) writeRootUsage(stdout)
return 0 return 0
@@ -253,10 +251,10 @@ func Run(args []string, stdout, stderr io.Writer) int {
} }
if args[0] == "process" { if args[0] == "process" {
return runProcess(args[1:], stdout, stderr) return runProcess(ctx, args[1:], stdout, stderr)
} }
if args[0] == "config" { if args[0] == "config" {
return runConfig(args[1:], stdout, stderr) return runConfig(ctx, args[1:], stdout, stderr)
} }
fmt.Fprintf(stderr, "audita: unknown command %q\n\n", args[0]) fmt.Fprintf(stderr, "audita: unknown command %q\n\n", args[0])
@@ -264,7 +262,7 @@ func Run(args []string, stdout, stderr io.Writer) int {
return 2 return 2
} }
func runProcess(args []string, stdout, stderr io.Writer) int { func runProcess(ctx context.Context, args []string, stdout, stderr io.Writer) int {
startedAt := time.Now().UTC() startedAt := time.Now().UTC()
input, help, err := parseConfigInput("process", args, stderr) input, help, err := parseConfigInput("process", args, stderr)
if err != nil { if err != nil {
@@ -280,7 +278,16 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
fmt.Fprintf(stderr, "audita process: %s\n", configInputError(err)) fmt.Fprintf(stderr, "audita process: %s\n", configInputError(err))
return 2 return 2
} }
llmRuntime, err := llm.NewRuntime(context.Background(), selected.Result.Config, selected.Path, nil) 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 { if err != nil {
fmt.Fprintf(stderr, "audita process: %s\n", llm.RedactError(err, selected.Result.Config)) fmt.Fprintf(stderr, "audita process: %s\n", llm.RedactError(err, selected.Result.Config))
if llm.IsAssetError(err) { if llm.IsAssetError(err) {
@@ -294,7 +301,7 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
TranscriptPath: input.TranscriptPath, TranscriptPath: input.TranscriptPath,
GlossaryPath: input.GlossaryPath, GlossaryPath: input.GlossaryPath,
OutputPath: input.OutputPath, OutputPath: input.OutputPath,
ReportJSONPath: input.ReportPath, ReportDir: reportDir,
Config: selected.Result.Config, Config: selected.Result.Config,
ConfigResult: selected.Result, ConfigResult: selected.Result,
LLMRuntime: llmRuntime, LLMRuntime: llmRuntime,
@@ -304,7 +311,7 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
ConfigVersion: &version, ConfigVersion: &version,
} }
normSummary, chunkSummary, runOutput, runDir, runErr := processRunner(inv, stdout) normSummary, chunkSummary, runOutput, runDir, runErr := processRunner(ctx, inv, stdout)
completedAt := time.Now().UTC() completedAt := time.Now().UTC()
if runErr != nil { if runErr != nil {
@@ -320,17 +327,8 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
errorMessage = llm.RedactError(fmt.Errorf("%s", errorMessage), inv.Config) errorMessage = llm.RedactError(fmt.Errorf("%s", errorMessage), inv.Config)
report := processreport.Build(processReportInput("failed", inv, runDir, startedAt, completedAt, errorMessage, errorPhase, nil, nil, runOutput)) report := processreport.Build(processReportInput("failed", inv, runDir, startedAt, completedAt, errorMessage, errorPhase, nil, nil, runOutput))
if strings.TrimSpace(inv.ReportJSONPath) != "" {
if err := reporting.WriteProcessReport(inv.ReportJSONPath, report, llm.ConfiguredSecrets(&inv.Config)...); err != nil {
fmt.Fprintf(stderr, "audita process: %v\n", err)
}
}
if runDir != nil { if runDir != nil {
_ = runDir.WriteReport(report) _ = runDir.WriteReport(report)
_ = runDir.ApplyRetention(diagnostics.RetentionDecisionInput{
RunSucceeded: false,
})
} }
fmt.Fprintf(stderr, "audita process: %s\n", llm.RedactError(runErr, inv.Config)) fmt.Fprintf(stderr, "audita process: %s\n", llm.RedactError(runErr, inv.Config))
@@ -351,28 +349,8 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
report := processreport.Build(processReportInput("success", inv, runDir, startedAt, completedAt, "", "", normSummary, chunkSummary, runOutput)) report := processreport.Build(processReportInput("success", inv, runDir, startedAt, completedAt, "", "", normSummary, chunkSummary, runOutput))
if strings.TrimSpace(inv.ReportJSONPath) != "" {
if err := reporting.WriteProcessReport(inv.ReportJSONPath, report, llm.ConfiguredSecrets(&inv.Config)...); err != nil {
if runDir != nil {
_ = runDir.WriteErrorLog(fmt.Sprintf("report_write: %v", err))
_ = runDir.ApplyRetention(diagnostics.RetentionDecisionInput{
RunSucceeded: false,
})
}
fmt.Fprintf(stderr, "audita process: %v\n", err)
return 1
}
}
if runDir != nil { if runDir != nil {
_ = runDir.WriteReport(report) _ = runDir.WriteReport(report)
if err := runDir.ApplyRetention(diagnostics.RetentionDecisionInput{
RunSucceeded: true,
HasSkippedCorrections: processreport.HasSkippedCorrections(runOutput),
}); err != nil {
fmt.Fprintf(stderr, "audita process: failed to apply work-dir retention: %v\n", err)
return 1
}
} }
return 0 return 0
@@ -386,7 +364,7 @@ func writeExecutionArtifacts(runDir *diagnostics.RunDirectory, output *runner.Ru
_ = runDir.WriteJSONArtifact(diagnostics.ArtifactUtilizationSummary, processreport.BuildUtilization(output, cfg.Backends)) _ = runDir.WriteJSONArtifact(diagnostics.ArtifactUtilizationSummary, processreport.BuildUtilization(output, cfg.Backends))
} }
func runConfig(args []string, stdout, stderr io.Writer) int { func runConfig(ctx context.Context, args []string, stdout, stderr io.Writer) int {
if len(args) == 0 || isHelpCommand(args) || args[0] == "--help" || args[0] == "-h" { if len(args) == 0 || isHelpCommand(args) || args[0] == "--help" || args[0] == "-h" {
writeConfigUsage(stdout) writeConfigUsage(stdout)
return 0 return 0
@@ -394,9 +372,9 @@ func runConfig(args []string, stdout, stderr io.Writer) int {
switch args[0] { switch args[0] {
case "validate": case "validate":
return runConfigValidate(args[1:], stdout, stderr) return runConfigValidate(ctx, args[1:], stdout, stderr)
case "print-effective": case "print-effective":
return runConfigPrintEffective(args[1:], stdout, stderr) return runConfigPrintEffective(ctx, args[1:], stdout, stderr)
default: default:
fmt.Fprintf(stderr, "audita config: unknown command %q\n\n", args[0]) fmt.Fprintf(stderr, "audita config: unknown command %q\n\n", args[0])
writeConfigUsage(stderr) writeConfigUsage(stderr)
@@ -404,7 +382,7 @@ func runConfig(args []string, stdout, stderr io.Writer) int {
} }
} }
func runConfigValidate(args []string, stdout, stderr io.Writer) int { func runConfigValidate(ctx context.Context, args []string, stdout, stderr io.Writer) int {
input, help, err := parseConfigInput("validate", args, stderr) input, help, err := parseConfigInput("validate", args, stderr)
if err != nil { if err != nil {
fmt.Fprintf(stderr, "audita config validate: %s\n", configInputError(err)) fmt.Fprintf(stderr, "audita config validate: %s\n", configInputError(err))
@@ -419,7 +397,7 @@ func runConfigValidate(args []string, stdout, stderr io.Writer) int {
fmt.Fprintf(stderr, "audita config validate: %s\n", configInputError(err)) fmt.Fprintf(stderr, "audita config validate: %s\n", configInputError(err))
return 2 return 2
} }
if _, err := llm.NewRuntime(context.Background(), selected.Result.Config, selected.Path, nil); err != nil { 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)) fmt.Fprintf(stderr, "audita config validate: %s\n", llm.RedactError(err, selected.Result.Config))
if llm.IsAssetError(err) { if llm.IsAssetError(err) {
return 1 return 1
@@ -430,7 +408,7 @@ func runConfigValidate(args []string, stdout, stderr io.Writer) int {
return 0 return 0
} }
func runConfigPrintEffective(args []string, stdout, stderr io.Writer) int { func runConfigPrintEffective(ctx context.Context, args []string, stdout, stderr io.Writer) int {
input, help, err := parseConfigInput("print-effective", args, stderr) input, help, err := parseConfigInput("print-effective", args, stderr)
if err != nil { if err != nil {
fmt.Fprintf(stderr, "audita config print-effective: %s\n", configInputError(err)) fmt.Fprintf(stderr, "audita config print-effective: %s\n", configInputError(err))
@@ -445,7 +423,7 @@ func runConfigPrintEffective(args []string, stdout, stderr io.Writer) int {
fmt.Fprintf(stderr, "audita config print-effective: %s\n", configInputError(err)) fmt.Fprintf(stderr, "audita config print-effective: %s\n", configInputError(err))
return 2 return 2
} }
llmRuntime, err := llm.NewRuntime(context.Background(), selected.Result.Config, selected.Path, nil) llmRuntime, err := llm.NewRuntime(ctx, selected.Result.Config, selected.Path, nil)
if err != nil { if err != nil {
fmt.Fprintf(stderr, "audita config print-effective: %s\n", llm.RedactError(err, selected.Result.Config)) fmt.Fprintf(stderr, "audita config print-effective: %s\n", llm.RedactError(err, selected.Result.Config))
if llm.IsAssetError(err) { if llm.IsAssetError(err) {
@@ -575,15 +553,18 @@ func writeProcessUsage(w io.Writer) {
fmt.Fprintln(w) fmt.Fprintln(w)
fmt.Fprintln(w, "Flags:") fmt.Fprintln(w, "Flags:")
for _, name := range []string{ for _, name := range []string{
"config", "overlay", "glossary", "output", "report-json", "config", "overlay", "glossary", "output",
"proposal-profile", "validation-profile", "modules", "output-schema", "proposal-profile", "validation-profile", "modules", "output-schema",
"validation-max-prompt-tokens", "target-sections", "max-section-tokens", "min-section-tokens", "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", "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", "glossary-confidence-threshold", "homophones-confidence-threshold", "spoken-word-confidence-threshold", "grammar-confidence-threshold",
"transcript-description", "work-dir", "work-dir-retention", "transcript-description", "work-dir",
} { } {
fmt.Fprintf(w, " --%s\n", name) 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)
fmt.Fprintln(w, "Example:") fmt.Fprintln(w, "Example:")
fmt.Fprintln(w, " audita process transcript.json --glossary glossary.yaml --output corrected.json") fmt.Fprintln(w, " audita process transcript.json --glossary glossary.yaml --output corrected.json")

View File

@@ -10,15 +10,10 @@ import (
"gitea.maximumdirect.net/eric/audita/internal/core/outputschema" "gitea.maximumdirect.net/eric/audita/internal/core/outputschema"
) )
type WorkDirRetention string
const ( const (
WorkDirRetentionAuto WorkDirRetention = "auto" DefaultModulesCSV = "glossary,homophones,glossary,spoken_word,grammar"
WorkDirRetentionAlways WorkDirRetention = "always" DefaultTranscriptDescriptionMaxChars = 500
WorkDirRetentionNever WorkDirRetention = "never" DefaultValidationMaxPromptTokens = 2048
DefaultModulesCSV = "glossary,homophones,glossary,spoken_word,grammar"
DefaultTranscriptDescriptionMaxChars = 500
DefaultValidationMaxPromptTokens = 2048
) )
// Config is the version-2 application configuration. Invocation paths are not settings. // Config is the version-2 application configuration. Invocation paths are not settings.
@@ -34,6 +29,7 @@ type Config struct {
Normalization Normalization `yaml:"normalization"` Normalization Normalization `yaml:"normalization"`
Thresholds Thresholds `yaml:"thresholds"` Thresholds Thresholds `yaml:"thresholds"`
Context Context `yaml:"context"` Context Context `yaml:"context"`
Reporting Reporting `yaml:"reporting"`
Diagnostics Diagnostics `yaml:"diagnostics"` Diagnostics Diagnostics `yaml:"diagnostics"`
} }
@@ -80,8 +76,12 @@ type Context struct {
Description string `yaml:"description"` Description string `yaml:"description"`
} }
type Diagnostics struct { type Diagnostics struct {
WorkDir string `yaml:"work_dir"` Debug bool `yaml:"debug"`
Retention WorkDirRetention `yaml:"retention"` WorkDir string `yaml:"work_dir"`
}
type Reporting struct {
Enabled bool `yaml:"enabled"`
StdoutDir string `yaml:"stdout_dir"`
} }
func defaults() Config { func defaults() Config {
@@ -94,7 +94,8 @@ func defaults() Config {
Chunking: Chunking{MaxSectionTokens: 8192, MinSectionTokens: 2048}, Chunking: Chunking{MaxSectionTokens: 8192, MinSectionTokens: 2048},
Normalization: Normalization{MaxSegmentGap: 4, EllipsisGap: 3.5, MaxSegmentDuration: 60, MaxSegmentTokens: 2048}, Normalization: Normalization{MaxSegmentGap: 4, EllipsisGap: 3.5, MaxSegmentDuration: 60, MaxSegmentTokens: 2048},
Thresholds: Thresholds{Glossary: .8, Homophones: .8, SpokenWord: .8, Grammar: .8}, Thresholds: Thresholds{Glossary: .8, Homophones: .8, SpokenWord: .8, Grammar: .8},
Diagnostics: Diagnostics{WorkDir: "/tmp/audita", Retention: WorkDirRetentionAuto}, Reporting: Reporting{Enabled: true, StdoutDir: "."},
Diagnostics: Diagnostics{WorkDir: "/tmp/audita"},
} }
} }
@@ -170,8 +171,6 @@ func validate(c *Config, v *ck.Validation) {
check(threshold >= 0 && threshold <= 1, ck.Keys("thresholds", name), "confidence threshold must be between zero and one") check(threshold >= 0 && threshold <= 1, ck.Keys("thresholds", name), "confidence threshold must be between zero and one")
} }
check(len(strings.TrimSpace(c.Context.Description)) <= DefaultTranscriptDescriptionMaxChars, ck.Keys("context", "description"), fmt.Sprintf("description must be at most %d bytes after trimming", DefaultTranscriptDescriptionMaxChars)) check(len(strings.TrimSpace(c.Context.Description)) <= DefaultTranscriptDescriptionMaxChars, ck.Keys("context", "description"), fmt.Sprintf("description must be at most %d bytes after trimming", DefaultTranscriptDescriptionMaxChars))
check(strings.TrimSpace(c.Diagnostics.WorkDir) != "", ck.Keys("diagnostics", "work_dir"), "work directory is required")
check(c.Diagnostics.Retention == WorkDirRetentionAuto || c.Diagnostics.Retention == WorkDirRetentionAlways || c.Diagnostics.Retention == WorkDirRetentionNever, ck.Keys("diagnostics", "retention"), "retention must be auto, always, or never")
} }
// ParseModulesCSV is used only for an explicitly supplied --modules override. // ParseModulesCSV is used only for an explicitly supplied --modules override.

View File

@@ -56,7 +56,7 @@ func TestStrictInputAndConstraints(t *testing.T) {
{"invalid section relation", base + "chunking:\n min_section_tokens: 10000\n"}, {"invalid section relation", base + "chunking:\n min_section_tokens: 10000\n"},
{"string normalization", base + "normalization:\n max_segment_gap: 4s\n"}, {"string normalization", base + "normalization:\n max_segment_gap: 4s\n"},
{"invalid threshold", base + "thresholds:\n glossary: 1.1\n"}, {"invalid threshold", base + "thresholds:\n glossary: 1.1\n"},
{"invalid retention", base + "diagnostics:\n retention: sometimes\n"}, {"removed retention", base + "diagnostics:\n retention: always\n"},
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
if _, err := loadText(tc.text, ""); err == nil { if _, err := loadText(tc.text, ""); err == nil {