Files
weatherreporter/docs/roadmap/implementation.md

792 lines
26 KiB
Markdown

# Modular Data Package Implementation Roadmap
This roadmap is a staged implementation plan for
[`docs/roadmap/modules.md`](modules.md). It is future-work planning only. The
target audience is an LLM coding agent implementing each stage in order.
## Purpose
Implement a pre-release hard cutover from report-shaped briefing packages to a
module-oriented prompt package architecture:
```text
CollectedFacts -> DerivedFacts -> ModuleOutput
```
The implementation should produce YAML `data_package` artifacts with named
stanzas for Scriptorium prompts, persist JSON module snapshots for inspection
and Recent Changes, and make report composition configurable through ordered
module IDs plus typed module options.
## Source Roadmap
[`docs/roadmap/modules.md`](modules.md) is authoritative for the conceptual
policy, user intent, target prompt shape, boundaries, and acceptance criteria
for this refactor. This document is authoritative for implementation order,
stage scope, file/package guidance, and validation commands.
If this implementation plan appears to conflict with `modules.md`, stop and
reconcile the roadmap before changing code. Do not infer a different policy
from stage sequencing.
## Locked Decisions
- Do a clean break. Do not preserve old report-shaped briefing JSON as a
compatibility layer.
- Introduce `internal/weatherdata` for normalized collected source types.
- Keep `internal/forecast` for forecast-specific derivation algorithms such as
period selection, daily summaries, daypart grouping, and precipitation
timing.
- Introduce an internal fact contract for `CollectedFacts` and `DerivedFacts`.
- Introduce a narrow module contract for module IDs, typed options, outputs,
and snapshots.
- Persist both artifacts:
- JSON module snapshots for state, inspection, and Recent Changes;
- YAML prompt data packages passed to Scriptorium as `data_package`.
- Replace `weatherreporter inspect briefing` with
`weatherreporter inspect modules`.
- Support typed module options from the first configurable composition pass.
- Use named YAML stanzas, not a generic array of module objects.
- Omit QPF fields until a real upstream QPF source is represented in
`CollectedFacts`.
- Keep public generate/run command names, report IDs, prompt IDs, RunID format,
managed Markdown report paths, and distributor upload source stable.
- Do not introduce plugins, dynamic loading, generic workflow engines, or
module-owned upstream fetching.
## Target Packages
Implementation should converge on this package ownership:
- `internal/weatherdata`: normalized collected source facts, source metadata,
source warnings, and broad weather data types.
- `internal/forecast`: deterministic forecast-specific algorithms over
`weatherdata` types.
- `internal/facts`: `CollectedFacts`, `DerivedFacts`, and their builders.
- `internal/module`: stable module IDs, module output envelope, module snapshot
shape, module config item shape, and shared option/output contracts that must
be imported by both `internal/report` and `internal/briefing`.
- `internal/briefing`: module registry and module builders.
- `internal/report`: report definitions, valid periods, output identity,
comparison strategy, and default module composition.
- `internal/config`: YAML config structs, defaults, loading, and validation for
module composition overrides.
- `internal/promptinput`: YAML prompt package assembly, validation, and save
behavior.
- `internal/changes`: structured comparison over module snapshots.
- `internal/state`: module snapshot paths, YAML data package paths, metadata
links, and inspection loads.
- `internal/app`: orchestration only.
Avoid import cycles. In particular, `internal/report` may import
`internal/module` for module IDs, but `internal/module` must not import
`internal/report`.
## Target Artifacts
Use explicit schema versions:
- Module snapshot JSON: `weatherreporter.modules.v1`
- YAML prompt data package: `weatherreporter.data_package.v2`
Target workspace paths:
```text
workspace/
snapshots/<artifact_group>/<valid_date>/<run_id>.modules.json
snapshots/<artifact_group>/<valid_date>/<run_id>.metadata.json
data-packages/<artifact_group>/<valid_date>/<run_id>.data_package.yaml
```
Metadata should link the module snapshot and YAML data package paths. Existing
metadata links for preflight, rendered report, source warnings, source hashes,
and distributor notification artifacts should remain.
## Target Module Defaults
Initial implemented default module IDs should cover current behavior without
QPF-specific fields:
- `metadata`
- `current_conditions`
- `derived_daily_summary`
- `derived_daypart_summaries`
- `precip_timing`
- `alert_digest`
- `area_forecast_discussion`
- `weather_story`
- `forecast_delta`
- `outdoor_windows`
- `weekend_planning`
- `storm_window_summary`
Default report composition should be declared in `internal/report`:
- Daily Today:
`metadata`, `current_conditions`, `derived_daily_summary`,
`derived_daypart_summaries`, `precip_timing`, `alert_digest`,
`forecast_delta`, `area_forecast_discussion`, `weather_story`,
`outdoor_windows`
- Daily Tomorrow:
same as Daily Today, plus any tomorrow-planning module needed to preserve
current tomorrow behavior.
- 3-Day:
`metadata`, `current_conditions`, `derived_daypart_summaries`,
`precip_timing`, `alert_digest`, `forecast_delta`,
`area_forecast_discussion`, `weather_story`, `outdoor_windows`
- Weekend:
`metadata`, `current_conditions`, `derived_daypart_summaries`,
`precip_timing`, `alert_digest`, `area_forecast_discussion`,
`weather_story`, `outdoor_windows`, `weekend_planning`
- Storm:
`metadata`, `current_conditions`, `hourly_table`, `precip_timing`,
`alert_digest`, `area_forecast_discussion`, `weather_story`,
`storm_window_summary`
If preserving a current report behavior requires a narrower module, add a
specific module rather than keeping old report-shaped containers.
## Stage 1: Weatherdata Package Split
Goal: separate broad normalized weather data from forecast-specific derivation.
Files/packages to change:
- create `internal/weatherdata`;
- update `internal/forecast`;
- update `internal/adapters/weatherapi`;
- update packages that currently import normalized source types from
`internal/forecast`.
Implementation guidance:
- Move normalized source/domain types out of `internal/forecast` when they are
not forecast algorithms:
- bundle/source metadata/warnings;
- current conditions;
- observation run types if present;
- alert run and alert overlap source types;
- forecast run and forecast period source types;
- discussion and discussion section types;
- weather story types.
- Keep deterministic derivation functions in `internal/forecast`.
- Update Weather API adapter return types to use `weatherdata.Bundle`.
- Keep JSON field names and Weather API fixture behavior unchanged.
- Do not change CLI behavior, artifact paths, or prompt input yet.
Acceptance criteria:
- Weather API adapter tests pass with `weatherdata` types.
- Forecast derivation tests pass using `weatherdata` inputs.
- No external adapter dependency types leak into `weatherdata`.
- Existing generated report behavior is unchanged at this stage.
Validation:
```bash
go test ./internal/weatherdata ./internal/forecast ./internal/adapters/weatherapi
go test ./internal/app ./internal/briefing ./internal/promptinput
```
This stage is suitable for one implementation prompt if kept mechanical.
## Stage 2: Fact Contracts
Goal: add explicit `CollectedFacts` and `DerivedFacts` contracts.
Files/packages to change:
- create `internal/facts`;
- update `internal/app`;
- update `internal/forecast` tests as needed.
Implementation guidance:
- Define `CollectedFacts` as normalized upstream facts collected once per
report run.
- Define `DerivedFacts` as conservative, reusable, report-scoped
transformations.
- Add builders:
- `BuildCollected(bundle *weatherdata.Bundle) CollectedFacts`
- `BuildDerived(req BuildDerivedRequest) (DerivedFacts, error)`
- `BuildDerivedRequest` should include the resolved report, timezone,
configured dayparts, and `CollectedFacts`.
- `DerivedFacts` may include:
- valid-period hourly periods;
- valid-period narrative periods;
- alert overlaps;
- daily summaries;
- daypart summaries where reusable;
- precipitation timing if reused by multiple modules.
- Do not put prompt wording, prose strings, module-specific ranking, or
one-off presentation decisions in `DerivedFacts`.
- Keep source provenance and warnings separate from ordinary fact access.
Acceptance criteria:
- `CollectedFacts` can be built once from a fetched bundle.
- `DerivedFacts` can be built for Daily, Tomorrow, 3-Day, Weekend, and Storm.
- Derived fact builders have tests for valid-period slicing, daypart grouping,
alert overlaps, and missing optional sources.
- No module or prompt code exists yet that fetches upstream data.
Validation:
```bash
go test ./internal/facts ./internal/forecast ./internal/app
go test ./internal/...
```
This stage is suitable for one implementation prompt.
## Stage 3: Module Core Contracts
Goal: define module IDs, options, outputs, snapshots, and registry mechanics.
Files/packages to change:
- create `internal/module`;
- update `internal/report`;
- update `internal/briefing`.
Implementation guidance:
- Define:
- `module.ID`;
- module ID constants;
- `module.ConfigItem`;
- `module.Output`;
- `module.Snapshot`;
- shared schema version constants.
- `module.Output` should contain module ID, stanza name, and typed value.
- `module.Snapshot` should preserve ordered outputs and support typed stanza
lookup for comparison code.
- Add duplicate module and duplicate stanza-name validation.
- Add typed option structs for initial modules. Empty option structs are fine
for modules without options.
- Add a module registry in `internal/briefing` that maps module IDs to builder
definitions.
- Module definitions should declare:
- ID;
- stanza name;
- option type;
- default options;
- required collected facts;
- required derived facts;
- supported report IDs or report categories;
- missing-data behavior.
- Do not execute modules from app orchestration yet unless needed for tests.
Acceptance criteria:
- Report definitions can refer to `module.ID` without import cycles.
- Module registry tests reject unknown modules, duplicate module IDs, duplicate
stanza names, incompatible reports, and invalid option shapes.
- Module output and snapshot JSON marshal deterministically enough for tests.
Validation:
```bash
go test ./internal/module ./internal/briefing ./internal/report
```
This stage is suitable for one implementation prompt.
## Stage 4: Base Modules
Goal: implement source-oriented modules that mostly pass through normalized or
lightly selected facts.
Files/packages to change:
- `internal/briefing`;
- `internal/module`;
- tests under `internal/briefing`.
Implementation guidance:
- Implement these modules:
- `metadata`;
- `current_conditions`;
- `alert_digest`;
- `area_forecast_discussion`;
- `weather_story`.
- The `metadata` module should expose report metadata, configured location,
units, timezone, valid period, source warnings summary, and alert checked
status where appropriate.
- `area_forecast_discussion` should expose key messages, short-term text, and
long-term text when present.
- `weather_story` should expose structured story fields when present and omit
the stanza when missing/suppressed by missing-source policy.
- `alert_digest` should distinguish checked/no-active-alerts from missing alert
source data.
- Ordinary modules should not expose endpoint, hash, or transport provenance;
provenance should remain metadata/source-warning oriented.
Acceptance criteria:
- Each module has focused tests for available data, missing optional data, and
empty output omission.
- No module fetches upstream data or reads/writes durable state.
- Output field names use YAML-friendly snake_case and unit suffixes where
needed.
Validation:
```bash
go test ./internal/briefing ./internal/module ./internal/facts
```
This stage is suitable for one implementation prompt.
## Stage 5: Derived Fact Modules
Goal: implement deterministic modules that package reusable forecast
derivations for the LLM.
Files/packages to change:
- `internal/forecast`;
- `internal/facts`;
- `internal/briefing`;
- `internal/module`.
Implementation guidance:
- Implement:
- `derived_daily_summary`;
- `derived_daypart_summaries`;
- `precip_timing`;
- `outdoor_windows`;
- any tomorrow-planning module needed to preserve Tomorrow output quality.
- `derived_daily_summary` should include current implementable fields:
- `high_temp_f`;
- `low_temp_f`;
- `max_pop_percent`;
- `max_pop_window`;
- `first_precip_hour`;
- `last_precip_hour`;
- `thunder_mentioned`;
- `max_wind_gust_mph`;
- `heat_index_max_f` when source data supports it.
- Do not implement `measurable_qpf_total_in` or `max_hourly_qpf_in` until QPF
exists in `CollectedFacts`.
- `derived_daypart_summaries` should expose daypart keyed values using the
configured daypart definitions.
- Keep broad reusable calculations in `DerivedFacts`; keep prompt-shape
packaging inside modules.
Acceptance criteria:
- Derived modules have fixture coverage across ordinary, dry, rainy, windy,
cold/heat, and missing-data scenarios.
- QPF fields are absent unless an upstream QPF source exists.
- Daily and Tomorrow module outputs contain enough data to replace current
report-shaped daily briefing content.
Validation:
```bash
go test ./internal/forecast ./internal/facts ./internal/briefing ./internal/module
```
This stage may be too large for one prompt if all modules are implemented at
once. Split into Daily-derived modules first, then outlook/storm derived
modules if needed.
## Stage 6: Report Composition And Config Overrides
Goal: make report definitions and config the source of module composition.
Files/packages to change:
- `internal/report`;
- `internal/config`;
- `examples/config.yml`;
- config tests.
Implementation guidance:
- Extend `report.Definition` with default ordered module IDs.
- Keep valid-period resolution, prompt IDs, output naming, generated flag, and
comparison strategy in `internal/report`.
- Add config support:
```yaml
reports:
daily:
deterministic_modules:
- current_conditions
- id: area_forecast_discussion
options:
sections:
- short_term
```
- Support both string shorthand and object form for module entries.
- Normalize config into typed `module.ConfigItem` values.
- Decode module options into typed option structs during validation or before
module execution.
- Reject:
- unknown report IDs;
- unknown module IDs;
- duplicate modules unless explicitly allowed by that module;
- duplicate stanza names;
- incompatible report/module combinations;
- invalid options.
- Built-in defaults should work when no report module config is present.
- Example config may omit module overrides unless an example is needed.
Acceptance criteria:
- Defaults reproduce intended module composition for all implemented reports.
- A config edit can add/remove an implemented module for a report.
- Invalid module config errors are actionable and do not mention raw internal
panic/details.
- Config examples load.
Validation:
```bash
go test ./internal/report ./internal/config ./internal/briefing
go run ./cmd/weatherreporter --help
```
This stage is suitable for one implementation prompt.
## Stage 7: Module Snapshot State
Goal: persist and inspect JSON module snapshots without changing Scriptorium
input yet.
Files/packages to change:
- `internal/state`;
- `internal/app`;
- `internal/cli`;
- app/state/CLI tests.
Implementation guidance:
- Add state paths for `<run_id>.modules.json`.
- Add save/load methods for module snapshots.
- Update metadata to include `ModuleSnapshotPath`.
- Add `weatherreporter inspect modules [--config PATH] RUN_ID`.
- Remove `inspect briefing` from parser support and help text in this stage.
- Keep old data package generation in place only until Stage 8, but do not
leave generation without a module snapshot.
Acceptance criteria:
- Generated runs persist module snapshots before prompt package construction.
- `inspect modules` returns the module snapshot.
- `inspect briefing` is gone from help text and parser tests.
- Metadata links the module snapshot path.
- Existing report generation still succeeds with fake Scriptorium.
Validation:
```bash
go test ./internal/state ./internal/cli ./internal/app
go run ./cmd/weatherreporter --help
```
This stage is suitable for one implementation prompt.
## Stage 8: YAML Prompt Package Cutover
Goal: replace JSON prompt data packages with YAML named-stanza data packages.
Files/packages to change:
- `internal/promptinput`;
- `internal/state`;
- `internal/adapters/scriptorium` tests;
- `internal/app`.
Implementation guidance:
- Set prompt package schema version to `weatherreporter.data_package.v2`.
- Build prompt package content from module snapshots, report metadata, recent
changes, and source warnings.
- Save prompt packages as `.data_package.yaml`.
- Continue passing Scriptorium input as `--input data_package=<path>`.
- Update render/run tests to avoid assuming `.json` filenames.
- Ensure YAML uses named stanzas under `briefing`.
- Omit empty optional fields.
- Keep module snapshot JSON as the comparison/inspection source.
- Update metadata `DataPackagePath` to point to YAML.
Acceptance criteria:
- Scriptorium render and run receive a YAML `data_package` path.
- YAML output is deterministic enough for tests.
- `inspect data-package` returns YAML content or a parsed representation
chosen consistently in CLI tests.
- No code assumes data package paths end in `.json`.
Validation:
```bash
go test ./internal/promptinput ./internal/adapters/scriptorium ./internal/state ./internal/app ./internal/cli
go test ./...
```
This stage is suitable for one implementation prompt.
## Stage 9: App Orchestration Cutover
Goal: make module execution the only generation path for all implemented
reports.
Files/packages to change:
- `internal/app`;
- `internal/briefing`;
- `internal/facts`;
- app workflow tests.
Implementation guidance:
- In `GenerateReport`, fetch Weather API data once, build `CollectedFacts`,
build `DerivedFacts`, execute configured modules, save module snapshot, build
YAML prompt package, then continue preflight/run/metadata/distributor flow.
- Preserve ordering:
1. resolve prior comparable metadata;
2. fetch bundle;
3. build facts;
4. execute modules;
5. save module snapshot;
6. compute Recent Changes;
7. save YAML data package;
8. run render preflight;
9. save metadata;
10. run Scriptorium;
11. copy optional output;
12. save final metadata;
13. notify distributor if enabled.
- Do not use `--out` or `--out-dir` copies for distributor notification.
- Do not invoke modules after Scriptorium failures.
- Keep batch behavior unchanged: continue independent reports, return nonzero
aggregate status if any report fails.
Acceptance criteria:
- Daily, Tomorrow, 3-Day, Weekend, and Storm generation all use module
snapshots and YAML data packages.
- Existing public CLI syntax remains stable except `inspect modules` replacing
`inspect briefing`.
- Managed Markdown report paths and distributor upload source remain stable.
- App tests assert generated module snapshots and YAML data packages.
Validation:
```bash
go test ./internal/app ./internal/cli ./internal/state ./internal/briefing ./internal/promptinput
go test ./...
```
This stage may be large. Split by report family if needed: Daily/Tomorrow,
Outlooks, then Storm.
## Stage 10: Recent Changes Migration
Goal: compare structured module snapshots instead of report-shaped briefing
packages.
Files/packages to change:
- `internal/changes`;
- `internal/state`;
- `internal/app`;
- changes tests.
Implementation guidance:
- Define which module stanzas each comparison strategy consumes.
- Daily comparison should use `derived_daily_summary`,
`derived_daypart_summaries`, `alert_digest`, and `precip_timing` where
present.
- 3-Day and Weekend comparisons should use module snapshot outputs that replace
current outlook day comparisons.
- Storm comparison should remain explicit-window based and consume storm
module outputs when implemented.
- Do not compare rendered Markdown or rendered YAML text.
- If a comparison-required module is missing, return an actionable error or an
inspectable warning according to the report policy chosen in code. Prefer an
error for required comparison modules and no-op only for optional comparison
stanzas.
Acceptance criteria:
- Prior snapshot lookup still uses report compatibility and valid-period rules.
- Recent Changes output remains deterministic.
- Tests cover unchanged forecasts, threshold-crossing changes, alert changes,
precip timing changes, and missing comparison stanzas.
- Old `briefing.Package` comparison code is removed.
Validation:
```bash
go test ./internal/changes ./internal/state ./internal/app
go test ./...
```
This stage is suitable for one implementation prompt if module snapshots are
already available.
## Stage 11: Remove Old Briefing Shapes
Goal: remove obsolete report-shaped briefing containers and stale JSON package
assumptions.
Files/packages to change:
- `internal/briefing`;
- `internal/promptinput`;
- `internal/state`;
- `internal/app`;
- tests throughout `internal`.
Implementation guidance:
- Remove old `Daily`, `ThreeDay`, `Weekend`, and `Storm` briefing container
structs when no longer used.
- Remove old `briefing.Package` if it no longer represents the module
snapshot. If the package keeps a `Package` type, it must be module-oriented.
- Remove tests that construct old report-shaped briefing fixtures.
- Remove stale `.data_package.json` assumptions.
- Remove dead helper functions that only supported old report-shaped output.
- Keep generated report Markdown behavior stable.
Acceptance criteria:
- `rg -n "data_package\\.json|inspect briefing|briefing\\.Package" internal docs -g '!docs/roadmap/**'`
has no production-code matches, except deliberate roadmap/history references
where appropriate.
- No old report-shaped content structs remain on the generation path.
- All tests pass.
Validation:
```bash
rg -n "data_package\\.json|inspect briefing|briefing\\.Package" internal docs -g '!docs/roadmap/**'
go test ./...
go run ./cmd/weatherreporter --help
git diff --check
```
This stage is suitable for one implementation prompt.
## Stage 12: Documentation And Example Alignment
Goal: align non-roadmap docs with implemented module behavior.
Files to inspect/update:
- `README.md`, only if the orientation or quickstart changed;
- `docs/cli.md`;
- `docs/config.md`;
- `docs/operations.md`;
- `docs/troubleshooting.md`;
- `docs/internal/app-orchestration.md`;
- `docs/internal/briefing.md` or replacement module internals doc;
- `docs/internal/changes.md`;
- `docs/internal/forecast-derivation.md`;
- `docs/internal/prompt-input.md`;
- `docs/internal/state.md`;
- `docs/internal/weather-data.md`;
- `docs/integrations/scriptorium.md`;
- `examples/config.yml`.
Implementation guidance:
- Document only implemented behavior outside `docs/roadmap/`.
- Add or update an internal module contract document if module behavior is now
implemented.
- Document `inspect modules` and remove `inspect briefing`.
- Document YAML data packages and JSON module snapshots.
- Document report module overrides and typed options only if implemented.
- Keep QPF as future-only unless upstream support was added.
- Keep Scriptorium contract focused on `--input data_package=<path>` and the
actual file format now passed.
Acceptance criteria:
- Non-roadmap docs no longer describe old report-shaped briefing packages.
- Config examples load.
- CLI examples match `weatherreporter --help`.
- Docs clearly distinguish module snapshots from prompt data packages.
Validation:
```bash
go test ./...
go run ./cmd/weatherreporter --help
git diff --check
rg -n "inspect briefing|data_package\\.json|report-shaped|vars-file|promptvars" README.md docs examples internal -g '!docs/roadmap/**'
```
This stage is suitable for one implementation prompt.
## Stage 13: Final Validation
Goal: run full validation and catch stale assumptions after the cutover.
Required commands:
```bash
go test ./...
go run ./cmd/weatherreporter --help
git diff --check
```
Required grep checks:
```bash
rg -n "inspect briefing|data_package\\.json|briefing\\.Package|Daily struct|ThreeDay struct|Weekend struct|Storm struct" internal docs examples -g '!docs/roadmap/**'
rg -n "measurable_qpf_total_in|max_hourly_qpf_in" internal docs examples -g '!docs/roadmap/**'
```
Expected grep results:
- no production-code references to `inspect briefing`;
- no production-code assumption that prompt packages are JSON;
- no production-code dependence on old report-shaped briefing containers;
- QPF references appear only as future-target docs or omitted-field tests until
upstream QPF exists.
Manual review:
- Generate command output still writes managed Markdown reports.
- Batch behavior still continues independent reports and returns nonzero on
aggregate failure.
- Distributor notification still uploads the managed Markdown report, not
module snapshots or YAML prompt packages.
- Secrets are not printed or persisted.
- YAML prompt package is readable and contains named stanzas.
## Deferred Work
Do not include these in the initial module cutover:
- plugin architecture;
- dynamic module loading;
- YAML-defined module schemas;
- user-authored module code;
- module-owned Weather API fetching;
- QPF fields before upstream QPF exists;
- SPC modules before upstream SPC data exists;
- radar modules before upstream radar data exists;
- event/storm-review reports unless a separate roadmap implements them.
## Open Questions
No blocking open questions remain for this implementation plan. The previously
identified choices are locked above:
- persist JSON module snapshots and YAML prompt data packages;
- split broad normalized source types into `internal/weatherdata`;
- replace `inspect briefing` with `inspect modules`;
- support typed module options from the first config implementation.