diff --git a/cmd/audita/main_integration_test.go b/cmd/audita/main_integration_test.go index 246aa2d..7c703a7 100644 --- a/cmd/audita/main_integration_test.go +++ b/cmd/audita/main_integration_test.go @@ -799,6 +799,41 @@ func TestSubprocessStdoutAndSelectedReportDirectory(t *testing.T) { } } +func TestSubprocessUsesIndependentTranscriptReportAndDebugDestinations(t *testing.T) { + f := newProcessFixture(t, "normal", "[grammar]") + transcriptDir := filepath.Join(f.dir, "transcripts") + reportDir := filepath.Join(f.dir, "reports") + debugDir := filepath.Join(f.dir, "debug") + for _, dir := range []string{transcriptDir, reportDir} { + if err := os.Mkdir(dir, 0700); err != nil { + t.Fatal(err) + } + } + f.output = filepath.Join(transcriptDir, "corrected.json") + code, stdout, stderr := f.run(t, true, "--report-dir", reportDir, "--debug", "--work-dir", debugDir) + if code != 0 || stdout != "" || !strings.HasPrefix(stderr, "Completed:") { + t.Fatalf("code=%d stdout=%q stderr=%q", code, stdout, stderr) + } + if output, err := os.ReadFile(f.output); err != nil || !bytes.Contains(output, []byte("Hello world")) { + t.Fatalf("transcript: %s %v", output, err) + } + for _, name := range []string{"errors.yml", "recoveries.yml", "report.md"} { + if _, err := os.Stat(filepath.Join(reportDir, name)); err != nil { + t.Fatalf("report %s: %v", name, err) + } + if _, err := os.Stat(filepath.Join(transcriptDir, name)); !os.IsNotExist(err) { + t.Fatalf("report %s appeared beside transcript: %v", name, err) + } + } + dirs, err := filepath.Glob(filepath.Join(debugDir, "run-*")) + if err != nil || len(dirs) != 1 { + t.Fatalf("debug runs: %v %v", dirs, err) + } + if _, err := os.Stat(filepath.Join(dirs[0], "correction-ledger.json")); err != nil { + t.Fatalf("debug capture: %v", err) + } +} + func TestSubprocessReportingAndDebugSelections(t *testing.T) { var expectedOutput, expectedSummary string for _, selection := range []struct{ reporting, debug bool }{ @@ -1016,6 +1051,44 @@ func TestSubprocessInvalidInputLeavesFailedReports(t *testing.T) { } } +func TestSubprocessInvalidInputWritesFailedReportsToSelectedDirectory(t *testing.T) { + f := newProcessFixture(t, "normal", "[grammar]") + if err := os.WriteFile(f.transcript, []byte("not JSON"), 0600); err != nil { + t.Fatal(err) + } + reportDir := filepath.Join(f.dir, "failed-reports") + if err := os.Mkdir(reportDir, 0700); err != nil { + t.Fatal(err) + } + code, _, _ := f.run(t, true, "--report-dir", reportDir) + if code != 1 || f.provider.callCount() != 0 { + t.Fatalf("invalid input dispatched: code=%d calls=%d", code, f.provider.callCount()) + } + raw, err := os.ReadFile(filepath.Join(reportDir, "errors.yml")) + if err != nil { + t.Fatal(err) + } + var document reporting.ErrorsDocument + if err := yaml.Unmarshal(raw, &document); err != nil { + t.Fatal(err) + } + if document.Status != reporting.StatusFailed || len(document.Errors) != 1 || document.Errors[0].Scope != "run" { + t.Fatalf("failed report: %+v", document) + } + if _, err := os.Stat(filepath.Join(reportDir, "recoveries.yml")); err != nil { + t.Fatal(err) + } + if raw, err := os.ReadFile(filepath.Join(reportDir, "report.md")); err != nil || !bytes.Contains(raw, []byte("Status: failed")) { + t.Fatalf("Markdown status: %s %v", raw, err) + } + if _, err := os.Stat(filepath.Join(f.dir, "errors.yml")); !os.IsNotExist(err) { + t.Fatalf("failed report appeared in default directory: %v", err) + } + if _, err := os.Stat(f.output); !os.IsNotExist(err) { + t.Fatalf("failed transcript published: %v", err) + } +} + func TestSubprocessOutputFailureBeforeModelDispatch(t *testing.T) { f := newProcessFixture(t, "normal", "[grammar]") f.output = filepath.Join(f.dir, "missing", "output.json") diff --git a/docs/cli.md b/docs/cli.md index 12ab86e..2752f3e 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -48,12 +48,17 @@ validation. The shared setting flags are: An omitted or empty output value selects stdout. Nonempty output filenames are used literally, including whitespace. Reporting defaults to enabled. `--reporting=false` disables all three ordinary -reports; an explicit false overrides a true file or overlay setting. With file -output, reports go beside the transcript and `--report-dir` has no effect. With -stdout output, `--report-dir` selects the report directory, defaulting to the -invocation working directory. `--debug` separately enables retained detailed -artifacts under `--work-dir`. Configuration inspection does not create or probe -these output directories. +reports; an explicit false overrides a true file or overlay setting. For stdout +output, reports use the effective `reporting.stdout_dir`, which defaults to the +invocation working directory. For file output, they go beside the transcript +unless `--report-dir` is explicitly supplied. The flag selects the report +directory for either transcript destination; a config-file or overlay value for +`reporting.stdout_dir` alone affects stdout output only. A selected report +directory must be nonblank when reporting is enabled. With reporting disabled, +the report destination is inactive and is not probed. `--debug` separately +enables retained detailed artifacts under `--work-dir`; selecting `--work-dir` +alone does not enable capture. Configuration inspection does not create or +probe these output directories. Per-role model, base URL, key, timeout, retry, and concurrency flags have been removed. Models and generation controls belong to PromptKit profiles, backend credentials and capacity to configuration, and retry/deadline settings to the diff --git a/docs/config.md b/docs/config.md index 27a89fd..015733f 100644 --- a/docs/config.md +++ b/docs/config.md @@ -62,17 +62,20 @@ field provenance separately from ConfigKit's redacted settings YAML. | `thresholds.glossary`, `.homophones`, `.spoken_word`, `.grammar` | `0.8` each | Confidence thresholds in `[0, 1]`. | | `context.description` | Empty | Trimmed prompt context, at most 500 bytes. | | `reporting.enabled` | `true` | Write the ordinary three-file report bundle when processing. | -| `reporting.stdout_dir` | `.` | Report directory for stdout transcript output; ignored for file output or disabled reporting. | +| `reporting.stdout_dir` | `.` | Report directory for stdout transcript output; a `--report-dir` flag can select reports for either transcript destination. | | `diagnostics.debug` | `false` | Persist detailed debug artifacts when processing. | | `diagnostics.work_dir` | `/tmp/audita` | Parent of the retained debug run directory; required only when debug is enabled. | Module keys select correction modules; validator chains remain built into them. The loader preserves module order and repetitions. Normalization time settings accept numeric seconds only. The operation timeout uses a duration string. -`reporting.stdout_dir` must be nonblank only for reporting-enabled stdout runs; -`diagnostics.work_dir` must be nonblank only with debug enabled. These path -requirements are checked when choosing process destinations, not during -configuration inspection. +`reporting.stdout_dir` must be nonblank when it supplies the active report +destination: for stdout output, or for file output when `--report-dir` is +explicitly supplied. File output without that flag uses the transcript's parent, +so the configured value is inactive. Reporting-disabled runs do not select a +report directory. `diagnostics.work_dir` must be nonblank only with debug +enabled. These path requirements are checked when choosing process destinations, +not during configuration inspection. ## Credentials diff --git a/docs/integrations/subprocess.md b/docs/integrations/subprocess.md index 29ac78b..47c1a4f 100644 --- a/docs/integrations/subprocess.md +++ b/docs/integrations/subprocess.md @@ -9,11 +9,14 @@ The [CLI reference](../cli.md) owns command syntax, streams, and exit semantics; Use an argument array with the transcript path and required glossary argument, following the [process examples](../cli.md#commands-and-help). For machine workflows, select an explicit transcript file destination and a configuration path. Reports -then go beside the transcript. For stdout output, select `--report-dir` or use -the invocation working directory. Debug is independent and off by default. -Supplying a configuration path also -makes file selection predictable; only explicitly referenced credential variables -are read from the environment. +go beside the transcript unless an explicit `--report-dir` selects another +directory. For stdout output, reports use the effective `reporting.stdout_dir` +(defaulting to the invocation working directory), including a `--report-dir` +override. A config-file or overlay value for `reporting.stdout_dir` alone does +not redirect file-output reports. `--work-dir` selects a debug root, while +configuration or `--debug` enables capture; the path flag alone does not. +Supplying a configuration path also makes file selection predictable; only +explicitly referenced credential variables are read from the environment. Resolve the executable deliberately and set the child's working directory so relative input/output paths and reported artifact paths have a known base. @@ -28,7 +31,8 @@ Consume transcript/report data through the [output-file contract](output-files.md), and treat stderr as human-readable detail. The three reports may be absent after early configuration or destination failures. After initialization, failed reports can describe invalid input or -partial processing. Exit `0` can include recoverable model errors and applied +partial processing and use the same selected report directory as successful +runs. Exit `0` can include recoverable model errors and applied corrections; inspect `errors.yml` and `recoveries.yml` for those outcomes. A transcript may have been written before a later required report failure, so the file alone does not prove completion. diff --git a/docs/operations.md b/docs/operations.md index 5a2336f..7adda89 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -17,10 +17,14 @@ settings, and the [output contract](integrations/output-files.md) owns schemas. ## Required Output Files Reporting is enabled by default. For file transcript output, `errors.yml`, -`recoveries.yml`, and `report.md` go in the transcript output's parent directory. -For stdout transcript output, they go in `reporting.stdout_dir` (default `.`), -overridden by `--report-dir`. `--reporting=false` suppresses all three reports -and removes their path requirements. It does not suppress the stderr summary or +`recoveries.yml`, and `report.md` go in the transcript output's parent directory +unless `--report-dir` is explicitly supplied. For stdout transcript output, they +go in the effective `reporting.stdout_dir` (default `.`), including a +`--report-dir` override. A config-file or overlay value for +`reporting.stdout_dir` alone does not redirect file-output reports. An active +selected report directory must be nonblank. `--reporting=false` suppresses all +three reports and leaves their destination inactive, so it is not probed merely +because `--report-dir` was supplied. It does not suppress the stderr summary or fatal errors. Stdout contains only transcript JSON; file output leaves it empty. Existing report directories and parents for regular transcript files must be @@ -32,8 +36,9 @@ Preparation never truncates an existing transcript. Device and pipe destinations (such as `/dev/null` and named pipes) receive ordinary writes at publication time; they are not opened during preflight or replaced with regular files. A pipe write can wait for its reader, and stream write errors are fatal. With reporting enabled, -reports still use the output path's parent; disable reporting when that placement -is unsuitable for a stream destination. +reports still use the output path's parent unless an explicit `--report-dir` +selects another directory; disable reporting when that placement is unsuitable +for a stream destination. With reporting enabled, all three reports are initialized as `in_progress` before input processing. Output is encoded fully before publication. Each regular output @@ -48,10 +53,12 @@ finalized where possible. An uncatchable termination can leave `in_progress`. ## Diagnostics Directory -`--debug` enables detailed evidence independently of reporting. One directory -named `run-` is created under `diagnostics.work_dir` (default `/tmp/audita`) -and retained on success or failure. Without debug, no directory or payload -files are created. The parent is created when needed only for debug. Failure to +`--debug` or `diagnostics.debug` enables detailed evidence independently of +reporting. One directory named `run-` is created under +`diagnostics.work_dir` (default `/tmp/audita`), which `--work-dir` can override, +and retained on success or failure. Selecting `--work-dir` alone does not enable +capture. Without debug, no directory or payload files are created. The parent +is created when needed only for debug. Failure to write optional debug evidence prints a redacted stderr diagnostic but does not change correction decisions or the exit status. diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 706fd68..d98ff03 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:** Stage 1 complete; Stage 2 pending. +**Status:** Complete; Stages 1–2 implemented and verified. **Audience:** gpt-5.6-terra coding agent. ## Scope and Execution @@ -94,6 +94,9 @@ destination constraints, then commit the coherent code/test unit. **Files:** `cmd/audita/main_integration_test.go`, `docs/cli.md`, `docs/config.md`, `docs/operations.md`, `docs/integrations/subprocess.md`, and `docs/roadmap/narratio-feature.md`. + +**Completion:** Complete — the full Go test suite, maintained configuration +examples, affected Markdown links, and whitespace checks pass. Touch another document only if its current statement actually conflicts with the new behavior. diff --git a/docs/roadmap/narratio-feature.md b/docs/roadmap/narratio-feature.md index 8c4b2f2..82bb5ae 100644 --- a/docs/roadmap/narratio-feature.md +++ b/docs/roadmap/narratio-feature.md @@ -2,10 +2,10 @@ **Last updated:** 2026-10-03 -**Status:** Proposed; not implemented. This roadmap records the intended external -behavior and the reasons for it. The [implementation plan](implementation.md) -owns the ordered work; the [Narratio request](feature-request-narratio.md) -provides the consumer context. +**Status:** Implemented and verified. This roadmap records the external behavior +and the reasons for it. The [implementation plan](implementation.md) records the +completed work; the [Narratio request](feature-request-narratio.md) provides the +consumer context. ## Purpose @@ -21,8 +21,9 @@ working-state lifecycle; Narratio owns its destination paths and operator views. - `--output FILE` writes the transcript to `FILE`, or stdout when omitted. - Ordinary reporting is enabled by default. With file output, `errors.yml`, `recoveries.yml`, and `report.md` go beside the transcript. With stdout output, - `--report-dir DIR` or `reporting.stdout_dir` selects their directory. The - current selection logic ignores `--report-dir` when `--output` is supplied. + `--report-dir DIR` or `reporting.stdout_dir` selects their directory. An + explicit `--report-dir` also selects reports for file output; without it, the + configured `reporting.stdout_dir` value remains stdout-only. - Reports are initialized before input processing, then updated on completion or failure where possible. Early configuration or destination failures can occur before report initialization and therefore leave no report files. @@ -30,10 +31,10 @@ working-state lifecycle; Narratio owns its destination paths and operator views. enable debug capture. Capture is enabled by `diagnostics.debug` or an explicit `--debug`; when enabled, Audita retains a `run-` directory beneath the selected root. Debug remains optional, including after unsuccessful runs. -- The output session already receives distinct transcript and report paths and - checks active destinations for conflicts with each other and protected inputs. - The limitation is primarily in CLI report-directory selection, not in report - encoding or the output session's basic shape. +- The output session receives distinct transcript and report paths and checks + active destinations for conflicts with each other and protected inputs. The + CLI selects the report destination while the output session retains its + existing collision checks, preflight, initialization, and finalization. The current contracts are in [CLI](../cli.md), [configuration](../config.md), [operations](../operations.md), [subprocess guidance](../integrations/subprocess.md), diff --git a/internal/cli/config_input_test.go b/internal/cli/config_input_test.go index fba092a..7b19f41 100644 --- a/internal/cli/config_input_test.go +++ b/internal/cli/config_input_test.go @@ -215,6 +215,17 @@ func TestReportingAndDebugOverridesRemainIndependent(t *testing.T) { t.Fatalf("%s did not honor explicit false: %+v", command, selected.Result.Config) } } + processInput, _, err := parseConfigInput("process", []string{"input.json", "--glossary", "glossary.yml", "--config", main, "--overlay", "testing"}, io.Discard) + if err != nil { + t.Fatal(err) + } + processConfig, err := loadConfigInput(processInput, "process") + if err != nil { + t.Fatal(err) + } + if got, err := selectedReportDir(processConfig.Result.Config, filepath.Join("out", "corrected.json"), processInput.ReportDirSet); err != nil || got != "out" { + t.Fatalf("overlay stdout directory redirected file output: %q %v", got, err) + } } func TestReportDirectorySelectionOnlyUsesActiveDestination(t *testing.T) {