Files
weatherreporter/docs/policy/development.md

17 KiB
Raw Blame History

Weatherreporter Package Layout

This document defines the proposed package layout for weatherreporter, a Go application that prepares human-facing weather reports from normalized weather data collected by weatherfeeder and rendered through scriptorium.

The application should remain a small, explicit, dependency-light Go program. Domain logic should live outside CLI, transport, and external-adapter packages. External systems should be isolated behind narrow adapters. Report-specific behavior should be selected through a registry or equivalent mechanism rather than scattered conditionals.

Architectural Summary

weatherreporter is a deterministic weather briefing and report-preparation application. It should:

  1. Fetch normalized weather data from an internal weather API backed by weatherfeeder.
  2. Derive report-specific briefing packages from the normalized forecast bundle.
  3. Compare current briefing snapshots against prior comparable snapshots to produce optional Recent Changes.
  4. Build structured prompt variables for a specific report type.
  5. Invoke scriptorium as an external prompt runner.
  6. Persist the rendered Markdown report, briefing snapshot, and generation metadata.

The preferred data flow is:

weatherfeeder-backed internal API
    -> weather API adapter
    -> forecast bundle
    -> report-specific briefing builder
    -> recent-change comparison
    -> prompt variable package
    -> scriptorium subprocess adapter
    -> Markdown report + metadata + stored snapshot

The application should not treat the LLM as the source of weather facts. The Go code should select the relevant data, compute daypart and period summaries, attach alerts and NWS context, identify meaningful changes, and send the LLM a curated briefing package. The LLM should synthesize and phrase the report for humans.

Proposed Directory Layout

cmd/weatherreporter/
  main.go

internal/app/
  generate.go
  scheduled.go
  storm.go

internal/cli/
  root.go
  generate.go
  run.go
  inspect.go

internal/config/
  config.go
  defaults.go
  load.go
  validate.go

internal/adapters/weatherapi/
  client.go
  types.go

internal/adapters/scriptorium/
  runner.go
  types.go

internal/forecast/
  bundle.go
  dayparts.go
  derive.go
  select.go
  thresholds.go

internal/report/
  definition.go
  registry.go
  period.go
  daily.go
  tomorrow.go
  three_day.go
  weekend.go
  storm.go

internal/briefing/
  package.go
  daily.go
  tomorrow.go
  three_day.go
  weekend.go
  storm.go

internal/changes/
  compare.go
  thresholds.go
  summary.go

internal/state/
  store.go
  filesystem.go
  metadata.go

internal/promptvars/
  build.go
  schema.go

internal/timeutil/
  clock.go
  periods.go

This layout can be simplified during early prototyping if a package has only one file, but the package boundaries should remain conceptually stable.

Dependency Direction

The intended dependency direction is:

cmd/weatherreporter
    -> internal/cli
    -> internal/app
    -> internal/config
    -> internal/report
    -> internal/briefing
    -> internal/forecast
    -> internal/changes
    -> internal/state
    -> internal/adapters/*

Rules:

  • cmd/weatherreporter should only bootstrap the CLI.
  • internal/cli should parse commands and flags, then call internal/app.
  • internal/app should orchestrate workflows but avoid embedding detailed forecast logic.
  • internal/adapters/* should not contain domain policy.
  • internal/forecast, internal/report, internal/briefing, and internal/changes should be testable without real external services.
  • internal/state should expose a storage interface so filesystem state can later be replaced or supplemented.
  • scriptorium details should not leak outside internal/adapters/scriptorium.

Package Responsibilities

cmd/weatherreporter

Entry point for the compiled binary.

Responsibilities:

  • Construct the root command from internal/cli.
  • Execute the command.
  • Handle final process exit behavior.

Non-responsibilities:

  • No configuration loading details.
  • No forecast logic.
  • No direct calls to weather APIs, state stores, or scriptorium.

internal/cli

Defines the user-facing command tree, flags, arguments, and command wiring.

Responsibilities:

  • Define commands such as:
    • weatherreporter generate daily
    • weatherreporter generate tomorrow
    • weatherreporter generate three-day
    • weatherreporter generate weekend
    • weatherreporter generate storm
    • weatherreporter run morning
    • weatherreporter run evening
    • weatherreporter inspect snapshot
  • Parse CLI flags and convert them into app-layer request structs.
  • Load configuration through internal/config.
  • Present concise user-facing errors.

Non-responsibilities:

  • No report-building logic.
  • No direct subprocess execution.
  • No direct weather API calls.
  • No state comparison logic.

Suggested command shape:

weatherreporter generate daily --location home --date today --out ./daily.md
weatherreporter generate tomorrow --location home --out ./tomorrow.md
weatherreporter generate three-day --location home --out ./three_day.md
weatherreporter generate weekend --location home --out ./weekend.md
weatherreporter generate storm --location home --out ./storm.md
weatherreporter run morning --location home
weatherreporter run evening --location home

internal/config

Owns configuration structures, defaults, loading, precedence, and validation.

Responsibilities:

  • Define application configuration structs.
  • Provide built-in defaults in defaults.go.
  • Load YAML configuration from /usr/local/etc/weatherreporter/config.yml or a CLI-supplied path.
  • Apply precedence rules.
  • Validate required settings.
  • Normalize paths, durations, report settings, locations, and daypart definitions.

Suggested configuration areas:

  • Weather API base URL, timeout, and location endpoints.
  • Locations and time zones.
  • scriptorium binary, profile, timeout, and optional extra arguments.
  • Workspace and output directories.
  • Report enablement and output naming.
  • Daypart definitions.
  • Recent-change thresholds.

Non-responsibilities:

  • No command execution.
  • No HTTP calls.
  • No report-building logic.

internal/app

Application orchestration and top-level use cases.

Responsibilities:

  • Implement use cases such as:
    • Generate one report.
    • Run the morning batch.
    • Run the evening batch.
    • Generate a manual storm report.
  • Coordinate config, weather API adapter, report registry, briefing builders, state store, change comparison, prompt variable builder, and scriptorium runner.
  • Enforce workflow order.
  • Ensure each generation run persists enough artifacts for inspection and future comparison.

The core generation workflow should be approximately:

resolve report definition
resolve location and valid period
fetch current weather bundle
build current briefing package
load prior comparable briefing snapshot
compute recent changes
build prompt variables
write vars file
invoke scriptorium
persist report metadata and briefing snapshot

Non-responsibilities:

  • No detailed daypart calculations.
  • No direct parsing of NWS text unless delegated to domain packages.
  • No direct shell command construction outside the scriptorium adapter.

internal/adapters/weatherapi

HTTP adapter for the internal weather API backed by weatherfeeder.

Responsibilities:

  • Fetch normalized weather data for a configured location.
  • Decode API responses into adapter-owned DTOs or directly into stable internal types if those types are intentionally owned by weatherreporter.
  • Apply request timeouts and context cancellation.
  • Return actionable errors containing endpoint and operation context.

Expected data categories:

  • Hourly forecast data.
  • Daily forecast data.
  • NWS narrative forecast periods.
  • NWS alerts.
  • NWS forecast discussion.
  • NWS weather story.

Non-responsibilities:

  • No daypart grouping.
  • No Recent Changes comparison.
  • No prompt variable construction.
  • No scriptorium calls.

internal/adapters/scriptorium

Subprocess adapter for invoking scriptorium.

Responsibilities:

  • Provide a narrow runner interface, such as:
type Runner interface {
    Run(ctx context.Context, req RunRequest) (*RunResult, error)
}
  • Execute scriptorium run with exec.CommandContext.
  • Pass arguments as an argv slice, not through a shell.
  • Prefer a vars file path over large inline JSON.
  • Capture stdout/stderr with reasonable size limits.
  • Treat nonzero exits as actionable errors.
  • Keep all scriptorium-specific flag details inside the adapter.

Suggested command form:

scriptorium run \
  --prompt weather.daily_report \
  --vars-file ./workspace/daily.vars.json \
  --out ./workspace/daily_report.md

Non-responsibilities:

  • No weather logic.
  • No report registry logic.
  • No decision about which prompt to run.

Future note:

  • A native LLM client can later replace or supplement this adapter behind a similar interface.

internal/forecast

Core forecast-domain processing.

Responsibilities:

  • Define the normalized Bundle consumed by report builders.
  • Group hourly forecast data into configured dayparts.
  • Compute derived facts, including:
    • Temperature ranges.
    • Apparent-temperature ranges, if available.
    • Max precipitation probability.
    • Peak wind and wind gusts.
    • Precipitation windows.
    • Thunder mentions.
    • Snow/ice/freezing risk indicators.
    • Alert overlap with relevant periods.
  • Select forecast elements relevant to a report period.
  • Provide threshold helpers for impact detection.

Non-responsibilities:

  • No CLI behavior.
  • No external API calls.
  • No rendered prose.
  • No direct scriptorium calls.

internal/report

Report definitions, registry, period resolution, and report-level contracts.

Responsibilities:

  • Define report IDs and report definition contracts.
  • Register report types and variants.
  • Resolve valid periods for each report.
  • Associate report types with prompt IDs.
  • Define comparison strategies and output naming behavior.

Suggested report definitions:

daily_today       -> prompt weather.daily_report
daily_tomorrow    -> prompt weather.daily_report or weather.tomorrow_report
three_day         -> prompt weather.three_day_outlook
weekend           -> prompt weather.weekend_outlook
storm             -> prompt weather.storm_report

A report definition should describe:

  • Report ID.
  • Human-readable name.
  • Prompt ID.
  • Valid-period resolver.
  • Briefing builder ID or function.
  • Recent-change comparison strategy.
  • Default output naming pattern.
  • Whether the report participates in morning or evening scheduled batches.

Non-responsibilities:

  • No detailed forecast computation.
  • No state storage.
  • No subprocess execution.

internal/briefing

Builds report-specific briefing packages from forecast bundles and report definitions.

Responsibilities:

  • Convert a forecast bundle into a report-specific structured briefing package.
  • Keep each reports briefing shape explicit and testable.
  • Attach relevant NWS narrative periods, alerts, forecast discussion context, and weather story context.
  • Provide inputs suitable for LLM prompt variables.

Report-specific builders should exist for:

  • Daily Report.
  • Tomorrow Planning Brief.
  • 3-Day Outlook.
  • Weekend Outlook.
  • Storm Report.

Non-responsibilities:

  • No external API fetching.
  • No final prose rendering.
  • No state persistence, except through app orchestration.

Design note:

  • This package is the architectural center of the application. A clean briefing package makes scriptorium a renderer rather than a source of weather reasoning.

internal/changes

Structured comparison of current and prior briefing snapshots.

Responsibilities:

  • Compare current briefing packages against prior comparable snapshots.
  • Apply meaningful-change thresholds.
  • Produce compact structured change summaries for prompt variables.
  • Avoid comparison of rendered Markdown report text.

Meaningful changes may include:

  • Temperature changes crossing configured thresholds.
  • Precipitation probability changes by category.
  • Precipitation timing shifts.
  • New, canceled, extended, upgraded, or expanded alerts.
  • Wind gust threshold crossings.
  • Snow/ice/freezing risk changes.
  • Severe-weather wording or risk changes.
  • Confidence or uncertainty changes, if represented in structured briefing data.

Non-responsibilities:

  • No fetching prior state directly unless mediated through app/state contracts.
  • No final report prose.
  • No external calls.

internal/state

Durable state store for reports, snapshots, metadata, and comparison lookup.

Responsibilities:

  • Persist generated report metadata.
  • Persist briefing snapshots.
  • Persist prompt variable files when useful for inspection.
  • Locate prior comparable snapshots for Recent Changes.
  • Use atomic writes where practical.
  • Keep filesystem layout narrow and predictable.

Initial backend:

  • Filesystem state.

Potential future backend:

  • SQLite or another state database, behind the same store interface.

Suggested state layout:

workspace/
  locations/
    home/
      snapshots/
        daily/
          2026-05-30/
            2026-05-29T050000-0500.briefing.json
            2026-05-29T050000-0500.metadata.json
        three-day/
        weekend/
        storm/
      reports/
        daily/
          2026-05-30.md
        three-day/
        weekend/
        storm/
      vars/
        daily/
          2026-05-29T050000-0500.vars.json

Non-responsibilities:

  • No weather derivation.
  • No report prose generation.
  • No CLI formatting decisions.

internal/promptvars

Builds the final variable payload passed to scriptorium.

Responsibilities:

  • Combine report metadata, briefing package, Recent Changes, and selected source context into a prompt variable document.
  • Validate required prompt variables before invoking scriptorium.
  • Keep prompt variable schemas explicit enough to test.
  • Write vars files to the workspace when requested by the app layer.

Non-responsibilities:

  • No weather API calls.
  • No forecast derivation.
  • No subprocess execution.

internal/timeutil

Time, clock, and period helpers.

Responsibilities:

  • Provide an injectable clock for deterministic tests.
  • Resolve local dates using the configured location time zone.
  • Handle daypart spans, including overnight windows.
  • Normalize valid periods.
  • Provide helpers for recurring scheduled batches.

Non-responsibilities:

  • No report-specific forecast logic unless delegated by internal/report.
  • No external calls.

Report Types and Valid-Period Identity

Each generated report must be associated with explicit metadata:

  • Report type.
  • Report variant, if applicable.
  • Location ID.
  • Generation time.
  • Valid period start.
  • Valid period end.
  • Source product timestamps and/or hashes.
  • Briefing snapshot path.
  • Prompt variable path.
  • Rendered report path.

The valid period should identify what weather period the report covers, independent of when the report was generated.

Examples:

  • A 5 PM Tomorrow Planning Brief for Saturday and a 5 AM Saturday Daily Report both cover the same valid date.
  • A Saturday Weekend Outlook covers the remaining weekend, while a Friday Weekend Outlook may cover Friday evening through Sunday night.
  • A Storm Report covers a forecast event window, not a fixed calendar day.

This identity is required for reliable Recent Changes behavior.

Scheduled Batch Semantics

The app should support scheduled batches but should not need to be a daemon in the initial version.

Suggested batches:

morning:
  - daily_today
  - three_day
  - weekend, except Sunday

evening:
  - daily_tomorrow

External scheduling should be handled by systemd timers, cron, or another orchestrator. weatherreporter should simply provide deterministic commands that can be scheduled.

Storm Report Direction

The initial version should support manual Storm Report generation:

weatherreporter generate storm --location home

Future storm monitoring should use a staged design:

incoming weather data
    -> deterministic candidate detector
    -> LLM event evaluator
    -> storm lifecycle state
    -> storm report generation or skip decision

Potential storm lifecycle states:

none -> monitoring -> active_report -> escalated -> deescalating -> resolved

This future behavior should not be built before the core scheduled reports are stable, but the package layout should leave room for it.

Testing Expectations

Core tests should not require real external services.

Priority test areas:

  • Configuration loading and validation.
  • Daypart grouping, especially overnight periods.
  • Valid-period resolution for each report type.
  • Briefing package construction from fixtures.
  • Recent Changes threshold behavior.
  • Prior snapshot lookup.
  • scriptorium adapter behavior using a fake executable or command runner.
  • CLI command parsing for major workflows.

Design Invariants

Preserve these invariants as the project evolves:

  • Weather facts come from normalized source data, not from the LLM.
  • The LLM receives curated briefing packages, not unbounded raw weather payloads.
  • Recent Changes are based on structured snapshot comparison, not Markdown diffing.
  • Report types are registered or otherwise centrally defined.
  • External integrations are thin adapters.
  • CLI code wires workflows but does not own domain logic.
  • The first durable state backend is filesystem-based and inspectable.
  • scriptorium is an adapter boundary, not an application dependency that leaks across packages.