diff --git a/cmd/audita/main_integration_test.go b/cmd/audita/main_integration_test.go index 3d32018..246aa2d 100644 --- a/cmd/audita/main_integration_test.go +++ b/cmd/audita/main_integration_test.go @@ -805,9 +805,14 @@ func TestSubprocessReportingAndDebugSelections(t *testing.T) { {false, false}, {false, true}, {true, false}, {true, true}, } { f := newProcessFixture(t, "normal", "[grammar]") - extra := []string{"--reporting=false", "--report-dir", filepath.Join(f.dir, "missing")} + reportDir := filepath.Join(f.dir, "missing") + extra := []string{"--reporting=false", "--report-dir", reportDir} if selection.reporting { - extra = []string{"--reporting=true", "--report-dir", filepath.Join(f.dir, "missing")} + reportDir = filepath.Join(f.dir, "selected-reports") + if err := os.Mkdir(reportDir, 0700); err != nil { + t.Fatal(err) + } + extra = []string{"--reporting=true", "--report-dir", reportDir} } if selection.debug { extra = append(extra, "--debug") @@ -827,7 +832,7 @@ func TestSubprocessReportingAndDebugSelections(t *testing.T) { t.Fatalf("selection changed processing: %+v summary=%q output=%s", selection, summary, output) } for _, name := range []string{"errors.yml", "recoveries.yml", "report.md"} { - _, err := os.Stat(filepath.Join(f.dir, name)) + _, err := os.Stat(filepath.Join(reportDir, name)) if selection.reporting && err != nil || !selection.reporting && !os.IsNotExist(err) { t.Fatalf("selection=%+v report %s: %v", selection, name, err) } diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index c5a5948..706fd68 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,7 +1,7 @@ # Implementation Plan: Independent Output Destinations **Last updated:** 2026-10-03 -**Status:** Ready for implementation. +**Status:** Stage 1 complete; Stage 2 pending. **Audience:** gpt-5.6-terra coding agent. ## Scope and Execution @@ -52,6 +52,8 @@ before report initialization remain outside that guarantee. ## Stage 1 — Implement Explicit Report-Destination Selection +**Completion:** Complete — targeted CLI and subprocess tests pass; reviewed diff and whitespace checks. + **Files:** `internal/cli/config_input.go`, `internal/cli/process.go`, `internal/cli/run.go`, `internal/cli/config_input_test.go`, and the existing affected subprocess test in `cmd/audita/main_integration_test.go`. diff --git a/internal/cli/config_input.go b/internal/cli/config_input.go index 65ea32a..e46c255 100644 --- a/internal/cli/config_input.go +++ b/internal/cli/config_input.go @@ -24,6 +24,7 @@ type configInput struct { TranscriptPath string GlossaryPath string OutputPath string + ReportDirSet bool Overrides []ck.Override } @@ -124,6 +125,9 @@ func parseConfigInput(command string, args []string, output io.Writer) (configIn if f.Name == "config" { input.ConfigPathSet = true } + if f.Name == "report-dir" { + input.ReportDirSet = true + } path, ok := settingPaths[f.Name] if !ok { return @@ -208,15 +212,22 @@ func configInputError(err error) string { return strings.Join(messages, "; ") } -func selectedReportDir(cfg config.Config, outputPath string) (string, error) { +func selectedReportDir(cfg config.Config, outputPath string, reportDirSet bool) (string, error) { if !cfg.Reporting.Enabled { return "", nil } + var reportDir string if outputPath != "" { - return filepath.Dir(outputPath), nil + if reportDirSet { + reportDir = cfg.Reporting.StdoutDir + } else { + reportDir = filepath.Dir(outputPath) + } + } else { + reportDir = cfg.Reporting.StdoutDir } - if strings.TrimSpace(cfg.Reporting.StdoutDir) == "" { - return "", fmt.Errorf("report directory is required when reporting is enabled and transcript output is stdout") + if strings.TrimSpace(reportDir) == "" { + return "", fmt.Errorf("report directory is required when reporting is enabled for the selected destination") } - return cfg.Reporting.StdoutDir, nil + return reportDir, nil } diff --git a/internal/cli/config_input_test.go b/internal/cli/config_input_test.go index 5843aa0..fba092a 100644 --- a/internal/cli/config_input_test.go +++ b/internal/cli/config_input_test.go @@ -219,18 +219,27 @@ func TestReportingAndDebugOverridesRemainIndependent(t *testing.T) { func TestReportDirectorySelectionOnlyUsesActiveDestination(t *testing.T) { cfg := config.Config{Reporting: config.Reporting{Enabled: true, StdoutDir: "reports"}} - if got, err := selectedReportDir(cfg, ""); err != nil || got != "reports" { + if got, err := selectedReportDir(cfg, "", false); 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" { + if got, err := selectedReportDir(cfg, filepath.Join("out", "corrected.json"), false); err != nil || got != "out" { t.Fatalf("file destination ignored stdout setting: %q %v", got, err) } - if _, err := selectedReportDir(cfg, ""); err == nil { + if got, err := selectedReportDir(cfg, filepath.Join("out", "corrected.json"), true); err != nil || got != "reports" { + t.Fatalf("explicit file report directory: %q %v", got, err) + } + cfg.Reporting.StdoutDir = "" + if got, err := selectedReportDir(cfg, filepath.Join("out", "corrected.json"), false); err != nil || got != "out" { + t.Fatalf("inactive blank configured report directory rejected for file output: %q %v", got, err) + } + if _, err := selectedReportDir(cfg, filepath.Join("out", "corrected.json"), true); err == nil { + t.Fatal("active explicit blank report directory accepted for file output") + } + if _, err := selectedReportDir(cfg, "", false); err == nil { t.Fatal("enabled stdout reporting accepted blank destination") } cfg.Reporting.Enabled = false - if got, err := selectedReportDir(cfg, ""); err != nil || got != "" { + if got, err := selectedReportDir(cfg, filepath.Join("out", "corrected.json"), true); err != nil || got != "" { t.Fatalf("disabled reporting probed destination: %q %v", got, err) } } @@ -247,4 +256,14 @@ func TestBareBooleanFlagsAndReportDirectoryOverride(t *testing.T) { 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) } + if !input.ReportDirSet { + t.Fatal("explicit --report-dir presence was not recorded") + } + withoutReportDir, _, err := parseConfigInput("process", []string{"input.json", "--glossary", "glossary.yml"}, io.Discard) + if err != nil { + t.Fatal(err) + } + if withoutReportDir.ReportDirSet { + t.Fatal("absent --report-dir was recorded as explicit") + } } diff --git a/internal/cli/process.go b/internal/cli/process.go index 2a247ee..57b1c0a 100644 --- a/internal/cli/process.go +++ b/internal/cli/process.go @@ -46,7 +46,7 @@ func runProcess(ctx context.Context, args []string, stdout, stderr io.Writer) in return 2 } cfg := selected.Result.Config - reportDir, err := selectedReportDir(cfg, input.OutputPath) + reportDir, err := selectedReportDir(cfg, input.OutputPath, input.ReportDirSet) if err != nil { fmt.Fprintf(stderr, "audita process: %s\n", llm.RedactError(err, cfg)) return 2 diff --git a/internal/cli/run.go b/internal/cli/run.go index a665fda..c2cff64 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -225,7 +225,7 @@ func writeProcessUsage(w io.Writer) { 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, " --report-dir DIR Select the report directory (stdout setting for stdout; output parent by default for files)") fmt.Fprintln(w, " --debug[=true|false] Persist detailed diagnostics (default false)") fmt.Fprintln(w) fmt.Fprintln(w, "Example:")