Document configured output directories

This commit is contained in:
2026-08-02 01:41:03 +00:00
parent 2af6a5cfd2
commit 0dc6a06cd3
10 changed files with 138 additions and 27 deletions

View File

@@ -14,9 +14,10 @@ weatherreporter generate today
Configure a Weather API endpoint first; see the
[configuration reference](docs/config.md). The report is written to
`today.md` in the current directory; use `--out` to choose another destination.
See the [CLI reference](docs/cli.md) and [operations guide](docs/operations.md)
for command and operating details.
`today.md` in the current directory when `output.directory` is not configured.
Set that configuration value for an ordinary publication directory, or use
`--out` for one command. See the [CLI reference](docs/cli.md) and [operations
guide](docs/operations.md) for command and operating details.
## Documentation

View File

@@ -9,9 +9,10 @@ It has no command for inspecting prior runs or application-owned state.
weatherreporter generate today
```
The command uses the configured Weather API and writes an atomically replaced
`today.md` in the current directory. See the [configuration reference](config.md)
to supply the required Weather API endpoint.
The command uses the configured Weather API and atomically writes `today.md`.
With no configured output directory, it writes in the current directory. See
the [configuration reference](config.md) to supply the required Weather API
endpoint and choose an ordinary output directory.
## Commands And Usage
@@ -36,7 +37,7 @@ builds report `development`.
| `generate today` | Accepts an optional `--date YYYY-MM-DD`; without it, the current local date in the effective report timezone is used. Its default filename is `today.md`. |
| `generate tomorrow` | Uses the next local civil day and writes `tomorrow.md` by default. |
| `generate hourly` | Covers the next six hours in the effective report timezone and writes `hourly.md` by default. It does not accept `--date`, `--hours`, or `--duration`. |
| `run morning` and `run evening` | Run their defined report batches and write each selected report beneath the current directory unless `--out-dir` selects another directory. `--out` is not accepted. |
| `run morning` and `run evening` | Run their defined report batches beneath the configured output directory, or the current directory when none is configured. `--out-dir` selects another directory. `--out` is not accepted. |
`generate` accepts the four report command names shown above. `run` accepts
only `morning` and `evening`. Batch membership and notification ordering are
@@ -44,12 +45,14 @@ described in the [operations guide](operations.md).
## Output, Errors, And Quiet Mode
For `generate`, the default output is the report's filename in the current
directory. `--out PATH` selects one output file instead. A relative path is
resolved from the current directory; an absolute path is used as given. For a
batch, the equivalent default is the current directory and `--out-dir PATH`
selects its output directory. Successful summaries always report the resulting
absolute `outputPath` values.
For `generate`, the report's default filename is placed beneath
`output.directory` when configured, otherwise the current directory. `--out
PATH` selects one complete output file instead. A relative path is resolved
from the current directory; an absolute path is used as given. For a batch,
the configured directory has the same role and `--out-dir PATH` selects its
output directory instead. Successful summaries always report the resulting
absolute `outputPath` values. See the [configuration reference](config.md) for
the field's validation and path rules.
Outputs are written atomically. A generation, rendering, write, or cancellation
failure before publication leaves an existing destination unchanged. A
@@ -118,9 +121,9 @@ batch=morning total=2 succeeded=2 failed=0
| `--config PATH` | all commands | Load `PATH` instead of `/usr/local/etc/weatherreporter/config.yml`. |
| `--units VALUE` | `generate`, `run` | Override `weather_api.units` for this command. |
| `--tz NAME` | `generate`, `run` | Override `weather_api.timezone` for this command. |
| `--out PATH` | every `generate` command | Write the report to this file instead of its current-directory default. |
| `--out PATH` | every `generate` command | Write the report to this complete file destination instead of the configured or current-directory default. |
| `--llm-debug-dir PATH` | every `generate` and `run` command | Write requested sensitive prompt diagnostics under this absolute path. |
| `--out-dir PATH` | `run morning`, `run evening` | Write batch reports beneath this directory instead of the current directory. |
| `--out-dir PATH` | `run morning`, `run evening` | Write batch reports beneath this directory instead of the configured or current-directory default. |
| `--quiet` | `generate`, `run` | Suppress action summaries and routine batch status output. |
| `--date YYYY-MM-DD` | `generate daily`, `generate today` | Required for Daily; optional for Today. |

View File

@@ -81,6 +81,30 @@ Missing directories, unreadable files, subdirectories, symlinks, non-regular
files, and invalid names fail configuration loading. Put only secret values in
this directory, never in the YAML file.
### `output`
`output.directory` selects the ordinary operator-owned publication directory
for both individual reports and batches.
| Field | Default | Rules |
| --- | --- | --- |
| `directory` | empty | An omitted or empty value uses the invocation working directory. A nonempty value must contain at least one non-whitespace character. |
The configured value is preserved while configuration loads: it is not cleaned,
made absolute, inspected, created, or expanded through environment variables or
a home-directory shortcut. At execution, an absolute directory is used as
given; a relative directory resolves from the invocation working directory, not
from the configuration file's location. A missing directory is created when a
report is successfully published. An existing non-directory or an uninspectable
path fails output preflight before prompt inspection, weather collection, or
publication.
For one `generate` command, `--out` is a complete file destination and takes
precedence over `output.directory`. For `run`, `--out-dir` takes precedence.
Those explicit flags do not inspect or rebase beneath the configured directory.
See the [CLI reference](cli.md) for command selection and the [operations
guide](operations.md) for publication and failure handling.
### `notify.distributor`
Distributor notification is disabled by default. Its fields are:

View File

@@ -4,7 +4,7 @@
## Single-Report Flow
`GenerateDetailed` resolves the requested report and output destination, then initializes an optional explicit debug writer. It validates the exact Promptkit prompt and selected profile before collecting weather data. The resolved profile, backend, and model are carried in the active result.
`GenerateDetailed` resolves the requested report and output destination before initializing an optional explicit debug writer. An explicit output file wins; otherwise the configured output directory is used, falling back to the captured working directory. It validates the exact Promptkit prompt and selected profile before collecting weather data. The resolved profile, backend, and model are carried in the active result.
The workflow builds facts, a module snapshot, briefing metadata, and the YAML prompt package in memory. It executes Promptkit, validates the returned generated text, builds a render context, and renders Markdown. `fileutil` atomically writes the completed Markdown to the selected output path. Only after that write succeeds does single-report notification run.
@@ -12,7 +12,7 @@ Failures return an active partial result with safe identity, profile, warning, v
## Batches
`RunBatchDetailed` captures one output directory, creates at most one explicit debug writer, and uses one executor. Before collection it validates the prompt and profile candidates for the selected batch. It collects once, calculates the data-dependent plan, then validates and retains the final output path for every planned report before invoking the same generation core sequentially.
`RunBatchDetailed` selects an explicit output directory first, otherwise the configured directory and then the captured working directory. It does this before creating at most one explicit debug writer or validating prompt and profile candidates for the selected batch. It collects once, calculates the data-dependent plan, then validates and retains the final output path for every planned report before invoking the same generation core sequentially.
Each item has an independent result. A failed item does not stop later items; successful items retain their published output paths. Per-report notification is suppressed during a batch. Batch notification runs only after every planned report has published successfully. It is skipped when any item failed. Batch result counters count report items only; a batch notification failure is represented by the top-level notification result and still produces a failed batch outcome.

View File

@@ -4,7 +4,7 @@
The root `--version` flag reports the build version supplied by `internal/buildinfo`. Tagged release builds replace its development default at link time.
For each `generate` or `run` action, `Runner` constructs one project-owned Promptkit executor after configuration loads. It captures an absolute working directory, resolves a relative output override against it, and passes the working directory, resolved override, and any `--llm-debug-dir` request to the app. With no override, the app derives the report filename in that working directory. `run` uses the same resolution rule for `--out-dir`.
For each `generate` or `run` action, `Runner` constructs one project-owned Promptkit executor after configuration loads. It captures an absolute working directory, resolves only a relative explicit output override against it, and passes the working directory, loaded configuration, resolved override, and any `--llm-debug-dir` request to the app. The raw configured fallback remains in the configuration for app-owned destination selection. `run` uses the same explicit-resolution rule for `--out-dir`.
The CLI dispatches only generation and batch actions. It has no persisted-run or inspection dispatch. Summaries include report identity, status, output path, effective profile/backend/model, source warnings, validation, requested debug path, and notification result when available. They intentionally exclude prompt input, raw generated text, render context, endpoints, credentials, and full Distributor payloads. A failed action with a partial result still emits its safe summary before its error is returned.

View File

@@ -13,12 +13,20 @@ After configuring a Weather API endpoint, generate one report:
weatherreporter generate today
```
The command writes `today.md` in the current directory. Choose a different
operator-owned file with `--out`; a relative path is resolved from the current
directory and an absolute path is used directly. Weatherreporter renders in
memory and atomically replaces the selected destination only after generation
and rendering succeed. It does not create a default workspace, metadata,
receipts, or intermediate output files.
With no configured output directory, the command writes `today.md` in the
current directory. Set `output.directory` to use one ordinary publication
directory for reports, or choose a one-command operator-owned file with
`--out`; a relative path is resolved from the current directory and an absolute
path is used directly. The explicit flag takes precedence over the configured
directory. Weatherreporter renders in memory and atomically replaces the
selected destination only after generation and rendering succeed. It does not
create a default workspace, metadata, receipts, or intermediate output files.
A missing configured directory is created only as part of successful report
publication. If its existing path is not a directory or cannot be inspected,
the command stops before prompt inspection or weather collection, leaving any
existing report unchanged. See the [configuration reference](config.md) for the
field definition and validation rules.
Before a destination is published, provider, validation, rendering, write, and
cancellation failures leave an existing report unchanged. A notification
@@ -36,7 +44,9 @@ Run a scheduled batch with an explicit output directory when appropriate:
weatherreporter run morning --out-dir ./reports
```
Without `--out-dir`, batch reports are written beneath the current directory.
Without `--out-dir`, batch reports are written beneath `output.directory` when
configured, otherwise the current directory. The explicit directory applies
only to that command and takes precedence over the configured fallback.
Morning runs Today, Tomorrow, and every eligible dated Daily Report; evening
runs Tomorrow and the same eligible Daily Reports. Eligible Daily dates begin
after tomorrow and require complete hourly coverage for their local civil day.

View File

@@ -62,6 +62,8 @@ directly.
pre-publication failure, including cancellation observed immediately before
publication, does not replace an existing destination; a notification failure
does not remove a newly published output.
- Configuration or explicit CLI input selects that operator-owned destination;
it does not create an application-owned state boundary.
- Distributor uploads use only the published Markdown output, never a scan of
local files. Single notification follows publication; batch notification
follows publication of every selected report. Batch counters describe report

View File

@@ -14,6 +14,9 @@ location:
secrets:
directory: ""
output:
directory: /var/lib/weatherreporter/reports
notify:
distributor:
enabled: false

View File

@@ -7,6 +7,7 @@ import (
"errors"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
@@ -50,7 +51,8 @@ func TestResolveRunActionConstructsOneExecutor(t *testing.T) {
func TestResolveGenerateActionUsesInjectedWorkingDirectoryForOutputOverrides(t *testing.T) {
workingDir := t.TempDir()
configPath := filepath.Join(t.TempDir(), "config.yml")
if err := os.WriteFile(configPath, []byte("weather_api:\n base_url: https://weather.api.example.com/\n"), 0o600); err != nil {
const configuredDirectory = "configured/../reports"
if err := os.WriteFile(configPath, []byte("weather_api:\n base_url: https://weather.api.example.com/\noutput:\n directory: "+configuredDirectory+"\n"), 0o600); err != nil {
t.Fatal(err)
}
absoluteOutput := filepath.Join(t.TempDir(), "daily.md")
@@ -76,13 +78,76 @@ func TestResolveGenerateActionUsesInjectedWorkingDirectoryForOutputOverrides(t *
args = append(args, "--out", scenario.out)
}
req, _, err := runner.resolveGenerateAction(args)
if err != nil || req.WorkingDir != workingDir || req.OutputPath != scenario.want {
if err != nil || req.WorkingDir != workingDir || req.OutputPath != scenario.want || req.Config.Output.Directory != configuredDirectory {
t.Fatalf("resolveGenerateAction() request/error = %#v/%v", req, err)
}
})
}
}
func TestResolveActionsPreserveConfiguredOutputDirectoryWithoutAnOverride(t *testing.T) {
workingDir := t.TempDir()
absoluteDirectory := filepath.Join(t.TempDir(), "reports")
for _, configuredDirectory := range []string{"configured/../reports", absoluteDirectory} {
t.Run(configuredDirectory, func(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "config.yml")
contents := "weather_api:\n base_url: https://weather.api.example.com/\noutput:\n directory: " + strconv.Quote(configuredDirectory) + "\n"
if err := os.WriteFile(configPath, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
runner := Runner{
Clock: timeutil.FixedClock{Time: time.Date(2026, 5, 29, 8, 0, 0, 0, time.UTC)},
WorkingDir: workingDir,
ExecutorFactory: func(PromptExecutorConfig) (promptexec.Executor, error) {
return &factoryExecutor{}, nil
},
}
generateReq, _, generateErr := runner.resolveGenerateAction([]string{"daily", "--date", "2026-05-29", "--config", configPath})
if generateErr != nil || generateReq.Config.Output.Directory != configuredDirectory || generateReq.OutputPath != "" {
t.Fatalf("resolveGenerateAction() request/error = %#v/%v", generateReq, generateErr)
}
batchReq, _, batchErr := runner.resolveRunAction([]string{"morning", "--config", configPath})
if batchErr != nil || batchReq.Config.Output.Directory != configuredDirectory || batchReq.OutputDir != "" {
t.Fatalf("resolveRunAction() request/error = %#v/%v", batchReq, batchErr)
}
})
}
}
func TestResolveRunActionUsesInjectedWorkingDirectoryForOutputOverrides(t *testing.T) {
workingDir := t.TempDir()
configPath := filepath.Join(t.TempDir(), "config.yml")
const configuredDirectory = "configured/../reports"
if err := os.WriteFile(configPath, []byte("weather_api:\n base_url: https://weather.api.example.com/\noutput:\n directory: "+configuredDirectory+"\n"), 0o600); err != nil {
t.Fatal(err)
}
absoluteOutput := filepath.Join(t.TempDir(), "reports")
for _, scenario := range []struct {
name string
out string
want string
}{
{name: "relative", out: "reports/../published", want: filepath.Join(workingDir, "published")},
{name: "absolute", out: absoluteOutput, want: absoluteOutput},
} {
t.Run(scenario.name, func(t *testing.T) {
runner := Runner{
Clock: timeutil.FixedClock{Time: time.Date(2026, 5, 29, 8, 0, 0, 0, time.UTC)},
WorkingDir: workingDir,
ExecutorFactory: func(PromptExecutorConfig) (promptexec.Executor, error) {
return &factoryExecutor{}, nil
},
}
req, _, err := runner.resolveRunAction([]string{"morning", "--config", configPath, "--out-dir", scenario.out})
if err != nil || req.WorkingDir != workingDir || req.OutputDir != scenario.want || req.Config.Output.Directory != configuredDirectory {
t.Fatalf("resolveRunAction() request/error = %#v/%v", req, err)
}
})
}
}
func TestRunActionReturnsFailureForBatchNotificationFailure(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "config.yml")
if err := os.WriteFile(configPath, []byte("weather_api:\n base_url: https://weather.api.example.com/\n"), 0o600); err != nil {

View File

@@ -214,6 +214,9 @@ func TestLoadExampleConfig(t *testing.T) {
if cfg.Location.ID != "home" || cfg.Location.Name != "Brentwood" || cfg.Location.Region != "St. Louis Metro" {
t.Fatalf("Location = %#v, want example location", cfg.Location)
}
if cfg.Output.Directory != "/var/lib/weatherreporter/reports" {
t.Fatalf("Output.Directory = %q, want maintained example value", cfg.Output.Directory)
}
if cfg.Notify.Distributor.PipelineIDTemplate != "weatherreporter.{report_id}" {
t.Fatalf("PipelineIDTemplate = %q, want example pipeline template", cfg.Notify.Distributor.PipelineIDTemplate)
}