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
import (
"context"
"os"
"os/signal"
"syscall"
"gitea.maximumdirect.net/eric/audita/internal/cli"
)
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 (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
@@ -23,7 +24,7 @@ func TestProcessSubprocessHelper(t *testing.T) {
}
for i, arg := range os.Args {
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)

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
**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
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

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
@@ -23,7 +24,6 @@ type configInput struct {
TranscriptPath string
GlossaryPath string
OutputPath string
ReportPath string
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"),
"grammar-confidence-threshold": ck.Keys("thresholds", "grammar"),
"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-retention": ck.Keys("diagnostics", "retention"),
"modules": ck.Keys("pipeline", "modules"),
}
for name := range settingPaths {
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":
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":
@@ -78,7 +82,6 @@ func parseConfigInput(command string, args []string, output io.Writer) (configIn
if command == "process" {
fs.String("glossary", "", "Glossary YAML file")
fs.String("output", "", "Corrected transcript output file")
fs.String("report-json", "", "Process report output file")
}
parseArgs := args
front := ""
@@ -102,7 +105,6 @@ func parseConfigInput(command string, args []string, output io.Writer) (configIn
input.TranscriptPath = positions[0]
input.GlossaryPath = fs.Lookup("glossary").Value.String()
input.OutputPath = fs.Lookup("output").Value.String()
input.ReportPath = fs.Lookup("report-json").Value.String()
if strings.TrimSpace(input.GlossaryPath) == "" {
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()
switch f.Name {
case "reporting", "debug":
value, _ = strconv.ParseBool(f.Value.String())
case "modules":
modules, err := config.ParseModulesCSV(f.Value.String())
if err != nil {
@@ -203,3 +207,16 @@ func configInputError(err error) string {
}
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"
ck "gitea.maximumdirect.net/eric/ConfigKit"
"gitea.maximumdirect.net/eric/audita/internal/core/config"
)
func TestSharedConfigInput(t *testing.T) {
@@ -43,7 +44,7 @@ func TestSharedConfigInput(t *testing.T) {
func TestConfigInputRejectsRemovedAndProcessOnlyFlags(t *testing.T) {
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"}
if command == "process" {
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")
}
}
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"
"gitea.maximumdirect.net/eric/audita/internal/core/normalization"
"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/framework/contracts"
"gitea.maximumdirect.net/eric/audita/internal/framework/llm"
@@ -28,7 +27,7 @@ type processInvocation struct {
TranscriptPath string
GlossaryPath string
OutputPath string
ReportJSONPath string
ReportDir string
Config config.Config
ConfigResult *ck.Result[config.Config]
LLMRuntime *llm.Runtime
@@ -38,8 +37,8 @@ type processInvocation struct {
ConfigVersion *int
}
var processRunner = func(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)...)
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)
}
@@ -54,7 +53,6 @@ var processRunner = func(inv processInvocation, stdout io.Writer) (*normalizatio
TranscriptPath: inv.TranscriptPath,
GlossaryPath: inv.GlossaryPath,
OutputPath: inv.OutputPath,
ReportJSONPath: inv.ReportJSONPath,
ConfigPath: inv.ConfigPath,
ConfigSource: inv.ConfigSource,
Overlay: inv.Overlay,
@@ -168,7 +166,7 @@ var processRunner = func(inv processInvocation, stdout io.Writer) (*normalizatio
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,
Transcript: normalizedTranscript,
Glossary: glossary,
@@ -231,7 +229,7 @@ func sourceToCanonicalTranscript(source *schema.SourceTranscript) *schema.Transc
}
// 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 {
writeRootUsage(stdout)
return 0
@@ -253,10 +251,10 @@ func Run(args []string, stdout, stderr io.Writer) int {
}
if args[0] == "process" {
return runProcess(args[1:], stdout, stderr)
return runProcess(ctx, args[1:], stdout, stderr)
}
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])
@@ -264,7 +262,7 @@ func Run(args []string, stdout, stderr io.Writer) int {
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()
input, help, err := parseConfigInput("process", args, stderr)
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))
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 {
fmt.Fprintf(stderr, "audita process: %s\n", llm.RedactError(err, selected.Result.Config))
if llm.IsAssetError(err) {
@@ -294,7 +301,7 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
TranscriptPath: input.TranscriptPath,
GlossaryPath: input.GlossaryPath,
OutputPath: input.OutputPath,
ReportJSONPath: input.ReportPath,
ReportDir: reportDir,
Config: selected.Result.Config,
ConfigResult: selected.Result,
LLMRuntime: llmRuntime,
@@ -304,7 +311,7 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
ConfigVersion: &version,
}
normSummary, chunkSummary, runOutput, runDir, runErr := processRunner(inv, stdout)
normSummary, chunkSummary, runOutput, runDir, runErr := processRunner(ctx, inv, stdout)
completedAt := time.Now().UTC()
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)
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 {
_ = runDir.WriteReport(report)
_ = runDir.ApplyRetention(diagnostics.RetentionDecisionInput{
RunSucceeded: false,
})
}
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))
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 {
_ = 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
@@ -386,7 +364,7 @@ func writeExecutionArtifacts(runDir *diagnostics.RunDirectory, output *runner.Ru
_ = 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" {
writeConfigUsage(stdout)
return 0
@@ -394,9 +372,9 @@ func runConfig(args []string, stdout, stderr io.Writer) int {
switch args[0] {
case "validate":
return runConfigValidate(args[1:], stdout, stderr)
return runConfigValidate(ctx, args[1:], stdout, stderr)
case "print-effective":
return runConfigPrintEffective(args[1:], stdout, stderr)
return runConfigPrintEffective(ctx, args[1:], stdout, stderr)
default:
fmt.Fprintf(stderr, "audita config: unknown command %q\n\n", args[0])
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)
if err != nil {
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))
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))
if llm.IsAssetError(err) {
return 1
@@ -430,7 +408,7 @@ func runConfigValidate(args []string, stdout, stderr io.Writer) int {
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)
if err != nil {
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))
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 {
fmt.Fprintf(stderr, "audita config print-effective: %s\n", llm.RedactError(err, selected.Result.Config))
if llm.IsAssetError(err) {
@@ -575,15 +553,18 @@ func writeProcessUsage(w io.Writer) {
fmt.Fprintln(w)
fmt.Fprintln(w, "Flags:")
for _, name := range []string{
"config", "overlay", "glossary", "output", "report-json",
"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", "work-dir-retention",
"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")

View File

@@ -10,15 +10,10 @@ import (
"gitea.maximumdirect.net/eric/audita/internal/core/outputschema"
)
type WorkDirRetention string
const (
WorkDirRetentionAuto WorkDirRetention = "auto"
WorkDirRetentionAlways WorkDirRetention = "always"
WorkDirRetentionNever WorkDirRetention = "never"
DefaultModulesCSV = "glossary,homophones,glossary,spoken_word,grammar"
DefaultTranscriptDescriptionMaxChars = 500
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.
@@ -34,6 +29,7 @@ type Config struct {
Normalization Normalization `yaml:"normalization"`
Thresholds Thresholds `yaml:"thresholds"`
Context Context `yaml:"context"`
Reporting Reporting `yaml:"reporting"`
Diagnostics Diagnostics `yaml:"diagnostics"`
}
@@ -80,8 +76,12 @@ type Context struct {
Description string `yaml:"description"`
}
type Diagnostics struct {
WorkDir string `yaml:"work_dir"`
Retention WorkDirRetention `yaml:"retention"`
Debug bool `yaml:"debug"`
WorkDir string `yaml:"work_dir"`
}
type Reporting struct {
Enabled bool `yaml:"enabled"`
StdoutDir string `yaml:"stdout_dir"`
}
func defaults() Config {
@@ -94,7 +94,8 @@ func defaults() Config {
Chunking: Chunking{MaxSectionTokens: 8192, MinSectionTokens: 2048},
Normalization: Normalization{MaxSegmentGap: 4, EllipsisGap: 3.5, MaxSegmentDuration: 60, MaxSegmentTokens: 2048},
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(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.

View File

@@ -56,7 +56,7 @@ func TestStrictInputAndConstraints(t *testing.T) {
{"invalid section relation", base + "chunking:\n min_section_tokens: 10000\n"},
{"string normalization", base + "normalization:\n max_segment_gap: 4s\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) {
if _, err := loadText(tc.text, ""); err == nil {