Files
weatherreporter/docs/roadmap/implementation.md

17 KiB

Configurable Output Directory Implementation Plan

Status: Complete.

Purpose And Authority

This plan implements the accepted Configurable Output Directory Roadmap. The feature roadmap is authoritative for user intent, scope, policy choices, non-goals, and the desired end state. This document owns implementation order, concrete changes, verification, and exit gates.

The target is one optional output.directory configuration value that selects the ordinary publication directory for both single reports and batches while preserving explicit CLI overrides, current defaults, stateless execution, atomic publication, and notification ordering.

This plan follows the repository's architecture, documentation, and testing policies.

Implementation Rules

  1. Implement the stages in numeric order. Keep the repository buildable and the default suite passing at every stage.
  2. Use GOWORK=off for all Go validation so the sibling workspace cannot supply unpublished dependency changes.
  3. Keep this change within the configurable-output-directory scope. Do not implement any part of the accepted LLM profile comparison roadmap.
  4. Preserve one operator-owned Markdown output per generated report. Do not add a workspace, manifest, history, cache, retention policy, directory scan, or managed cleanup behavior.
  5. Preserve current report filenames, report periods, batch membership, Promptkit inspection and execution, generated-text validation, rendering, cancellation, atomic file publication, action summaries, and Distributor notification ordering.
  6. Keep CLI flag parsing and current-working-directory capture in internal/cli, configuration loading and validation in internal/config, and effective destination resolution and publication in internal/app.
  7. Keep tests deterministic, offline, credential-free, and independent of process-wide os.Chdir or machine-specific directories. Use t.TempDir() and injected working directories.
  8. Update current-state documentation only when the corresponding behavior is implemented. Do not create release notes, select a release version, tag, or publish a release in these stages.
  9. Run gofmt on changed Go files and git diff --check before completing every stage. Do not weaken unrelated tests or rewrite historical release notes.

Locked Product And Path Decisions

Implement exactly this optional configuration shape:

output:
  directory: /var/lib/weatherreporter/reports

The following decisions are fixed:

  • Add config.OutputConfig with one Directory string field and add Config.Output with YAML key output. Do not add other output settings.
  • The built-in default is the empty string. Omitted and explicitly empty values preserve current-directory behavior.
  • A whitespace-only configured value is invalid. Preserve a nonblank value as supplied during configuration loading; do not trim it into a different filesystem path.
  • Configuration loading performs no filesystem lookup and creates no directory. Runtime output preflight owns path cleaning and filesystem inspection.
  • A relative configured directory resolves against the absolute working directory captured for the invocation, never against the configuration-file directory. There is no environment-variable or home-directory expansion.
  • A nonexistent configured directory is valid and is created only as needed by ordinary atomic report publication.
  • An existing configured path that is not a directory, or cannot be inspected, fails through the existing output-preflight boundary before prompt inspection, weather collection, model execution, or output replacement.
  • Single-report precedence is explicit --out path, then output.directory plus the report's existing default filename, then the working directory plus that filename.
  • Batch precedence is explicit --out-dir, then output.directory, then the working directory.
  • An explicit --out is the complete file destination. A relative --out remains relative to the invocation working directory and is never rebased beneath output.directory.
  • Configuration must still pass strict validation before any command runs. After a valid configuration loads, explicit output flags completely ignore the configured directory for runtime resolution and filesystem inspection. They retain their current absolute and relative semantics.
  • Successful result and summary paths remain cleaned absolute paths. Default filenames and all output, cancellation, and notification safety guarantees remain unchanged.

Stage 1: Add The Configuration Contract

Goal

Make output.directory a strict, validated configuration value without changing runtime destination behavior or current-state documentation yet.

Work

  1. In internal/config/config.go, add:
    • Output OutputConfig to Config with YAML key output; and
    • OutputConfig containing only Directory string with YAML key directory.
  2. In internal/config/defaults.go, initialize Output.Directory to the empty string explicitly so the public default is visible beside the other configuration defaults.
  3. In internal/config/validate.go, reject Output.Directory only when it is nonempty and strings.TrimSpace finds no non-whitespace character. Use an actionable error that identifies output.directory. Do not clean, make absolute, stat, create, or otherwise mutate the path during validation.
  4. Rely on the existing yaml.Decoder.KnownFields(true) boundary for strict nested decoding. Do not introduce custom YAML unmarshalling or a second unknown-field mechanism for OutputConfig.
  5. Add focused configuration tests that prove:
    • the exact empty default;
    • omission and explicit directory: "" are accepted;
    • representative absolute and relative values load without mutation;
    • a whitespace-only value fails with output.directory context;
    • an unknown field beneath output fails strict loading; and
    • constructed and YAML-loaded configurations receive the same validation result.
  6. Keep the maintained example and current-state documentation unchanged in this stage; they are updated after runtime behavior lands.

Tests

Run:

GOWORK=off go test -count=1 ./internal/config
GOWORK=off go test -count=1 ./...
git diff --check

Exit Gate

The configuration package exposes and validates the additive field, strict loading rejects malformed output configuration, existing configurations remain valid, and runtime output behavior is still unchanged.

Stage 2: Centralize And Apply Effective Destination Resolution

Goal

Apply the locked precedence to single-report and batch orchestration through one coherent app-owned output-resolution boundary.

Work

  1. Create internal/app/output.go and move the existing app-owned output helpers out of app.go without changing their established safety behavior: prepareBatchOutputs, plannedBatchOutputPath, resolveReportOutputPath, resolveOutputDir, resolveOutputPath, validateWorkingDir, and validateOutputPath. Clean up imports in app.go; do not export these helpers or introduce a new package.
  2. Add one narrow directory-precedence helper in output.go. It must select a nonempty explicit directory first, otherwise the configured directory, and then pass that selected value to the existing directory resolver, whose empty case returns the validated working directory. Do not inspect the lower-precedence configured path when an explicit directory is present.
  3. Extend the single-report resolver to accept the configured directory and implement this exact branch:
    • obtain the existing default output name from report.Resolved;
    • when GenerateRequest.OutputPath is nonempty, resolve and validate that complete explicit path against WorkingDir and ignore the configured directory;
    • otherwise resolve the configured directory, falling back to WorkingDir, and place the existing default filename beneath it; and
    • validate and return one cleaned absolute final file path.
  4. Change GenerateDetailed to pass req.Config.Output.Directory into that resolver before debug-writer construction, prompt inspection, weather collection, or model execution. Preserve its current partial-result and destination-preservation behavior on resolution failure.
  5. Change RunBatchDetailed to resolve its base directory from req.OutputDir, req.Config.Output.Directory, and req.WorkingDir in that order before debug-writer construction, prompt inspection, or collection. Preserve data-dependent batch planning and the later preflight of every final report file.
  6. Do not add output-directory fields to GenerateRequest or BatchRequest. Their existing explicit override fields remain CLI/action inputs, while the configured fallback remains in Config.
  7. Add focused internal/app tests, using existing real filesystem behavior and project-owned fakes, that prove:
    • omitted configuration retains current-directory destinations for a representative single report and batch;
    • absolute and relative configured directories produce the expected cleaned absolute destinations;
    • single-report OutputPath and batch OutputDir overrides win and do not inspect or use a lower-precedence configured path;
    • a configured existing non-directory fails before prompt inspection, collection, model execution, publication, or notification;
    • a missing configured directory is created through successful ordinary publication;
    • existing atomic replacement and cancellation tests continue to pass; and
    • result paths and Distributor source paths still identify only the final selected Markdown files.
  8. Prefer a compact resolver table plus representative assembled workflows. Do not duplicate every path case at configuration, CLI, and app layers.

Tests

Run:

GOWORK=off go test -count=1 ./internal/app ./internal/fileutil
GOWORK=off go test -count=1 ./...
git diff --check

Exit Gate

Direct app callers, ordinary generate, and batches use the locked CLI/configuration/working-directory precedence; paths are validated before expensive work; and output, cancellation, and notification invariants remain intact.

Stage 3: Complete CLI Coverage And Current-State Documentation

Goal

Protect the assembled CLI-to-app contract and make every canonical document and maintained example describe the now-implemented behavior exactly once.

Work

  1. Keep internal/cli.resolveOutputOverride limited to explicit --out and --out-dir values. It must continue resolving relative flag values against the captured working directory. Do not resolve output.directory in the CLI or replace the raw configured value in Config; the app owns final precedence and runtime path validation.

  2. Add or update focused internal/cli request-construction tests to prove:

    • a configured relative or absolute directory survives configuration loading and reaches the app request while the absent CLI override remains empty;
    • relative and absolute --out and --out-dir values retain their current resolution; and
    • an explicit flag coexists with the loaded configured value so app-owned precedence can ignore the latter. Keep these tests narrow; app tests own final path and publication outcomes.
  3. Add this illustrative stanza to examples/config.yml:

    output:
      directory: /var/lib/weatherreporter/reports
    

    Keep examples/minimal-config.yml unchanged so it continues to demonstrate the empty-default path. Extend TestLoadExampleConfig to assert the exact maintained example value.

  4. Update canonical current-state documentation according to docs/policy/documentation.md:

    • docs/config.md owns the field, empty default, whitespace validation, absolute and relative semantics, lack of expansion, and configuration versus flag precedence;
    • docs/cli.md owns the unchanged flag syntax and the effective flag/configuration/working-directory selection visible to users;
    • docs/operations.md owns normal operation with a configured publication directory, explicit one-command overrides, missing-directory creation, atomic replacement, and failure handling;
    • README.md updates only its short quickstart destination summary and links to the canonical configuration/CLI references;
    • docs/policy/architecture.md states that configuration or explicit CLI input selects the operator-owned output destination without changing the stateless boundary;
    • docs/internal/cli.md describes capture and transport of the configured fallback plus explicit overrides without duplicating the field reference; and
    • docs/internal/app-orchestration.md describes app-owned effective destination precedence and the unchanged single and batch preflight ordering.
  5. Do not change historical release notes. Do not duplicate the complete field contract outside docs/config.md, complete flag syntax outside docs/cli.md, or operational procedures outside docs/operations.md.

  6. Review all changed commands, values, paths, and examples against executable code. Follow every added or changed repository-relative link and confirm its target exists.

Tests

Run:

GOWORK=off go test -count=1 ./internal/config ./internal/cli ./internal/app
GOWORK=off go run ./cmd/weatherreporter --help
GOWORK=off go test -count=1 ./...
git diff --check

Confirm the maintained full and minimal examples both load through the configuration test suite and no current-state document still claims that the working directory is always the no-flag destination.

Exit Gate

The CLI-to-app boundary is protected without duplicating app behavior tests, the maintained example is executable configuration, and all canonical current-state documentation agrees with the implemented contract.

Stage 4: Reconcile Roadmaps And Run The Final Gate

Goal

Verify the complete feature in the same module-isolated environment used for release preparation and leave roadmap status accurate for post-implementation review.

Work

  1. Review the complete diff against the feature roadmap, this plan, and all documents under docs/policy/. Remove incidental refactors, redundant tests, duplicate documentation, and any behavior outside the accepted scope.
  2. Confirm repository hygiene:
    • no tracked go.work or go.work.sum;
    • no vendor directory or go.mod replacement;
    • no generated binaries, test output, credentials, private paths, or temporary filesystem artifacts; and
    • no unintended changes to the profile-comparison feature.
  3. After every implementation and documentation gate passes, update docs/roadmap/output-directory.md from Accepted; unimplemented to Implemented; retained temporarily for post-implementation review. Preserve its target-state purpose and policy content until roadmap cleanup is separately authorized.
  4. Update the prerequisite section in docs/roadmap/profile-comparison.md to state that configurable output directories are implemented. Link its current behavior summary to docs/config.md and docs/operations.md rather than duplicating their contracts. Do not otherwise advance or implement profile comparison.
  5. Mark this plan Status: Complete only after the final gate succeeds. Do not delete either roadmap or this plan in this stage; repository roadmap cleanup follows post-implementation review.

Tests

Run from the repository root:

set -eu
test -z "$(git ls-files go.work go.work.sum)"
test ! -e vendor
if grep -Eq '^[[:space:]]*replace([[:space:]]|\()' go.mod; then
  printf '%s\n' 'go.mod contains a replacement' >&2
  exit 1
fi
GOWORK=off go test -count=1 ./...
GOWORK=off go test -race -count=1 ./...
GOWORK=off go vet ./...
GOWORK=off go build ./...
GOWORK=off go mod tidy -diff
unformatted="$(git ls-files '*.go' | while IFS= read -r file; do gofmt -l "$file"; done)"
test -z "$unformatted"
GOWORK=off go run ./cmd/weatherreporter --help >/dev/null
git diff --check
git diff --cached --check

Follow every added or changed local documentation link and verify the full and minimal maintained configuration examples through their tests.

Exit Gate

The complete repository passes normal, race-enabled, vet, build, module, formatting, help, documentation, and hygiene checks with GOWORK=off; roadmap status accurately distinguishes the implemented output-directory prerequisite from the still-unimplemented profile-comparison feature; and no required work remains.

Open Questions

None. The feature roadmap and this plan define every product, architecture, precedence, path, compatibility, testing, documentation, and completion choice required for implementation.