Compare commits
21 Commits
v0.8.0
...
2e0fb65a8b
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e0fb65a8b | |||
| 5e96790d85 | |||
| 35f4f82e94 | |||
| b605596bcb | |||
| 9303502b32 | |||
| f9eef80233 | |||
| c6f8570474 | |||
| 1130d807dc | |||
| ff2e664c62 | |||
| 2f3558cf33 | |||
| 154d31c3e8 | |||
| 6b1ff862f3 | |||
| 0c27fab384 | |||
| ad3b788f8c | |||
| 82acb8dc1a | |||
| 3aaddda676 | |||
| 7f989839cd | |||
| 27506168f8 | |||
| dc11e08e22 | |||
| fdddb5f08d | |||
| f78186b020 |
@@ -1,4 +1 @@
|
|||||||
Please carefully review the documents in `docs/policy` before making any changes to this repository.
|
Please review `docs/development.md` for initial orientation in this repository and follow its task-specific reading guide.
|
||||||
- `architecture.md` provides the canonical high-level architecture policy for this repository.
|
|
||||||
- `development.md` provides more granular development policy for this repository.
|
|
||||||
- `documentation.md` provides the canonical documentation policy for this repository.
|
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -1,10 +1,10 @@
|
|||||||
# weatherreporter
|
# weatherreporter
|
||||||
|
|
||||||
`weatherreporter` is a Go application for preparing human-facing weather
|
Weatherreporter is a Go CLI that turns normalized weather data into managed,
|
||||||
reports from normalized forecast data. It builds JSON module snapshots, passes
|
human-facing Markdown reports.
|
||||||
YAML prompt data packages to `scriptorium`, and keeps inspectable artifacts
|
|
||||||
under a local workspace. It can also upload successfully generated managed
|
It provides repeatable reports with inspectable local artifacts, so operators
|
||||||
Markdown reports to a configured `distributor` HTTP upload endpoint.
|
can review what was collected and generated for every run.
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
@@ -12,11 +12,14 @@ Markdown reports to a configured `distributor` HTTP upload endpoint.
|
|||||||
weatherreporter generate today --out ./today.md
|
weatherreporter generate today --out ./today.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Configure a Weather API endpoint first; see the
|
||||||
|
[configuration reference](docs/config.md).
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [CLI reference](docs/cli.md)
|
- [CLI reference](docs/cli.md)
|
||||||
- [Configuration reference](docs/config.md)
|
- [Configuration reference](docs/config.md)
|
||||||
- [Operations guide](docs/operations.md)
|
- [Operations guide](docs/operations.md)
|
||||||
- [Troubleshooting](docs/troubleshooting.md)
|
- [Troubleshooting](docs/troubleshooting.md)
|
||||||
|
- [Development guide](docs/development.md)
|
||||||
- [Architecture policy](docs/policy/architecture.md)
|
- [Architecture policy](docs/policy/architecture.md)
|
||||||
- [Development policy](docs/policy/development.md)
|
|
||||||
|
|||||||
211
docs/cli.md
211
docs/cli.md
@@ -1,7 +1,7 @@
|
|||||||
# Weatherreporter CLI
|
# Weatherreporter CLI
|
||||||
|
|
||||||
`weatherreporter` generates Markdown weather reports, runs scheduled report
|
`weatherreporter` generates weather reports, runs report batches, and inspects
|
||||||
batches, and inspects stored artifacts.
|
artifacts already stored in its workspace.
|
||||||
|
|
||||||
## Shortest Useful Command
|
## Shortest Useful Command
|
||||||
|
|
||||||
@@ -9,14 +9,11 @@ batches, and inspects stored artifacts.
|
|||||||
weatherreporter generate today --out ./today.md
|
weatherreporter generate today --out ./today.md
|
||||||
```
|
```
|
||||||
|
|
||||||
This loads configuration, collects weather data, writes managed workspace
|
The command uses the configured Weather API and writes an extra Markdown copy
|
||||||
artifacts, runs `scriptorium render` as a preflight check, runs structured
|
at `./today.md`. See the [configuration reference](config.md) to supply the
|
||||||
`scriptorium run`, validates generated text, renders the embedded Today
|
required Weather API endpoint.
|
||||||
template, and writes an extra Markdown copy to `./today.md`. If distributor
|
|
||||||
notification is enabled in configuration, the command also uploads the managed
|
|
||||||
Markdown report after final metadata is saved.
|
|
||||||
|
|
||||||
## Commands
|
## Commands And Usage
|
||||||
|
|
||||||
```text
|
```text
|
||||||
weatherreporter --help
|
weatherreporter --help
|
||||||
@@ -37,57 +34,38 @@ weatherreporter inspect prior [--config PATH] RUN_ID
|
|||||||
weatherreporter inspect sources [--config PATH] RUN_ID
|
weatherreporter inspect sources [--config PATH] RUN_ID
|
||||||
```
|
```
|
||||||
|
|
||||||
Implemented `generate` commands emit a compact JSON summary to stdout on
|
| Command | Contract |
|
||||||
success. The summary includes command identity, report identity, RunID, status,
|
| --- | --- |
|
||||||
valid period, and managed artifact paths. They also write a JSON module
|
| `generate daily` | Requires `--date YYYY-MM-DD`; the date is interpreted in the effective report timezone. |
|
||||||
snapshot, YAML data package, preflight artifact, managed Markdown report, and
|
| `generate today` | Accepts an optional `--date YYYY-MM-DD`; without it, the current local date in the effective report timezone is used. |
|
||||||
metadata under the configured workspace. `--out` writes an extra Markdown copy
|
| `generate tomorrow`, `three-day`, `weekend` | Use their report-defined valid period and accept the common generate flags. |
|
||||||
for the operator; distributor notification uses the managed report path, not
|
| `generate hourly` | Covers the next six hours in the effective report timezone. It does not accept `--date`, `--start`, `--end`, `--hours`, or `--duration`. |
|
||||||
the extra copy. `generate daily`,
|
| `generate storm` | Requires both `--start TIME` and `--end TIME`. Each time may be `YYYY-MM-DDTHH:MM` in the effective timezone or an RFC3339 timestamp with an explicit offset. |
|
||||||
`generate today`, `generate tomorrow`, and `generate hourly` write managed
|
| `run morning` and `run evening` | Run their defined report batches. `--out-dir` writes extra Markdown copies; `--out` is not accepted. |
|
||||||
generated-text artifacts, validate structured text from Scriptorium, and render
|
|
||||||
the managed Markdown report from embedded templates. `generate daily` requires
|
|
||||||
`--date YYYY-MM-DD` for the selected local civil day; omitting `--date` is a
|
|
||||||
command error and stops before weather data is collected. `generate hourly`
|
|
||||||
covers the next six hours in the effective report timezone and does not accept
|
|
||||||
date or event window flags. `generate storm` requires explicit event-window
|
|
||||||
bounds with `--start` and `--end`.
|
|
||||||
|
|
||||||
`run morning` generates Today Report, Tomorrow Report, and a dated Daily Report
|
`generate` accepts all seven report command names shown above. `run` accepts
|
||||||
for each later future local civil day with complete hourly forecast coverage.
|
only `morning` and `evening`. Batch membership, workspace artifacts, and
|
||||||
`run evening` generates Tomorrow Report and the same eligible future Daily
|
notification sequencing are described in the [operations guide](operations.md).
|
||||||
reports. Future Daily expansion starts with the day after tomorrow and skips
|
|
||||||
days that do not have every hourly forecast period for the local civil day.
|
|
||||||
Batch commands collect weather data once before planning; a collection failure
|
|
||||||
stops the batch before any report is generated. Batch runs continue independent
|
|
||||||
reports after a later report failure, print a JSON summary to stdout, write
|
|
||||||
compact status lines to stderr, and return nonzero when any report failed.
|
|
||||||
`--out-dir` writes extra Markdown copies for the operator; distributor
|
|
||||||
notification uses managed report paths, not the extra copies. Today and
|
|
||||||
Tomorrow use their report default copy names, and dynamic Daily copies use
|
|
||||||
`daily-YYYY-MM-DD.md`. When distributor and batch notification are enabled, a
|
|
||||||
fully successful batch uploads one distributor bundle after report generation
|
|
||||||
finishes. The JSON summary exposes that upload as a top-level `notification`
|
|
||||||
object, and stderr includes one `batchNotification` status line. If any planned
|
|
||||||
report fails, the batch notification is skipped for the whole batch.
|
|
||||||
|
|
||||||
Hourly Report, 3-Day Outlook, and Weekend Outlook are explicit only; they are
|
## Output, Errors, And Quiet Mode
|
||||||
not included in `run morning` or `run evening`.
|
|
||||||
|
|
||||||
`inspect` commands read existing workspace artifacts and emit the requested
|
Action commands (`generate` and `run`) write a JSON summary to stdout unless
|
||||||
JSON data to stdout. They do not collect weather data or invoke `scriptorium`.
|
`--quiet` is set. `run` also writes compact per-report and batch status lines
|
||||||
Inspection commands do not accept `--quiet`.
|
to stderr. A pre-run error, such as an invalid flag, missing required argument,
|
||||||
|
or configuration-load failure, produces no partial JSON summary. When an action
|
||||||
|
fails after it has produced a result, its summary has `"status": "failed"` and
|
||||||
|
an `error` field.
|
||||||
|
|
||||||
## Output
|
`--quiet` is supported by action commands only. It suppresses action summaries
|
||||||
|
and routine batch status output; it does not suppress command errors.
|
||||||
|
|
||||||
Action commands, meaning `generate` and `run`, emit JSON summaries to stdout by
|
Inspection commands always write their requested JSON value to stdout and do
|
||||||
default. Pre-run errors, such as invalid flags, missing required arguments, or
|
not accept `--quiet`.
|
||||||
configuration load failures, return an error without emitting partial JSON.
|
|
||||||
`--quiet` suppresses successful action-command stdout and routine stderr. It
|
|
||||||
does not hide returned errors. Inspection commands are data-output commands;
|
|
||||||
they always write the requested JSON to stdout and are not quietable.
|
|
||||||
|
|
||||||
Generate summaries have this shape:
|
### Generate Summary
|
||||||
|
|
||||||
|
A generate summary always identifies the command, report, run, generation
|
||||||
|
time, valid period, and status:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -101,102 +79,60 @@ Generate summaries have this shape:
|
|||||||
"validPeriod": {
|
"validPeriod": {
|
||||||
"start": "2026-05-29T00:00:00-05:00",
|
"start": "2026-05-29T00:00:00-05:00",
|
||||||
"end": "2026-05-30T00:00:00-05:00"
|
"end": "2026-05-30T00:00:00-05:00"
|
||||||
},
|
}
|
||||||
"reportPath": "workspace/reports/today/2026-05-29/report.20260529T120000.000000000Z_today.md",
|
|
||||||
"metadataPath": "workspace/snapshots/today/2026-05-29/metadata.20260529T120000.000000000Z_today.json",
|
|
||||||
"dataPackagePath": "workspace/data-packages/today/2026-05-29/data_package.20260529T120000.000000000Z_today.yaml",
|
|
||||||
"preflightPath": "workspace/preflight/today/2026-05-29/render.20260529T120000.000000000Z_today.json",
|
|
||||||
"generatedTextRawPath": "workspace/snapshots/today/2026-05-29/generated_text_raw.20260529T120000.000000000Z_today.json",
|
|
||||||
"generatedTextResultPath": "workspace/snapshots/today/2026-05-29/generated_text_result.20260529T120000.000000000Z_today.json",
|
|
||||||
"generatedTextPath": "workspace/snapshots/today/2026-05-29/generated_text.20260529T120000.000000000Z_today.json",
|
|
||||||
"renderContextPath": "workspace/snapshots/today/2026-05-29/render_context.20260529T120000.000000000Z_today.json"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Markdown-path reports omit the generated-text fields. If distributor
|
When available, the summary also includes `reportPath`, `metadataPath`,
|
||||||
notification is attempted, summaries include `notificationPath`; successful
|
`dataPackagePath`, and `preflightPath`. Generated-text reports additionally
|
||||||
notification also includes a compact `notification` object. If notification
|
include `generatedTextRawPath`, `generatedTextResultPath`,
|
||||||
fails after report artifacts exist, the summary has `"status": "failed"` and an
|
`generatedTextPath`, and `renderContextPath`. `outputPath` is included only
|
||||||
`error` string while retaining inspectable artifact paths.
|
when `--out` wrote an extra copy. Distributor notification, when attempted,
|
||||||
|
adds `notificationPath` and may add a compact `notification` object.
|
||||||
|
|
||||||
Run summaries have this shape:
|
### Run Summary And Stderr
|
||||||
|
|
||||||
```json
|
A run summary contains `command`, `batch`, `status`, `startedAt`, `finishedAt`,
|
||||||
{
|
`total`, `succeeded`, `failed`, and a `reports` array. It may also contain a
|
||||||
"command": "run",
|
top-level `notification` object and `error`. Batch status is `failed` if any
|
||||||
"batch": "morning",
|
report or the batch notification fails.
|
||||||
"status": "succeeded",
|
|
||||||
"startedAt": "2026-05-29T12:00:00Z",
|
|
||||||
"finishedAt": "2026-05-29T12:01:00Z",
|
|
||||||
"total": 1,
|
|
||||||
"succeeded": 1,
|
|
||||||
"failed": 0,
|
|
||||||
"reports": [
|
|
||||||
{
|
|
||||||
"reportId": "today",
|
|
||||||
"reportName": "Today Report",
|
|
||||||
"promptId": "weather.today_generated_text",
|
|
||||||
"runId": "20260529T120000.000000000Z_today",
|
|
||||||
"status": "succeeded",
|
|
||||||
"generatedAt": "2026-05-29T12:00:00Z",
|
|
||||||
"validPeriod": {
|
|
||||||
"start": "2026-05-29T00:00:00-05:00",
|
|
||||||
"end": "2026-05-30T00:00:00-05:00"
|
|
||||||
},
|
|
||||||
"reportPath": "workspace/reports/today/2026-05-29/report.20260529T120000.000000000Z_today.md",
|
|
||||||
"metadataPath": "workspace/snapshots/today/2026-05-29/metadata.20260529T120000.000000000Z_today.json",
|
|
||||||
"dataPackagePath": "workspace/data-packages/today/2026-05-29/data_package.20260529T120000.000000000Z_today.yaml",
|
|
||||||
"preflightPath": "workspace/preflight/today/2026-05-29/render.20260529T120000.000000000Z_today.json"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`run` status is `failed` when any report failed or the top-level batch
|
Without `--quiet`, batch status lines use this form:
|
||||||
notification failed. Batch stderr uses compact status lines, for example:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
report=today status=succeeded output="reports/today.md"
|
report=today status=succeeded output="reports/today.md"
|
||||||
batch=morning total=2 succeeded=2 failed=0
|
batch=morning total=2 succeeded=2 failed=0
|
||||||
```
|
```
|
||||||
|
|
||||||
## Flags
|
## Flag Reference
|
||||||
|
|
||||||
- `-h`, `--help`: show help.
|
| Flag | Accepted by | Meaning |
|
||||||
- `--config PATH`: load configuration from `PATH` instead of `/usr/local/etc/weatherreporter/config.yml`.
|
| --- | --- | --- |
|
||||||
- `--units VALUE`: override configured Weather API units for `generate` and `run`.
|
| `-h`, `--help` | top level | Show help. |
|
||||||
- `--tz NAME`: override configured Weather API timezone for `generate` and `run`.
|
| `--config PATH` | all commands | Load `PATH` instead of `/usr/local/etc/weatherreporter/config.yml`. |
|
||||||
- `--out PATH`: write an extra Markdown report copy where supported by the `generate` command.
|
| `--units VALUE` | `generate`, `run` | Override `weather_api.units` for this command. |
|
||||||
- `--out-dir PATH`: write extra Markdown report copies for `run morning` and `run evening`.
|
| `--tz NAME` | `generate`, `run` | Override `weather_api.timezone` for this command. |
|
||||||
- `--quiet`: suppress successful stdout and routine stderr for `generate` and `run`.
|
| `--out PATH` | every `generate` command | Write an extra Markdown report copy. |
|
||||||
- `--date YYYY-MM-DD`: required date for `generate daily`; optional date for `generate today`, defaulting to the current local date in the configured timezone.
|
| `--out-dir PATH` | `run morning`, `run evening` | Write extra Markdown report copies in `PATH`. |
|
||||||
- `--start TIME`: required start time for `generate storm`.
|
| `--quiet` | `generate`, `run` | Suppress action summaries and routine batch status output. |
|
||||||
- `--end TIME`: required end time for `generate storm`.
|
| `--date YYYY-MM-DD` | `generate daily`, `generate today` | Required for Daily; optional for Today. |
|
||||||
- `--limit N`: maximum records for `inspect reports`; defaults to `20`, and `0` means no limit.
|
| `--start TIME`, `--end TIME` | `generate storm` | Required storm-event bounds. |
|
||||||
|
| `--limit N` | `inspect reports` | Maximum runs to list. Defaults to `20`; `0` means no limit. |
|
||||||
|
|
||||||
Storm times accept `YYYY-MM-DDTHH:MM` in the configured timezone or RFC3339
|
Distributor notification is configured through `notify.distributor`; there are
|
||||||
timestamps with explicit offsets.
|
no Distributor-specific CLI flags. See the [configuration reference](config.md).
|
||||||
|
|
||||||
Distributor notification is configured only through `notify.distributor`; there
|
## Invocation Examples
|
||||||
are no distributor-specific CLI flags.
|
|
||||||
|
|
||||||
## Common Workflows
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
weatherreporter generate today --out ./today.md
|
|
||||||
weatherreporter generate daily --date 2026-05-29 --out ./daily.md
|
weatherreporter generate daily --date 2026-05-29 --out ./daily.md
|
||||||
weatherreporter generate tomorrow --out ./tomorrow.md
|
weatherreporter generate today --date 2026-05-29 --out ./today.md
|
||||||
weatherreporter generate hourly
|
weatherreporter generate hourly --out ./hourly.md
|
||||||
weatherreporter generate three-day --out ./three-day.md
|
|
||||||
weatherreporter generate weekend --out ./weekend.md
|
|
||||||
weatherreporter generate storm --start 2026-05-29T18:00 --end 2026-05-30T06:00 --out ./storm.md
|
weatherreporter generate storm --start 2026-05-29T18:00 --end 2026-05-30T06:00 --out ./storm.md
|
||||||
weatherreporter run morning --out-dir ./reports
|
weatherreporter run morning --out-dir ./reports
|
||||||
weatherreporter run evening --out-dir ./reports
|
|
||||||
weatherreporter generate today --quiet
|
|
||||||
weatherreporter run morning --quiet
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Inspection
|
## Inspection Commands
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
weatherreporter inspect reports --limit 10
|
weatherreporter inspect reports --limit 10
|
||||||
@@ -205,12 +141,17 @@ weatherreporter inspect modules 20260529T100000.000000000Z_today
|
|||||||
weatherreporter inspect data-package 20260529T100000.000000000Z_today
|
weatherreporter inspect data-package 20260529T100000.000000000Z_today
|
||||||
weatherreporter inspect prior 20260529T100000.000000000Z_today
|
weatherreporter inspect prior 20260529T100000.000000000Z_today
|
||||||
weatherreporter inspect sources 20260529T100000.000000000Z_today
|
weatherreporter inspect sources 20260529T100000.000000000Z_today
|
||||||
weatherreporter inspect metadata 20260529T100000.000000000Z_daily
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`inspect reports` lists recent generated runs with artifact paths and source
|
| Command | JSON returned |
|
||||||
warning counts. The other inspect commands require a RunID. `inspect modules`
|
| --- | --- |
|
||||||
returns the persisted ordered module snapshot for a run. `inspect prior`
|
| `inspect reports` | Recent generated runs, including artifact paths and source-warning counts. |
|
||||||
returns the prior comparable snapshot metadata selected from stored metadata, or
|
| `inspect metadata RUN_ID` | Persisted metadata for the run. |
|
||||||
`null` when none exists. `inspect sources` shows source provenance and source
|
| `inspect modules RUN_ID` | The run's persisted ordered module snapshot. |
|
||||||
warnings without dumping full weather payloads.
|
| `inspect data-package RUN_ID` | The run's persisted prompt data package. |
|
||||||
|
| `inspect prior RUN_ID` | Prior comparable snapshot metadata, or `null` when none exists. |
|
||||||
|
| `inspect sources RUN_ID` | Source provenance and source warnings without full weather payloads. |
|
||||||
|
|
||||||
|
Inspection is read-only: it does not collect weather data or invoke
|
||||||
|
`scriptorium`. See the [operations guide](operations.md) for artifact lifecycle
|
||||||
|
and recovery.
|
||||||
|
|||||||
387
docs/config.md
387
docs/config.md
@@ -1,319 +1,206 @@
|
|||||||
# Weatherreporter Configuration
|
# Weatherreporter Configuration
|
||||||
|
|
||||||
Configuration is YAML. By default, `weatherreporter` reads:
|
Weatherreporter reads YAML configuration. The default path is:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/usr/local/etc/weatherreporter/config.yml
|
/usr/local/etc/weatherreporter/config.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `--config PATH` to load a different file. If the default file is absent,
|
If the default file is absent, Weatherreporter uses built-in defaults. An
|
||||||
built-in defaults are used. If `--config PATH` points to a missing file, loading
|
explicit `--config PATH` must exist. Values are applied in this order:
|
||||||
fails.
|
|
||||||
|
|
||||||
Precedence is:
|
1. built-in defaults;
|
||||||
|
2. the configuration file, when present; and
|
||||||
|
3. the `--units` and `--tz` command-line overrides.
|
||||||
|
|
||||||
1. CLI flags
|
Environment variables do not override configuration fields. Output flags write
|
||||||
2. configuration file
|
extra report copies for a command and do not change configuration.
|
||||||
3. built-in defaults
|
|
||||||
|
|
||||||
The CLI configuration overrides are `--units` and `--tz`. Output flags control
|
## Maintained Examples
|
||||||
report copies for the current command but do not change configuration files.
|
|
||||||
Environment variables do not override configuration fields.
|
|
||||||
|
|
||||||
## Minimal Config
|
- [minimal-config.yml](../examples/minimal-config.yml) is the smallest useful
|
||||||
|
collection and generation configuration.
|
||||||
|
- [config.yml](../examples/config.yml) is a representative production-oriented
|
||||||
|
configuration using synthetic endpoints and no credentials.
|
||||||
|
|
||||||
See [examples/minimal-config.yml](../examples/minimal-config.yml).
|
Both files are loaded by the configuration test suite.
|
||||||
|
|
||||||
|
## Minimal Configuration
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
weather_api:
|
weather_api:
|
||||||
base_url: https://weather.api.example.com/
|
base_url: https://weather.api.example.com/
|
||||||
```
|
```
|
||||||
|
|
||||||
`weather_api.base_url` is required for commands that collect weather data.
|
`weather_api.base_url` is required for workflows that collect weather data.
|
||||||
Other fields fall back to defaults.
|
All omitted fields use their built-in defaults.
|
||||||
|
|
||||||
## Production-Oriented Config
|
|
||||||
|
|
||||||
See [examples/config.yml](../examples/config.yml). The example is loaded by the
|
|
||||||
config test suite.
|
|
||||||
|
|
||||||
## Field Reference
|
## Field Reference
|
||||||
|
|
||||||
### `weather_api`
|
### `weather_api`
|
||||||
|
|
||||||
- `base_url`: absolute base URL for the Weather API. Required for generation and collection workflows.
|
| Field | Default | Rules |
|
||||||
- `timeout`: HTTP timeout duration. Default: `10s`.
|
| --- | --- | --- |
|
||||||
- `precision`: numeric precision query value. Default: `1`.
|
| `base_url` | empty | Absolute Weather API URL. Required for collection and generation. |
|
||||||
- `units`: Weather API units query value. Default: `us`.
|
| `timeout` | `10s` | Must be greater than zero. |
|
||||||
- `timezone`: report timezone and Weather API timezone query value where supported. Default: `America/Chicago`.
|
| `precision` | `0` | Must be zero or greater. Sent as the Weather API precision query value. |
|
||||||
- `format`: Weather API response format. Must be `json`. Default: `json`.
|
| `units` | `us` | Required Weather API units query value; `--units` overrides it for one command. |
|
||||||
|
| `timezone` | `America/Chicago` | Required report and Weather API timezone; `--tz` overrides it for one command. |
|
||||||
|
| `format` | `json` | Required and must be `json`. |
|
||||||
|
|
||||||
Timezone values may be IANA names, configured aliases such as `Chicago` and
|
Timezone values may be IANA names, configured aliases such as `Chicago` and
|
||||||
`Stl`, US timezone abbreviations, or UTC offsets such as `-5` and `+09:30`.
|
`Stl`, US timezone abbreviations, or UTC offsets such as `-5` and `+09:30`.
|
||||||
|
|
||||||
### `location`
|
### `location`
|
||||||
|
|
||||||
`location` is descriptive prompt context included in module metadata and
|
`location` supplies descriptive prompt context; it does not choose a Weather
|
||||||
Scriptorium data packages. It does not select a Weather API endpoint or enable
|
API endpoint or configure multiple forecast locations.
|
||||||
multiple configured forecast locations.
|
|
||||||
|
|
||||||
- `id`: short local identifier. Default: `home`.
|
| Field | Default |
|
||||||
- `name`: human-readable location name. Default: `Brentwood`.
|
| --- | --- |
|
||||||
- `region`: broader forecast area context. Default: `St. Louis Metro`.
|
| `id` | `home` |
|
||||||
|
| `name` | `Brentwood` |
|
||||||
|
| `region` | `St. Louis Metro` |
|
||||||
|
|
||||||
The prompt-facing location object also includes `timezone`, derived from the
|
The prompt-facing location timezone is derived from the effective
|
||||||
effective `weather_api.timezone` after CLI overrides such as `--tz`.
|
`weather_api.timezone` after command-line overrides.
|
||||||
|
|
||||||
### `secrets`
|
### `secrets`
|
||||||
|
|
||||||
- `directory`: optional directory of file-backed environment secrets. Default:
|
`secrets.directory` defaults to empty, which disables secret loading. When it
|
||||||
empty, which disables secret loading.
|
is set, every regular file directly in that directory is loaded after the file
|
||||||
|
and command-line overrides. A file basename must match
|
||||||
|
`[A-Za-z_][A-Za-z0-9_]*`; it becomes an environment variable name, and the
|
||||||
|
file contents replace any existing value. One trailing LF or CRLF is removed.
|
||||||
|
|
||||||
When configured, each regular file directly under `secrets.directory` is loaded
|
Missing directories, unreadable files, subdirectories, symlinks, non-regular
|
||||||
after config file parsing and CLI overrides. The file basename must be a valid
|
files, and invalid names fail configuration loading. Put only secret values in
|
||||||
environment variable name matching `[A-Za-z_][A-Za-z0-9_]*`; the file contents
|
this directory, never in the YAML file.
|
||||||
become the environment variable value and overwrite any existing value. One
|
|
||||||
trailing LF or CRLF is stripped. Subdirectories, symlinks, invalid filenames,
|
|
||||||
missing directories, and unreadable files fail config loading.
|
|
||||||
|
|
||||||
### `notify`
|
### `notify.distributor`
|
||||||
|
|
||||||
`notify.distributor` controls distributor uploads after successful report
|
Distributor notification is disabled by default. Its fields are:
|
||||||
rendering. It is disabled by default and does not add CLI flags. When enabled,
|
|
||||||
`generate <report>` uploads one distributor bundle for the generated report
|
|
||||||
after final metadata is saved. `run morning` and `run evening` use
|
|
||||||
`notify.distributor.batch`: when batch notification is enabled and every
|
|
||||||
planned report succeeds, weatherreporter uploads one distributor bundle that
|
|
||||||
contains all managed Markdown reports from that batch.
|
|
||||||
|
|
||||||
- `enabled`: whether distributor notification config is active. Default:
|
| Field | Default | Rules when notification is enabled |
|
||||||
`false`.
|
| --- | --- | --- |
|
||||||
- `endpoint`: absolute distributor endpoint URL. Required when enabled.
|
| `enabled` | `false` | Activates Distributor notification validation. |
|
||||||
Default: `https://distributor.example.com`.
|
| `endpoint` | `https://distributor.example.com` | Must be an absolute URL. |
|
||||||
- `token_env`: environment variable name that will contain the distributor
|
| `token_env` | `DISTRIBUTOR_UPLOAD_TOKEN` | Must name a valid environment variable. |
|
||||||
upload token. Required when enabled. Default: `DISTRIBUTOR_UPLOAD_TOKEN`.
|
| `timeout` | `30s` | Must be greater than zero. |
|
||||||
- `timeout`: distributor operation timeout. Must be greater than zero when
|
| `failure_policy` | `error` | Must be `error`. |
|
||||||
enabled. Default: `30s`.
|
| `pipeline_id_template` | empty | Required single-report pipeline ID template. |
|
||||||
- `failure_policy`: must be `error` when enabled. Default: `error`.
|
| `bundle_id_template` | `weatherreporter.{location_id}.{report_id}` | Required single-report bundle ID template. |
|
||||||
- `pipeline_id_template`: template for single-report distributor pipeline IDs.
|
| `idempotency_key_template` | `{bundle_id}.{run_id}` | Required single-report idempotency-key template. |
|
||||||
Required when enabled. Default: empty.
|
| `batch.enabled` | `true` | Activates batch notification validation when Distributor notification is enabled. |
|
||||||
- `bundle_id_template`: template for single-report distributor bundle IDs.
|
| `batch.pipeline_id_template` | `weatherreporter` | Required when batch notification is enabled. |
|
||||||
Default: `weatherreporter.{location_id}.{report_id}`.
|
| `batch.bundle_id_template` | `weatherreporter.{location_id}.{batch}` | Required when batch notification is enabled. |
|
||||||
- `idempotency_key_template`: template for single-report distributor
|
| `batch.idempotency_key_template` | `{bundle_id}.{batch_run_id}` | Required when batch notification is enabled. |
|
||||||
idempotency keys. Default: `{bundle_id}.{run_id}`.
|
|
||||||
- `batch.enabled`: whether batch distributor notification config is active
|
|
||||||
when distributor notification is enabled. Default: `true`.
|
|
||||||
- `batch.pipeline_id_template`: template for batch distributor pipeline IDs.
|
|
||||||
Required when distributor notification and batch notification are enabled.
|
|
||||||
Default: `weatherreporter`.
|
|
||||||
- `batch.bundle_id_template`: template for batch distributor bundle IDs.
|
|
||||||
Required when distributor notification and batch notification are enabled.
|
|
||||||
Default: `weatherreporter.{location_id}.{batch}`.
|
|
||||||
- `batch.idempotency_key_template`: template for batch distributor idempotency
|
|
||||||
keys. Required when distributor notification and batch notification are
|
|
||||||
enabled. Default: `{bundle_id}.{batch_run_id}`.
|
|
||||||
|
|
||||||
Single-report templates support `location_id`, `report_id`, `run_id`,
|
The upload token is read from the environment variable named by `token_env`.
|
||||||
|
Use `secrets.directory` when a file-backed secret is appropriate.
|
||||||
|
|
||||||
|
Single-report bundle templates accept `location_id`, `report_id`, `run_id`,
|
||||||
`artifact_group`, `batch_output_name`, `valid_start_date`, `valid_end_date`,
|
`artifact_group`, `batch_output_name`, `valid_start_date`, `valid_end_date`,
|
||||||
`valid_start_time`, `valid_end_time`, `valid_start_stamp`, `valid_end_stamp`,
|
`valid_start_time`, `valid_end_time`, `valid_start_stamp`, `valid_end_stamp`,
|
||||||
and `storm_id`. Date values use `YYYY-MM-DD`, time values use `HHMM`, and
|
and `storm_id`. Pipeline and idempotency-key templates may also use
|
||||||
stamp values use `YYYY-MM-DDTHHMM` in the effective report timezone.
|
`bundle_id`. Dates use `YYYY-MM-DD`; times use `HHMM`; and stamps use
|
||||||
`storm_id` is derived from the storm report valid period as
|
`YYYY-MM-DDTHHMM` in the effective report timezone. `storm_id` is
|
||||||
`{valid_start_stamp}-{valid_end_stamp}`; it renders empty for non-storm
|
`{valid_start_stamp}-{valid_end_stamp}` for Storm Report and empty otherwise.
|
||||||
reports. `pipeline_id_template` and `idempotency_key_template` may also use
|
|
||||||
`bundle_id`.
|
|
||||||
|
|
||||||
The rendered pipeline ID selects the configured distributor `http_upload`
|
Batch bundle and pipeline templates accept `location_id`, `batch`,
|
||||||
workflow. The rendered bundle ID is the stable logical source identity for the
|
`batch_run_id`, and `batch_started_date`; batch idempotency-key templates may
|
||||||
report stream. The rendered idempotency key is the per-run retry identity.
|
also use `bundle_id`. `batch_started_date` is the batch start date in the
|
||||||
|
effective report timezone.
|
||||||
|
|
||||||
Batch templates support `location_id`, `batch`, `batch_run_id`, and
|
`reports.<report>.distributor.path_templates` overrides the default ordered
|
||||||
`batch_started_date`. Batch idempotency templates may also use `bundle_id`.
|
Distributor paths for that report. Each rendered path must be a unique relative
|
||||||
`batch_started_date` is the batch start date in the effective report timezone.
|
path with `/` separators. Backslashes, empty segments, `.` and `..` segments,
|
||||||
Batch bundle IDs identify a logical batch stream; batch idempotency keys
|
`manifest.json`, and the reserved Distributor sidecar basename are rejected.
|
||||||
identify a specific retryable batch attempt.
|
The default paths are:
|
||||||
|
|
||||||
Rendered report paths must be unique relative paths with `/` separators. They
|
| Report | Paths |
|
||||||
must not contain backslashes, empty path segments, `.`, `..`, `manifest.json`,
|
| --- | --- |
|
||||||
or the reserved distributor sidecar basename, formed from a leading dot plus
|
| `hourly` | `hourly/index.md` |
|
||||||
`distributor.json`. In a batch upload, uniqueness is checked across every
|
| `daily` | `daily/{valid_start_date}/{run_id}.md`, `daily/{valid_start_date}/index.md` |
|
||||||
rendered bundle path for every included report before distributor is called.
|
| `today` | `daily/{valid_start_date}/{run_id}.md`, `daily/{valid_start_date}/index.md`, `today/index.md` |
|
||||||
Managed Markdown report paths are the only upload source files; copies written
|
| `tomorrow` | `daily/{valid_start_date}/{run_id}.md`, `daily/{valid_start_date}/index.md`, `tomorrow/index.md` |
|
||||||
with `--out` or `--out-dir` are never uploaded.
|
| `three_day` | `three-day/{valid_start_date}/{run_id}.md`, `three-day/{valid_start_date}/index.md` |
|
||||||
|
| `weekend` | `weekend/{valid_start_date}/{run_id}.md`, `weekend/{valid_start_date}/index.md` |
|
||||||
|
| `storm` | `storm/{storm_id}/{run_id}.md`, `storm/{storm_id}/index.md` |
|
||||||
|
|
||||||
Distributor bundle paths are report-specific. Weatherreporter uses
|
See the [operations guide](operations.md) for notification timing, uploaded
|
||||||
`reports.<report>.distributor.path_templates` when that override is configured;
|
artifact selection, and failure handling.
|
||||||
otherwise it uses the report definition defaults:
|
|
||||||
|
|
||||||
- `hourly`: `hourly/index.md`
|
|
||||||
- `daily`: `daily/{valid_start_date}/{run_id}.md`, `daily/{valid_start_date}/index.md`
|
|
||||||
- `today`: `daily/{valid_start_date}/{run_id}.md`, `daily/{valid_start_date}/index.md`, `today/index.md`
|
|
||||||
- `tomorrow`: `daily/{valid_start_date}/{run_id}.md`, `daily/{valid_start_date}/index.md`, `tomorrow/index.md`
|
|
||||||
- `three_day`: `three-day/{valid_start_date}/{run_id}.md`, `three-day/{valid_start_date}/index.md`
|
|
||||||
- `weekend`: `weekend/{valid_start_date}/{run_id}.md`, `weekend/{valid_start_date}/index.md`
|
|
||||||
- `storm`: `storm/{storm_id}/{run_id}.md`, `storm/{storm_id}/index.md`
|
|
||||||
|
|
||||||
The upload token is read from the environment variable named by `token_env`
|
|
||||||
after config loading and `secrets.directory` processing. Config files should
|
|
||||||
name the variable only; they should not contain the token value.
|
|
||||||
|
|
||||||
### `missing_source`
|
### `missing_source`
|
||||||
|
|
||||||
- `default`: missing-source behavior for optional sources. One of `error`, `warn`, or `none`. Default: `warn`.
|
`missing_source.default` defaults to `warn` and accepts `error`, `warn`, or
|
||||||
- `sources`: optional map of source-specific overrides, using the same policy values.
|
`none`. `missing_source.sources` optionally overrides that policy by source.
|
||||||
|
Hourly forecast data is required for generated reports. Supported optional
|
||||||
Hourly forecast data is required for generated reports. Optional sources use
|
source keys are `observations`, `current`, `narrative`, `alerts`, `discussion`,
|
||||||
the missing-source policy. Source override keys include `observations`,
|
`weather_story`, and `spc_convective_outlooks`.
|
||||||
`current`, `narrative`, `alerts`, `discussion`, `weather_story`, and
|
|
||||||
`spc_convective_outlooks`.
|
|
||||||
|
|
||||||
### `scriptorium`
|
### `scriptorium`
|
||||||
|
|
||||||
- `binary`: `scriptorium` executable name or path. Default: `scriptorium`.
|
| Field | Default | Rules |
|
||||||
- `config_path`: optional Scriptorium config path passed to the adapter.
|
| --- | --- | --- |
|
||||||
- `profile`: optional Scriptorium profile passed to the adapter.
|
| `binary` | `scriptorium` | Required executable name or path. |
|
||||||
- `timeout`: subprocess timeout. Default: `2m`.
|
| `config_path` | empty | Optional Scriptorium configuration path. |
|
||||||
- `extra_args`: optional additional arguments passed to Scriptorium commands.
|
| `profile` | empty | Optional Scriptorium profile. |
|
||||||
|
| `timeout` | `2m` | Must be greater than zero. |
|
||||||
|
| `extra_args` | empty | Optional extra arguments passed to Scriptorium commands. |
|
||||||
|
|
||||||
### `workspace`
|
### `workspace`
|
||||||
|
|
||||||
- `root`: workspace root for managed artifacts. Default: `workspace`.
|
| Field | Default |
|
||||||
- `snapshots_dir`: module snapshot and metadata directory under `workspace.root`. Default: `snapshots`.
|
| --- | --- |
|
||||||
- `reports_dir`: managed Markdown report directory under `workspace.root`. Default: `reports`.
|
| `root` | `workspace` |
|
||||||
- `data_packages_dir`: prompt input package directory under `workspace.root`. Default: `data-packages`.
|
| `snapshots_dir` | `snapshots` |
|
||||||
- `preflight_dir`: Scriptorium render output directory under `workspace.root`. Default: `preflight`.
|
| `reports_dir` | `reports` |
|
||||||
- `notifications_dir`: distributor notification debug artifact directory under `workspace.root`. Default: `notifications`.
|
| `data_packages_dir` | `data-packages` |
|
||||||
|
| `preflight_dir` | `preflight` |
|
||||||
|
| `notifications_dir` | `notifications` |
|
||||||
|
|
||||||
Workspace subdirectories must be relative paths that stay inside
|
`workspace.root` is required. Each workspace subdirectory must be a relative
|
||||||
`workspace.root`. Managed artifact paths below those directories are grouped by
|
path that stays within the root. See the [operations guide](operations.md) for
|
||||||
artifact group and valid-period start date; the path template is not
|
the managed workspace layout and lifecycle.
|
||||||
configurable.
|
|
||||||
|
|
||||||
### `dayparts`
|
### `dayparts`
|
||||||
|
|
||||||
`dayparts` is a list of named local-time windows used by forecast derivation.
|
`dayparts` is a non-empty list of named local-time windows used in forecast
|
||||||
Each entry has:
|
derivation. Every item needs `name`, `start`, and `end`; start and end use
|
||||||
|
`HH:MM`. Defaults are `overnight` (`00:00`–`06:00`), `morning`
|
||||||
- `name`
|
(`06:00`–`10:00`), `midday` (`10:00`–`15:00`), `afternoon`
|
||||||
- `start`
|
(`15:00`–`17:00`), and `evening` (`17:00`–`24:00`).
|
||||||
- `end`
|
|
||||||
|
|
||||||
`start` and `end` use `HH:MM`. The default entries are overnight, morning,
|
|
||||||
midday, afternoon, and evening.
|
|
||||||
|
|
||||||
### `recent_change`
|
### `recent_change`
|
||||||
|
|
||||||
- `temperature_degrees`: temperature change threshold. Default: `5`.
|
| Field | Default |
|
||||||
- `precip_probability_points`: precipitation probability threshold. Default: `20`.
|
| --- | --- |
|
||||||
- `wind_gust_miles_per_hour`: wind gust change threshold. Default: `10`.
|
| `temperature_degrees` | `5` |
|
||||||
- `precip_timing_shift_minutes`: precipitation timing shift threshold. Default: `120`.
|
| `precip_probability_points` | `20` |
|
||||||
|
| `wind_gust_miles_per_hour` | `10` |
|
||||||
|
| `precip_timing_shift_minutes` | `120` |
|
||||||
|
|
||||||
Recent Changes are added to prompt input when a prior comparable module
|
These thresholds control when Recent Changes are included in prompt input for a
|
||||||
snapshot exists and a threshold is crossed.
|
prior comparable module snapshot.
|
||||||
|
|
||||||
### `reports`
|
### `reports`
|
||||||
|
|
||||||
`reports` optionally overrides the ordered deterministic modules declared by
|
`reports` optionally overrides a report's ordered deterministic modules and
|
||||||
report definitions. Omit a report entry to use its default module order.
|
Distributor path templates. Omit a report entry to retain its defaults.
|
||||||
|
|
||||||
Supported report keys are `daily`, `today`, `tomorrow`, `hourly`,
|
Supported report keys are `daily`, `today`, `tomorrow`, `hourly`, `three_day`,
|
||||||
`three_day`, `weekend`, and `storm`. Canonical report IDs and accepted aliases
|
`weekend`, and `storm`. Configuration also accepts `three_day_outlook`,
|
||||||
are also valid, including `three_day_outlook`, `weekend_outlook`, and
|
`weekend_outlook`, and `storm_report`; hyphens and underscores are equivalent.
|
||||||
`storm_report`. Hyphens and underscores are treated equivalently in report
|
|
||||||
keys. Retired report keys are not supported.
|
|
||||||
|
|
||||||
`reports.today` applies only to the Today Report. `reports.daily` applies only
|
Each report entry can contain:
|
||||||
to the dated Daily Report.
|
|
||||||
|
|
||||||
Each report entry supports:
|
- `deterministic_modules`: an ordered list of module IDs, or objects with `id`
|
||||||
|
and optional `options`.
|
||||||
|
- `distributor.path_templates`: an optional, non-empty ordered list of
|
||||||
|
Distributor paths for that report.
|
||||||
|
|
||||||
- `deterministic_modules`: ordered module list. Entries may be string module
|
Unknown reports and modules, duplicate modules, incompatible report-module
|
||||||
IDs or objects with `id` and optional `options`.
|
combinations, duplicate stanza names, invalid path templates, and invalid
|
||||||
- `distributor.path_templates`: optional ordered distributor bundle path
|
module options fail configuration loading. The accepted module IDs and module
|
||||||
templates for this report. If omitted, the report definition defaults are
|
option contracts are documented in the [module contract internals](internal/module.md).
|
||||||
used. If present, the list must contain at least one template.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
reports:
|
|
||||||
daily:
|
|
||||||
distributor:
|
|
||||||
path_templates:
|
|
||||||
- "daily/{valid_start_date}/{run_id}.md"
|
|
||||||
- "daily/{valid_start_date}/index.md"
|
|
||||||
deterministic_modules:
|
|
||||||
- metadata
|
|
||||||
- current_conditions
|
|
||||||
- narrative_forecast
|
|
||||||
- alert_digest
|
|
||||||
- spc_convective_outlooks
|
|
||||||
- id: area_forecast_discussion
|
|
||||||
options:
|
|
||||||
sections:
|
|
||||||
- long_term
|
|
||||||
- spc_convective_discussion
|
|
||||||
- daily_planning
|
|
||||||
- hourly_forecast
|
|
||||||
today:
|
|
||||||
deterministic_modules:
|
|
||||||
- metadata
|
|
||||||
- current_conditions
|
|
||||||
- narrative_forecast
|
|
||||||
- derived_daily_summary
|
|
||||||
- derived_daypart_summaries
|
|
||||||
- precip_timing
|
|
||||||
- alert_digest
|
|
||||||
- spc_convective_outlooks
|
|
||||||
- area_forecast_discussion
|
|
||||||
- spc_convective_discussion
|
|
||||||
- weather_story
|
|
||||||
- outdoor_windows
|
|
||||||
- hourly_forecast
|
|
||||||
- today_planning
|
|
||||||
hourly:
|
|
||||||
deterministic_modules:
|
|
||||||
- metadata
|
|
||||||
- current_conditions
|
|
||||||
- hourly_forecast
|
|
||||||
- precip_timing
|
|
||||||
- alert_digest
|
|
||||||
- spc_convective_outlooks
|
|
||||||
- id: area_forecast_discussion
|
|
||||||
options:
|
|
||||||
sections:
|
|
||||||
- key_messages
|
|
||||||
- short_term
|
|
||||||
- spc_convective_discussion
|
|
||||||
- weather_story
|
|
||||||
```
|
|
||||||
|
|
||||||
Unknown reports, unknown modules, duplicate modules, incompatible report/module
|
|
||||||
combinations, duplicate stanza names, and invalid options fail config loading.
|
|
||||||
`area_forecast_discussion.options.sections` may contain `product`,
|
|
||||||
`key_messages`, `short_term`, and `long_term`. Empty or omitted `sections`
|
|
||||||
includes all available AFD sections. Default report definitions may choose a
|
|
||||||
smaller report-specific subset, such as daily reports using only `long_term`.
|
|
||||||
|
|
||||||
The module registry accepts all module IDs documented in
|
|
||||||
[Module Contract Internals](internal/module.md). Unknown or unimplemented
|
|
||||||
module IDs fail validation instead of being skipped.
|
|
||||||
|
|
||||||
## Secrets
|
|
||||||
|
|
||||||
Configuration files should not contain raw secrets. Use `secrets.directory` to
|
|
||||||
load secret values from files into environment variables for integrations that
|
|
||||||
read credentials from the environment. Secret file names become environment
|
|
||||||
variable names, and secret file contents become values. For distributor
|
|
||||||
notification, this allows a file such as
|
|
||||||
`<secrets.directory>/DISTRIBUTOR_UPLOAD_TOKEN` to supply the token referenced by
|
|
||||||
`notify.distributor.token_env`.
|
|
||||||
|
|
||||||
## Maintained Examples
|
|
||||||
|
|
||||||
- [examples/minimal-config.yml](../examples/minimal-config.yml): smallest
|
|
||||||
useful config for generation and fetching.
|
|
||||||
- [examples/config.yml](../examples/config.yml): production-oriented config
|
|
||||||
covering maintained fields.
|
|
||||||
|
|
||||||
Both example files are loaded by the config test suite.
|
|
||||||
|
|||||||
86
docs/development.md
Normal file
86
docs/development.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Development
|
||||||
|
|
||||||
|
This is the first-read guide for people and coding agents working on
|
||||||
|
Weatherreporter. It provides a concise repository orientation and routes each
|
||||||
|
kind of change to its canonical documentation.
|
||||||
|
|
||||||
|
Weatherreporter is a Go CLI that collects normalized weather data, derives
|
||||||
|
deterministic report facts and module snapshots, invokes Scriptorium for
|
||||||
|
generated text, renders managed Markdown reports, and can upload completed
|
||||||
|
reports through Distributor. Start with the [README](../README.md) for product
|
||||||
|
context and the [architecture policy](policy/architecture.md) for system
|
||||||
|
boundaries and invariants.
|
||||||
|
|
||||||
|
## What To Read
|
||||||
|
|
||||||
|
| When working on | Read | Why |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Product behavior or the shortest useful workflow | [README](../README.md), [CLI reference](cli.md), and [operations guide](operations.md) | These own product orientation, invocation, and normal operation. |
|
||||||
|
| Application shape, package boundaries, dependency direction, safety properties, or architectural invariants | [Architecture policy](policy/architecture.md) and relevant ADRs under `docs/adr/`, when present | Architecture defines the intended system; ADRs preserve significant decision rationale. |
|
||||||
|
| Any documentation addition, revision, move, or removal | [Documentation policy](policy/documentation.md) | It defines canonical owners, audience boundaries, current-state rules, and document lifecycle. |
|
||||||
|
| Adding, changing, reviewing, or deleting tests | [Testing policy](policy/testing.md) and focused package tests | The policy defines risk-based sufficiency, durable test boundaries, doubles, and test-maintenance criteria. |
|
||||||
|
| CLI commands, flags, output, quiet mode, or command wiring | [CLI reference](cli.md) and [CLI internals](internal/cli.md) | The reference owns the user contract; the internal guide owns command composition and output flow. |
|
||||||
|
| Configuration fields, defaults, loading, overrides, validation, or secrets | [Configuration reference](config.md), [architecture policy](policy/architecture.md), and tests under `internal/config` | These separate the user-visible contract, architectural rules, and executable behavior. |
|
||||||
|
| Top-level generation, batch, collection, inspection, or notification workflow | [App orchestration internals](internal/app-orchestration.md) | It owns workflow ordering, persistence points, failure propagation, and orchestration invariants. |
|
||||||
|
| Weather API transport, source envelopes, source warnings, or collection | [Weather API integration](integrations/weatherapi.md), [weather-data internals](internal/weather-data.md), and [collection internals](internal/collect.md) | These separate the external contract, normalized source facts, and app-facing collection behavior. |
|
||||||
|
| Forecast periods, weather derivation, collected facts, or derived facts | [Forecast derivation internals](internal/forecast-derivation.md) and [fact contracts](internal/facts.md) | They own deterministic derivation and the fact boundaries used by reports. |
|
||||||
|
| Report definitions, valid periods, report IDs, output naming, or batch composition | [Report registry internals](internal/report-registry.md) and [app orchestration internals](internal/app-orchestration.md) | Report definitions own selection and period rules; orchestration owns execution. |
|
||||||
|
| Module IDs, module composition, briefing values, or prompt-facing exports | [Module contract internals](internal/module.md), [module builder internals](internal/briefing.md), and [prompt-input internals](internal/prompt-input.md) | These own module contracts, value construction, and the curated prompt-package boundary. |
|
||||||
|
| Recent Changes comparison | [Changes internals](internal/changes.md) and [operations guide](operations.md) | The internal guide owns structured comparison; operations owns user-visible artifact behavior. |
|
||||||
|
| Scriptorium commands, subprocess execution, prompt inputs, or result handling | [Scriptorium integration](integrations/scriptorium.md), [Scriptorium adapter internals](internal/scriptorium-adapter.md), and [prompt-input internals](internal/prompt-input.md) | These separate the external CLI contract, subprocess boundary, and input construction. |
|
||||||
|
| Generated-text schemas, validation, render contexts, templates, or Markdown rendering | [Generated-text internals](internal/generatedtext.md), [report-template internals](internal/reporttemplate.md), and [report template guide](templates.md) | These own structured text, renderer implementation, and the maintainer-facing template surface. |
|
||||||
|
| Workspace paths, metadata, atomic persistence, lookup, inspection, or recovery | [State internals](internal/state.md), [operations guide](operations.md), and [troubleshooting guide](troubleshooting.md) | These separate implementation, operator workflows, and symptom-based recovery. |
|
||||||
|
| Distributor bundles, uploads, notification artifacts, or failures | [Distributor adapter internals](internal/distributor-adapter.md), [Distributor integration contracts](integrations/distributor/), and [operations guide](operations.md) | These separate adapter behavior, external contracts, and operational lifecycle. |
|
||||||
|
| Maintained example configuration | [Configuration reference](config.md) and files under `examples/` | The reference owns field meaning; examples own complete copyable files. |
|
||||||
|
| Proposed, deferred, or unimplemented work | Documents under `docs/roadmap/` | Future behavior and implementation status belong only in roadmaps until implemented. |
|
||||||
|
|
||||||
|
For an existing subsystem, inspect its focused internal document, package-local
|
||||||
|
types, and tests before changing behavior. Use the package boundaries already
|
||||||
|
present before introducing a new package or abstraction.
|
||||||
|
|
||||||
|
## Repository Map
|
||||||
|
|
||||||
|
| Area | Responsibility |
|
||||||
|
| --- | --- |
|
||||||
|
| `cmd/weatherreporter` | Binary entry point. |
|
||||||
|
| `internal/cli` | Command parsing, flags, help, output, and command wiring. |
|
||||||
|
| `internal/app` | Generation, batches, collection coordination, notification, and inspection orchestration. |
|
||||||
|
| `internal/config` | Configuration defaults, loading, precedence, secrets, and validation. |
|
||||||
|
| `internal/adapters` | Weather API, Scriptorium, and Distributor boundaries. |
|
||||||
|
| `internal/weatherdata`, `internal/forecast`, `internal/facts` | Normalized source facts and deterministic derivation. |
|
||||||
|
| `internal/report`, `internal/module`, `internal/briefing`, `internal/changes` | Report registry, module contracts and values, and structured comparison. |
|
||||||
|
| `internal/promptinput`, `internal/generatedtext`, `internal/reporttemplate` | Prompt packages, generated-text validation, render contexts, and Markdown templates. |
|
||||||
|
| `internal/state`, `internal/fileutil`, `internal/timeutil` | Durable artifacts, atomic file operations, clocks, dates, timezones, and periods. |
|
||||||
|
| `docs` | User, operator, integration, internal, policy, and roadmap documentation. |
|
||||||
|
| `examples` | Maintained copyable configuration. |
|
||||||
|
|
||||||
|
The [architecture policy](policy/architecture.md) is authoritative for
|
||||||
|
normative boundaries. Focused documents under `docs/internal/` own detailed
|
||||||
|
implemented subsystem behavior.
|
||||||
|
|
||||||
|
## Contributor Workflow
|
||||||
|
|
||||||
|
1. Read the documents and focused tests identified by the task guide.
|
||||||
|
2. Use focused package checks while iterating.
|
||||||
|
3. Run `gofmt -w` on changed Go files.
|
||||||
|
4. Update the canonical documentation and maintained examples in the same
|
||||||
|
change when behavior changes.
|
||||||
|
5. Run repository-wide validation before considering the work complete.
|
||||||
|
|
||||||
|
Preserve actionable error context, keep secrets out of logs and fixtures, and
|
||||||
|
avoid validation that requires live Weather API, Scriptorium, or Distributor
|
||||||
|
services. The architecture and testing policies own the detailed rules.
|
||||||
|
|
||||||
|
## Baseline Validation
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go test ./...
|
||||||
|
go run ./cmd/weatherreporter --help
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Use focused package tests during development and add broader or race-enabled
|
||||||
|
checks when required by the [testing policy](policy/testing.md) and the risks of
|
||||||
|
the change.
|
||||||
@@ -1,136 +1,70 @@
|
|||||||
# Upstream Producer Integration
|
# Distributor HTTP Upload Contract
|
||||||
|
|
||||||
Audience: developers and LLM coding agents adding `distributor` support to an upstream Go producer application.
|
Weatherreporter integrates with the HTTP upload API provided by
|
||||||
|
`gitea.maximumdirect.net/eric/distributor v0.5.0`. It submits source bundles to
|
||||||
|
a configured pipeline and reads the resulting run status. Configuration fields
|
||||||
|
and notification lifecycle are documented in the [configuration reference](../../config.md)
|
||||||
|
and [operations guide](../../operations.md).
|
||||||
|
|
||||||
This document is the copyable implementation guide for submitting producer outputs to a `distributor` pipeline whose source backend is `http_upload`.
|
## Upload Admission
|
||||||
|
|
||||||
## Required Inputs
|
Weatherreporter uses an absolute HTTP(S) endpoint as a base URL. The client
|
||||||
|
posts a gzip-compressed source bundle to:
|
||||||
|
|
||||||
The upstream application needs these values from deployment or operator configuration:
|
```text
|
||||||
|
POST /v1/pipelines/<pipeline_id>/upload
|
||||||
- distributor endpoint: the HTTP server base URL, such as `https://distributor.example.com`;
|
Authorization: Bearer <token>
|
||||||
- upload token: bearer token that authenticates the producer;
|
Content-Type: application/gzip
|
||||||
- pipeline id: configured `http_upload` pipeline that should process this upload;
|
Idempotency-Key: <key>
|
||||||
- generated files: regular local files to include in the source bundle;
|
|
||||||
- bundle id: stable identifier for the logical report stream or artifact;
|
|
||||||
- idempotency key: unique key for one producer run, reused only when retrying that same run.
|
|
||||||
|
|
||||||
Do not put destination routing, public URLs, transform settings, or credentials in the source manifest. Those belong in the `distributor` pipeline configuration.
|
|
||||||
|
|
||||||
The token, pipeline id, bundle id, and idempotency key have different jobs. The token authenticates the producer. The pipeline id selects the configured distributor workflow, including destinations and publishing policy. The bundle id tells `distributor` whether a new upload is a newer version of the same source; keep it stable across runs that should replace the same managed destination artifact. The idempotency key tells `distributor` whether an upload request is a retry; change it for each distinct producer run so new content is enqueued.
|
|
||||||
|
|
||||||
## Recommended Workflow
|
|
||||||
|
|
||||||
Use `gitea.maximumdirect.net/eric/distributor/pkg/upload`.
|
|
||||||
|
|
||||||
For most producers, use `UploadFiles`. It accepts producer-generated files, builds a temporary valid source bundle with `pkg/bundle`, uploads a gzip-compressed tar archive, and removes temporary files when the call returns.
|
|
||||||
|
|
||||||
Use `UploadBundle` only when the producer already assembled a complete bundle directory containing `manifest.json`.
|
|
||||||
|
|
||||||
Add the dependency from the upstream application:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
go get gitea.maximumdirect.net/eric/distributor
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Minimal Go Example
|
The authenticated token must be allowed to use the selected upload pipeline.
|
||||||
|
A successful response is `202 Accepted` with JSON containing `run_id` and
|
||||||
|
`status`. Acceptance means Distributor staged and validated the source bundle;
|
||||||
|
it does not mean downstream destinations have published it.
|
||||||
|
|
||||||
```go
|
The adapter requires a pipeline ID, bundle ID, idempotency key, and at least one
|
||||||
package reports
|
source-file mapping before calling Distributor. It reads the bearer token from
|
||||||
|
the configured environment variable and redacts that value from errors. Request
|
||||||
|
construction and timeout handling belong to the [Distributor adapter](../../internal/distributor-adapter.md).
|
||||||
|
|
||||||
import (
|
## Idempotency
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/distributor/pkg/bundle"
|
Distributor scopes idempotency to the token, pipeline ID, and key. Keys must be
|
||||||
"gitea.maximumdirect.net/eric/distributor/pkg/upload"
|
non-empty ASCII values of at most 128 bytes using letters, digits, `.`, `_`,
|
||||||
)
|
`-`, and `:`. Weatherreporter always supplies a rendered key; it does not rely
|
||||||
|
on the client library's generated-key fallback.
|
||||||
|
|
||||||
func SubmitReport(reportPath, summaryPath string) error {
|
Reusing a key for the same normalized source manifest returns the original
|
||||||
endpoint := os.Getenv("DISTRIBUTOR_UPLOAD_ENDPOINT")
|
accepted run. Reusing it for different content returns `409 Conflict`, which
|
||||||
token := os.Getenv("DISTRIBUTOR_UPLOAD_TOKEN")
|
the adapter exposes as a Weatherreporter idempotency-conflict error. A distinct
|
||||||
if endpoint == "" || token == "" {
|
report or batch run therefore needs a distinct key; reuse a key only when
|
||||||
return fmt.Errorf("distributor endpoint and token are required")
|
retrying that same upload.
|
||||||
}
|
|
||||||
|
|
||||||
pipelineID := "weather-hourly"
|
## Run Status And Retention
|
||||||
reportID := "weather.hourly.brentwood"
|
|
||||||
runID := time.Now().UTC().Format("20060102T150405.000000000Z")
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
client, err := upload.NewClient(upload.ClientOptions{
|
After acceptance, Weatherreporter reads:
|
||||||
Endpoint: endpoint,
|
|
||||||
Token: token,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := client.UploadFiles(ctx, upload.UploadFilesOptions{
|
```text
|
||||||
PipelineID: pipelineID,
|
GET /runs/<run_id>
|
||||||
ID: reportID,
|
Authorization: Bearer <token>
|
||||||
IdempotencyKey: reportID + "." + runID,
|
|
||||||
Files: []bundle.BundleFile{
|
|
||||||
{SourcePath: reportPath, Path: "report.md"},
|
|
||||||
{SourcePath: summaryPath, Path: "summary.txt"},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
var conflict *upload.IdempotencyConflictError
|
|
||||||
if errors.As(err, &conflict) {
|
|
||||||
return fmt.Errorf("idempotency key was reused for different bundle content: %w", err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("distributor accepted run %s\n", result.RunID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Producer Responsibilities
|
The status record provides `run_id`, `pipeline_id`, status timestamps, optional
|
||||||
|
JSON `report`, and an `error` for failures. Statuses are `accepted`, `queued`,
|
||||||
|
`running`, `succeeded`, and `failed`. A terminal `failed` status makes the
|
||||||
|
notification fail; the adapter preserves the returned status details for the
|
||||||
|
application to record.
|
||||||
|
|
||||||
- Use a stable bundle id for the logical producer output that should replace the same destination artifact, such as `weather.hourly.brentwood`.
|
Run and idempotency records are in-memory. Completed records expire according
|
||||||
- Set `PipelineID` to the configured upload pipeline that should process the bundle.
|
to Distributor's `server.http.retention`, and a Distributor restart removes
|
||||||
- Do not include per-run timestamps, random values, or job ids in the bundle id unless each run should be treated as a different source.
|
retained status and idempotency state. Status polling decisions and persistence
|
||||||
- Use an idempotency key that changes for every distinct producer run, such as `<bundle-id>.<run-id>`.
|
of notification artifacts are internal orchestration behavior; see the
|
||||||
- Reuse the same idempotency key only when retrying the exact same producer run with the same source manifest.
|
[Distributor adapter](../../internal/distributor-adapter.md) and
|
||||||
- Map each generated file to a clean slash-separated bundle path, such as `report.md` or `assets/chart.png`.
|
[application orchestration](../../internal/app-orchestration.md).
|
||||||
- Include only regular files. Symlinks, directories as files, devices, FIFOs, and sockets are rejected.
|
|
||||||
- Keep file contents stable after upload inputs are selected. Bundle digests are calculated from file bytes.
|
|
||||||
- Treat upload success as admission only. `UploadFiles` and `UploadBundle` return after the server accepts and validates the upload, not after all destinations publish.
|
|
||||||
|
|
||||||
Valid bundle paths are relative slash paths. They must not be empty, absolute, contain backslashes, contain `.` or `..` path segments, contain empty path segments, or use reserved basenames such as `manifest.json` and the distributor sidecar basename formed from a leading dot plus `distributor.json`.
|
## Compatibility Reference
|
||||||
|
|
||||||
## Idempotency And Status
|
The upstream canonical HTTP wire contract is
|
||||||
|
`docs/integrations/http-upload.md` in the Distributor repository. This page
|
||||||
`pkg/upload` sends `Idempotency-Key` on every upload. If the caller omits one, the package generates a random key for that call and reuses it for in-process retries. That is enough for transient network retry within one process, but it does not give cross-process retry identity.
|
documents only the portion exercised by Weatherreporter.
|
||||||
|
|
||||||
For producer jobs that may retry after process restart, supply a key derived from the producer run, such as `<bundle-id>.<run-id>`. Reusing the same key with the same token, pipeline id, and normalized source manifest returns the original accepted run. Reusing the same key with different source content in that scope returns a conflict. Reusing one key across multiple distinct report generations prevents those generations from being treated as new uploads.
|
|
||||||
|
|
||||||
`Status` polls `/runs/<run-id>` while the distributor server retains the in-memory status record. Status values are `accepted`, `queued`, `running`, `succeeded`, and `failed`. Completed records expire according to the server's `server.http.retention` setting, and server restart clears status and idempotency records.
|
|
||||||
|
|
||||||
Optional status check:
|
|
||||||
|
|
||||||
```go
|
|
||||||
status, err := client.Status(ctx, result.RunID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if status.Status == "failed" {
|
|
||||||
return fmt.Errorf("distributor run failed: %s", status.Error)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
In the `distributor` source tree:
|
|
||||||
|
|
||||||
- `docs/consumers/pkg-upload.md`: Go upload package workflow.
|
|
||||||
- `docs/consumers/pkg-bundle.md`: Go bundle package workflow.
|
|
||||||
- `docs/integrations/http-upload.md`: canonical HTTP upload wire contract.
|
|
||||||
- `docs/integrations/source-bundle.md`: canonical source bundle file-format contract.
|
|
||||||
|
|||||||
@@ -1,91 +1,36 @@
|
|||||||
# `pkg/bundle`
|
# Distributor Source Bundle Mapping
|
||||||
|
|
||||||
Audience: upstream Go producer developers and LLM coding agents using `distributor` source bundle helpers.
|
Weatherreporter uses the source-bundle format through Distributor's
|
||||||
|
`pkg/upload.UploadFiles` helper. It does not create bundle directories or call
|
||||||
|
`pkg/bundle` directly. The helper creates a temporary bundle, writes and
|
||||||
|
validates `manifest.json`, archives it, and removes the temporary bundle when
|
||||||
|
the upload call returns.
|
||||||
|
|
||||||
Import path:
|
## File Mappings
|
||||||
|
|
||||||
```go
|
Every mapping pairs a managed Markdown report source with one bundle-relative
|
||||||
import "gitea.maximumdirect.net/eric/distributor/pkg/bundle"
|
path. A single-report notification maps its one managed report to each rendered
|
||||||
```
|
path configured for that report. A batch notification combines mappings for
|
||||||
|
every included managed report and rejects duplicate bundle paths.
|
||||||
|
|
||||||
`pkg/bundle` builds, writes, parses, and validates local source bundles. Use it directly when a producer writes bundles for `distributor` to discover, or when a producer wants to assemble and validate a bundle before using another transport.
|
The report source is never an `--out` copy or an arbitrary workspace scan. The
|
||||||
|
application selects it and renders notification paths; see the [operations guide](../../operations.md)
|
||||||
|
for the managed-upload rule and the [Distributor adapter](../../internal/distributor-adapter.md)
|
||||||
|
for the adapter boundary.
|
||||||
|
|
||||||
The canonical source bundle file-format contract is [Source Bundle Contract](../integrations/source-bundle.md).
|
Bundle paths must be clean, relative, slash-separated paths. They cannot be
|
||||||
|
empty or absolute, contain backslashes, empty segments, `.` or `..`, or use
|
||||||
|
`manifest.json` or `.distributor.json` as a basename. The mapped source must be
|
||||||
|
a regular file. File mapping order is preserved and affects the bundle digest.
|
||||||
|
|
||||||
## Preferred Complete-Bundle Workflow
|
The bundle manifest uses schema version `1`, carries the rendered bundle ID and
|
||||||
|
creation time, and records each mapped file's path, SHA-256 digest, and size.
|
||||||
|
Destination routing, publication, and Distributor-managed destination state are
|
||||||
|
not source-bundle fields.
|
||||||
|
|
||||||
Use `WriteBundle` when producer-generated files live outside the final bundle root.
|
## Compatibility Reference
|
||||||
|
|
||||||
```go
|
The upstream canonical file-format contract is
|
||||||
manifest, err := bundle.WriteBundle(bundle.WriteBundleOptions{
|
`docs/integrations/source-bundle.md` in the Distributor repository. It defines
|
||||||
Root: "/var/spool/distributor/weather/hourly-2026-06-07T15",
|
the complete manifest and archive format; this page records only the mapping and
|
||||||
ID: "weather.hourly.brentwood",
|
path constraints Weatherreporter relies on.
|
||||||
Files: []bundle.BundleFile{
|
|
||||||
{SourcePath: "/tmp/weather/report.md", Path: "report.md"},
|
|
||||||
{SourcePath: "/tmp/weather/summary.txt", Path: "summary.txt"},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = manifest
|
|
||||||
```
|
|
||||||
|
|
||||||
`WriteBundle` copies each source file into a staged bundle root, writes `manifest.json`, validates the staged bundle, and promotes it into place. Set `Overwrite: true` only when the producer intentionally replaces an existing bundle root.
|
|
||||||
|
|
||||||
## Existing Bundle Root Workflow
|
|
||||||
|
|
||||||
Use `BuildManifest` and `WriteManifest` when files are already staged under the final bundle root.
|
|
||||||
|
|
||||||
```go
|
|
||||||
root := "/var/spool/distributor/weather/hourly-2026-06-07T15"
|
|
||||||
manifest, err := bundle.BuildManifest(bundle.BuildOptions{
|
|
||||||
Root: root,
|
|
||||||
ID: "weather.hourly.brentwood",
|
|
||||||
Files: []string{"report.md", "summary.txt"},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := bundle.WriteManifest(root, manifest, bundle.WriteManifestOptions{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := bundle.ValidateBundle(root, manifest); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `Scan: true` instead of `Files` only when every valid regular file under the root should be included. Scan mode includes dotfiles, skips reserved metadata files, rejects symlinks, and sorts paths lexically.
|
|
||||||
|
|
||||||
## Paths And Ordering
|
|
||||||
|
|
||||||
Bundle paths are slash-separated paths relative to the bundle root.
|
|
||||||
|
|
||||||
Invalid paths include:
|
|
||||||
|
|
||||||
- empty paths;
|
|
||||||
- absolute paths;
|
|
||||||
- paths containing backslashes;
|
|
||||||
- `.` or `..` path segments;
|
|
||||||
- empty path segments;
|
|
||||||
- any reserved basename, including `manifest.json` and the distributor sidecar
|
|
||||||
basename formed from a leading dot plus `distributor.json`.
|
|
||||||
|
|
||||||
Explicit file lists preserve caller order. File order is part of the bundle digest, so producers should choose it deliberately and keep it stable.
|
|
||||||
|
|
||||||
The manifest `ID` is the logical source identity used by `distributor` destination comparison. Keep it stable for runs that should replace the same managed destination artifact. If every run uses a different manifest `ID`, `distributor` treats those runs as different sources and may report a destination conflict instead of replacing older output.
|
|
||||||
|
|
||||||
## Validation And Digest Helpers
|
|
||||||
|
|
||||||
Use `ValidateBundle` before handing an existing local bundle to another process. It verifies manifest semantics, file existence, regular-file type, file size, per-file SHA-256 digests, and bundle digest.
|
|
||||||
|
|
||||||
Useful helpers:
|
|
||||||
|
|
||||||
- `LoadManifest`: read `manifest.json` from a bundle root.
|
|
||||||
- `ParseManifest` and `MarshalManifest`: parse or write manifest bytes.
|
|
||||||
- `ValidateManifest`: validate manifest-only semantics.
|
|
||||||
- `FileDigest`, `BundleDigest`, and `ValidateDigest`: digest helpers for diagnostics and tests.
|
|
||||||
|
|
||||||
## Boundaries
|
|
||||||
|
|
||||||
`pkg/bundle` does not upload bundles, publish destinations, transform Markdown, select pipelines, configure credentials, or write destination state. Those concerns belong to `pkg/upload` or the `distributor` application.
|
|
||||||
|
|||||||
@@ -1,122 +1,51 @@
|
|||||||
# `pkg/upload`
|
# Distributor Upload Client Contract
|
||||||
|
|
||||||
Audience: upstream Go producer developers and LLM coding agents submitting bundles to `distributor serve`.
|
Weatherreporter uses `gitea.maximumdirect.net/eric/distributor/pkg/upload` at
|
||||||
|
the pinned module version `v0.5.0`. It constructs one client per notification
|
||||||
|
attempt and calls `UploadFiles`, followed by `Status` for the accepted run.
|
||||||
|
|
||||||
Import path:
|
## Client And Upload
|
||||||
|
|
||||||
```go
|
The adapter constructs the client with the configured endpoint, bearer token,
|
||||||
import "gitea.maximumdirect.net/eric/distributor/pkg/upload"
|
and an HTTP client whose timeout is the configured Distributor timeout. It
|
||||||
```
|
passes no custom retry options, so the pinned client's defaults apply: three
|
||||||
|
attempts, 100 ms base delay, and one-second maximum delay.
|
||||||
|
|
||||||
`pkg/upload` is the producer-facing HTTP upload client. It builds on `pkg/bundle`, packages valid source bundles as gzip-compressed tar archives, sends bearer authentication, routes uploads to a configured pipeline, includes idempotency keys, and exposes a status polling helper.
|
For each notification, Weatherreporter calls `UploadFiles` with:
|
||||||
|
|
||||||
`UploadFiles` examples also use:
|
- the rendered pipeline ID;
|
||||||
|
- the rendered bundle ID as the source manifest ID;
|
||||||
|
- the report or batch generation time as `Created`;
|
||||||
|
- the managed-report-to-bundle-path mappings described in the
|
||||||
|
[bundle mapping contract](pkg-bundle.md); and
|
||||||
|
- a rendered idempotency key.
|
||||||
|
|
||||||
```go
|
It leaves bundle validation enabled. `UploadFiles` creates the temporary source
|
||||||
import "gitea.maximumdirect.net/eric/distributor/pkg/bundle"
|
bundle and sends it as a gzip-compressed tar archive; Weatherreporter does not
|
||||||
```
|
call `UploadBundle` or submit prebuilt bundle roots.
|
||||||
|
|
||||||
The canonical HTTP wire contract is [HTTP Upload API Contract](../integrations/http-upload.md).
|
## Retry, Conflict, And Status
|
||||||
|
|
||||||
## Client Construction
|
The pinned upload client retries only `503 Service Unavailable` and retryable
|
||||||
|
network failures. It does not retry successful `202` responses or other HTTP
|
||||||
|
errors. Because every Weatherreporter request supplies an idempotency key, a
|
||||||
|
retry keeps the same upload identity.
|
||||||
|
|
||||||
```go
|
The client decodes the accepted upload result (`run_id`, `status`) and the run
|
||||||
client, err := upload.NewClient(upload.ClientOptions{
|
status record. A `409` response is an upstream idempotency conflict; the
|
||||||
Endpoint: "https://distributor.example.com",
|
adapter translates it to its own conflict error without exposing the token.
|
||||||
Token: token,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`Endpoint` is the distributor server base URL. The client derives `/v1/pipelines/<pipeline-id>/upload` and `/runs/<run-id>`. `Token` is required and is sent as `Authorization: Bearer <token>`. Token values are redacted from client errors.
|
The adapter then calls `Status` for the accepted run. A terminal `failed`
|
||||||
|
status is a notification failure. A status lookup failure or a timeout before a
|
||||||
|
terminal status remains attached to the otherwise accepted upload as diagnostic
|
||||||
|
status information. Polling cadence, final failure handling, redaction, and
|
||||||
|
notification artifact persistence are internal behavior documented in the
|
||||||
|
[Distributor adapter](../../internal/distributor-adapter.md) and
|
||||||
|
[application orchestration](../../internal/app-orchestration.md).
|
||||||
|
|
||||||
`HTTPClient` and `Retry` are optional. Defaults use a 30 second HTTP timeout and safe retry settings.
|
## Compatibility Reference
|
||||||
|
|
||||||
## Upload Producer Files
|
The upstream package workflow is documented in
|
||||||
|
`docs/consumers/pkg-upload.md` in the Distributor repository. Weatherreporter
|
||||||
Use `UploadFiles` when the producer has generated output files but has not assembled a bundle directory.
|
uses only the client construction, `UploadFiles`, retry/conflict behavior, and
|
||||||
|
`Status` operations described here.
|
||||||
```go
|
|
||||||
result, err := client.UploadFiles(ctx, upload.UploadFilesOptions{
|
|
||||||
PipelineID: "weather-hourly",
|
|
||||||
ID: "weather.hourly.brentwood",
|
|
||||||
IdempotencyKey: "weather.hourly.brentwood.20260607T150000Z",
|
|
||||||
Files: []bundle.BundleFile{
|
|
||||||
{SourcePath: "/tmp/weather/report.md", Path: "report.md"},
|
|
||||||
{SourcePath: "/tmp/weather/summary.txt", Path: "summary.txt"},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = result.RunID
|
|
||||||
```
|
|
||||||
|
|
||||||
`PipelineID` is required and selects the configured distributor workflow for this upload. `ID` is the source manifest id and identifies the logical artifact inside that workflow. `UploadFiles` creates a temporary bundle, writes and validates a manifest, uploads the archive, and removes temporary files when the call returns. It does not write into producer source directories.
|
|
||||||
|
|
||||||
## Upload An Existing Bundle
|
|
||||||
|
|
||||||
Use `UploadBundle` when the producer already has a complete local bundle root containing `manifest.json`.
|
|
||||||
|
|
||||||
```go
|
|
||||||
result, err := client.UploadBundle(ctx, upload.UploadBundleOptions{
|
|
||||||
PipelineID: "weather-hourly",
|
|
||||||
Root: "/var/spool/weather/hourly-2026-06-07T15",
|
|
||||||
IdempotencyKey: "weather.hourly.brentwood.20260607T150000Z",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = result.RunID
|
|
||||||
```
|
|
||||||
|
|
||||||
`PipelineID` is required for existing bundles too. `UploadBundle` validates the local bundle by default and uploads only `manifest.json` plus manifest-listed files. Unlisted files are not uploaded.
|
|
||||||
|
|
||||||
## Result And Status
|
|
||||||
|
|
||||||
Upload success means the server returned `202 Accepted` after staging and validating the upload. It does not mean all configured destinations have published.
|
|
||||||
|
|
||||||
Poll status while the server retains the in-memory run record:
|
|
||||||
|
|
||||||
```go
|
|
||||||
status, err := client.Status(ctx, result.RunID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if status.Status == "failed" {
|
|
||||||
return fmt.Errorf("distributor run failed: %s", status.Error)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Status values are `accepted`, `queued`, `running`, `succeeded`, and `failed`. Completed records expire according to `server.http.retention`; server restart clears run status and idempotency records.
|
|
||||||
|
|
||||||
## Idempotency And Retry
|
|
||||||
|
|
||||||
Every upload request includes `Idempotency-Key`.
|
|
||||||
|
|
||||||
If `IdempotencyKey` is omitted, the client generates a random 128-bit lowercase hexadecimal key for that upload operation and reuses it for retries within the same call. For cross-process retry safety, producers should pass a key derived from the producer run, such as `<bundle-id>.<run-id>`.
|
|
||||||
|
|
||||||
Do not reuse the same idempotency key for multiple distinct report generations. Reuse it only when retrying the exact same run with the same token, pipeline id, and source manifest. A repeated key with the same manifest in that scope returns the original accepted run instead of enqueueing another run; a repeated key with different content returns an idempotency conflict.
|
|
||||||
|
|
||||||
The client retries only safe cases:
|
|
||||||
|
|
||||||
- `503 Service Unavailable`;
|
|
||||||
- temporary network errors;
|
|
||||||
- ambiguous mid-upload failures.
|
|
||||||
|
|
||||||
It does not retry after `202 Accepted` and does not retry `400`, `401`, `403`, `404`, `409`, `413`, or `415`.
|
|
||||||
|
|
||||||
Detect conflicting key reuse with `errors.As`:
|
|
||||||
|
|
||||||
```go
|
|
||||||
var conflict *upload.IdempotencyConflictError
|
|
||||||
if errors.As(err, &conflict) {
|
|
||||||
return fmt.Errorf("idempotency key was reused for different bundle content: %w", err)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Boundaries
|
|
||||||
|
|
||||||
`pkg/upload` does not configure server pipelines, choose destinations, wait for publication completion automatically, persist client queues, provide durable idempotency across server restarts, or expose destination state. It submits complete source bundles to the configured HTTP upload API.
|
|
||||||
|
|||||||
@@ -1,118 +1,91 @@
|
|||||||
# Scriptorium Integration
|
# Scriptorium Integration
|
||||||
|
|
||||||
This document describes the external Scriptorium CLI contract used by
|
`weatherreporter` invokes the Scriptorium executable as a subprocess to
|
||||||
`weatherreporter`.
|
preflight prompt input and produce report artifacts. This is the limited CLI
|
||||||
|
contract Weatherreporter uses, not general Scriptorium documentation.
|
||||||
|
|
||||||
## Purpose
|
## Invocation
|
||||||
|
|
||||||
`weatherreporter` invokes Scriptorium as a subprocess to preflight prompt input
|
The configured `scriptorium.binary` is the executable name or path. When it is
|
||||||
and generate report artifacts. This page documents the CLI surface the adapter
|
empty, the adapter invokes `scriptorium`. Arguments are passed directly to the
|
||||||
uses, not the full Scriptorium product.
|
process, without a shell.
|
||||||
|
|
||||||
## Commands Used
|
For every command, arguments occur in this order:
|
||||||
|
|
||||||
Render preflight:
|
1. The subcommand.
|
||||||
|
2. `--config <path>` when `scriptorium.config_path` is set.
|
||||||
|
3. `--profile <profile>` when `scriptorium.profile` is set.
|
||||||
|
4. The command-specific arguments below.
|
||||||
|
5. Each configured `scriptorium.extra_args` item.
|
||||||
|
|
||||||
```bash
|
The adapter uses these exact command shapes:
|
||||||
scriptorium render \
|
|
||||||
--prompt <prompt_id> \
|
```text
|
||||||
--input data_package=<path> \
|
scriptorium render [--config <path>] [--profile <profile>] \
|
||||||
--format json
|
--prompt <prompt_id> --input data_package=<data_package_path> --format json \
|
||||||
|
[<extra_arg> ...]
|
||||||
|
|
||||||
|
scriptorium run [--config <path>] [--profile <profile>] \
|
||||||
|
--prompt <prompt_id> --input data_package=<data_package_path> --out <output_path> \
|
||||||
|
[<extra_arg> ...]
|
||||||
```
|
```
|
||||||
|
|
||||||
Report generation:
|
`render` is the preflight command. `run` writes either a Markdown report or a
|
||||||
|
raw generated-text artifact to the supplied `--out` path. The structured
|
||||||
|
generated-text use of `run` has the same argv as Markdown generation; it does
|
||||||
|
not add `--format`, `--schema`, `--schema-path`, or `--json-schema` flags.
|
||||||
|
Prompt configuration selected by `<prompt_id>` controls that output.
|
||||||
|
|
||||||
```bash
|
## Inputs and Outputs
|
||||||
scriptorium run \
|
|
||||||
--prompt <prompt_id> \
|
|
||||||
--input data_package=<path> \
|
|
||||||
--out <artifact_path>
|
|
||||||
```
|
|
||||||
|
|
||||||
Structured generated-text report generation uses the same command shape:
|
Weatherreporter always supplies exactly one prompt input:
|
||||||
|
`--input data_package=<data_package_path>`. The path identifies the YAML data
|
||||||
|
package produced by the [prompt-input builder](../internal/prompt-input.md).
|
||||||
|
Its schema and the separate JSON module snapshots are internal artifacts, not
|
||||||
|
part of this CLI contract.
|
||||||
|
|
||||||
```bash
|
The application supplies an already-managed output path to every `run` call.
|
||||||
scriptorium run \
|
For direct reports it is the Markdown artifact path. For generated-text
|
||||||
--prompt <prompt_id> \
|
reports it is the raw JSON artifact path; subsequent validation and Markdown
|
||||||
--input data_package=<path> \
|
rendering are owned by [generated-text processing](../internal/generatedtext.md).
|
||||||
--out <generated_text_raw_path>
|
|
||||||
```
|
|
||||||
|
|
||||||
`weatherreporter` always passes prompt input as
|
`render` has no output-path argument. Its JSON-formatted stdout remains
|
||||||
`--input data_package=<path>`. The data package is structured YAML created by
|
captured output: the adapter records it and does not parse it into a separate
|
||||||
`internal/promptinput`; module snapshots remain separate JSON artifacts for
|
CLI result type. Likewise, the adapter records `run` output metadata without
|
||||||
inspection and Recent Changes.
|
decoding the artifact written at `--out`.
|
||||||
|
|
||||||
For generated-text reports, Scriptorium selects the structured output schema
|
## Execution and Results
|
||||||
from the prompt configuration associated with the prompt ID. `weatherreporter`
|
|
||||||
does not pass `--format`, schema path, or JSON Schema flags for structured
|
|
||||||
generation.
|
|
||||||
|
|
||||||
## Configured Arguments
|
`scriptorium.timeout`, when greater than zero, creates a timeout for each
|
||||||
|
subprocess invocation. Parent-context cancellation and that timeout stop the
|
||||||
|
command through the process context.
|
||||||
|
|
||||||
The adapter can prepend configured flags before prompt-specific arguments:
|
Stdout and stderr are captured independently, each up to 1 MiB. Every returned
|
||||||
|
result records the complete argv as `command`, the captured `stdout` and
|
||||||
|
`stderr`, `exitCode`, and `stdoutTruncated` and `stderrTruncated` when a stream
|
||||||
|
was capped. Results from both forms of `run` also record `outputPath`, the
|
||||||
|
requested `--out` value.
|
||||||
|
|
||||||
- `--config <path>` from `scriptorium.config_path`
|
The [Scriptorium adapter](../internal/scriptorium-adapter.md) owns process
|
||||||
- `--profile <profile>` from `scriptorium.profile`
|
execution and result capture. [Application orchestration](../internal/app-orchestration.md)
|
||||||
|
owns when preflight output, report artifacts, and generated-text artifacts are
|
||||||
It appends `scriptorium.extra_args` after the built-in arguments. Extra
|
persisted.
|
||||||
arguments are passed directly as argv items.
|
|
||||||
|
|
||||||
`scriptorium.binary` selects the executable name or path. If unset inside the
|
|
||||||
adapter, it falls back to `scriptorium`.
|
|
||||||
|
|
||||||
## Execution Behavior
|
|
||||||
|
|
||||||
The adapter runs Scriptorium without shell interpolation. Arguments are passed
|
|
||||||
through `exec.CommandContext`.
|
|
||||||
|
|
||||||
`scriptorium.timeout` limits each subprocess call when configured. Context
|
|
||||||
cancellation or timeout returns an execution error.
|
|
||||||
|
|
||||||
Stdout and stderr are captured separately. Each stream is capped at 1 MiB and
|
|
||||||
the result records whether truncation occurred.
|
|
||||||
|
|
||||||
## Results
|
|
||||||
|
|
||||||
Render results include:
|
|
||||||
|
|
||||||
- full argv recorded as `command`
|
|
||||||
- stdout
|
|
||||||
- stderr
|
|
||||||
- exit code
|
|
||||||
- truncation flags when applicable
|
|
||||||
|
|
||||||
Run results include the same fields plus the requested output path. Structured
|
|
||||||
generated-text run results use the same captured fields and output-path
|
|
||||||
recording, with the output path pointing at the raw generated-text JSON
|
|
||||||
artifact.
|
|
||||||
|
|
||||||
`weatherreporter` persists render preflight JSON when orchestration reaches the
|
|
||||||
preflight save point. For direct Markdown reports, Scriptorium writes the
|
|
||||||
managed Markdown artifact to the `--out` path. For generated-text-template
|
|
||||||
reports, Scriptorium writes raw JSON to the `--out` path; later
|
|
||||||
weatherreporter workflow steps validate those bytes and render Markdown from an
|
|
||||||
embedded template.
|
|
||||||
|
|
||||||
## Failure Behavior
|
## Failure Behavior
|
||||||
|
|
||||||
The adapter validates required request fields before starting Scriptorium:
|
Before starting Scriptorium, the adapter requires a prompt ID and data-package
|
||||||
|
path for every command, plus an output path for `run`. Missing fields fail
|
||||||
|
without executing a subprocess.
|
||||||
|
|
||||||
- prompt ID
|
A nonzero process exit returns the captured result and an error that includes
|
||||||
- data package path
|
the exit code and stderr. An output file written before such an exit does not
|
||||||
- output path for `run` and structured generated-text `run`
|
make the request successful. Failures to start the command, context
|
||||||
|
cancellation, and timeout return an error rather than a successful result.
|
||||||
|
|
||||||
Nonzero exits return both the captured result and an error containing the exit
|
## Operational Notes
|
||||||
code and stderr. A `run` exit code such as `2` is still treated as an error by
|
|
||||||
the adapter, even if Scriptorium wrote output to the requested artifact path.
|
|
||||||
|
|
||||||
Subprocess start failures, context cancellation, and timeouts return errors
|
- Extra arguments are argv items; they are not shell-interpreted.
|
||||||
without fabricating a successful result.
|
- Prompt input, generated artifacts, stdout, and stderr can contain
|
||||||
|
operationally sensitive weather data.
|
||||||
## Security Notes
|
- Provide API keys through the Scriptorium environment or its configuration,
|
||||||
|
not through Weatherreporter CLI arguments.
|
||||||
- The adapter does not invoke a shell.
|
|
||||||
- Generated artifacts, rendered prompt context, stdout, and stderr can contain
|
|
||||||
operationally sensitive data.
|
|
||||||
- API keys should be provided through the Scriptorium environment or
|
|
||||||
Scriptorium configuration, not through `weatherreporter` CLI arguments.
|
|
||||||
|
|||||||
@@ -1,26 +1,51 @@
|
|||||||
# Weather API Integration
|
# Weather API Integration
|
||||||
|
|
||||||
This document describes the external Weather API contract used by
|
Weatherreporter fetches normalized weather inputs from a configured Weather API
|
||||||
`weatherreporter`.
|
base URL. This guide defines the HTTP contract the service must satisfy; it is
|
||||||
|
not a general Weather API reference. Configuration values are defined in the
|
||||||
|
[configuration reference](../config.md). Normalization and collection behavior
|
||||||
|
are documented in [Weather data internals](../internal/weather-data.md) and
|
||||||
|
[Collection internals](../internal/collect.md).
|
||||||
|
|
||||||
## Purpose
|
## Base URL And Requests
|
||||||
|
|
||||||
`weatherreporter` uses a configured Weather API base URL to fetch normalized
|
`weather_api.base_url` must be an absolute URL. Weatherreporter joins each
|
||||||
weather source data and assemble a `weatherdata.Bundle`. This is an integration
|
endpoint path to the configured base URL path, so a service hosted under a path
|
||||||
contract for the project adapter, not a complete public API reference for the
|
prefix must keep that prefix available. Requests use `GET` and carry the
|
||||||
upstream service.
|
configured timeout on every HTTP attempt.
|
||||||
|
|
||||||
## Base URL
|
Every request sends `format` and, except where noted below, `units`. The
|
||||||
|
configured format must be `json`.
|
||||||
|
|
||||||
`weather_api.base_url` must be an absolute URL. Adapter requests join this base
|
Before retrieving sources, Weatherreporter warms up
|
||||||
URL with the endpoint paths listed below. Generation and explicit bundle fetches
|
`/conditions/current` with the same `format`, `units`, and `precision` query
|
||||||
fail before any HTTP request when the base URL is empty or not absolute.
|
parameters used for current conditions. The warmup only requires a readable
|
||||||
|
2xx response; its body is not decoded. Failure after its internal retry budget
|
||||||
|
stops the fetch before source requests begin.
|
||||||
|
|
||||||
The HTTP client uses `weather_api.timeout`.
|
## Endpoints And Query Parameters
|
||||||
|
|
||||||
|
The adapter makes one source request for each endpoint after a successful
|
||||||
|
warmup, subject to retry on transient failures.
|
||||||
|
|
||||||
|
| Source | Endpoint | Query parameters | Availability |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Observations | `/observations` | `format`, `units`, `precision` | Optional |
|
||||||
|
| Current conditions | `/conditions/current` | `format`, `units`, `precision` | Optional |
|
||||||
|
| Hourly forecast | `/forecast/hourly` | `format`, `units`, `precision`, `tz` | Required |
|
||||||
|
| Narrative forecast | `/forecast/narrative` | `format`, `units`, `precision`, `tz` | Optional |
|
||||||
|
| Active alerts | `/alerts/active` | `format`, `units` | Optional; `data: null` means checked with no active alerts |
|
||||||
|
| Forecast discussion | `/discussion` | `format`, `units`, `tz` | Optional |
|
||||||
|
| Weather story | `/weatherstories/latest` | `format` | Optional |
|
||||||
|
| SPC convective outlooks | `/outlooks/convective` | `format`, `tz` | Optional; non-null empty lists are checked empty data |
|
||||||
|
|
||||||
|
`precision` comes from `weather_api.precision`; `tz` comes from
|
||||||
|
`weather_api.timezone`. Weatherreporter does not call day-slice forecast or
|
||||||
|
discussion-subsection endpoints.
|
||||||
|
|
||||||
## Response Envelope
|
## Response Envelope
|
||||||
|
|
||||||
Every response used by the adapter must be JSON with a top-level `data` field:
|
Each endpoint response must be JSON with a top-level `data` member:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -28,171 +53,95 @@ Every response used by the adapter must be JSON with a top-level `data` field:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
For most sources, `data: null` is treated as a missing source. Missing optional
|
An absent `data` member is treated as a missing source. For ordinary sources,
|
||||||
sources follow the configured missing-source policy. Missing hourly forecast
|
`data: null` is also missing. The active-alert exception is listed above: its
|
||||||
data fails bundle fetching because hourly periods are required for report
|
explicit `null` payload represents an empty alert result.
|
||||||
generation.
|
|
||||||
|
|
||||||
`/alerts/active` is the exception: a successful response with `data: null`
|
Hourly forecast data must be present and contain at least one `period`; a
|
||||||
means the endpoint was checked and there are no current active alerts. The
|
missing, malformed, or empty hourly product fails collection. The remaining
|
||||||
adapter records a non-missing alerts source and an empty alert run.
|
sources follow the configured missing-source policy. Under `error`, collection
|
||||||
|
fails; under `warn`, the source is omitted and an inspectable warning is
|
||||||
|
recorded; under `none`, the source is omitted without a warning. A per-source
|
||||||
|
policy overrides the default. See [Configuration](../config.md) for policy
|
||||||
|
settings and [Weather data internals](../internal/weather-data.md) for recorded
|
||||||
|
source metadata.
|
||||||
|
|
||||||
For `/outlooks/convective`, `data: null` means no latest run is available and
|
Malformed top-level JSON envelopes and HTTP failures are direct request errors.
|
||||||
follows missing-source policy. A non-null run with empty `outlooks` and
|
Malformed `data` for an optional source follows its missing-source policy.
|
||||||
`discussions` arrays is checked empty data, not a missing source.
|
|
||||||
|
|
||||||
Malformed JSON envelopes, non-2xx statuses, and response read failures include
|
## Payload Fields Used
|
||||||
endpoint context in returned errors. Decode errors include source context when
|
|
||||||
they fail the fetch; optional malformed sources follow the missing-source policy.
|
|
||||||
|
|
||||||
## Query Parameters
|
Weatherreporter decodes only the fields below; additional upstream fields are
|
||||||
|
ignored. Timestamps must be JSON values accepted by Go's `time.Time` decoder.
|
||||||
|
|
||||||
The adapter sends these query parameters:
|
### Observations And Current Conditions
|
||||||
|
|
||||||
- `format`: from `weather_api.format`; configuration validation requires `json`
|
`/observations` uses `stationId`, `stationName`, `timestamp`, `conditionCode`,
|
||||||
- `units`: from `weather_api.units`
|
`isDay`, `textDescription`, `temperatureC`, `temperatureF`, `dewpointC`,
|
||||||
- `precision`: from `weather_api.precision` on observations, current
|
`dewpointF`, `windSpeedKmh`, `windSpeedMph`, `windGustKmh`, `windGustMph`,
|
||||||
conditions, hourly forecast, and narrative forecast requests
|
`windDirectionDegrees`, `barometricPressurePa`, `barometricPressureInHg`,
|
||||||
- `tz`: from `weather_api.timezone` on hourly forecast, narrative forecast,
|
`visibilityMeters`, `visibilityMiles`, `relativeHumidityPercent`,
|
||||||
discussion, and SPC convective outlook requests
|
`apparentTemperatureC`, `apparentTemperatureF`, and `presentWeather`.
|
||||||
|
|
||||||
Alerts do not receive `precision` or `tz`. Weather story requests receive only
|
`/conditions/current` uses `conditionText`, `isDay`,
|
||||||
`format=json`. SPC convective outlook requests receive only `format=json` and
|
`relativeHumidityPercent`, `windDirectionDegrees`, `temperatureC`,
|
||||||
`tz`; they do not receive `units` or `precision`.
|
`temperatureF`, `apparentTemperatureC`, `apparentTemperatureF`, `dewpointC`,
|
||||||
|
`dewpointF`, `windSpeedKmh`, and `windSpeedMph`.
|
||||||
|
|
||||||
## SPC Convective Outlooks
|
### Hourly And Narrative Forecasts
|
||||||
|
|
||||||
The adapter fetches SPC convective outlook data from:
|
Both forecast endpoints use run-level `locationId`, `locationName`, `issuedAt`,
|
||||||
|
`updatedAt`, `product`, `latitude`, `longitude`, `elevationMeters`,
|
||||||
|
`elevationFeet`, and `periods`.
|
||||||
|
|
||||||
```text
|
Each `periods` item uses `startTime`, `endTime`, `name`, `isDay`,
|
||||||
GET /outlooks/convective?format=json&tz=<weather_api.timezone>
|
`conditionCode`, `textDescription`, `temperatureC`, `temperatureF`,
|
||||||
```
|
`temperatureCMin`, `temperatureFMin`, `temperatureCMax`, `temperatureFMax`,
|
||||||
|
`dewpointC`, `dewpointF`, `windSpeedKmh`, `windSpeedMph`, `windGustKmh`,
|
||||||
|
`windGustMph`, `windDirectionDegrees`, `barometricPressurePa`,
|
||||||
|
`barometricPressureInHg`, `visibilityMeters`, `visibilityMiles`,
|
||||||
|
`apparentTemperatureC`, `apparentTemperatureF`, `cloudCoverPercent`,
|
||||||
|
`probabilityOfPrecipitationPercent`, `precipitationAmountMm`,
|
||||||
|
`precipitationAmountIn`, `snowfallDepthMM`, `snowfallDepthIn`, `uvIndex`, and
|
||||||
|
`relativeHumidityPercent`.
|
||||||
|
|
||||||
The response uses the standard `data` envelope. `data: null` means no latest
|
### Alerts, Discussion, And Weather Story
|
||||||
run is available and follows missing-source policy. A non-null object with
|
|
||||||
empty `outlooks` and `discussions` arrays is accepted as checked empty data.
|
|
||||||
|
|
||||||
Run fields consumed by weatherreporter:
|
`/alerts/active` uses the `asOf` timestamp and keeps each item in `alerts` as
|
||||||
|
an alert payload. Weatherreporter does not require a separate alert-item schema
|
||||||
|
at this integration boundary.
|
||||||
|
|
||||||
- `locationId`
|
`/discussion` uses `officeId`, `officeName`, `product`, `issuedAt`,
|
||||||
- `locationName`
|
`updatedAt`, `keyMessages`, and the `shortTerm` and `longTerm` sections. Each
|
||||||
- `asOf`
|
section uses `qualifier`, `text`, and `issuedAt`.
|
||||||
- `issuedAt`
|
|
||||||
- `updatedAt`
|
|
||||||
- `product`
|
|
||||||
- `outlooks`
|
|
||||||
- `discussions`
|
|
||||||
|
|
||||||
Outlook fields consumed:
|
`/weatherstories/latest` uses `officeId`, `startTime`, `endTime`, `updatedAt`,
|
||||||
|
`title`, `description`, `altText`, `priority`, `order`, and `downloadUrl`.
|
||||||
|
|
||||||
- `id`
|
### SPC Convective Outlooks
|
||||||
- `provider`
|
|
||||||
- `product`
|
|
||||||
- `day`
|
|
||||||
- `outlookType`
|
|
||||||
- `label`
|
|
||||||
- `labelText`
|
|
||||||
- `forecaster`
|
|
||||||
- `severityRank`
|
|
||||||
- `validFrom`
|
|
||||||
- `validTo`
|
|
||||||
- `issuedAt`
|
|
||||||
- `expiresAt`
|
|
||||||
- `sourceUrl`
|
|
||||||
- `imageUrl`
|
|
||||||
- `containsLocation`
|
|
||||||
- `geometry`
|
|
||||||
|
|
||||||
Discussion fields consumed:
|
`/outlooks/convective` uses run-level `locationId`, `locationName`, `asOf`,
|
||||||
|
`issuedAt`, `updatedAt`, `product`, `outlooks`, and `discussions`.
|
||||||
|
|
||||||
- `day`
|
Each outlook uses `id`, `provider`, `product`, `day`, `outlookType`, `label`,
|
||||||
- `headline`
|
`labelText`, `forecaster`, `severityRank`, `validFrom`, `validTo`, `issuedAt`,
|
||||||
- `summary`
|
`expiresAt`, `sourceUrl`, `imageUrl`, `containsLocation`, and GeoJSON
|
||||||
- `discussion`
|
`geometry`. Each discussion uses `day`, `headline`, `summary`, `discussion`,
|
||||||
- `updatedAt`
|
and `updatedAt`.
|
||||||
|
|
||||||
GeoJSON `geometry` is decoded into collected weather facts and persisted in
|
## Timeouts, Retries, And Failures
|
||||||
bundle/debug artifacts, but prompt-facing SPC module output omits geometry.
|
|
||||||
|
|
||||||
## Endpoints Used
|
The configured Weather API timeout applies to each warmup and source HTTP
|
||||||
|
attempt. Weatherreporter retries transient transport and response-read failures
|
||||||
|
and these response statuses: `408`, `429`, `500`, `502`, `503`, and `504`.
|
||||||
|
It does not retry other HTTP statuses, malformed envelopes, missing data, or
|
||||||
|
payload decoding failures. A canceled context also stops an in-progress retry
|
||||||
|
delay.
|
||||||
|
|
||||||
The adapter fetches these endpoints once per bundle:
|
The adapter reads at most 10 MiB from one response body. A non-2xx response,
|
||||||
|
request construction failure, read failure, or decode failure includes endpoint
|
||||||
|
context in its error.
|
||||||
|
|
||||||
- `/observations`
|
Retry counts and delays are adapter behavior rather than Weather API request
|
||||||
- `/conditions/current`
|
parameters. Do not depend on a particular attempt count when implementing the
|
||||||
- `/forecast/hourly`
|
service.
|
||||||
- `/forecast/narrative`
|
|
||||||
- `/alerts/active`
|
|
||||||
- `/discussion`
|
|
||||||
- `/weatherstories/latest`
|
|
||||||
- `/outlooks/convective`
|
|
||||||
|
|
||||||
`weatherreporter` does not call day-slice forecast endpoints or discussion
|
|
||||||
subsection endpoints. Report-period selection and daypart summarization happen
|
|
||||||
inside Go after the full hourly and narrative products are fetched.
|
|
||||||
|
|
||||||
## Required And Optional Sources
|
|
||||||
|
|
||||||
Hourly forecast is required:
|
|
||||||
|
|
||||||
- `data: null` for `/forecast/hourly` fails the fetch.
|
|
||||||
- an hourly forecast with no `periods` fails the fetch.
|
|
||||||
- malformed hourly data fails the fetch.
|
|
||||||
|
|
||||||
Other fetched sources are optional and follow `missing_source.default` or a
|
|
||||||
source-specific `missing_source.sources` policy:
|
|
||||||
|
|
||||||
- `observations` for `/observations`
|
|
||||||
- `current` for `/conditions/current`
|
|
||||||
- `narrative` for `/forecast/narrative`
|
|
||||||
- `alerts` for `/alerts/active`
|
|
||||||
- `discussion` for `/discussion`
|
|
||||||
- `weather_story` for `/weatherstories/latest`
|
|
||||||
- `spc_convective_outlooks` for `/outlooks/convective`
|
|
||||||
|
|
||||||
Policy behavior:
|
|
||||||
|
|
||||||
- `error`: fail the fetch for that source
|
|
||||||
- `warn`: omit the source data, add a warning, and continue
|
|
||||||
- `none`: omit the source data and continue without a warning
|
|
||||||
|
|
||||||
For `/alerts/active`, an HTTP error or missing `data` field still fails or
|
|
||||||
follows the relevant error path, but explicit `data: null` is not a
|
|
||||||
missing-source condition.
|
|
||||||
|
|
||||||
For `/outlooks/convective`, a non-null data object with empty outlook and
|
|
||||||
discussion arrays is accepted as checked empty data.
|
|
||||||
|
|
||||||
## Source Identity
|
|
||||||
|
|
||||||
For source payloads accepted into the bundle, including the explicit `null`
|
|
||||||
alerts payload, the adapter records:
|
|
||||||
|
|
||||||
- source name
|
|
||||||
- endpoint path
|
|
||||||
- query parameters sent
|
|
||||||
- fetch time
|
|
||||||
- source issue and update timestamps when present in the payload
|
|
||||||
- SHA-256 hash of the compact raw `data` JSON
|
|
||||||
|
|
||||||
Warnings are recorded both on the affected source and on the bundle-level
|
|
||||||
warnings list.
|
|
||||||
|
|
||||||
## Compatibility Assumptions
|
|
||||||
|
|
||||||
The adapter expects payload fields compatible with the internal weather data
|
|
||||||
bundle types in `internal/weatherdata/bundle.go`, including:
|
|
||||||
|
|
||||||
- observation timestamps and observation values
|
|
||||||
- current condition values
|
|
||||||
- forecast run metadata and `periods`
|
|
||||||
- active alert run data
|
|
||||||
- discussion metadata, key messages, and short/long-term section text
|
|
||||||
- latest weather story title, description, timing, priority, order, alt text,
|
|
||||||
and download URL
|
|
||||||
- SPC convective outlook run metadata, outlooks, discussions, and GeoJSON
|
|
||||||
geometry
|
|
||||||
|
|
||||||
The adapter intentionally keeps upstream transport and envelope details inside
|
|
||||||
`internal/adapters/weatherapi`; downstream packages consume the normalized
|
|
||||||
bundle.
|
|
||||||
|
|||||||
@@ -1,233 +1,110 @@
|
|||||||
# App Orchestration Internals
|
# Application Orchestration Internals
|
||||||
|
|
||||||
This document describes the workflow coordinator in `internal/app`.
|
`internal/app` composes top-level generation, batch, collection-save, and
|
||||||
|
inspection workflows after CLI parsing and configuration loading. It owns
|
||||||
## Purpose
|
workflow ordering, request composition, partial-result handling, and the
|
||||||
|
application-facing interfaces used for tests.
|
||||||
`internal/app` coordinates the top-level use cases after CLI parsing and config
|
|
||||||
loading are complete. It resolves report definitions, collects weather data
|
|
||||||
through `internal/collect`, builds collected and derived facts, builds module
|
|
||||||
snapshots and prompt-input artifacts, invokes Scriptorium through the adapter
|
|
||||||
boundary, optionally notifies distributor through an app-owned notifier
|
|
||||||
boundary, persists managed state, runs batches, and reads existing artifacts
|
|
||||||
for inspection.
|
|
||||||
|
|
||||||
## Inputs And Outputs
|
## Inputs And Outputs
|
||||||
|
|
||||||
Inputs:
|
The package accepts generate, resolved-report, batch, explicit-collection, and
|
||||||
|
inspection requests. Generation and batch requests may supply collector,
|
||||||
|
renderer, store, and notifier implementations for tests; production defaults
|
||||||
|
use the focused packages.
|
||||||
|
|
||||||
- `GenerateRequest` for one report command
|
A report result contains the module snapshot, prompt package, available
|
||||||
- `BatchRequest` for morning or evening batch commands
|
Scriptorium results, generated-text artifacts when used, report and metadata
|
||||||
- `FetchBundleRequest` for explicit bundle collection and save workflows
|
paths, prior snapshot, Recent Changes, and notification information. A batch
|
||||||
- `ReportRequest` for single-report generation
|
result contains aggregate counts, per-report outcomes, and an optional batch
|
||||||
- resolved report definitions from `internal/report`
|
notification. Inspection returns persisted values only.
|
||||||
- collection results from `internal/collect`
|
|
||||||
- prior snapshots loaded from `internal/state`
|
|
||||||
- optional collector, renderer, notifier, and state-store fakes for tests
|
|
||||||
|
|
||||||
Outputs:
|
Exact public command syntax, configuration fields, workspace layout, external
|
||||||
|
protocols, and report definitions belong in [the CLI reference](../cli.md),
|
||||||
|
[the configuration reference](../config.md), [operations](../operations.md),
|
||||||
|
and their focused integration and internal documents.
|
||||||
|
|
||||||
- generated report results with JSON module snapshot, YAML data package,
|
## Single-Report Workflow
|
||||||
preflight, report, metadata, prior snapshot, Recent Changes, Scriptorium
|
|
||||||
result details, generated-text artifact paths when applicable, and
|
|
||||||
notification result when attempted
|
|
||||||
- batch summaries with per-report status, artifact paths, error text, and
|
|
||||||
one top-level batch notification result when attempted or skipped
|
|
||||||
- saved Weather API bundle JSON for explicit bundle collection workflows
|
|
||||||
- inspection JSON values for reports, metadata, module snapshots, data
|
|
||||||
packages, prior snapshots, and source provenance
|
|
||||||
|
|
||||||
## Boundaries
|
`GenerateDetailed` first collects weather data, then resolves the requested
|
||||||
|
report using the configured registry and current time, and finally calls
|
||||||
|
`GenerateReport` with that explicit collection. It returns no result when
|
||||||
|
collection or resolution fails.
|
||||||
|
|
||||||
`internal/app` owns workflow order and request composition. It does not parse
|
`GenerateReport` requires a non-nil normalized bundle and then performs this
|
||||||
CLI flags, load YAML files directly, implement HTTP transport, own fact
|
ordered work:
|
||||||
derivation algorithms, define report periods, compare rendered Markdown, or
|
|
||||||
construct Scriptorium argv.
|
|
||||||
|
|
||||||
Report selection and report identity policy come from `internal/report`.
|
1. Select a state store, determine artifact destinations, and locate a prior
|
||||||
Collected and derived fact contracts come from `internal/facts`.
|
compatible snapshot.
|
||||||
Weather API transport stays in `internal/adapters/weatherapi`, and app-facing
|
2. Build report facts and deterministic module snapshots, then save the module
|
||||||
upstream collection stays in `internal/collect`. Scriptorium subprocess
|
snapshot and calculate Recent Changes.
|
||||||
behavior stays in `internal/adapters/scriptorium`. Distributor upload behavior
|
3. Build and save the prompt data package, run Scriptorium render preflight,
|
||||||
stays in `internal/adapters/distributor`. Filesystem layout and persisted
|
save any preflight result, and save initial metadata.
|
||||||
metadata stay in `internal/state`.
|
4. Produce managed Markdown according to the report generation mode.
|
||||||
|
5. Optionally make an output copy, save final metadata, optionally notify
|
||||||
|
Distributor from the managed report path, and save metadata again when a
|
||||||
|
notification path is produced.
|
||||||
|
|
||||||
## Data Flow Terms
|
Direct-Markdown reports prepare the managed report and invoke the Scriptorium
|
||||||
|
run boundary. Generated-text-template reports look up their catalog definition,
|
||||||
|
run structured Scriptorium output to the raw artifact, preserve any structured
|
||||||
|
run result, validate and save generated text, build and save a render context,
|
||||||
|
then render the embedded Markdown template. Schema, template, and subprocess
|
||||||
|
details remain in their [generated-text](generatedtext.md),
|
||||||
|
[report-template](reporttemplate.md), and [Scriptorium adapter](scriptorium-adapter.md)
|
||||||
|
owners.
|
||||||
|
|
||||||
- `collect.Result` is the app-facing upstream collection result. It carries the
|
If preflight returns a result with an error, the result and initial metadata are
|
||||||
normalized `weatherdata.Bundle` used by report generation.
|
saved before the error returns. If report generation fails after a managed path
|
||||||
- `CollectedFacts` are normalized source facts derived from a collected Weather
|
is prepared, metadata still records that path; output copies and notification
|
||||||
API bundle and made available to derivation and module builders.
|
are skipped. Generated-text failures preserve the latest artifact reached
|
||||||
- `DerivedFacts` are deterministic calculations over collected facts, the
|
before failure when it was saved.
|
||||||
resolved valid period, daypart configuration, and report-specific windows.
|
|
||||||
- `module.Output` values are ordered deterministic stanzas built from collected
|
|
||||||
and derived facts for prompt input and inspection.
|
|
||||||
- `GeneratedText` is structured prose returned by Scriptorium for
|
|
||||||
generated-text-template reports and validated by `internal/generatedtext`.
|
|
||||||
- `RenderContext` is the typed template input built from report metadata,
|
|
||||||
module outputs, and validated generated text before Markdown rendering.
|
|
||||||
|
|
||||||
## Config Fields Used
|
## Batch And Inspection Workflows
|
||||||
|
|
||||||
- `weather_api.*` for Weather API client construction and module metadata
|
`RunBatchDetailed` collects once, asks the report registry to plan the batch
|
||||||
- `scriptorium.*` for renderer construction
|
from that collection, and invokes `GenerateReport` independently for every
|
||||||
- `workspace.*` for filesystem state
|
planned report using the same collection and state store. Per-report
|
||||||
- `dayparts` for daily and outlook summarization
|
notification is suppressed. A failed report is recorded and does not prevent
|
||||||
- `recent_change.*` for structured Recent Changes thresholds
|
later planned reports from running.
|
||||||
- `notify.distributor.*` for optional single-report and batch notification
|
|
||||||
after report generation
|
|
||||||
|
|
||||||
Output copy flags are command request fields. They are not configuration
|
After report generation, the batch notifier is considered once. It is omitted
|
||||||
defaults.
|
when Distributor or batch notification is disabled, skipped when any report
|
||||||
|
failed, and otherwise receives one multi-file request. A batch notification
|
||||||
|
failure increments the aggregate failure count but does not rewrite successful
|
||||||
|
report items. Notification identities, path mappings, polling, and redaction
|
||||||
|
are owned by the [Distributor adapter](distributor-adapter.md).
|
||||||
|
|
||||||
## Generation Workflow
|
Inspection methods create a state store and load existing report records,
|
||||||
|
metadata, module snapshots, prompt packages, prior snapshots, or source
|
||||||
|
provenance. They neither collect data nor invoke Scriptorium or Distributor.
|
||||||
|
|
||||||
Single-report commands validate the report command, collect once through
|
## Boundaries And Failure Propagation
|
||||||
`internal/collect`, resolve the requested report, and pass the resolved report
|
|
||||||
plus explicit collection into `GenerateReport`. `GenerateDetailed` returns the
|
|
||||||
resulting `ReportResult`; `Generate` wraps the same workflow for error-only
|
|
||||||
callers.
|
|
||||||
|
|
||||||
`GenerateReport` then uses this setup:
|
The app layer does not parse flags, load configuration files, implement Weather
|
||||||
|
API transport, construct Scriptorium argv, or define report registry policy. It
|
||||||
|
coordinates the relevant collaborators and preserves their error context.
|
||||||
|
|
||||||
1. Create or use a filesystem store.
|
- Collection failure stops a single report or batch before resolution or
|
||||||
2. Locate any prior compatible snapshot through `internal/state`.
|
planning completes.
|
||||||
3. Build collected and derived facts from the supplied collection.
|
- State, fact, module, prompt-input, or preflight failures stop that report
|
||||||
4. Execute configured modules and save the module snapshot.
|
before report generation.
|
||||||
5. Compute Recent Changes from structured prior and current module snapshots.
|
- A terminal Distributor failure is returned with the saved notification
|
||||||
6. Build and save the YAML Scriptorium `data_package`.
|
information when available.
|
||||||
7. Run Scriptorium render preflight.
|
- Batch failures are represented per report and through aggregate batch status.
|
||||||
8. Save preflight JSON when a render result is available.
|
- Persisted artifact paths are carried in results so callers can inspect work
|
||||||
9. Save metadata for inspection.
|
completed before a later failure.
|
||||||
|
|
||||||
For `scriptorium_markdown` reports, generation then:
|
## Tests And Invariants
|
||||||
|
|
||||||
10. Runs Scriptorium report generation to the managed report path.
|
Focused tests are in `internal/app/app_test.go` and
|
||||||
|
`internal/app/batch_plan_test.go`, with collection coverage in
|
||||||
|
`internal/collect/collect_test.go`.
|
||||||
|
|
||||||
For `generated_text_template` reports, generation then:
|
- Production workflows collect through `internal/collect`.
|
||||||
|
- A report uses one explicit normalized collection throughout its generation.
|
||||||
10. Looks up the generated-text catalog entry for the report schema/template
|
- Render preflight precedes report generation.
|
||||||
IDs.
|
- Recent Changes compare structured module snapshots.
|
||||||
11. Runs structured Scriptorium generation to the raw generated-text JSON path.
|
- Generated-text reports render from a validated typed context, never directly
|
||||||
12. Saves the structured Scriptorium run result.
|
from a raw prompt package.
|
||||||
13. Validates and saves normalized generated text.
|
- Only managed Markdown reports are notification sources; output copies are
|
||||||
14. Builds and saves a typed render context.
|
never uploaded.
|
||||||
15. Renders Markdown from the embedded template to the managed report path.
|
|
||||||
|
|
||||||
After either mode has produced a managed Markdown report, shared finalization:
|
|
||||||
|
|
||||||
1. Copies the managed report to the requested `--out` or `--out-dir` path when
|
|
||||||
provided.
|
|
||||||
2. Saves final metadata with the managed report path and any generated-text
|
|
||||||
artifact paths already produced.
|
|
||||||
3. If distributor notification is enabled, notifies using the managed report
|
|
||||||
path as the source file.
|
|
||||||
4. Saves a distributor notification debug artifact and updates metadata with
|
|
||||||
its path.
|
|
||||||
|
|
||||||
If render preflight returns both a result and an error, preflight JSON and
|
|
||||||
metadata are persisted before the error is returned. If Scriptorium report
|
|
||||||
generation returns an error after writing output, the managed report and
|
|
||||||
metadata remain inspectable. Notification is not attempted after collection,
|
|
||||||
module snapshot, prompt input, render, Scriptorium run, or metadata-save
|
|
||||||
failures.
|
|
||||||
Generated-text report failures are returned with report ID, RunID, and the
|
|
||||||
failed operation. When available, the app preserves the latest generated-text
|
|
||||||
artifacts already reached by the workflow: preflight output, structured run
|
|
||||||
result, raw generated text, validated generated text, and render context.
|
|
||||||
When notification is attempted, the debug artifact records request identity,
|
|
||||||
including rendered pipeline ID, bundle paths, accepted upload fields,
|
|
||||||
distributor status fields, raw status report JSON when available, and redacted
|
|
||||||
failure context.
|
|
||||||
`--out` copies are never used as notification source files.
|
|
||||||
|
|
||||||
## Batch Workflow
|
|
||||||
|
|
||||||
`run morning` collects once, plans Today Report, Tomorrow Report, and eligible
|
|
||||||
future Daily Reports from the collected hourly forecast, then passes the same
|
|
||||||
collection into each report generation. `run evening` uses the same collection
|
|
||||||
and planning rules, but starts with Tomorrow Report. Future Daily reports start
|
|
||||||
with the day after tomorrow and require complete hourly forecast coverage for
|
|
||||||
the target local civil day. Dynamic Daily `--out-dir` copies use
|
|
||||||
`daily-YYYY-MM-DD.md`; other batch copies use report definition output names.
|
|
||||||
A collection failure stops the batch before planning or report generation.
|
|
||||||
After planning succeeds, batch generation continues independent reports after a
|
|
||||||
failure, records each result, writes compact status lines to stderr, emits a
|
|
||||||
JSON summary to stdout, and returns an aggregate error when any report failed.
|
|
||||||
|
|
||||||
Batch report generation suppresses per-report distributor notification. After
|
|
||||||
all planned reports finish, app orchestration evaluates batch notification:
|
|
||||||
|
|
||||||
1. If distributor notification is disabled, the batch notification result is
|
|
||||||
omitted.
|
|
||||||
2. If batch notification is disabled, the batch notification result is omitted
|
|
||||||
and there is no per-report fallback upload.
|
|
||||||
3. If any planned report failed, the batch notification result is `skipped`
|
|
||||||
with reason `one or more reports failed`, and distributor is not called.
|
|
||||||
4. If every report succeeded, app orchestration renders batch pipeline, bundle
|
|
||||||
ID, and idempotency key templates, renders report-specific distributor
|
|
||||||
paths for each included report, validates every managed source path and
|
|
||||||
bundle path, checks duplicate bundle paths across the batch, calls the
|
|
||||||
notifier once with a multi-file request, and saves a batch notification
|
|
||||||
debug artifact.
|
|
||||||
|
|
||||||
Batch notification failure records a top-level failed notification, increments
|
|
||||||
the aggregate batch failure count, and returns an aggregate batch error without
|
|
||||||
marking individual report items failed. `--out-dir` copies are never used as
|
|
||||||
notification source files.
|
|
||||||
|
|
||||||
## Inspection Workflow
|
|
||||||
|
|
||||||
Inspection workflows load existing filesystem state only. They do not fetch
|
|
||||||
weather data or invoke Scriptorium. Run-specific inspect commands share the same
|
|
||||||
store and metadata lookup path, then load the requested artifact or derived
|
|
||||||
inspection view.
|
|
||||||
|
|
||||||
## Failure Behavior
|
|
||||||
|
|
||||||
- Resolve errors stop the requested workflow before collection.
|
|
||||||
- Collection and module execution errors stop that report before Scriptorium
|
|
||||||
runs.
|
|
||||||
- Prompt input validation fails before render preflight.
|
|
||||||
- Render and run errors preserve Scriptorium stderr and exit-code context.
|
|
||||||
- Generated-text report errors preserve available intermediate artifacts and do
|
|
||||||
not create extra output copies.
|
|
||||||
- Single-report notification errors are wrapped with report ID, RunID, and
|
|
||||||
managed report path context. Detailed generation returns the inspectable
|
|
||||||
report, metadata, and notification artifact paths when finalization has
|
|
||||||
already saved them.
|
|
||||||
- Batch notification errors are recorded on the top-level batch notification
|
|
||||||
result and do not change individual report item status.
|
|
||||||
- Metadata and artifact path errors include filesystem context.
|
|
||||||
- Batch failures are recorded per report and surfaced through an aggregate
|
|
||||||
batch error.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Inspect:
|
|
||||||
|
|
||||||
- `internal/app/app_test.go`
|
|
||||||
- `internal/app/batch_plan_test.go`
|
|
||||||
- `internal/collect/collect_test.go`
|
|
||||||
- `internal/cli/root_test.go`
|
|
||||||
- `internal/state/filesystem_test.go`
|
|
||||||
|
|
||||||
## Invariants
|
|
||||||
|
|
||||||
- Report behavior is resolved through `internal/report`.
|
|
||||||
- Generate and run commands collect once before report generation.
|
|
||||||
- Batch planning is app-owned because future Daily membership depends on
|
|
||||||
collected hourly forecast coverage.
|
|
||||||
- Generated reports use the same app request and result types regardless of
|
|
||||||
report ID.
|
|
||||||
- Render preflight precedes Scriptorium report generation.
|
|
||||||
- Generated-text reports render Markdown from a curated render context, not from
|
|
||||||
a raw data package.
|
|
||||||
- Recent Changes are computed from structured module snapshots.
|
|
||||||
- Metadata links artifacts produced for a run.
|
|
||||||
- Single-report distributor notification maps the managed Markdown report path
|
|
||||||
to configured bundle paths.
|
|
||||||
- Batch distributor notification maps each included managed Markdown report
|
|
||||||
path to bundle paths rendered for that report and uploads once for the
|
|
||||||
batch.
|
|
||||||
- Extra output copies are not upload sources.
|
|
||||||
|
|||||||
@@ -1,160 +1,69 @@
|
|||||||
# Module Builder Internals
|
# Module Builder Internals
|
||||||
|
|
||||||
This document describes module builder behavior in `internal/briefing`.
|
`internal/briefing` builds typed module outputs from resolved report context,
|
||||||
|
collected facts, and derived facts. It owns the module registry, including
|
||||||
|
module support, fact requirements, option types, missing-data policy, builders,
|
||||||
|
and prompt-export hooks. It does not collect data, derive periods, write a
|
||||||
|
snapshot, construct YAML, invoke Scriptorium, or render a report.
|
||||||
|
|
||||||
## Purpose
|
## Registry and construction
|
||||||
|
|
||||||
`internal/briefing` turns report metadata, collected weather data, and derived
|
Every `ModuleDefinition` declares an ID, stanza name, default option value,
|
||||||
forecast facts into prompt-facing module outputs. The package also owns the
|
required collected and derived facts, supported report IDs, missing-data
|
||||||
module registry used to validate report composition and config overrides.
|
behavior, duplicate policy, builder, and optional prompt exporter.
|
||||||
|
|
||||||
Module outputs are structured prompt inputs. They are not rendered report prose
|
`BuildModule` first verifies the requested module, report compatibility, and
|
||||||
and they are not persisted by this package.
|
option shape. It then applies the declared missing-data behavior:
|
||||||
|
|
||||||
## Inputs And Outputs
|
- `omit` returns no output for unavailable optional facts;
|
||||||
|
- `error` returns the missing fact requirements; and
|
||||||
|
- `empty` allows the builder to emit an explicit checked-empty value.
|
||||||
|
|
||||||
Inputs:
|
Unsupported `warn` behavior, missing builders, duplicate registry IDs or
|
||||||
|
stanza names, output ID or stanza mismatches, and exporter failures all return
|
||||||
|
errors with module context. A successful builder gets a pass-through prompt
|
||||||
|
value unless its definition supplies an exporter.
|
||||||
|
|
||||||
- resolved report definition, generation time, timezone, and valid period
|
## Built value families
|
||||||
- collected facts built from `weatherdata.Bundle`
|
|
||||||
- derived daily, daypart, precipitation, alert, and storm-window facts where
|
|
||||||
required
|
|
||||||
- configured units, timezone, and descriptive location context
|
|
||||||
- typed module options from report defaults or config overrides
|
|
||||||
|
|
||||||
Outputs:
|
Source-oriented builders shape report metadata, current conditions, narrative
|
||||||
|
and hourly forecasts, alert digest, SPC outlooks and discussion, area forecast
|
||||||
|
discussion, and weather story. Derived builders shape daily and daypart
|
||||||
|
summaries, precipitation timing, outdoor windows, and the report-specific
|
||||||
|
Daily, Today, and Tomorrow planning values.
|
||||||
|
|
||||||
- `ModuleDefinition` values with module ID, stanza name, option type,
|
The module registry preserves rich values for templates and snapshots while
|
||||||
supported reports, fact requirements, missing-data behavior, and builder
|
curating prompt exports where needed. In particular, source warnings are a
|
||||||
- `module.Output` values for source-oriented stanzas:
|
metadata summary, checked-empty alerts and SPC outlooks remain distinct from
|
||||||
`metadata`, `current_conditions`, `narrative_forecast`, `hourly_forecast`,
|
missing sources, and prompt-safe SPC values omit geometry and other
|
||||||
`alert_digest`, `spc_convective_outlooks`,
|
template-only or source details. The complete module composition is in
|
||||||
`area_forecast_discussion`, `spc_convective_discussion`, and
|
[module internals](module.md); fact derivation is in [fact contracts](facts.md).
|
||||||
`weather_story`
|
|
||||||
- `module.Output` values for derived stanzas:
|
|
||||||
`derived_daily_summary`, `derived_daypart_summaries`, `precip_timing`,
|
|
||||||
`outdoor_windows`, `today_planning`, `tomorrow_planning`, and
|
|
||||||
`daily_planning`
|
|
||||||
|
|
||||||
Every registered composition entry has a builder. Unknown or unimplemented
|
`area_forecast_discussion` accepts an optional typed section filter. Planning
|
||||||
module IDs fail validation instead of being skipped.
|
modules are report-specific: `daily_planning` supports Daily,
|
||||||
|
`today_planning` supports Today, and `tomorrow_planning` supports Tomorrow.
|
||||||
|
|
||||||
Daily Report supports the Daily-style civil-day modules plus `daily_planning`
|
## Missing data and boundaries
|
||||||
and `hourly_forecast`; those outputs feed the dated Daily GeneratedText prompt
|
|
||||||
package and embedded Markdown template.
|
|
||||||
|
|
||||||
Tomorrow Report supports the Daily-style civil-day modules plus
|
Optional current conditions, narrative products, discussions, and weather
|
||||||
`tomorrow_planning` and `hourly_forecast`; those outputs feed the Tomorrow
|
stories may be omitted. Required derived modules fail when their declared facts
|
||||||
GeneratedText prompt package and embedded Markdown template.
|
are unavailable. Empty alert and outlook runs can still produce checked-empty
|
||||||
|
modules. SPC discussion is omitted unless a retained categorical outlook meets
|
||||||
|
the package's severity criterion and matching discussion text exists.
|
||||||
|
|
||||||
Today Report supports the Daily-style civil-day modules plus `today_planning`
|
Effective units, timezone, and location context arrive in `ModuleContext` from
|
||||||
and `hourly_forecast`; those outputs feed the Today GeneratedText prompt
|
configuration and resolved report metadata. Field defaults are owned by
|
||||||
package and embedded Markdown template.
|
[configuration](../config.md), and prompt-package layout is owned by
|
||||||
|
[prompt input](prompt-input.md).
|
||||||
|
|
||||||
`today_planning` is a Today-specific deterministic planning stanza with
|
## Verification and invariants
|
||||||
morning readiness, commute/school/workday concerns, outdoor planning, and
|
|
||||||
late-day change-watch fields. It is compatible with `report.Today` only.
|
|
||||||
|
|
||||||
`daily_planning` is a dated Daily deterministic planning stanza with morning
|
Focused tests cover source and derived values, registry validation, option
|
||||||
readiness, commute/school/workday concerns, and overnight change-watch fields.
|
handling, prompt exporters, support rules, and missing-data behavior:
|
||||||
It is compatible only with the `daily` report ID value. The default Daily
|
|
||||||
Report composition includes it.
|
|
||||||
|
|
||||||
Hourly Report supports source and valid-period modules that operate over its
|
```sh
|
||||||
rolling six-hour period: `metadata`, `current_conditions`, `hourly_forecast`,
|
go test ./internal/briefing
|
||||||
`precip_timing`, `alert_digest`, `spc_convective_outlooks`,
|
```
|
||||||
`area_forecast_discussion`, `spc_convective_discussion`, and `weather_story`.
|
|
||||||
It does not support daily/daypart-only modules such as
|
|
||||||
`derived_daily_summary`, `derived_daypart_summaries`, `outdoor_windows`,
|
|
||||||
`today_planning`, `tomorrow_planning`, or `daily_planning`.
|
|
||||||
|
|
||||||
Prompt-facing module values use local, human-readable date and time labels
|
Builders emit structured facts, never report prose. The app collects their
|
||||||
where the LLM is expected to reason about report content. Canonical timestamps
|
outputs into a module snapshot, and state persists that snapshot.
|
||||||
remain in report metadata, source provenance, and integration artifacts.
|
|
||||||
|
|
||||||
## Boundaries
|
|
||||||
|
|
||||||
- This package selects and shapes already-collected weather facts for prompts.
|
|
||||||
- It validates module composition against report compatibility and option
|
|
||||||
types.
|
|
||||||
- It does not collect weather data, compare prior snapshots, write module
|
|
||||||
snapshots, build YAML data packages, invoke Scriptorium, or write workflow
|
|
||||||
metadata.
|
|
||||||
|
|
||||||
## Config Fields Used
|
|
||||||
|
|
||||||
The app layer passes effective units, timezone, and location context into the
|
|
||||||
module context. `internal/facts` consumes daypart configuration before module
|
|
||||||
builders run. Configured `location` values are prompt context only; Weather API
|
|
||||||
`sourceLocationId` and `sourceLocation` remain source provenance.
|
|
||||||
|
|
||||||
`area_forecast_discussion` uses optional `sections` configuration to include a
|
|
||||||
subset of discussion fields. Hourly Report defaults this module to
|
|
||||||
`key_messages` and `short_term`; Daily Report defaults it to `long_term`.
|
|
||||||
|
|
||||||
`spc_convective_outlooks` uses collected SPC run metadata and derived
|
|
||||||
report-period outlooks. It emits `checked: true` for a successfully fetched
|
|
||||||
empty run, reports `outlook_count`, and includes prompt-facing outlook fields
|
|
||||||
such as risk label, `period_begins`, `period_ends`, image URL, and whether the
|
|
||||||
outlook contains the configured location. It also emits a curated `risk_digest`
|
|
||||||
for categorical outlooks that overlap the report period, contain the location,
|
|
||||||
and meet the configured-in-code minimum severity for report rendering. It does
|
|
||||||
not emit GeoJSON geometry, source URL, expiration time, or severity rank.
|
|
||||||
|
|
||||||
Prompt-facing module intervals use friendly local `period_begins` and
|
|
||||||
`period_ends` labels. Canonical report metadata, source provenance,
|
|
||||||
`issued_at`, `updated_at`, and point-in-time fields remain separate.
|
|
||||||
|
|
||||||
`spc_convective_discussion` uses the same derived report-period outlooks and
|
|
||||||
discussion records. It is omitted unless at least one retained categorical
|
|
||||||
outlook for the same SPC day has severity rank `3` or higher and matching
|
|
||||||
discussion text exists.
|
|
||||||
|
|
||||||
## External Adapters Used
|
|
||||||
|
|
||||||
None directly.
|
|
||||||
|
|
||||||
## State Or Manifest Behavior
|
|
||||||
|
|
||||||
None. `internal/app` collects module outputs into a `module.Snapshot`, and
|
|
||||||
`internal/state` persists that snapshot.
|
|
||||||
|
|
||||||
## Skip And Resume Behavior
|
|
||||||
|
|
||||||
None. Builders either emit a module output, omit optional unavailable data, or
|
|
||||||
return an error for invalid required inputs.
|
|
||||||
|
|
||||||
## Failure Behavior
|
|
||||||
|
|
||||||
- Required derived modules return errors when their dependent facts are not
|
|
||||||
available.
|
|
||||||
- Module registry construction rejects duplicate module IDs and duplicate
|
|
||||||
stanza names.
|
|
||||||
- Composition validation rejects unknown modules, duplicate modules,
|
|
||||||
incompatible report/module combinations, duplicate stanza names, and invalid
|
|
||||||
option shapes.
|
|
||||||
- Source-oriented module builders omit missing optional current conditions,
|
|
||||||
forecast discussion, and weather story stanzas.
|
|
||||||
- Alert digest output distinguishes checked empty alert data from missing alert
|
|
||||||
source data.
|
|
||||||
- SPC convective outlook output distinguishes checked empty outlook data from
|
|
||||||
missing outlook source data and omits GeoJSON geometry from prompt-facing
|
|
||||||
fields.
|
|
||||||
- SPC convective discussion output is omitted unless a retained outlook has
|
|
||||||
severity rank `3` or higher and matching discussion text is available.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Inspect:
|
|
||||||
|
|
||||||
- `internal/briefing/base_modules_test.go`
|
|
||||||
- `internal/briefing/derived_modules_test.go`
|
|
||||||
- `internal/briefing/modules_test.go`
|
|
||||||
- `internal/app/app_test.go`
|
|
||||||
|
|
||||||
## Invariants
|
|
||||||
|
|
||||||
- Module outputs contain structured weather facts and source context.
|
|
||||||
- Common metadata includes RunID, report ID, prompt ID, valid period, source
|
|
||||||
provenance, source hashes, source warnings, and configured prompt location.
|
|
||||||
- Prompt input packaging and Scriptorium execution remain outside this package.
|
|
||||||
|
|||||||
@@ -1,75 +1,61 @@
|
|||||||
# Changes Internals
|
# Changes Internals
|
||||||
|
|
||||||
This document describes structured Recent Changes comparison.
|
`internal/changes` deterministically compares a compatible prior module
|
||||||
|
snapshot with the current snapshot. It returns compact structured changes for
|
||||||
|
prompt input; it never reads state, finds a prior report, renders Markdown, or
|
||||||
|
compares generated text. Snapshot construction belongs to
|
||||||
|
[module internals](module.md), and prior-snapshot discovery belongs to
|
||||||
|
[state internals](state.md).
|
||||||
|
|
||||||
## Purpose
|
## Comparison inputs and output
|
||||||
|
|
||||||
`internal/changes` compares current and prior module snapshots and emits
|
Each comparator receives a prior snapshot, a current snapshot, and
|
||||||
compact change records for prompt input data packages.
|
`Thresholds`. A `Change` has a stable type and message plus previous and
|
||||||
|
current values where useful. Changes are sorted by type and then message, so
|
||||||
|
the same inputs always yield the same order.
|
||||||
|
|
||||||
## Inputs And Outputs
|
Threshold values are supplied by application orchestration from the
|
||||||
|
[Recent Changes configuration](../config.md#recent_change); this package does
|
||||||
|
not load configuration or choose defaults. Numeric changes are emitted when
|
||||||
|
the absolute difference meets the configured threshold. Precipitation also
|
||||||
|
requires a change between its low, possible, likely, and high categories.
|
||||||
|
|
||||||
Inputs:
|
## Strategies
|
||||||
|
|
||||||
- prior module snapshot
|
| Comparator | Required snapshot data | Compared values |
|
||||||
- current module snapshot
|
| --- | --- | --- |
|
||||||
- comparison thresholds from configuration
|
| `CompareDaily` | `derived_daily_summary`, `derived_daypart_summaries` | Low and high temperature, daily precipitation probability and timing, peak gust, alerts, and aggregate indicators |
|
||||||
|
| `CompareThreeDay` | `derived_daypart_summaries` | Per-day temperatures, precipitation probability and timing, peak gust, indicators, and added or removed outlook days |
|
||||||
|
| `CompareWeekend` | `derived_daypart_summaries` | The three-day values with weekend-prefixed change types |
|
||||||
|
|
||||||
Outputs:
|
For daily comparison, `alert_digest` and `precip_timing` are optional: alerts
|
||||||
|
are compared when present, and timing is compared only when both snapshots
|
||||||
|
contain it. The multi-day comparators build their day map from daypart
|
||||||
|
summaries. A missing or added day becomes a dedicated change rather than a
|
||||||
|
comparison against invented data.
|
||||||
|
|
||||||
- ordered `changes.Change` items with type, message, previous value, and current
|
The application selects a comparator only after state lookup establishes a
|
||||||
value where useful
|
compatible prior snapshot. Daily, Today, and Tomorrow use the daily comparator;
|
||||||
|
Three-day and Weekend use their named comparators. Other report types, such as
|
||||||
|
Storm, produce no Recent Changes list.
|
||||||
|
|
||||||
## Boundaries
|
## Missing data and failures
|
||||||
|
|
||||||
- This package compares structured module snapshot data only.
|
Required stanzas that are absent or cannot be decoded return an error with the
|
||||||
- It does not read filesystem state, find prior snapshots, render Markdown,
|
snapshot and stanza context. Optional stanzas may be absent. A snapshot with no
|
||||||
invoke Scriptorium, or compare generated report text.
|
eligible predecessor is not a comparison failure: the caller supplies an empty
|
||||||
|
change list without invoking this package.
|
||||||
|
|
||||||
## Config Fields Used
|
The package has no filesystem, transport, CLI, renderer, or persistence
|
||||||
|
behavior. It does not decide report compatibility or retain snapshots.
|
||||||
|
|
||||||
The app maps these fields into comparison thresholds:
|
## Verification and invariants
|
||||||
|
|
||||||
- `recent_change.temperature_degrees`
|
Focused tests cover the daily, three-day, and weekend strategies, threshold
|
||||||
- `recent_change.precip_probability_points`
|
boundaries, indicator and alert changes, and missing required stanzas:
|
||||||
- `recent_change.wind_gust_miles_per_hour`
|
|
||||||
- `recent_change.precip_timing_shift_minutes`
|
|
||||||
|
|
||||||
## External Adapters Used
|
```sh
|
||||||
|
go test ./internal/changes
|
||||||
|
```
|
||||||
|
|
||||||
None.
|
Recent Changes always compare structured snapshot values, never report prose.
|
||||||
|
|
||||||
## State Or Manifest Behavior
|
|
||||||
|
|
||||||
None directly. The app loads prior module snapshots through `internal/state`
|
|
||||||
before calling comparison functions.
|
|
||||||
|
|
||||||
## Skip And Resume Behavior
|
|
||||||
|
|
||||||
No resume behavior. When the app has no prior comparable snapshot, it sends an
|
|
||||||
empty Recent Changes list without calling a comparison function.
|
|
||||||
|
|
||||||
## Failure Behavior
|
|
||||||
|
|
||||||
- Daily comparison requires `derived_daily_summary` and
|
|
||||||
`derived_daypart_summaries` stanzas. It also uses `alert_digest` and
|
|
||||||
`precip_timing` when present.
|
|
||||||
- 3-Day comparison requires `derived_daypart_summaries`.
|
|
||||||
- Weekend comparison requires `derived_daypart_summaries`.
|
|
||||||
- Storm Report comparison returns no changes.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Inspect:
|
|
||||||
|
|
||||||
- `internal/changes/daily_test.go`
|
|
||||||
- `internal/changes/three_day_test.go`
|
|
||||||
- `internal/changes/weekend_test.go`
|
|
||||||
- `internal/app/app_test.go`
|
|
||||||
|
|
||||||
## Invariants
|
|
||||||
|
|
||||||
- Recent Changes are based on structured snapshots, not Markdown report text.
|
|
||||||
- Report compatibility is determined outside this package by report definitions
|
|
||||||
and state lookup.
|
|
||||||
- Output stays compact enough for prompt input.
|
|
||||||
|
|||||||
@@ -1,67 +1,65 @@
|
|||||||
# CLI Internals
|
# CLI Internals
|
||||||
|
|
||||||
This document describes command output ownership in `internal/cli`.
|
`internal/cli` turns process arguments into application requests and translates
|
||||||
|
application results into terminal output. The user-facing command, flag, and
|
||||||
|
output contract belongs in the [CLI reference](../cli.md).
|
||||||
|
|
||||||
## Purpose
|
## Responsibilities
|
||||||
|
|
||||||
`internal/cli` owns command parsing, app request construction, help text, and
|
`Runner.Run` dispatches the top-level action or inspection request. For actions,
|
||||||
presentation of command results. It converts app-layer results into stable CLI
|
the package parses command-specific and common flags, loads configuration with
|
||||||
summaries and writes stdout/stderr through shared output helpers.
|
CLI overrides, obtains the current time, and constructs either an
|
||||||
|
`app.GenerateRequest` or an `app.BatchRequest`. It delegates generation and
|
||||||
|
batch execution to `internal/app`.
|
||||||
|
|
||||||
## Command Categories
|
For inspection, it loads configuration, builds the appropriate app inspection
|
||||||
|
request, and writes the returned value. Inspection is read-only; the inspected
|
||||||
|
artifact types and user invocation remain owned by the [CLI reference](../cli.md)
|
||||||
|
and [operations guide](../operations.md).
|
||||||
|
|
||||||
- Action commands: `generate` and `run`. These perform work, write artifacts,
|
## Result Translation
|
||||||
and return compact summaries.
|
|
||||||
- Inspection commands: `inspect reports`, `inspect metadata`, `inspect
|
|
||||||
modules`, `inspect data-package`, `inspect prior`, and `inspect sources`.
|
|
||||||
These read existing artifacts and return requested data.
|
|
||||||
|
|
||||||
Future commands must declare which category they belong to before adding output
|
Action results become CLI-safe JSON summaries in `result.go`. Generate summaries
|
||||||
behavior.
|
carry report identity, status, relevant artifact paths, and notification
|
||||||
|
summary data. Batch summaries carry aggregate counts, per-report outcomes, and
|
||||||
|
the optional batch notification result. The translation deliberately excludes
|
||||||
|
full module snapshots, prompt packages, raw generated text, Scriptorium output,
|
||||||
|
and complete Distributor payloads.
|
||||||
|
|
||||||
## Stdout And Stderr
|
When an action returns both a result and an error, the CLI writes the failed
|
||||||
|
summary before returning that error. Parse, configuration-load, and other
|
||||||
|
failures that produce no application result return without a summary.
|
||||||
|
|
||||||
Action commands write JSON summaries to stdout by default. `run` also writes
|
`writeActionResult` writes action status information to stderr first, then JSON
|
||||||
compact status lines to stderr through `writeBatchStatus`. `generate` does not
|
to stdout. Batch execution supplies the status writer; single-report generation
|
||||||
write routine stderr today. Pre-run errors return without partial JSON.
|
does not emit routine stderr output. Quiet action requests suppress both normal
|
||||||
|
streams but still return errors. Inspection writes its JSON value to stdout and
|
||||||
|
does not accept quiet mode because stdout is the inspection result.
|
||||||
|
|
||||||
Inspection commands write requested JSON data to stdout with `writeJSON`. They
|
## Boundaries
|
||||||
do not use action output helpers and do not support quiet mode.
|
|
||||||
|
|
||||||
Returned errors are not hidden by output helpers. The caller remains
|
The package owns argument parsing, request adaptation, help text, and terminal
|
||||||
responsible for displaying command errors.
|
presentation. It does not implement report selection, collection, state
|
||||||
|
persistence, external transport, subprocess execution, or notification policy.
|
||||||
|
Those concerns remain in [application orchestration](app-orchestration.md) and
|
||||||
|
their focused owners.
|
||||||
|
|
||||||
## Quiet Mode
|
## Failure Behavior
|
||||||
|
|
||||||
`--quiet` is supported only by action commands. It suppresses successful stdout
|
- Invalid command names, flags, dates, and configuration fail before an app
|
||||||
and routine stderr by passing `outputOptions{Quiet: true}` to
|
request is executed.
|
||||||
`writeActionResult`. It does not suppress returned errors.
|
- Application errors retain their application context; output helpers do not
|
||||||
|
hide or replace them.
|
||||||
|
- JSON-encoding errors are returned directly.
|
||||||
|
- A failed batch summary causes the CLI to return an aggregate batch error even
|
||||||
|
when the detailed batch call has already returned its result.
|
||||||
|
|
||||||
Quiet mode is intentionally not accepted by inspection commands because
|
## Tests And Invariants
|
||||||
inspection stdout is the command result.
|
|
||||||
|
|
||||||
## Summary Ownership
|
Focused tests are in `internal/cli/root_test.go`, `internal/cli/output_test.go`,
|
||||||
|
and `internal/cli/result_test.go`.
|
||||||
|
|
||||||
CLI-safe summary structs live in `internal/cli/result.go`.
|
- CLI summaries are stable, bounded views of app results.
|
||||||
|
- Routine batch status lines precede the batch JSON summary.
|
||||||
- `newGenerateSummary` converts `*app.ReportResult` plus an optional error into
|
- A quiet action produces no successful or failure summary output.
|
||||||
the generate JSON contract.
|
- Inspection never invokes action-output helpers.
|
||||||
- `newBatchSummary` converts `*app.BatchResult` into the run JSON contract and
|
|
||||||
derives the top-level run status.
|
|
||||||
|
|
||||||
Summary types must not expose full app internals, module contents, data package
|
|
||||||
contents, raw generated text, Scriptorium result bodies, or full distributor
|
|
||||||
payloads.
|
|
||||||
|
|
||||||
## Helper Path
|
|
||||||
|
|
||||||
New action commands should:
|
|
||||||
|
|
||||||
1. parse command-specific flags into CLI option structs;
|
|
||||||
2. call the app-layer use case;
|
|
||||||
3. convert app results into a CLI summary type;
|
|
||||||
4. write through `writeActionResult`;
|
|
||||||
5. use a status writer only for routine stderr status lines.
|
|
||||||
|
|
||||||
New inspection commands should call the app inspection use case and write the
|
|
||||||
returned data through `writeJSON`.
|
|
||||||
|
|||||||
@@ -1,58 +1,43 @@
|
|||||||
# Collection Internals
|
# Collection Internals
|
||||||
|
|
||||||
This document describes the app-facing upstream collection boundary in
|
`internal/collect` is the application-facing boundary for collecting the
|
||||||
`internal/collect`.
|
normalized Weather API bundle. The external HTTP contract belongs in the
|
||||||
|
[Weather API integration guide](../integrations/weatherapi.md); normalized data
|
||||||
## Purpose
|
semantics belong in [weather-data internals](weather-data.md).
|
||||||
|
|
||||||
`internal/collect` is the canonical package used by app workflows to collect
|
|
||||||
upstream Weather API data. It constructs the Weather API adapter, fetches a
|
|
||||||
normalized bundle, and returns that bundle without applying report selection or
|
|
||||||
batch policy.
|
|
||||||
|
|
||||||
## Contract
|
## Contract
|
||||||
|
|
||||||
Inputs:
|
`Run` accepts a `context.Context` and a `Request` containing effective
|
||||||
|
`config.Config`. It constructs the Weather API adapter from that configuration,
|
||||||
|
calls `FetchBundle`, and returns `Result{Bundle: *weatherdata.Bundle}`.
|
||||||
|
|
||||||
- `collect.Request`, containing the effective `config.Config`
|
The package wraps adapter construction failures as weather-collection setup
|
||||||
- `context.Context` for cancellation
|
errors and fetch failures as bundle-collection errors. It does not retry,
|
||||||
|
persist, select reports, derive facts, build modules, invoke Scriptorium, or
|
||||||
|
notify Distributor.
|
||||||
|
|
||||||
Output:
|
## Application Composition
|
||||||
|
|
||||||
- `collect.Result`, containing `*weatherdata.Bundle`
|
`internal/app` owns the narrow `Collector` interface used by workflow tests;
|
||||||
|
the production implementation delegates to `collect.Run`. Generation, batch
|
||||||
|
execution, and explicit bundle fetching all use this boundary. Application
|
||||||
|
orchestration rejects a nil collector result or a nil bundle before report work
|
||||||
|
can continue.
|
||||||
|
|
||||||
`Run` returns an actionable error when Weather API adapter construction or
|
Single-report generation and a batch each collect once. A batch passes the same
|
||||||
bundle fetch fails. The package does not derive `facts.CollectedFacts`, build
|
normalized collection to planning and to every report it generates. Collection
|
||||||
modules, resolve report periods, select reports, write state, invoke
|
failure prevents later workflow work for that request.
|
||||||
Scriptorium, or notify distributor.
|
|
||||||
|
|
||||||
## App Usage
|
## Boundaries And Invariants
|
||||||
|
|
||||||
`internal/app` owns a narrow `Collector` interface for orchestration tests. The
|
Collection owns adapter creation and retrieval of one normalized bundle. It
|
||||||
default implementation calls `collect.Run`.
|
must not make report, period, batch, prompt, module, filesystem, or notification
|
||||||
|
decisions.
|
||||||
|
|
||||||
Single-report generation collects once, resolves the requested report, and
|
- App-facing Weather API collection always passes through this package.
|
||||||
passes the explicit collection into report generation. Batch generation
|
- The returned value is normalized source data, not facts or prompt input.
|
||||||
collects once before planning and passes the same collection into each planned
|
- Context cancellation is passed to the Weather API adapter.
|
||||||
report. If collection returns no bundle, app orchestration returns an error
|
- Errors retain whether setup or fetching failed.
|
||||||
before report generation.
|
|
||||||
|
|
||||||
## Boundaries
|
Focused tests are in `internal/collect/collect_test.go`; orchestration use is
|
||||||
|
also covered by `internal/app/app_test.go`.
|
||||||
Weather API HTTP details stay in `internal/adapters/weatherapi`. The collection
|
|
||||||
package returns normalized `weatherdata` only. It must not know about report
|
|
||||||
IDs, prompt IDs, batch names, Daily eligibility, module composition, Recent
|
|
||||||
Changes, state paths, or Scriptorium arguments.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Inspect:
|
|
||||||
|
|
||||||
- `internal/collect/collect_test.go`
|
|
||||||
- `internal/app/app_test.go`
|
|
||||||
|
|
||||||
## Invariants
|
|
||||||
|
|
||||||
- App-facing Weather API collection goes through `internal/collect`.
|
|
||||||
- Collection returns normalized source data, not report facts or prompt input.
|
|
||||||
- Report and batch policy belongs outside `internal/collect`.
|
|
||||||
|
|||||||
@@ -1,134 +1,63 @@
|
|||||||
# Distributor Adapter Internals
|
# Distributor Adapter Internals
|
||||||
|
|
||||||
This document describes the distributor upload adapter in
|
`internal/adapters/distributor` translates a local delivery request into the
|
||||||
`internal/adapters/distributor`.
|
Distributor Go client's upload and status calls, then returns a local delivery
|
||||||
|
result. The external API, authentication, and idempotency contract is owned by
|
||||||
|
the [Distributor API guide](../integrations/distributor/api.md) and
|
||||||
|
[Distributor bundle guide](../integrations/distributor/pkg-bundle.md).
|
||||||
|
|
||||||
## Purpose
|
## Client construction
|
||||||
|
|
||||||
The adapter submits generated weatherreporter Markdown reports to a configured
|
`Client` holds the endpoint, the name of the environment variable containing
|
||||||
distributor HTTP upload endpoint. It supports one or more file mappings per
|
the token, an optional timeout, and an injectable upstream-client factory.
|
||||||
upload request. It isolates distributor package types, token-env lookup, upload
|
`New` validates its configuration before creating the adapter. For each upload,
|
||||||
client construction, source-bundle file mapping, timeout handling, status
|
the adapter reads the token from the configured environment variable and builds
|
||||||
polling, and upload error wrapping from app orchestration.
|
the upstream client with that endpoint, token, and an HTTP client whose timeout
|
||||||
|
matches the local positive timeout.
|
||||||
|
|
||||||
## Inputs And Outputs
|
The upstream client is an implementation dependency, not a source of
|
||||||
|
application configuration: retry ownership, pipeline selection, path
|
||||||
|
templates, and report rendering are defined by
|
||||||
|
[configuration](../config.md) and [application orchestration](app-orchestration.md).
|
||||||
|
|
||||||
Inputs:
|
## Upload translation
|
||||||
|
|
||||||
- distributor endpoint URL
|
Before calling the dependency, `Upload` validates the endpoint and token
|
||||||
- token environment variable name
|
configuration plus the local pipeline ID, bundle ID, idempotency key, and every
|
||||||
- upload timeout
|
file's source and bundle paths. It maps the request as follows:
|
||||||
- pipeline ID
|
|
||||||
- bundle ID
|
|
||||||
- idempotency key
|
|
||||||
- source Markdown report paths and bundle-relative path mappings
|
|
||||||
- bundle created timestamp
|
|
||||||
- context for cancellation
|
|
||||||
|
|
||||||
Outputs:
|
| Local request | Distributor client value |
|
||||||
|
| --- | --- |
|
||||||
|
| Pipeline ID | Upload pipeline identifier |
|
||||||
|
| Bundle ID | Bundle identifier |
|
||||||
|
| Idempotency key | Upload idempotency key |
|
||||||
|
| File source and bundle paths | Bundle file entries |
|
||||||
|
| Creation timestamp | Bundle creation time |
|
||||||
|
|
||||||
- accepted distributor run ID
|
The call inherits the caller's context and applies the configured positive
|
||||||
- accepted distributor upload status
|
timeout. The adapter does not read report files, construct bundle layouts, or
|
||||||
- distributor run status, status polling error, and raw run report JSON when available
|
persist notification artifacts.
|
||||||
- weatherreporter-owned idempotency conflict error when applicable
|
|
||||||
|
|
||||||
## Boundaries
|
## Status and errors
|
||||||
|
|
||||||
`internal/adapters/distributor` is the only weatherreporter package that imports
|
An accepted upload is followed by one status request. When a timeout is
|
||||||
`gitea.maximumdirect.net/eric/distributor/pkg/upload` or
|
configured, a nonterminal result is polled until `succeeded` or `failed`, or
|
||||||
`gitea.maximumdirect.net/eric/distributor/pkg/bundle`.
|
until the context ends. The translated `UploadResult` contains the run ID,
|
||||||
|
status, and `RunStatus`, including pipeline ID, lifecycle timestamps, report,
|
||||||
|
and remote error details.
|
||||||
|
|
||||||
The app layer passes weatherreporter-owned request values to the adapter. The
|
Status lookup or polling errors are preserved in `UploadResult.StatusError` so
|
||||||
adapter does not choose report types, render templates, select output copies,
|
the caller can record an accepted-but-unconfirmed delivery. A terminal failed
|
||||||
decide whether an upload represents one report or a batch, configure
|
run returns that result and an error. Upload failures return no result. Upstream
|
||||||
destinations, wait for downstream publication, transform Markdown, or persist
|
idempotency conflicts become the local `IdempotencyConflictError`, which adds
|
||||||
notification state.
|
endpoint, pipeline, bundle, idempotency, and file-path context while redacting
|
||||||
|
the token.
|
||||||
|
|
||||||
Full upstream distributor package and HTTP contract details stay under
|
## Verification
|
||||||
`docs/integrations/distributor/`.
|
|
||||||
|
|
||||||
## Config Fields Used
|
Focused tests cover configuration validation, request mapping, timeouts and
|
||||||
|
polling, status translation, conflict handling, and token redaction:
|
||||||
|
|
||||||
The adapter is built from `notify.distributor` config:
|
```sh
|
||||||
|
go test ./internal/adapters/distributor
|
||||||
- `endpoint`
|
```
|
||||||
- `token_env`
|
|
||||||
- `timeout`
|
|
||||||
|
|
||||||
The app layer renders single-report pipeline ID, bundle ID, idempotency key,
|
|
||||||
and bundle paths from:
|
|
||||||
|
|
||||||
- `pipeline_id_template`
|
|
||||||
- `bundle_id_template`
|
|
||||||
- `idempotency_key_template`
|
|
||||||
- report-specific path templates
|
|
||||||
|
|
||||||
For batch uploads, the app layer renders pipeline ID, bundle ID, and
|
|
||||||
idempotency key from `notify.distributor.batch.*`, resolves report-specific
|
|
||||||
path templates once per included report, and passes the resulting multi-file
|
|
||||||
request to this adapter.
|
|
||||||
|
|
||||||
Report-specific path resolution happens entirely in the app layer. Explicit
|
|
||||||
`reports.<report>.distributor.path_templates` overrides take precedence over
|
|
||||||
report definition defaults.
|
|
||||||
|
|
||||||
The token value is read from the environment variable named by `token_env`
|
|
||||||
after config loading and `secrets.directory` processing.
|
|
||||||
|
|
||||||
## Upload Behavior
|
|
||||||
|
|
||||||
The adapter calls distributor `UploadFiles` with one or more file mappings:
|
|
||||||
|
|
||||||
- pipeline ID: the rendered distributor workflow selector
|
|
||||||
- source paths: managed Markdown report paths selected by app orchestration
|
|
||||||
- bundle paths: rendered bundle-relative report paths for each source
|
|
||||||
- created: the report or batch generation timestamp
|
|
||||||
|
|
||||||
The adapter creates a distributor upload client with the configured endpoint,
|
|
||||||
bearer token, and timeout-backed HTTP client. It also wraps the upload context
|
|
||||||
with the configured timeout when the timeout is greater than zero.
|
|
||||||
|
|
||||||
After upload acceptance, the adapter polls distributor `Status` for the accepted
|
|
||||||
run ID until the run reaches `succeeded` or `failed`, or until the configured
|
|
||||||
timeout expires. It returns the latest status, error text, and raw report JSON in
|
|
||||||
weatherreporter-owned types so app orchestration can persist them in the
|
|
||||||
notification debug artifact. Status lookup failures or timeout before a terminal
|
|
||||||
state are kept as debug status errors on an otherwise accepted upload. A
|
|
||||||
terminal distributor run status of `failed` is returned as a notification failure
|
|
||||||
with the status report preserved.
|
|
||||||
|
|
||||||
## Failure Behavior
|
|
||||||
|
|
||||||
The adapter validates required endpoint, token env name, token value, pipeline
|
|
||||||
ID, bundle ID, idempotency key, upload files, source paths, bundle paths, and
|
|
||||||
upload client inputs before uploading.
|
|
||||||
|
|
||||||
Upload failures include endpoint, pipeline ID, bundle ID, idempotency key,
|
|
||||||
source paths, and bundle paths context. Token values are redacted from adapter
|
|
||||||
errors.
|
|
||||||
|
|
||||||
Distributor idempotency conflicts are exposed as a weatherreporter-owned
|
|
||||||
`IdempotencyConflictError`, so callers do not depend on upstream distributor
|
|
||||||
types.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Inspect:
|
|
||||||
|
|
||||||
- `internal/adapters/distributor/client_test.go`
|
|
||||||
- `internal/app/app_test.go`
|
|
||||||
- `internal/cli/root_test.go`
|
|
||||||
|
|
||||||
Adapter tests use a fake upload client factory and do not require a live
|
|
||||||
distributor service.
|
|
||||||
|
|
||||||
## Invariants
|
|
||||||
|
|
||||||
- Distributor package types do not leak outside the adapter.
|
|
||||||
- Only managed Markdown report paths selected by app orchestration are
|
|
||||||
uploaded.
|
|
||||||
- The adapter never scans the workspace.
|
|
||||||
- Token values are not included in errors, CLI output, metadata, docs, or
|
|
||||||
examples.
|
|
||||||
- Destination routing and Markdown-to-HTML transformation belong to
|
|
||||||
distributor, not weatherreporter.
|
|
||||||
|
|||||||
@@ -1,94 +1,67 @@
|
|||||||
# Fact Contracts Internals
|
# Fact Contracts Internals
|
||||||
|
|
||||||
This document describes the fact contract boundary.
|
`internal/facts` is the deterministic boundary between a collected weather
|
||||||
|
bundle and report-scoped facts. It preserves normalized source values and then
|
||||||
|
selects and summarizes the values needed for one resolved report. Provider
|
||||||
|
transport and normalized bundle semantics belong to
|
||||||
|
[weather-data internals](weather-data.md); report identity and valid-period
|
||||||
|
selection belong to [report registry internals](report-registry.md).
|
||||||
|
|
||||||
## Purpose
|
## Collected facts
|
||||||
|
|
||||||
`internal/facts` separates normalized upstream facts collected for a report run
|
`BuildCollected` projects a `weatherdata.Bundle` into `CollectedFacts`. It
|
||||||
from conservative report-scoped facts derived from them. The package gives app
|
retains the fetched timestamp and every normalized product: observations,
|
||||||
orchestration one place to build reusable facts before module execution.
|
current conditions, hourly, narrative, alerts, discussion, daily, weather
|
||||||
|
story, and convective outlook data. Source provenance and warnings are copied
|
||||||
|
into their own slices so downstream consumers can inspect data completeness
|
||||||
|
without treating it as an ordinary weather fact.
|
||||||
|
|
||||||
## Inputs And Outputs
|
A nil bundle produces an empty collected value. Collection itself, missing
|
||||||
|
source policy, and source hashes are outside this package.
|
||||||
|
|
||||||
Inputs:
|
## Report-scoped derivation
|
||||||
|
|
||||||
- `weatherdata.Bundle` from the Weather API adapter
|
`BuildDerived` requires a valid resolved period and a valid report timezone. It
|
||||||
- resolved report definition and valid period
|
uses half-open period overlap to select hourly, narrative, daily, and alert
|
||||||
- report timezone
|
data; it also derives precipitation timing. Convective outlooks are retained
|
||||||
- configured daypart definitions
|
only when their valid interval overlaps the report period, with discussions
|
||||||
|
kept for represented outlook days. Both collections are sorted deterministically.
|
||||||
|
|
||||||
Outputs:
|
Report identity controls the summary shape:
|
||||||
|
|
||||||
- `facts.CollectedFacts` with normalized source facts plus separate source
|
| Report family | Derived summary |
|
||||||
provenance and warnings. SPC convective outlook source data is carried
|
| --- | --- |
|
||||||
through when present in the bundle, including upstream geometry and source
|
| Hourly | Rolling-period selections and precipitation timing; no daily or daypart summary |
|
||||||
provenance.
|
| Daily, Today, Tomorrow | One local civil-day summary and its dayparts |
|
||||||
- `facts.DerivedFacts` with valid-period forecast slices, alert overlaps,
|
| Three-day, Weekend | One clipped daily summary for each overlapping local day |
|
||||||
report-period SPC convective outlooks and discussions, daily summaries,
|
| Storm | One summary for the explicit report window |
|
||||||
daypart summaries, and Storm Report window summary
|
|
||||||
|
|
||||||
Hourly Report uses the generic valid-period hourly and narrative selection
|
`DaypartSummaries` is collected from the resulting daily or storm summaries.
|
||||||
for its rolling six-hour window. Its derived facts include precipitation timing
|
The detailed grouping, daypart-window, and alert rules are owned by
|
||||||
from the selected hourly periods, alert overlaps for the six-hour period, and
|
[forecast derivation](forecast-derivation.md).
|
||||||
SPC outlooks/discussions overlapping that period. It does not build daily
|
|
||||||
summaries, daypart summaries, or a storm-window summary.
|
|
||||||
|
|
||||||
## Boundaries
|
## Missing data and failures
|
||||||
|
|
||||||
- This package owns fact assembly and reusable deterministic derivation for a
|
Optional normalized products remain nil or yield empty selections; the package
|
||||||
report run.
|
does not create substitute values. A present convective-outlook run with no
|
||||||
- SPC convective outlook derivation selects already-collected outlooks whose
|
matching outlooks produces non-nil empty outlook and discussion slices, while
|
||||||
half-open valid intervals overlap the resolved report period and retains
|
a missing run produces nil slices.
|
||||||
discussions for represented outlook days.
|
|
||||||
- Derived SPC outlook records preserve the collected outlook fields, including
|
|
||||||
geometry, for downstream components that need source-level facts. Prompt
|
|
||||||
modules decide which fields are exposed to Scriptorium.
|
|
||||||
- It does not fetch upstream data, build prompt wording, compare prior
|
|
||||||
snapshots, write workflow state, invoke Scriptorium, or define modules.
|
|
||||||
|
|
||||||
## Config Fields Used
|
Derivation fails for an invalid report period, invalid timezone, unsupported
|
||||||
|
report ID, or when a requested daily summary has no hourly forecast data.
|
||||||
|
Invalid daypart definitions surface from forecast derivation. The package does
|
||||||
|
not access the CLI, filesystem, subprocesses, or network.
|
||||||
|
|
||||||
- `dayparts[].name`
|
## Verification and invariants
|
||||||
- `dayparts[].start`
|
|
||||||
- `dayparts[].end`
|
|
||||||
- `weather_api.timezone`
|
|
||||||
|
|
||||||
## External Adapters Used
|
Focused tests cover collected-fact separation, report-period selection,
|
||||||
|
hourly and storm behavior, daily and partial-day summaries, and convective
|
||||||
|
outlook selection:
|
||||||
|
|
||||||
None directly. Collected facts are built from `weatherdata.Bundle`.
|
```sh
|
||||||
|
go test ./internal/facts
|
||||||
|
```
|
||||||
|
|
||||||
## State Or Manifest Behavior
|
Facts are derived once for a resolved report from already collected data.
|
||||||
|
They remain reusable structured values: prompt wording, state persistence,
|
||||||
None. Source provenance and warnings remain data fields for downstream metadata
|
prior-report comparison, and template presentation are owned elsewhere.
|
||||||
and inspection.
|
|
||||||
|
|
||||||
## Failure Behavior
|
|
||||||
|
|
||||||
- Invalid or missing report valid periods return an error.
|
|
||||||
- Invalid timezone names return an error.
|
|
||||||
- Missing required hourly forecast data returns the underlying forecast
|
|
||||||
derivation error for reports that require daily summaries.
|
|
||||||
- Hourly Report can derive its default module facts without daily or
|
|
||||||
daypart summaries.
|
|
||||||
- Missing optional narrative, alert, discussion, daily, or weather story data
|
|
||||||
produces empty or nil derived fields.
|
|
||||||
- Missing optional SPC convective outlook data produces a nil collected field.
|
|
||||||
- A present SPC convective outlook source with no report-period matches
|
|
||||||
produces non-nil empty derived outlook and discussion slices.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Inspect:
|
|
||||||
|
|
||||||
- `internal/facts/facts_test.go`
|
|
||||||
- `internal/app/app_test.go`
|
|
||||||
|
|
||||||
## Invariants
|
|
||||||
|
|
||||||
- Collected facts are built once from a fetched bundle.
|
|
||||||
- Derived facts are scoped to one resolved report.
|
|
||||||
- SPC convective outlook selection uses the resolved report period and the
|
|
||||||
already-collected outlook run.
|
|
||||||
- Source provenance and warnings stay separate from ordinary fact fields.
|
|
||||||
- Prompt-specific wording and one-off presentation decisions stay outside this
|
|
||||||
package.
|
|
||||||
|
|||||||
@@ -1,76 +1,63 @@
|
|||||||
# Forecast Derivation Internals
|
# Forecast Derivation Internals
|
||||||
|
|
||||||
This document describes deterministic forecast summarization in
|
`internal/forecast` deterministically selects and summarizes normalized
|
||||||
`internal/forecast`.
|
forecast data. It has no transport, filesystem, CLI, subprocess, or report
|
||||||
|
registry dependency. Its summaries are consumed by
|
||||||
|
[fact contracts](facts.md) and later module builders.
|
||||||
|
|
||||||
## Purpose
|
## Period and daypart semantics
|
||||||
|
|
||||||
`internal/forecast` converts normalized weather data into daily and period
|
Selections use `timeutil.Period` half-open overlap: a value is selected only
|
||||||
summaries used by fact builders and module builders.
|
when both intervals share time. `BuildDailySummary` creates one local civil
|
||||||
|
day; `BuildPeriodDailySummaries` intersects every local civil day with the
|
||||||
|
requested period, preserving partial first and last days.
|
||||||
|
|
||||||
## Inputs And Outputs
|
`ResolveDayparts` converts each configured name, start clock, and end clock
|
||||||
|
into a local window. An end clock at or before its start clock wraps into the
|
||||||
|
next civil day. The daypart and timezone defaults are defined in the
|
||||||
|
[configuration reference](../config.md), not here.
|
||||||
|
|
||||||
Inputs:
|
## Deterministic summaries
|
||||||
|
|
||||||
- `weatherdata.Bundle`
|
`BuildDailySummary` requires an hourly run with at least one period. It adds
|
||||||
- local date or resolved report period
|
the selected narrative periods, discussion, alert overlaps, source provenance,
|
||||||
- timezone
|
source warnings, and one `DaypartSummary` per resolved window. A daypart keeps
|
||||||
- configured daypart definitions
|
its selected hourly periods and derives temperature and apparent-temperature
|
||||||
|
ranges, timed precipitation and wind maxima, dominant and notable conditions,
|
||||||
|
and weather indicators.
|
||||||
|
|
||||||
Outputs:
|
Indicators are deterministic checks over normalized values and condition text:
|
||||||
|
heat, cold, and wind use package-owned numeric cutoffs; snow, ice, fog, and
|
||||||
|
wind text are detected from the forecast description. `BuildPrecipTiming`
|
||||||
|
sorts periods, records the maximum and first precipitation, groups contiguous
|
||||||
|
periods at or above its package-owned probability threshold, and records
|
||||||
|
thunder mentions.
|
||||||
|
|
||||||
- `forecast.DailySummary` for one local civil day
|
Alert overlap parsing supports the normalized alert payload's available timing
|
||||||
- one clipped daily summary per local day or partial day from
|
fields. Unparseable alerts and invalid intervals are ignored; valid overlaps
|
||||||
`BuildPeriodDailySummaries`
|
are clipped to the requested period and ordered by alert start time.
|
||||||
- daypart summaries with selected hourly periods, ranges, timed maximums,
|
|
||||||
conditions, indicators, and alert overlaps
|
|
||||||
|
|
||||||
## Boundaries
|
## Missing data and failures
|
||||||
|
|
||||||
- This package groups, selects, and summarizes already-normalized forecast
|
Empty selections yield empty summary fields rather than generated prose.
|
||||||
data.
|
Direct daily or period-summary calls fail when their required bundle, valid
|
||||||
- It does not perform HTTP calls, parse CLI flags, resolve report definitions,
|
period, hourly data, or daypart definitions are invalid. A nil location uses
|
||||||
compare prior snapshots, build prompt input packages, or invoke Scriptorium.
|
UTC when these APIs are called directly. Optional narrative, discussion, and
|
||||||
|
alerts remain absent when their normalized products are absent.
|
||||||
|
|
||||||
## Config Fields Used
|
Forecast thresholds used for brief indicators and precipitation timing are
|
||||||
|
implementation rules. User-configurable Recent Changes thresholds are applied
|
||||||
|
by [changes internals](changes.md), whose defaults are documented in
|
||||||
|
[configuration](../config.md).
|
||||||
|
|
||||||
- `dayparts[].name`
|
## Verification and invariants
|
||||||
- `dayparts[].start`
|
|
||||||
- `dayparts[].end`
|
|
||||||
|
|
||||||
Threshold constants for basic indicators live in forecast code rather than
|
Focused tests cover local civil days, clipped periods, daypart resolution,
|
||||||
configuration.
|
summary metrics, precipitation windows, threshold helpers, and alert overlap:
|
||||||
|
|
||||||
## External Adapters Used
|
```sh
|
||||||
|
go test ./internal/forecast ./internal/timeutil
|
||||||
|
```
|
||||||
|
|
||||||
None directly. Forecast data arrives through `weatherdata.Bundle`.
|
The package preserves normalized inputs as inspectable structured values and
|
||||||
|
never decides report identity, delivery, or presentation wording.
|
||||||
## State Or Manifest Behavior
|
|
||||||
|
|
||||||
None. Source warnings and provenance from the bundle are carried into summaries
|
|
||||||
for later metadata and module output.
|
|
||||||
|
|
||||||
## Skip And Resume Behavior
|
|
||||||
|
|
||||||
None. Missing optional source context can produce empty selections, but missing
|
|
||||||
required hourly data fails summarization.
|
|
||||||
|
|
||||||
## Failure Behavior
|
|
||||||
|
|
||||||
- A nil bundle or missing hourly forecast data returns an error.
|
|
||||||
- Invalid daypart definitions return parse errors with context.
|
|
||||||
- Alert records without parseable RFC3339 timing are skipped.
|
|
||||||
- Empty selected periods produce empty summaries rather than generated prose.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Inspect:
|
|
||||||
|
|
||||||
- `internal/forecast/derive_test.go`
|
|
||||||
- `internal/timeutil/periods_test.go`
|
|
||||||
|
|
||||||
## Invariants
|
|
||||||
|
|
||||||
- Go owns report-period selection and meteorological summarization.
|
|
||||||
- Weather facts come from normalized source data.
|
|
||||||
- Outputs remain JSON-inspectable and independent of CLI, state, and adapters.
|
|
||||||
|
|||||||
@@ -1,149 +1,51 @@
|
|||||||
# Generated Text Internals
|
# Generated Text Internals
|
||||||
|
|
||||||
This document describes structured generated-text handling in
|
`internal/generatedtext` validates the structured prose produced for generated-
|
||||||
`internal/generatedtext`.
|
text reports and turns validated prose plus rich module values into typed render
|
||||||
|
contexts. It owns the catalog that pairs a generated-text report definition
|
||||||
|
with its validator, schema ID, template ID, and context builder. The complete
|
||||||
|
maintainer-facing context fields belong to [report templates](../templates.md).
|
||||||
|
|
||||||
## Purpose
|
## Catalog and validation
|
||||||
|
|
||||||
`internal/generatedtext` validates structured text returned for
|
Only the Daily, Today, Tomorrow, and Hourly report definitions use the
|
||||||
generated-text-template reports and builds curated render contexts for
|
generated-text-template mode. `LookupDefinition` rejects a direct-Markdown
|
||||||
templates. It also owns the generated-text catalog that connects report
|
definition, unknown schema or template IDs, and unsupported schema/template
|
||||||
definitions to validators, render-context builders, schema assets, and template
|
pairs before the run begins. A handler validates raw JSON, returns a typed
|
||||||
assets.
|
value and canonical normalized JSON, loads its schema, builds a render context,
|
||||||
|
and renders through `internal/reporttemplate`.
|
||||||
|
|
||||||
## Inputs And Outputs
|
Daily, Today, and Tomorrow use a day-style value with required trimmed summary
|
||||||
|
and one or more nonblank discussion paragraphs. Hourly requires trimmed summary
|
||||||
|
and a single trimmed discussion string. Each form permits optional trimmed
|
||||||
|
precipitation-timing and confidence prose. Typed decoding rejects unknown JSON
|
||||||
|
fields; no general-purpose JSON Schema engine is used at runtime.
|
||||||
|
|
||||||
Inputs:
|
## Render contexts
|
||||||
|
|
||||||
- raw GeneratedText JSON for Daily, Today, Tomorrow Report, or Hourly Report
|
The catalog's report-specific builders receive briefing metadata, a rich module
|
||||||
- report metadata from `internal/briefing`
|
snapshot, collected facts, derived facts, and the matching validated generated
|
||||||
- a module snapshot from `internal/module`
|
text. They decode the module stanzas needed by the template and build typed
|
||||||
- validated generated text
|
Daily, Today, Tomorrow, or Hourly contexts. Context construction validates
|
||||||
|
metadata and periods, preserves rich module values, and uses ordered slices for
|
||||||
|
template iteration rather than maps.
|
||||||
|
|
||||||
Outputs:
|
Optional source stanzas become nil or fallback context fields. Missing required
|
||||||
|
stanzas, type-decoding failures, invalid metadata, or a generated-text type
|
||||||
|
that does not match the chosen handler fail before template execution. Prompt
|
||||||
|
packages, raw Scriptorium output, state persistence, and template asset lookup
|
||||||
|
remain outside this package.
|
||||||
|
|
||||||
- typed `Daily` generated text
|
## Verification and invariants
|
||||||
- typed `Today` generated text
|
|
||||||
- typed `Tomorrow` generated text
|
|
||||||
- typed `Hourly` generated text
|
|
||||||
- normalized stable JSON for validated generated text
|
|
||||||
- typed `DailyRenderContext` values for `internal/reporttemplate`
|
|
||||||
- typed `TodayRenderContext` values for `internal/reporttemplate`
|
|
||||||
- typed `TomorrowRenderContext` values for `internal/reporttemplate`
|
|
||||||
- typed `HourlyRenderContext` values for `internal/reporttemplate`
|
|
||||||
- generated-text catalog handlers for report definitions that use
|
|
||||||
`generated_text_template`
|
|
||||||
|
|
||||||
## JSON Contracts
|
Focused tests cover the catalog, each report-specific validator, normalization,
|
||||||
|
schema/template mismatches, context construction, optional modules, and typed
|
||||||
|
stanza errors:
|
||||||
|
|
||||||
Daily, Today, and Tomorrow use the same day-style generated-text JSON shape:
|
```sh
|
||||||
|
go test ./internal/generatedtext
|
||||||
```json
|
|
||||||
{
|
|
||||||
"summary": "string",
|
|
||||||
"forecast_discussion": ["string"],
|
|
||||||
"precipitation_timing": "string",
|
|
||||||
"confidence": "string"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The day-style contract requires `summary` after trimming whitespace.
|
Generated text supplies prose slots only; deterministic weather facts remain in
|
||||||
`forecast_discussion` must contain at least one nonblank paragraph after
|
module and fact values. Every generated-text definition must resolve to exactly
|
||||||
trimming blank items. `precipitation_timing` and `confidence` are optional and
|
one supported catalog pair.
|
||||||
omitted from normalized JSON when blank. Unknown fields are rejected.
|
|
||||||
|
|
||||||
The report-specific Go API is:
|
|
||||||
|
|
||||||
| Report | Type | Validator | Schema ID | Template ID | Prompt ID |
|
|
||||||
| --- | --- | --- | --- | --- | --- |
|
|
||||||
| Daily Report | `Daily` | `ValidateDaily` | `daily` | `daily` | `weather.daily_generated_text` |
|
|
||||||
| Today Report | `Today` | `ValidateToday` | `today` | `today` | `weather.today_generated_text` |
|
|
||||||
| Tomorrow Report | `Tomorrow` | `ValidateTomorrow` | `tomorrow` | `tomorrow` | `weather.tomorrow_generated_text` |
|
|
||||||
|
|
||||||
Hourly generated text uses the same top-level field names, but
|
|
||||||
`forecast_discussion` is a single string:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"summary": "string",
|
|
||||||
"forecast_discussion": "string",
|
|
||||||
"precipitation_timing": "string",
|
|
||||||
"confidence": "string"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Hourly `summary` and `forecast_discussion` are required after trimming
|
|
||||||
whitespace. `precipitation_timing` and `confidence` are optional and omitted
|
|
||||||
from normalized JSON when blank. Unknown fields are rejected. The Hourly catalog
|
|
||||||
entry uses type `Hourly`, validator `ValidateHourly`, schema ID `hourly`,
|
|
||||||
template ID `hourly`, and prompt ID `weather.hourly_generated_text`.
|
|
||||||
|
|
||||||
## Render Contexts
|
|
||||||
|
|
||||||
Daily, Today, Tomorrow, and Hourly render contexts all include:
|
|
||||||
|
|
||||||
- display metadata derived from report metadata;
|
|
||||||
- validated generated text;
|
|
||||||
- typed module outputs decoded from the module snapshot;
|
|
||||||
- collected facts;
|
|
||||||
- derived facts.
|
|
||||||
|
|
||||||
Daily, Today, and Tomorrow share common civil-day render-context fields such as
|
|
||||||
forecast date labels, valid period, generated-at labels, current conditions,
|
|
||||||
hourly forecast, precipitation timing, alert digest, SPC outlooks, AFD, SPC
|
|
||||||
discussion, weather story, daily summary, and ordered daypart summaries.
|
|
||||||
|
|
||||||
Each civil-day report keeps its report-specific planning module:
|
|
||||||
|
|
||||||
- Daily exposes `DailyPlanning`.
|
|
||||||
- Today exposes `TodayPlanning`.
|
|
||||||
- Tomorrow exposes `TomorrowPlanning`.
|
|
||||||
|
|
||||||
Today's ordered daypart context omits unavailable or elapsed dayparts according
|
|
||||||
to Today report rules. Daily and Tomorrow use fallback daypart behavior.
|
|
||||||
|
|
||||||
## Boundaries
|
|
||||||
|
|
||||||
- This package owns typed generated-text validation and render-context shaping.
|
|
||||||
- It owns generated-text catalog lookup for schema/template combinations.
|
|
||||||
- It uses typed module snapshot decoding through `module.StanzaValue`.
|
|
||||||
- It does not invoke Scriptorium, write state artifacts, choose report
|
|
||||||
definitions, compare snapshots, or own embedded template/schema files.
|
|
||||||
- It renders through `internal/reporttemplate`; embedded asset lookup remains
|
|
||||||
in `internal/reporttemplate`.
|
|
||||||
- It does not use a Go JSON Schema dependency; schema enforcement in Go is
|
|
||||||
limited to typed JSON decoding, unknown-field rejection, and required-field
|
|
||||||
checks.
|
|
||||||
|
|
||||||
## Failure Behavior
|
|
||||||
|
|
||||||
- Malformed generated-text JSON fails with decode context.
|
|
||||||
- Unknown generated-text JSON fields fail during decoding.
|
|
||||||
- Empty required fields fail after trimming whitespace.
|
|
||||||
- Daily, Today, and Tomorrow forecast discussion fails when no nonblank
|
|
||||||
paragraphs remain.
|
|
||||||
- Missing optional render-context stanzas become nil module pointers.
|
|
||||||
- Invalid render metadata, including missing timezone, missing generated time,
|
|
||||||
or invalid valid period, fails before template rendering.
|
|
||||||
- Unsupported generated-text schema IDs, template IDs, or schema/template
|
|
||||||
combinations fail during catalog lookup with report ID context.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Inspect:
|
|
||||||
|
|
||||||
- `internal/generatedtext/hourly_test.go`
|
|
||||||
- `internal/generatedtext/daily_test.go`
|
|
||||||
- `internal/generatedtext/today_test.go`
|
|
||||||
- `internal/generatedtext/tomorrow_test.go`
|
|
||||||
- `internal/generatedtext/catalog_test.go`
|
|
||||||
- `internal/generatedtext/render_context_test.go`
|
|
||||||
|
|
||||||
## Invariants
|
|
||||||
|
|
||||||
- Render contexts are curated structs, not raw prompt-input packages.
|
|
||||||
- Required generated text is normalized before downstream artifact storage.
|
|
||||||
- Generated-text-template reports must have one catalog entry matching their
|
|
||||||
report definition schema and template IDs.
|
|
||||||
- Missing optional weather narrative stanzas produce empty or fallback render
|
|
||||||
context fields rather than forcing raw module data into templates.
|
|
||||||
|
|||||||
@@ -1,288 +1,68 @@
|
|||||||
# Module Contract Internals
|
# Module Contract Internals
|
||||||
|
|
||||||
This document describes the module contract in `internal/module`.
|
`internal/module` defines the stable envelope between report composition,
|
||||||
|
module builders, snapshots, comparisons, templates, and prompt packages. It
|
||||||
|
does not define a report, execute a builder, or choose prompt-export policy;
|
||||||
|
those responsibilities belong to [report registry](report-registry.md) and
|
||||||
|
[briefing](briefing.md).
|
||||||
|
|
||||||
## Purpose
|
## Outputs and snapshots
|
||||||
|
|
||||||
`internal/module` defines the shared identifiers and data envelopes used for
|
Each `Output` has a module ID, stanza name, rich `Value`, and runtime-only
|
||||||
report modules. Report definitions use module IDs for composition, module
|
`PromptValue`. `DataPackageValue` returns the prompt value when present and
|
||||||
builders produce rich outputs with stanza names, prompt input packages consume
|
otherwise the rich value. This permits custom prompt exports without shrinking
|
||||||
runtime prompt export values, and Recent Changes compares snapshot stanzas.
|
the template and inspection value.
|
||||||
|
|
||||||
## Inputs And Outputs
|
`NewSnapshot` builds the ordered `weatherreporter.modules.v1` snapshot and
|
||||||
|
validates it. Snapshot JSON persists IDs, stanza names, and rich values only;
|
||||||
|
`PromptValue` is deliberately excluded. `StanzaValue` decodes a named rich
|
||||||
|
stanza into a caller-supplied type, reporting a missing stanza separately from
|
||||||
|
a decoding error.
|
||||||
|
|
||||||
Inputs:
|
Snapshots reject missing schema versions, empty IDs or stanza names, and
|
||||||
|
duplicate IDs or stanza names. Output order is caller-owned and preserved.
|
||||||
|
|
||||||
- ordered `module.ConfigItem` values from report definitions or config
|
## Registered IDs and default composition
|
||||||
overrides
|
|
||||||
- `module.Output` values produced by module builders
|
|
||||||
|
|
||||||
Outputs:
|
The registered IDs are `metadata`, `current_conditions`,
|
||||||
|
`narrative_forecast`, `hourly_forecast`, `derived_daily_summary`,
|
||||||
|
`derived_daypart_summaries`, `precip_timing`, `alert_digest`,
|
||||||
|
`spc_convective_outlooks`, `area_forecast_discussion`,
|
||||||
|
`spc_convective_discussion`, `weather_story`, `outdoor_windows`,
|
||||||
|
`today_planning`, `tomorrow_planning`, and `daily_planning`.
|
||||||
|
|
||||||
- stable `module.ID` constants
|
The registry declares these ordered default compositions:
|
||||||
- typed option structs for registered modules
|
|
||||||
- `module.Snapshot` with schema version `weatherreporter.modules.v1`
|
|
||||||
- ordered snapshot outputs with module ID, stanza name, and typed value
|
|
||||||
- runtime-only prompt export values on module outputs
|
|
||||||
- `module.Output.DataPackageValue`, which selects the prompt export value and
|
|
||||||
falls back to the rich value for hand-built or loaded snapshots
|
|
||||||
- typed stanza lookup through `module.StanzaValue`
|
|
||||||
|
|
||||||
## Rich Values And Prompt Exports
|
| Report | Ordered modules |
|
||||||
|
| --- | --- |
|
||||||
|
| Daily | metadata, current conditions, narrative forecast, daily summary, daypart summaries, precipitation timing, alert digest, SPC outlooks, AFD (long term), SPC discussion, weather story, outdoor windows, daily planning, hourly forecast |
|
||||||
|
| Today | metadata, current conditions, narrative forecast, daily summary, daypart summaries, precipitation timing, alert digest, SPC outlooks, AFD, SPC discussion, weather story, outdoor windows, hourly forecast, today planning |
|
||||||
|
| Tomorrow | metadata, current conditions, narrative forecast, daily summary, daypart summaries, precipitation timing, alert digest, SPC outlooks, AFD, SPC discussion, weather story, outdoor windows, tomorrow planning, hourly forecast |
|
||||||
|
| Hourly | metadata, current conditions, hourly forecast, precipitation timing, alert digest, SPC outlooks, AFD (key messages and short term), SPC discussion, weather story |
|
||||||
|
| Three-day and Weekend | metadata, current conditions, daypart summaries, precipitation timing, alert digest, SPC outlooks, AFD, SPC discussion, weather story, outdoor windows |
|
||||||
|
| Storm | metadata, current conditions, precipitation timing, alert digest, SPC outlooks, AFD, SPC discussion, weather story |
|
||||||
|
|
||||||
Each `module.Output` has two value surfaces:
|
The only non-empty default option is the AFD section selection. It accepts a
|
||||||
|
`sections` list; omitted or empty selects all available sections. Report
|
||||||
|
definitions may narrow it as shown above. Option shape and report compatibility
|
||||||
|
are validated by the briefing registry.
|
||||||
|
|
||||||
- `Value`: the rich module value used by templates, module snapshots,
|
## Rich and prompt-facing values
|
||||||
inspection, Recent Changes, and render contexts.
|
|
||||||
- `PromptValue`: the runtime-only prompt export used when building Scriptorium
|
|
||||||
data packages.
|
|
||||||
|
|
||||||
`PromptValue` is deliberately excluded from module snapshot JSON. Persisted
|
Rich values remain available to snapshots, comparisons, and render contexts.
|
||||||
module snapshots keep only the rich `value` field so inspection and
|
Briefing attaches custom prompt exports only for current conditions, hourly
|
||||||
render-context reconstruction keep the full deterministic template surface.
|
forecast, and derived daypart summaries; all other current builders use
|
||||||
|
pass-through values. The prompt package owns how exported stanzas are grouped
|
||||||
|
and serialized; see [prompt input](prompt-input.md).
|
||||||
|
|
||||||
The `internal/briefing` module registry attaches prompt export values when it
|
## Verification and invariants
|
||||||
builds module outputs. Modules without a custom exporter use default
|
|
||||||
pass-through behavior, so their prompt value is the same as their rich value.
|
|
||||||
Modules with custom prompt export policy own typed prompt export structs near
|
|
||||||
the module builder. Custom prompt exports are:
|
|
||||||
|
|
||||||
- `current_conditions`
|
Focused tests cover snapshot validation and order, typed stanza lookup, and
|
||||||
- `hourly_forecast`
|
prompt-value fallback:
|
||||||
- `derived_daypart_summaries`
|
|
||||||
|
|
||||||
Custom exporters remove template-only helpers or confusing duplicates from the
|
```sh
|
||||||
data package without shrinking the rich module structs used by templates.
|
go test ./internal/module
|
||||||
Exporter failures include module ID and stanza context.
|
|
||||||
|
|
||||||
## Registered Module IDs
|
|
||||||
|
|
||||||
The registry recognizes these IDs:
|
|
||||||
|
|
||||||
- `metadata`
|
|
||||||
- `current_conditions`
|
|
||||||
- `narrative_forecast`
|
|
||||||
- `hourly_forecast`
|
|
||||||
- `derived_daily_summary`
|
|
||||||
- `derived_daypart_summaries`
|
|
||||||
- `precip_timing`
|
|
||||||
- `alert_digest`
|
|
||||||
- `spc_convective_outlooks`
|
|
||||||
- `area_forecast_discussion`
|
|
||||||
- `spc_convective_discussion`
|
|
||||||
- `weather_story`
|
|
||||||
- `outdoor_windows`
|
|
||||||
- `today_planning`
|
|
||||||
- `tomorrow_planning`
|
|
||||||
- `daily_planning`
|
|
||||||
|
|
||||||
Every registered module has a builder. Report composition entries that refer to
|
|
||||||
unknown or unimplemented module IDs fail validation instead of being skipped.
|
|
||||||
|
|
||||||
## Daily Composition
|
|
||||||
|
|
||||||
The default Daily Report module order is:
|
|
||||||
|
|
||||||
1. `metadata`
|
|
||||||
2. `current_conditions`
|
|
||||||
3. `narrative_forecast`
|
|
||||||
4. `derived_daily_summary`
|
|
||||||
5. `derived_daypart_summaries`
|
|
||||||
6. `precip_timing`
|
|
||||||
7. `alert_digest`
|
|
||||||
8. `spc_convective_outlooks`
|
|
||||||
9. `area_forecast_discussion`
|
|
||||||
10. `spc_convective_discussion`
|
|
||||||
11. `weather_story`
|
|
||||||
12. `outdoor_windows`
|
|
||||||
13. `daily_planning`
|
|
||||||
14. `hourly_forecast`
|
|
||||||
|
|
||||||
The embedded Daily template uses selected deterministic fields from these
|
|
||||||
module outputs after GeneratedText validation. Its `area_forecast_discussion`
|
|
||||||
item is configured to include only `long_term`.
|
|
||||||
|
|
||||||
## Today Composition
|
|
||||||
|
|
||||||
The default Today Report module order is:
|
|
||||||
|
|
||||||
1. `metadata`
|
|
||||||
2. `current_conditions`
|
|
||||||
3. `narrative_forecast`
|
|
||||||
4. `derived_daily_summary`
|
|
||||||
5. `derived_daypart_summaries`
|
|
||||||
6. `precip_timing`
|
|
||||||
7. `alert_digest`
|
|
||||||
8. `spc_convective_outlooks`
|
|
||||||
9. `area_forecast_discussion`
|
|
||||||
10. `spc_convective_discussion`
|
|
||||||
11. `weather_story`
|
|
||||||
12. `outdoor_windows`
|
|
||||||
13. `hourly_forecast`
|
|
||||||
14. `today_planning`
|
|
||||||
|
|
||||||
The embedded Today template uses selected deterministic fields from these
|
|
||||||
module outputs after GeneratedText validation.
|
|
||||||
|
|
||||||
## Tomorrow Composition
|
|
||||||
|
|
||||||
The default Tomorrow Report module order is:
|
|
||||||
|
|
||||||
1. `metadata`
|
|
||||||
2. `current_conditions`
|
|
||||||
3. `narrative_forecast`
|
|
||||||
4. `derived_daily_summary`
|
|
||||||
5. `derived_daypart_summaries`
|
|
||||||
6. `precip_timing`
|
|
||||||
7. `alert_digest`
|
|
||||||
8. `spc_convective_outlooks`
|
|
||||||
9. `area_forecast_discussion`
|
|
||||||
10. `spc_convective_discussion`
|
|
||||||
11. `weather_story`
|
|
||||||
12. `outdoor_windows`
|
|
||||||
13. `tomorrow_planning`
|
|
||||||
14. `hourly_forecast`
|
|
||||||
|
|
||||||
The embedded Tomorrow template uses selected deterministic fields from these
|
|
||||||
module outputs after GeneratedText validation.
|
|
||||||
|
|
||||||
## Daily Planning
|
|
||||||
|
|
||||||
`daily_planning` emits dated daily planning facts for the `daily` report ID.
|
|
||||||
Its output stanza is also named `daily_planning`. The module is supported only
|
|
||||||
by that report ID and depends on daily summaries for the selected local civil
|
|
||||||
day. The default Daily Report composition includes it.
|
|
||||||
|
|
||||||
The output uses this shape:
|
|
||||||
|
|
||||||
- `morning_readiness`
|
|
||||||
- `commute_school_workday_concerns`
|
|
||||||
- `overnight_change_watch`
|
|
||||||
|
|
||||||
The type is `briefing.DailyPlanningModule`; it is independent from
|
|
||||||
`briefing.TomorrowPlanningModule`.
|
|
||||||
|
|
||||||
## Today Planning
|
|
||||||
|
|
||||||
`today_planning` emits current-day planning facts for Today Report. Its output
|
|
||||||
stanza is also named `today_planning`. The module is supported only by Today
|
|
||||||
Report and depends on daily and daypart summaries for the current local civil
|
|
||||||
day.
|
|
||||||
|
|
||||||
The output uses this shape:
|
|
||||||
|
|
||||||
- `morning_readiness`
|
|
||||||
- `commute_school_workday_concerns`
|
|
||||||
- `outdoor_planning`
|
|
||||||
- `late_day_change_watch`
|
|
||||||
|
|
||||||
The type is `briefing.TodayPlanningModule`; it is independent from
|
|
||||||
`briefing.TomorrowPlanningModule`.
|
|
||||||
|
|
||||||
## Hourly Composition
|
|
||||||
|
|
||||||
The default Hourly Report module order is:
|
|
||||||
|
|
||||||
1. `metadata`
|
|
||||||
2. `current_conditions`
|
|
||||||
3. `hourly_forecast`
|
|
||||||
4. `precip_timing`
|
|
||||||
5. `alert_digest`
|
|
||||||
6. `spc_convective_outlooks`
|
|
||||||
7. `area_forecast_discussion`
|
|
||||||
8. `spc_convective_discussion`
|
|
||||||
9. `weather_story`
|
|
||||||
|
|
||||||
Hourly Report does not include daily or daypart summary modules by default.
|
|
||||||
Its `area_forecast_discussion` item is configured to include only
|
|
||||||
`key_messages` and `short_term`.
|
|
||||||
|
|
||||||
## Options
|
|
||||||
|
|
||||||
Most modules use an empty options struct, including
|
|
||||||
`spc_convective_outlooks` and `spc_convective_discussion`.
|
|
||||||
`area_forecast_discussion` accepts:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
sections:
|
|
||||||
- product
|
|
||||||
- key_messages
|
|
||||||
- short_term
|
|
||||||
- long_term
|
|
||||||
```
|
```
|
||||||
|
|
||||||
An omitted or empty `sections` list includes all available discussion sections.
|
Module IDs and stanza names are stable, every emitted output has one of each,
|
||||||
Invalid option shapes fail during config normalization or composition
|
and this package never imports the report registry.
|
||||||
validation.
|
|
||||||
|
|
||||||
## SPC Convective Module Outputs
|
|
||||||
|
|
||||||
`spc_convective_outlooks` emits a risk-product stanza with:
|
|
||||||
|
|
||||||
- `checked`
|
|
||||||
- `as_of`
|
|
||||||
- `issued_at`
|
|
||||||
- `location_id`
|
|
||||||
- `location_name`
|
|
||||||
- `outlook_count`
|
|
||||||
- `outlooks`
|
|
||||||
- `risk_digest`
|
|
||||||
|
|
||||||
Each outlook entry may include `day`, `outlook_type`, `label`, `label_text`,
|
|
||||||
`period_begins`, `period_ends`, `issued_at`, `contains_location`, and
|
|
||||||
`image_url`. It omits GeoJSON geometry, source URL, expiration time, and
|
|
||||||
severity rank.
|
|
||||||
|
|
||||||
The optional `risk_digest` list is a curated report-rendering subset of
|
|
||||||
categorical outlooks that overlap the report period, contain the configured
|
|
||||||
location, and meet the minimum severity threshold. Entries include `label_text`,
|
|
||||||
`risk_label`, `period_begins`, and `period_ends`; they do not expose severity
|
|
||||||
rank.
|
|
||||||
|
|
||||||
`spc_convective_discussion` emits a narrative stanza only when a retained
|
|
||||||
report-period categorical outlook has severity rank `3` or higher and matching
|
|
||||||
discussion text is available. Its output includes `included_because` and
|
|
||||||
`discussions`; each discussion may include `day`, `period_begins`,
|
|
||||||
`period_ends`, `headline`, `summary`, `discussion`, and `updated_at`.
|
|
||||||
Discussions are included only for SPC days whose retained categorical outlooks
|
|
||||||
meet the severity threshold.
|
|
||||||
|
|
||||||
## Boundaries
|
|
||||||
|
|
||||||
- This package owns module identifiers, config item envelopes, output
|
|
||||||
envelopes, snapshot validation, and typed stanza lookup.
|
|
||||||
- It does not define report IDs, execute builders, collect weather data, derive
|
|
||||||
forecast facts, write state, or invoke Scriptorium.
|
|
||||||
|
|
||||||
## State Or Manifest Behavior
|
|
||||||
|
|
||||||
`module.Snapshot` values are persisted by `internal/state` as JSON. Snapshot
|
|
||||||
validation rejects missing schema version, missing module IDs, missing stanza
|
|
||||||
names, duplicate module outputs, and duplicate stanza names while preserving
|
|
||||||
output order. Snapshot JSON contains rich module values only; runtime prompt
|
|
||||||
export values are not persisted.
|
|
||||||
|
|
||||||
## Failure Behavior
|
|
||||||
|
|
||||||
- Snapshot construction fails for duplicate module outputs or duplicate stanza
|
|
||||||
names.
|
|
||||||
- Typed stanza lookup returns `found=false` for missing stanzas.
|
|
||||||
- Typed stanza lookup wraps JSON marshal/decode failures with stanza context.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Inspect:
|
|
||||||
|
|
||||||
- `internal/module/module_test.go`
|
|
||||||
- `internal/briefing/modules_test.go`
|
|
||||||
- `internal/report/period_test.go`
|
|
||||||
|
|
||||||
## Invariants
|
|
||||||
|
|
||||||
- `internal/module` does not import `internal/report`.
|
|
||||||
- Module IDs are stable strings.
|
|
||||||
- Each emitted module output has exactly one stanza name and one rich typed
|
|
||||||
value.
|
|
||||||
- Built module outputs have a data-package value, either from a custom prompt
|
|
||||||
exporter or from default pass-through behavior.
|
|
||||||
- Snapshot output order is caller-owned and preserved.
|
|
||||||
|
|||||||
@@ -1,177 +1,61 @@
|
|||||||
# Prompt Input Internals
|
# Prompt Input Internals
|
||||||
|
|
||||||
This document describes YAML prompt data package construction in
|
`internal/promptinput` converts report metadata, an ordered module snapshot,
|
||||||
`internal/promptinput`.
|
Recent Changes, and source warnings into the YAML `data_package` consumed by
|
||||||
|
Scriptorium. It owns this package's schema, grouping, serialization, loading,
|
||||||
|
and validation—not weather collection, module construction, path choice, or
|
||||||
|
subprocess execution.
|
||||||
|
|
||||||
## Purpose
|
## Package construction
|
||||||
|
|
||||||
`internal/promptinput` converts report metadata, ordered module outputs, Recent
|
`Build` produces `weatherreporter.data_package.v3`. It copies the run ID;
|
||||||
Changes, and source warnings into the `data_package` file passed to
|
report ID, variant, prompt ID, generation time, timezone, local current date,
|
||||||
Scriptorium.
|
and valid period; ordered briefing stanzas; Recent Changes; and source
|
||||||
|
warnings. A nil Recent Changes slice becomes an empty `items` list.
|
||||||
|
|
||||||
The persisted data package is YAML with schema version
|
Briefing starts as a flat snapshot order and stanza-value map. `Build` uses
|
||||||
`weatherreporter.data_package.v3`. It is separate from the JSON module snapshot
|
each output's `DataPackageValue`, so runtime prompt exports take precedence and
|
||||||
used for inspection and comparison. Data packages serialize each module
|
rich values are used only as a fallback. Prompt exports are selected by the
|
||||||
output's prompt export value, not necessarily the full rich module value saved
|
[briefing registry](briefing.md), while the rich-versus-prompt contract is in
|
||||||
in the module snapshot.
|
[module internals](module.md).
|
||||||
|
|
||||||
## Inputs And Outputs
|
## YAML ordering and grouping
|
||||||
|
|
||||||
Inputs:
|
Serialization keeps `metadata` directly under `briefing`. Every other known
|
||||||
|
stanza is placed in exactly one category, emitted in category order and in its
|
||||||
|
original snapshot order within that category:
|
||||||
|
|
||||||
- report metadata from app/state orchestration
|
| Category | Current stanzas |
|
||||||
- `module.Snapshot`
|
| --- | --- |
|
||||||
- optional `[]changes.Change`
|
| `applicable_risk_products` | alert digest, SPC convective outlooks |
|
||||||
|
| `derived_summaries` | deterministic summaries, precipitation timing, outdoor windows, and planning values |
|
||||||
|
| `narrative_products` | narrative forecast, discussions, and weather story |
|
||||||
|
| `raw_data` | current conditions and hourly forecast |
|
||||||
|
|
||||||
Outputs:
|
This YAML presentation does not alter the flat snapshot model. `LoadYAML`
|
||||||
|
accepts the same category layout and reconstructs flat `Order` and `Values`,
|
||||||
|
rejecting misplaced, duplicate, unknown, or uncategorized stanzas.
|
||||||
|
|
||||||
- `promptinput.Package` with schema version, RunID, report metadata, named
|
## Validation and persistence
|
||||||
module stanzas grouped for prompt presentation, Recent Changes, and source
|
|
||||||
warnings
|
|
||||||
- YAML bytes from `promptinput.MarshalYAML`
|
|
||||||
- YAML file written atomically by `promptinput.Save`
|
|
||||||
|
|
||||||
The YAML shape includes:
|
`Validate` requires the current schema version, run and report identifiers,
|
||||||
|
prompt ID, generation timestamp, timezone, current local date, valid period,
|
||||||
|
and at least one ordered briefing stanza. It rejects duplicate stanza names,
|
||||||
|
missing values, and a missing category for every non-metadata stanza.
|
||||||
|
|
||||||
```yaml
|
`MarshalYAML` and `LoadYAML` validate their result. `Save` writes the serialized
|
||||||
schema_version: weatherreporter.data_package.v3
|
YAML atomically; managed workspace paths are owned by [state internals](state.md).
|
||||||
run_id: <run_id>
|
Generated-text artifacts and template render contexts are later workflow
|
||||||
report:
|
artifacts, not members of this package.
|
||||||
id: <report_id>
|
|
||||||
prompt_id: <prompt_id>
|
## Verification and invariants
|
||||||
briefing:
|
|
||||||
metadata: {}
|
Focused tests cover construction, curated exports, category ordering, YAML
|
||||||
applicable_risk_products:
|
round trips, invalid layout, validation, and atomic saves:
|
||||||
alert_digest: {}
|
|
||||||
spc_convective_outlooks: {}
|
```sh
|
||||||
derived_summaries:
|
go test ./internal/promptinput
|
||||||
derived_daily_summary: {}
|
|
||||||
derived_daypart_summaries: {}
|
|
||||||
precip_timing: {}
|
|
||||||
outdoor_windows: {}
|
|
||||||
narrative_products:
|
|
||||||
narrative_forecast: {}
|
|
||||||
area_forecast_discussion: {}
|
|
||||||
spc_convective_discussion: {}
|
|
||||||
weather_story: {}
|
|
||||||
raw_data:
|
|
||||||
current_conditions: {}
|
|
||||||
hourly_forecast: {}
|
|
||||||
recent_changes:
|
|
||||||
items: []
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The `briefing` mapping keeps `metadata` directly under `briefing` and groups
|
The package is narrower than a template render context and never infers changes
|
||||||
weather module stanzas under prompt-facing categories. This grouping is a YAML
|
from report prose.
|
||||||
presentation concern only: module snapshots remain flat, and loaded
|
|
||||||
`promptinput.Package` values expose flat stanza names in `Briefing.Values`.
|
|
||||||
Within each category, stanza order follows the module snapshot output order.
|
|
||||||
Prompt-facing module intervals use local `period_begins` and `period_ends`
|
|
||||||
labels; canonical report metadata and source timestamps remain structured
|
|
||||||
timestamps where applicable.
|
|
||||||
|
|
||||||
## Module Export Boundary
|
|
||||||
|
|
||||||
Data packages are curated prompt inputs. They are not full template render
|
|
||||||
contexts and should not be treated as a dump of every field available to Go
|
|
||||||
templates.
|
|
||||||
|
|
||||||
When module outputs are built by `internal/briefing`, the registry attaches a
|
|
||||||
runtime prompt export value. `internal/promptinput` serializes
|
|
||||||
`output.DataPackageValue()` for each stanza. That helper prefers the runtime
|
|
||||||
prompt export and falls back to the rich `Value` when no prompt export is set,
|
|
||||||
which keeps loaded snapshots and hand-built tests usable.
|
|
||||||
|
|
||||||
Modules without custom export policy use pass-through behavior. Modules with
|
|
||||||
custom exports currently include:
|
|
||||||
|
|
||||||
- `current_conditions`: omits lower-case condition text and duplicate
|
|
||||||
wind-direction text.
|
|
||||||
- `hourly_forecast`: omits hour labels, lower-case description text, and the
|
|
||||||
template precipitation-mention helper while keeping forecast facts.
|
|
||||||
- `derived_daypart_summaries`: omits deterministic sentence-construction
|
|
||||||
helpers while keeping daypart period, condition, temperature trend,
|
|
||||||
precipitation, wind, notable-condition, hazard, and alert-relevance facts.
|
|
||||||
|
|
||||||
The rich module snapshot and generated-template render context still contain
|
|
||||||
the helper fields used by deterministic Markdown templates.
|
|
||||||
|
|
||||||
Daily Report, Today Report, Tomorrow Report, and Hourly Report module snapshots
|
|
||||||
use the same package schema and categories when converted into prompt input.
|
|
||||||
The default hourly module list places
|
|
||||||
`precip_timing` under `derived_summaries`, alert and SPC outlooks under
|
|
||||||
`applicable_risk_products`, AFD/SPC discussion/weather story under
|
|
||||||
`narrative_products`, and current/hourly data under `raw_data`. It does not
|
|
||||||
include civil-day summary stanzas. Generated-text and render context artifacts
|
|
||||||
are produced later in app orchestration and are not part of the YAML data
|
|
||||||
package.
|
|
||||||
|
|
||||||
The default Daily, Today, and Tomorrow module lists include civil-day summary
|
|
||||||
stanzas, planning stanzas, and `hourly_forecast` in the data package before
|
|
||||||
structured GeneratedText is requested from Scriptorium. Daily uses
|
|
||||||
`daily_planning`, Today uses `today_planning`, and Tomorrow uses
|
|
||||||
`tomorrow_planning`.
|
|
||||||
|
|
||||||
Current categories are:
|
|
||||||
|
|
||||||
- `applicable_risk_products`: location-applicable alerts, warnings, outlooks,
|
|
||||||
and similar risk products. Current stanzas include `alert_digest` and
|
|
||||||
`spc_convective_outlooks`.
|
|
||||||
- `derived_summaries`: deterministic summaries and calculated report facts.
|
|
||||||
- `narrative_products`: official narrative text products and forecast stories.
|
|
||||||
Current stanzas include `narrative_forecast`,
|
|
||||||
`area_forecast_discussion`, `spc_convective_discussion`, and
|
|
||||||
`weather_story`.
|
|
||||||
- `raw_data`: minimally transformed underlying weather data.
|
|
||||||
|
|
||||||
## Boundaries
|
|
||||||
|
|
||||||
- This package owns prompt package schema, YAML marshaling, YAML loading, and
|
|
||||||
validation.
|
|
||||||
- It does not collect weather data, derive forecast summaries, execute modules,
|
|
||||||
choose module prompt export shapes, find prior snapshots, compare changes,
|
|
||||||
choose artifact paths, or invoke Scriptorium.
|
|
||||||
|
|
||||||
## Config Fields Used
|
|
||||||
|
|
||||||
None directly. Config-derived values such as timezone, units, and prompt
|
|
||||||
location are already present in report metadata and module stanzas before this
|
|
||||||
package runs.
|
|
||||||
|
|
||||||
## External Adapters Used
|
|
||||||
|
|
||||||
None.
|
|
||||||
|
|
||||||
## State Or Manifest Behavior
|
|
||||||
|
|
||||||
`promptinput.Save` writes YAML atomically. Managed workspace paths are owned by
|
|
||||||
`internal/state`.
|
|
||||||
|
|
||||||
## Skip And Resume Behavior
|
|
||||||
|
|
||||||
None. Recent Changes is always present as an `items` list and may be empty.
|
|
||||||
|
|
||||||
## Failure Behavior
|
|
||||||
|
|
||||||
Validation fails before render preflight when required top-level fields are
|
|
||||||
missing or inconsistent, when the valid period is invalid, or when no module
|
|
||||||
stanzas are present. Save failures include filesystem operation and path
|
|
||||||
context.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Inspect:
|
|
||||||
|
|
||||||
- `internal/promptinput/package_test.go`
|
|
||||||
- `internal/app/app_test.go`
|
|
||||||
|
|
||||||
## Invariants
|
|
||||||
|
|
||||||
- Scriptorium receives structured YAML through `--input data_package=<path>`.
|
|
||||||
- Module stanza order is deterministic within each prompt-facing category.
|
|
||||||
- Every non-metadata module stanza has exactly one prompt-input category.
|
|
||||||
- Data-package stanzas use curated module prompt exports when present and rich
|
|
||||||
values only as pass-through or fallback values.
|
|
||||||
- Data packages are narrower than generated-template render contexts.
|
|
||||||
- Recent Changes are provided by `internal/changes`; this package does not
|
|
||||||
infer changes from rendered report text.
|
|
||||||
|
|||||||
@@ -1,143 +1,76 @@
|
|||||||
# Report Registry Internals
|
# Report Registry Internals
|
||||||
|
|
||||||
This document describes report identity, valid-period resolution, output
|
`internal/report` owns the registry of report identities and the data declared
|
||||||
naming, artifact grouping, batch command names, and comparison declarations in
|
for each one: resolution, generation mode, prompt identity, comparison policy,
|
||||||
`internal/report`.
|
artifact group, output-copy name, default module composition, and Distributor
|
||||||
|
path declarations. The public command syntax is owned by the
|
||||||
|
[CLI reference](../cli.md); configuration aliases and overrides are owned by
|
||||||
|
the [configuration reference](../config.md).
|
||||||
|
|
||||||
## Purpose
|
## Definitions and resolution
|
||||||
|
|
||||||
`internal/report` is the canonical source for report definitions, public
|
Each `Definition` declares a stable ID and display name, prompt ID, generation
|
||||||
command names, config-key aliases, and batch command names. App, config, state,
|
mode, optional template and generated-text schema IDs, valid-period resolver,
|
||||||
module building, and CLI wiring consume report-owned helpers and resolved
|
comparison strategy, artifact group, batch-copy filename, Distributor path
|
||||||
definitions instead of owning report identity policy themselves.
|
templates, generation eligibility, compatible prior IDs, default modules, and
|
||||||
|
batch eligibility flags. `Resolved` combines that definition with the valid
|
||||||
|
period and run metadata for one invocation.
|
||||||
|
|
||||||
## Definition Fields
|
| Report ID | Mode | Period policy | Comparison | Registry batch flag | Output copy |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| `daily` | Generated text + template | Explicit local civil day | Same valid date | Dynamic Daily inclusion is app-owned | `daily.md` |
|
||||||
|
| `today` | Generated text + template | Selected or current local civil day | Same valid date | Morning | `today.md` |
|
||||||
|
| `tomorrow` | Generated text + template | Next local civil day | Same valid date | Evening | `tomorrow.md` |
|
||||||
|
| `hourly` | Generated text + template | Rolling six-hour interval | Rolling window | — | `hourly.md` |
|
||||||
|
| `three_day` | Scriptorium Markdown | Generation time through the third following local midnight | Same valid date | Morning | `three-day.md` |
|
||||||
|
| `weekend` | Scriptorium Markdown | Upcoming weekend window | Weekend window | Morning | `weekend.md` |
|
||||||
|
| `storm` | Scriptorium Markdown | Caller-supplied event window | Explicit window | — | `storm.md` |
|
||||||
|
|
||||||
Each report definition declares:
|
The four generated-text reports pair their report ID with matching template and
|
||||||
|
schema IDs. The three direct-Markdown reports leave both IDs empty. Exact
|
||||||
|
template fields and schema assets belong to [report templates](../templates.md)
|
||||||
|
and [generated-text internals](generatedtext.md).
|
||||||
|
|
||||||
- report ID and display name
|
All valid periods are half-open. Storm accepts local `YYYY-MM-DDTHH:MM` values
|
||||||
- Scriptorium prompt ID
|
in the effective report timezone or offset-bearing RFC3339 values; its end
|
||||||
- generation mode
|
must follow its start. Resolving Weekend directly on Sunday is rejected.
|
||||||
- valid-period resolver
|
|
||||||
- comparison strategy
|
|
||||||
- managed artifact group
|
|
||||||
- batch output copy filename
|
|
||||||
- generated-report eligibility
|
|
||||||
- prior-report compatibility list
|
|
||||||
- default ordered module composition
|
|
||||||
|
|
||||||
Report-owned helpers map public command names and config keys to report IDs.
|
## Registry collaborators
|
||||||
The generate command names are `daily`, `today`, `tomorrow`, `hourly`,
|
|
||||||
`three-day`, `weekend`, and `storm`. Config keys also accept selected
|
|
||||||
underscore and descriptive aliases such as `three_day_outlook`,
|
|
||||||
`weekend_outlook`, and `storm_report`.
|
|
||||||
|
|
||||||
`daily` resolves to the dated Daily Report ID `daily`. `today` resolves to the
|
`DefaultRegistry` is the only source of the seven report definitions.
|
||||||
independent Today report ID `today`. `reports.today` is not an alias for
|
`Lookup`, `Resolve`, and report-name helpers prevent callers from duplicating
|
||||||
`reports.daily`, and retired report keys are not supported.
|
report identity rules. Registry overrides clone a definition and replace its
|
||||||
|
module list only after the report ID is recognized.
|
||||||
|
|
||||||
Markdown report definitions use the `scriptorium_markdown` generation mode.
|
The definition's `DistributorPathTemplates` are internal declarations consumed
|
||||||
Their template and structured-text schema identifiers are empty. Daily Report,
|
by app orchestration. Their rendered external bundle paths and compatibility
|
||||||
Today Report, Tomorrow Report, and Hourly Report declare
|
contract are documented in the [Distributor bundle guide](../integrations/distributor/pkg-bundle.md), not repeated here.
|
||||||
`generated_text_template`; the app uses their template and schema identifiers
|
|
||||||
to validate generated text and render embedded Markdown templates.
|
|
||||||
|
|
||||||
## Reports
|
`morning` and `evening` are registry-owned batch names. Registry flags declare
|
||||||
|
fixed report eligibility; app orchestration determines data-dependent Daily
|
||||||
|
membership and produces the actual batch plan.
|
||||||
|
|
||||||
| Report | ID | Prompt | Generation mode | Artifact group | Batch copy | Prior compatibility |
|
## Module composition and failures
|
||||||
| --- | --- | --- | --- | --- | --- | --- |
|
|
||||||
| Daily Report | `daily` | `weather.daily_generated_text` | `generated_text_template` | `daily` | `daily.md` | Daily Report |
|
|
||||||
| Today Report | `today` | `weather.today_generated_text` | `generated_text_template` | `today` | `today.md` | Today Report |
|
|
||||||
| Tomorrow Report | `tomorrow` | `weather.tomorrow_generated_text` | `generated_text_template` | `tomorrow` | `tomorrow.md` | Tomorrow Report |
|
|
||||||
| Hourly Report | `hourly` | `weather.hourly_generated_text` | `generated_text_template` | `hourly` | `hourly.md` | Hourly Report |
|
|
||||||
| 3-Day Outlook | `three_day` | `weather.three_day_outlook` | `scriptorium_markdown` | `three-day` | `three-day.md` | 3-Day Outlook |
|
|
||||||
| Weekend Outlook | `weekend` | `weather.weekend_outlook` | `scriptorium_markdown` | `weekend` | `weekend.md` | Weekend Outlook |
|
|
||||||
| Storm Report | `storm` | `weather.storm_report` | `scriptorium_markdown` | `storm` | `storm.md` | Storm Report |
|
|
||||||
|
|
||||||
All report definitions are eligible for generation.
|
Each definition supplies an ordered `[]module.ConfigItem`; the complete
|
||||||
|
report-to-module mapping is maintained in [module internals](module.md).
|
||||||
|
`ArtifactGroup`, `BatchOutputName`, `Generated`, and comparison compatibility
|
||||||
|
are likewise consumed by state and orchestration rather than recomputed there.
|
||||||
|
|
||||||
## Valid Periods
|
Unknown report IDs or batch names, an invalid weekend resolution, and invalid
|
||||||
|
storm windows return errors. The registry never collects weather data, builds
|
||||||
|
modules, parses CLI flags, writes state, executes Scriptorium, or delivers a
|
||||||
|
report.
|
||||||
|
|
||||||
- Daily Report covers the selected local civil day and requires an explicit
|
## Verification and invariants
|
||||||
date.
|
|
||||||
- Today Report covers the selected local civil day, or the current local civil
|
|
||||||
day when no date override is supplied.
|
|
||||||
- Tomorrow Report covers the next local civil day from generation time.
|
|
||||||
- Hourly Report covers the half-open six-hour period from generation time in
|
|
||||||
the effective report timezone. The duration is an internal report constant,
|
|
||||||
not a configuration field.
|
|
||||||
- 3-Day Outlook covers the interval from generation time through local midnight
|
|
||||||
three days later.
|
|
||||||
- Weekend Outlook covers the upcoming weekend window.
|
|
||||||
- Storm Report covers an explicit event window supplied by the caller.
|
|
||||||
|
|
||||||
Storm event windows can be parsed from local `YYYY-MM-DDTHH:MM` timestamps in
|
Focused tests cover definition completeness, command and alias lookup, period
|
||||||
the configured timezone or RFC3339 timestamps with explicit offsets. End time
|
resolution, run IDs, path declarations, composition defaults, and override
|
||||||
must be after start time.
|
validation:
|
||||||
|
|
||||||
## Boundaries
|
```sh
|
||||||
|
go test ./internal/report
|
||||||
|
```
|
||||||
|
|
||||||
`internal/report` defines report metadata, public report names, batch command
|
All report selection goes through the registry, and the registry is the source
|
||||||
names, output naming, and time coverage. It does not collect weather data, plan
|
of truth for report identity—not rendered report text or app-local constants.
|
||||||
batch membership, build module values, compare snapshot contents, write state,
|
|
||||||
parse CLI flags, or invoke Scriptorium.
|
|
||||||
|
|
||||||
The CLI parses flags and command structure, then uses report-owned helpers for
|
|
||||||
report and batch command names. Config loading uses report-owned helpers for
|
|
||||||
report override keys.
|
|
||||||
|
|
||||||
## Config Fields Used
|
|
||||||
|
|
||||||
The app supplies `weather_api.timezone` as a loaded `time.Location`. Batch
|
|
||||||
output path copying uses batch output names from report definitions. Report
|
|
||||||
module overrides can use short keys such as `daily`, `today`, `tomorrow`, and
|
|
||||||
`hourly`, or descriptive names such as `three_day_outlook`.
|
|
||||||
|
|
||||||
## Batch Commands
|
|
||||||
|
|
||||||
`internal/report` owns the public batch command names `morning` and `evening`
|
|
||||||
and validates them through `BatchForCommandName`. Data-dependent batch
|
|
||||||
membership is owned by `internal/app`, because it depends on collected hourly
|
|
||||||
forecast coverage.
|
|
||||||
|
|
||||||
Report definitions still declare default batch output copy filenames. App
|
|
||||||
batch planning uses those filenames for fixed report entries and supplies
|
|
||||||
date-qualified names for dynamic Daily entries.
|
|
||||||
|
|
||||||
## State And App Usage
|
|
||||||
|
|
||||||
- State paths use `ArtifactGroup`.
|
|
||||||
- Batch output copies use `BatchOutputName`.
|
|
||||||
- Generation checks `Generated`.
|
|
||||||
- Module composition defaults use `Modules`.
|
|
||||||
- Prior lookup checks `CompatiblePriorIDs` and the comparison strategy.
|
|
||||||
- RunIDs include the resolved report ID.
|
|
||||||
|
|
||||||
## Failure Behavior
|
|
||||||
|
|
||||||
- Unknown report IDs and batch names return actionable errors.
|
|
||||||
- Weekend Outlook resolution returns an error when resolved directly on Sunday.
|
|
||||||
- Storm Report resolution requires start and end, with end after start.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Inspect:
|
|
||||||
|
|
||||||
- `internal/report/period_test.go`
|
|
||||||
- `internal/app/app_test.go`
|
|
||||||
- `internal/cli/root_test.go`
|
|
||||||
|
|
||||||
## Invariants
|
|
||||||
|
|
||||||
- Report selection goes through the registry.
|
|
||||||
- Public command names, config-key aliases, and batch command names are owned
|
|
||||||
by `internal/report`.
|
|
||||||
- Direct Markdown reports have empty template and generated-text schema IDs.
|
|
||||||
- Generated-text-template reports declare prompt, template, and schema IDs in
|
|
||||||
their report definition.
|
|
||||||
- Valid periods are half-open intervals independent of rendered report text.
|
|
||||||
- Artifact grouping, batch output filenames, generated-report eligibility,
|
|
||||||
default module composition, comparison compatibility, and comparison strategy
|
|
||||||
are declared by report definition.
|
|
||||||
- App-owned batch planning uses report definitions but does not live in the
|
|
||||||
report registry.
|
|
||||||
|
|||||||
@@ -1,125 +1,51 @@
|
|||||||
# Report Template Internals
|
# Report Template Internals
|
||||||
|
|
||||||
This document describes embedded Markdown templates and GeneratedText schemas
|
`internal/reporttemplate` embeds and renders the repository's native Markdown
|
||||||
in `internal/reporttemplate`.
|
templates and exposes their companion generated-text schemas. The current asset
|
||||||
|
IDs are `daily`, `today`, `tomorrow`, and `hourly`. The template files, partials,
|
||||||
|
and complete render-context field reference are maintained in
|
||||||
|
[report templates](../templates.md).
|
||||||
|
|
||||||
## Purpose
|
## Assets and lookup
|
||||||
|
|
||||||
`internal/reporttemplate` owns repository-native report templates and companion
|
The package embeds top-level templates, shared partials, and JSON schemas from
|
||||||
GeneratedText JSON schemas. The implemented template assets are Daily, Today,
|
its asset directories. `Template` and `Schema` return the requested embedded
|
||||||
Tomorrow, and Hourly.
|
asset and fail with the requested ID when it is unknown or unreadable.
|
||||||
|
|
||||||
The package embeds assets from:
|
Generated-text catalog handlers obtain schema bytes and template source through
|
||||||
|
these APIs. Prompt source files are repository assets for prompt registration;
|
||||||
|
they are not reporttemplate lookup assets. Report definitions select IDs, while
|
||||||
|
[generated-text internals](generatedtext.md) verifies the supported
|
||||||
|
schema/template pairing.
|
||||||
|
|
||||||
- `internal/reporttemplate/templates/*.md.tmpl`
|
## Rendering
|
||||||
- `internal/reporttemplate/templates/partials/*.md.tmpl`
|
|
||||||
- `internal/reporttemplate/schemas/*.schema.json`
|
|
||||||
|
|
||||||
Generated-text prompt source files live under
|
`Render` loads the top-level template, creates a `text/template` with helper
|
||||||
`internal/reporttemplate/prompts/`. They are repository assets for prompt
|
functions and `missingkey=error`, parses the template, parses every shared
|
||||||
registration, not embedded lookup APIs.
|
partial, and executes the result against the typed render context. This makes
|
||||||
|
missing context fields, bad template syntax, unreadable partials, and execution
|
||||||
|
failures actionable with template or partial context.
|
||||||
|
|
||||||
## Inputs And Outputs
|
Top-level templates decide which shared partials they invoke. The current
|
||||||
|
partials cover daypart forecast variants, alert digest, and precipitation
|
||||||
|
timing. Template code receives curated typed contexts rather than raw data
|
||||||
|
packages, and it must not reimplement weather selection or generated-text
|
||||||
|
validation.
|
||||||
|
|
||||||
Inputs:
|
## Boundaries and verification
|
||||||
|
|
||||||
- template ID from a report definition
|
This package does not collect weather data, build modules, validate generated
|
||||||
- typed render context built by `internal/generatedtext`
|
text, construct contexts, resolve report definitions, write state, execute
|
||||||
|
Scriptorium, or upload reports. It produces Markdown bytes for application
|
||||||
|
orchestration to persist.
|
||||||
|
|
||||||
Outputs:
|
Focused tests cover asset lookup, schema availability, rendering, partial
|
||||||
|
behavior, missing keys, and malformed context:
|
||||||
|
|
||||||
- template source for inspection and tests
|
```sh
|
||||||
- GeneratedText schema bytes for prompt/schema configuration
|
go test ./internal/reporttemplate
|
||||||
- rendered Markdown bytes for app orchestration to persist
|
```
|
||||||
|
|
||||||
The implemented template IDs are `daily`, `today`, `tomorrow`, and `hourly`.
|
Embedded assets stay as separate files, shared fragments stay under the partial
|
||||||
The implemented schema IDs are also `daily`, `today`, `tomorrow`, and
|
directory, and generated-text schemas describe prose slots rather than
|
||||||
`hourly`, backed by matching `*.generated_text.schema.json` files.
|
deterministic weather facts.
|
||||||
|
|
||||||
Generated-text prompt sources are maintained under
|
|
||||||
`internal/reporttemplate/prompts/`, including Daily's
|
|
||||||
`daily.generated_text.md` source for prompt ID `weather.daily_generated_text`.
|
|
||||||
|
|
||||||
## Boundaries
|
|
||||||
|
|
||||||
This package owns embedded asset lookup, Go template parsing, and Markdown
|
|
||||||
template execution. It does not collect weather data, build module outputs,
|
|
||||||
validate GeneratedText, construct render contexts, choose report definitions,
|
|
||||||
write artifacts, invoke Scriptorium, or notify distributor.
|
|
||||||
|
|
||||||
GeneratedText validation is owned by `internal/generatedtext`. App
|
|
||||||
orchestration uses `internal/generatedtext` catalog lookup to connect
|
|
||||||
`internal/report` definition schema/template IDs to the matching validator,
|
|
||||||
render-context builder, and embedded assets.
|
|
||||||
|
|
||||||
## Template Contracts
|
|
||||||
|
|
||||||
Daily, Today, Tomorrow, and Hourly rendering use typed render contexts with:
|
|
||||||
|
|
||||||
- report metadata labels such as title, location, valid period, and generation
|
|
||||||
time
|
|
||||||
- validated GeneratedText prose slots
|
|
||||||
- deterministic labels derived from module outputs, including current
|
|
||||||
conditions, hourly forecast rows, precipitation timing, alerts, SPC outlooks,
|
|
||||||
forecast discussion, SPC discussion, and weather story
|
|
||||||
|
|
||||||
Daily, Today, and Tomorrow additionally expose forecast-date labels, ordered
|
|
||||||
daypart forecast rows, daily/daypart summaries, planning facts, and a
|
|
||||||
multi-paragraph forecast discussion generated-text slot. The ordered daypart
|
|
||||||
slice is built in Go so templates do not range over maps.
|
|
||||||
|
|
||||||
The Daily template asset uses the same Markdown structure as Tomorrow's
|
|
||||||
template and renders from `generatedtext.DailyRenderContext`.
|
|
||||||
|
|
||||||
Templates use `text/template` with `missingkey=error`, so missing context fields
|
|
||||||
fail rendering instead of producing incomplete Markdown.
|
|
||||||
|
|
||||||
Daily and Tomorrow call the shared `daypart_forecast` partial. Today calls
|
|
||||||
`today_daypart_forecast` so it can omit elapsed or missing dayparts. Daily,
|
|
||||||
Today, Tomorrow, and Hourly call the shared `alert_digest` and
|
|
||||||
`precipitation_timing` partials. Partial files are parsed with each top-level
|
|
||||||
template at render time and receive the same typed render context as the
|
|
||||||
caller. The `alert_digest` partial renders the combined Alerts and Risk
|
|
||||||
Products section from relevant NWS alerts and curated SPC outlook digest
|
|
||||||
records.
|
|
||||||
|
|
||||||
## Schema Contract
|
|
||||||
|
|
||||||
The GeneratedText schemas describe the structured prose Scriptorium is expected
|
|
||||||
to write for each generated-text prompt. Hourly requires:
|
|
||||||
|
|
||||||
- `summary`
|
|
||||||
- `forecast_discussion`
|
|
||||||
|
|
||||||
Daily, Today, and Tomorrow require `summary` and a nonempty
|
|
||||||
`forecast_discussion` array. All generated-text schemas allow optional
|
|
||||||
`precipitation_timing` and `confidence`, and reject additional properties.
|
|
||||||
Weather truth remains in module outputs; GeneratedText is limited to prose
|
|
||||||
slots consumed by the template.
|
|
||||||
|
|
||||||
## Failure Behavior
|
|
||||||
|
|
||||||
- Unknown template IDs return actionable lookup errors.
|
|
||||||
- Unknown schema IDs return actionable lookup errors.
|
|
||||||
- Template parse errors include the template ID.
|
|
||||||
- Partial read or parse errors include the partial path.
|
|
||||||
- Template execution errors include the template ID and usually identify the
|
|
||||||
missing context field.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Inspect:
|
|
||||||
|
|
||||||
- `internal/reporttemplate/reporttemplate_test.go`
|
|
||||||
- `internal/generatedtext/render_context_test.go`
|
|
||||||
- `internal/app/app_test.go`
|
|
||||||
- `internal/cli/root_test.go`
|
|
||||||
|
|
||||||
## Invariants
|
|
||||||
|
|
||||||
- Embedded templates and schemas live as separate files, not inline Go strings.
|
|
||||||
- Shared Markdown partials live under `templates/partials/`.
|
|
||||||
- Report definitions select templates by ID.
|
|
||||||
- Templates render from curated render contexts, not raw data packages.
|
|
||||||
- GeneratedText schemas describe LLM prose slots, not deterministic weather
|
|
||||||
facts.
|
|
||||||
|
|||||||
@@ -1,110 +1,51 @@
|
|||||||
# Scriptorium Adapter Internals
|
# Scriptorium Adapter Internals
|
||||||
|
|
||||||
This document describes the subprocess adapter in
|
`internal/adapters/scriptorium` translates Weatherreporter render requests to
|
||||||
`internal/adapters/scriptorium`.
|
Scriptorium process arguments and translates process results back to local
|
||||||
|
types. The external CLI and output contract belongs to the
|
||||||
|
[Scriptorium integration guide](../integrations/scriptorium.md); prompts,
|
||||||
|
template inputs, and report ownership remain outside this adapter.
|
||||||
|
|
||||||
## Purpose
|
## Request-to-command translation
|
||||||
|
|
||||||
The adapter runs `scriptorium render` for prompt preflight and `scriptorium run`
|
`Runner` accepts a binary, config path, profile, timeout, extra arguments, and
|
||||||
for Markdown report generation or structured generated-text output. It isolates
|
an injectable command executor. Its defaults are the `scriptorium` binary and
|
||||||
subprocess execution, argv construction, timeout handling, output capture, and
|
the real `ExecRunner`. Optional configuration flags are placed before the
|
||||||
exit-code interpretation from app and domain packages.
|
operation-specific arguments, and extra arguments are appended last.
|
||||||
|
|
||||||
## Inputs And Outputs
|
| Local operation | Required values | Translated arguments |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Render` | prompt ID, data-package path | `render [--config …] [--profile …] --prompt <id> --input data_package=<path> --format json [extra …]` |
|
||||||
|
| `Run` | prompt ID, data-package path, output path | `run [--config …] [--profile …] --prompt <id> --input data_package=<path> --out <path> [extra …]` |
|
||||||
|
| `StructuredRun` | prompt ID, data-package path, output path | Same translation as `Run` |
|
||||||
|
|
||||||
Inputs:
|
Blank required values fail before a command starts. The adapter does not add
|
||||||
|
schema flags or interpret a prompt's payload; it only gives Scriptorium the
|
||||||
|
named `data_package` input.
|
||||||
|
|
||||||
- prompt ID
|
## Command execution and result translation
|
||||||
- YAML prompt input data package path
|
|
||||||
- report output path for `run`
|
|
||||||
- raw generated-text output path for structured `run`
|
|
||||||
- configured binary, config path, profile, timeout, and extra arguments
|
|
||||||
- context for cancellation
|
|
||||||
|
|
||||||
Outputs:
|
`ExecRunner` uses `exec.CommandContext`, never a shell. A positive configured
|
||||||
|
timeout creates a child context. Standard output and standard error are
|
||||||
|
captured independently, each with a 1 MiB limit, and the executed command is
|
||||||
|
retained for diagnostics.
|
||||||
|
|
||||||
- argv used for execution
|
`RenderResult`, `RunResult`, and `StructuredRunResult` expose the command,
|
||||||
- captured stdout and stderr
|
captured output, truncation markers, and exit code. Run results also retain the
|
||||||
- truncation flags for captured output
|
requested output path. Exit status zero is successful. A nonzero process exit
|
||||||
- exit code
|
returns its result and an error, while a start failure, cancellation, or
|
||||||
- report output path for `run`
|
deadline failure returns no result and the execution error.
|
||||||
- raw generated-text output path for structured `run`
|
|
||||||
|
|
||||||
## Boundaries
|
The adapter does not parse rendered JSON, validate a generated report, write
|
||||||
|
state, or upload a report. Those responsibilities sit with
|
||||||
|
[application orchestration](app-orchestration.md), [state internals](state.md), and the
|
||||||
|
relevant delivery adapter.
|
||||||
|
|
||||||
`internal/adapters/scriptorium` owns Scriptorium command construction and
|
## Verification
|
||||||
subprocess execution. It does not choose report types, build prompt input,
|
|
||||||
collect weather data, decide workflow order, or persist workflow metadata.
|
|
||||||
|
|
||||||
The adapter exposes request and result structs for render, Markdown run, and
|
Focused tests cover argument order, validation, bounded capture, timeout and
|
||||||
structured generated-text run operations. State persistence uses state-owned
|
cancellation handling, and exit-status translation:
|
||||||
artifact shapes; app orchestration converts adapter results before saving.
|
|
||||||
|
|
||||||
## Config Fields Used
|
```sh
|
||||||
|
go test ./internal/adapters/scriptorium
|
||||||
- `scriptorium.binary`
|
|
||||||
- `scriptorium.config_path`
|
|
||||||
- `scriptorium.profile`
|
|
||||||
- `scriptorium.timeout`
|
|
||||||
- `scriptorium.extra_args`
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
Render preflight argv starts with:
|
|
||||||
|
|
||||||
```text
|
|
||||||
scriptorium render --prompt <prompt_id> --input data_package=<path> --format json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Report generation argv starts with:
|
|
||||||
|
|
||||||
```text
|
|
||||||
scriptorium run --prompt <prompt_id> --input data_package=<path> --out <path>
|
|
||||||
```
|
|
||||||
|
|
||||||
Structured generated-text argv uses the same `scriptorium run` form, with the
|
|
||||||
`--out` value set to the raw generated-text JSON artifact path. The adapter
|
|
||||||
does not add `--format`, schema path, or JSON Schema flags for structured
|
|
||||||
generation; Scriptorium selects the structured output schema from prompt
|
|
||||||
configuration.
|
|
||||||
|
|
||||||
Configured `--config` and `--profile` flags are inserted after the subcommand
|
|
||||||
and before prompt-specific arguments. Extra arguments are appended after the
|
|
||||||
built-in arguments.
|
|
||||||
|
|
||||||
## Execution Behavior
|
|
||||||
|
|
||||||
The adapter runs commands without shell interpolation. The same private
|
|
||||||
execution path is used by render, Markdown run, and structured run after
|
|
||||||
command-specific request validation and argv construction.
|
|
||||||
|
|
||||||
When `scriptorium.timeout` is greater than zero, each subprocess call uses a
|
|
||||||
context with that timeout. Stdout and stderr are captured separately, capped at
|
|
||||||
1 MiB each, and marked as truncated when the cap is reached.
|
|
||||||
|
|
||||||
## Failure Behavior
|
|
||||||
|
|
||||||
- Missing prompt ID or data package path returns an error before subprocess
|
|
||||||
execution.
|
|
||||||
- Missing run output path returns an error before subprocess execution.
|
|
||||||
- Subprocess start errors, context cancellation, and timeouts are wrapped with
|
|
||||||
operation context by the caller-facing method.
|
|
||||||
- Nonzero render, Markdown run, and structured run exits return the captured
|
|
||||||
result plus an error containing the exit code and stderr.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Inspect:
|
|
||||||
|
|
||||||
- `internal/adapters/scriptorium/runner_test.go`
|
|
||||||
- `internal/app/app_test.go`
|
|
||||||
- `internal/cli/root_test.go`
|
|
||||||
|
|
||||||
## Invariants
|
|
||||||
|
|
||||||
- No shell interpolation is used.
|
|
||||||
- The Scriptorium input name is `data_package`.
|
|
||||||
- The file at the data package path is YAML produced by `internal/promptinput`.
|
|
||||||
- Render, Markdown run, and structured run preserve command-specific result
|
|
||||||
structs.
|
|
||||||
- Scriptorium-specific flags stay inside adapter and config boundaries.
|
|
||||||
|
|||||||
@@ -1,178 +1,91 @@
|
|||||||
# State Internals
|
# State Internals
|
||||||
|
|
||||||
This document describes filesystem state in `internal/state`.
|
The `internal/state` package owns filesystem-backed run state: safe path
|
||||||
|
derivation, metadata persistence, prior-report lookup, and read-only report
|
||||||
|
inspection. It does not decide which reports to generate or deliver. For the
|
||||||
|
operator-facing layout and retention procedures, see the
|
||||||
|
[operations guide](../operations.md).
|
||||||
|
|
||||||
## Purpose
|
## Store construction and artifact paths
|
||||||
|
|
||||||
`internal/state` owns managed workspace paths, atomic JSON writes, persisted
|
`NewFilesystemStore` requires a workspace root and rejects absolute or
|
||||||
metadata, prior snapshot lookup, and read-only artifact inspection helpers.
|
escaping values for every configured state directory. `Paths` then validates a
|
||||||
|
run ID and artifact group before deriving all paths from the report's valid
|
||||||
|
start date (`YYYY-MM-DD`). This keeps a run's artifacts together while making
|
||||||
|
the paths safe to use below the configured workspace.
|
||||||
|
|
||||||
## Inputs And Outputs
|
| Artifact | Derived location |
|
||||||
|
| --- | --- |
|
||||||
|
| Module snapshot | `snapshots/<group>/<date>/modules.<run-id>.json` |
|
||||||
|
| Metadata | `snapshots/<group>/<date>/metadata.<run-id>.json` |
|
||||||
|
| Data package | `data-packages/<group>/<date>/data_package.<run-id>.yaml` |
|
||||||
|
| Render preflight | `preflight/<group>/<date>/render.<run-id>.json` |
|
||||||
|
| Notification record | `notifications/<group>/<date>/distributor.<run-id>.json` |
|
||||||
|
| Managed report | `reports/<group>/<date>/report.<run-id>.md` |
|
||||||
|
| Generated text | `snapshots/<group>/<date>/generated_text.<run-id>.json` |
|
||||||
|
| Generated-text source and result | `snapshots/<group>/<date>/generated_text_raw.<run-id>.json` and `generated_text_result.<run-id>.json` |
|
||||||
|
| Generated-text render context | `snapshots/<group>/<date>/render_context.<run-id>.json` |
|
||||||
|
|
||||||
Inputs:
|
The configured notification root separates notification artifacts from report
|
||||||
|
artifacts; single-report notification paths use the report's valid date. Report
|
||||||
|
producers create parent directories as needed and write the report body; state
|
||||||
|
is responsible for the surrounding paths and saved run artifacts.
|
||||||
|
|
||||||
- workspace configuration
|
Batch Distributor notifications are derived separately as
|
||||||
- resolved report definition and valid period
|
`notifications/batches/<batch>/<local-date>/distributor.<batch-run-id>.json`.
|
||||||
- module snapshot
|
Their date is calculated from the batch start in its configured location, and
|
||||||
- prompt input data package
|
the batch identity and run ID receive the same path-segment validation as
|
||||||
- preflight artifact
|
single-report artifact identifiers.
|
||||||
- generated-text raw, run-result, validated text, and render-context artifacts
|
|
||||||
- rendered report path preparation request
|
|
||||||
- RunID for inspection lookups
|
|
||||||
|
|
||||||
Outputs:
|
## Metadata and durable writes
|
||||||
|
|
||||||
- module snapshot JSON path
|
`Metadata` is the durable inventory for a run. It records its schema version,
|
||||||
- prompt input data package YAML path
|
run identity, generated and valid timestamps, artifact group and mode, source
|
||||||
- render preflight JSON path
|
content and provenance, and the module snapshot, data-package, preflight,
|
||||||
- generated-text raw JSON path
|
report, generated-artifact, and notification locations when present.
|
||||||
- generated-text run-result JSON path
|
|
||||||
- validated generated-text JSON path
|
|
||||||
- render context JSON path
|
|
||||||
- managed Markdown report path
|
|
||||||
- metadata JSON path
|
|
||||||
- distributor notification debug artifact paths
|
|
||||||
- prior comparable snapshot metadata
|
|
||||||
- loaded module snapshot, data package, generated text, generated-text run
|
|
||||||
result, or render context
|
|
||||||
- recent report records for inspection
|
|
||||||
|
|
||||||
## Boundaries
|
`BuildMetadataFromBriefingMetadata` establishes the common fields; the
|
||||||
|
application adds locations as artifacts are produced. `SaveMetadata` requires
|
||||||
|
the run ID and the module snapshot, data-package, preflight, and metadata
|
||||||
|
paths. The package also saves module snapshots, data packages, preflight
|
||||||
|
records, generated-text artifacts, render contexts, and notifications. JSON
|
||||||
|
writes use `fileutil.WriteJSONAtomic`, so readers do not observe a partially
|
||||||
|
written state file.
|
||||||
|
|
||||||
`internal/state` owns local filesystem layout, path validation, durable writes,
|
The data package itself follows the shared
|
||||||
metadata reads, prior lookup, and report listing. It does not fetch weather
|
[prompt-input contract](prompt-input.md). Report text, templates, and external
|
||||||
data, derive forecasts, build prompt input content, compare module contents,
|
delivery payloads remain owned by their respective packages and integration
|
||||||
invoke Scriptorium, import adapter result types, or parse CLI flags.
|
references.
|
||||||
|
|
||||||
Preflight persistence uses the state-owned `PreflightArtifact` shape. The app
|
## Prior reports and inspection
|
||||||
converts adapter render results into that shape before saving.
|
|
||||||
|
|
||||||
## Config Fields Used
|
`FindPriorSnapshot` searches metadata rather than guessing from filenames. It
|
||||||
|
only considers an earlier compatible report in the same artifact group and
|
||||||
|
supports the comparison strategies defined by the report request:
|
||||||
|
|
||||||
- `workspace.root`
|
- `same_valid_date` finds an earlier generated report for the same valid day.
|
||||||
- `workspace.snapshots_dir`
|
- `weekend_window` finds a prior comparable weekend window.
|
||||||
- `workspace.reports_dir`
|
|
||||||
- `workspace.data_packages_dir`
|
|
||||||
- `workspace.preflight_dir`
|
|
||||||
- `workspace.notifications_dir`
|
|
||||||
|
|
||||||
Workspace subdirectories must be relative paths that stay under
|
The newest eligible metadata record wins; the current run is excluded.
|
||||||
`workspace.root`.
|
Unreadable or malformed candidate metadata is ignored so a damaged historical
|
||||||
|
record does not block a new run.
|
||||||
|
|
||||||
## Managed Layout
|
`ListReports` walks saved metadata, returns results ordered newest-first by
|
||||||
|
generation time, and treats a missing snapshots directory as an empty history.
|
||||||
|
`LoadMetadataByRunID` builds on that inspection path. These APIs are read-only;
|
||||||
|
repairing or pruning stored state is an operational concern.
|
||||||
|
|
||||||
Paths are derived from the resolved report definition's artifact group, the
|
## Boundaries and verification
|
||||||
valid-period start date, and the RunID. Filenames put the artifact kind before
|
|
||||||
the RunID.
|
|
||||||
|
|
||||||
```text
|
The package rejects unsafe path components and incomplete metadata before
|
||||||
<workspace.root>/
|
writing. Callers must provide a valid report request, artifact group, and
|
||||||
reports/<artifact_group>/<YYYY-MM-DD>/report.<run_id>.md
|
store configuration. Its focused tests cover path derivation, atomic
|
||||||
snapshots/<artifact_group>/<YYYY-MM-DD>/modules.<run_id>.json
|
persistence, metadata validation, comparison eligibility, and report listing:
|
||||||
snapshots/<artifact_group>/<YYYY-MM-DD>/metadata.<run_id>.json
|
|
||||||
snapshots/<artifact_group>/<YYYY-MM-DD>/generated_text_raw.<run_id>.json
|
```sh
|
||||||
snapshots/<artifact_group>/<YYYY-MM-DD>/generated_text_result.<run_id>.json
|
go test ./internal/state
|
||||||
snapshots/<artifact_group>/<YYYY-MM-DD>/generated_text.<run_id>.json
|
|
||||||
snapshots/<artifact_group>/<YYYY-MM-DD>/render_context.<run_id>.json
|
|
||||||
data-packages/<artifact_group>/<YYYY-MM-DD>/data_package.<run_id>.yaml
|
|
||||||
preflight/<artifact_group>/<YYYY-MM-DD>/render.<run_id>.json
|
|
||||||
notifications/<artifact_group>/<YYYY-MM-DD>/distributor.<run_id>.json
|
|
||||||
notifications/batches/<batch>/<YYYY-MM-DD>/distributor.<batch_run_id>.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Metadata is stored beside module snapshots and links the module snapshot, data
|
See [application orchestration](app-orchestration.md) for the order in which
|
||||||
package, preflight, report paths, notification path when attempted, and
|
these artifacts are created and [report templates](../templates.md) for the
|
||||||
configured prompt location. For generated-text-template reports, metadata also
|
user-facing report contract.
|
||||||
records the generated text schema ID and links the raw generated text,
|
|
||||||
Scriptorium run result, validated generated text, and render context artifacts.
|
|
||||||
Markdown-report metadata omits those generated-text fields. Report listing
|
|
||||||
walks metadata files under the snapshots directory.
|
|
||||||
|
|
||||||
Batch notification artifacts are stored under the notifications tree rather
|
|
||||||
than report metadata because they describe a batch-level upload. The date
|
|
||||||
directory is the batch start date in the effective report timezone.
|
|
||||||
|
|
||||||
## Prior Lookup
|
|
||||||
|
|
||||||
Prior snapshot lookup reads stored metadata through the shared lookup path and
|
|
||||||
selects the latest earlier snapshot whose report ID is compatible with the
|
|
||||||
current report definition.
|
|
||||||
|
|
||||||
- Daily Report compares with prior Daily Report snapshots for the same valid
|
|
||||||
local date.
|
|
||||||
- Today Report compares with prior Today Report snapshots for the same valid
|
|
||||||
local date.
|
|
||||||
- Tomorrow Report compares with prior Tomorrow Report snapshots for the same
|
|
||||||
valid local date.
|
|
||||||
- 3-Day Outlook compares with prior 3-Day snapshots for the same valid local
|
|
||||||
date.
|
|
||||||
- Weekend Outlook compares with prior Weekend snapshots for the same weekend
|
|
||||||
window.
|
|
||||||
- Hourly Report uses the rolling-window comparison strategy and currently
|
|
||||||
returns no prior snapshot from filesystem lookup.
|
|
||||||
- Storm Report has no prior lookup because explicit event-window comparison is
|
|
||||||
not searched by the filesystem store.
|
|
||||||
|
|
||||||
## Writes And Inspection
|
|
||||||
|
|
||||||
Durable JSON writes use shared atomic file helpers. Generated-text raw and
|
|
||||||
validated JSON artifacts are written atomically as bytes; generated-text run
|
|
||||||
result and render context artifacts are written atomically as JSON. Managed
|
|
||||||
Markdown reports are prepared by creating their parent directory; Scriptorium
|
|
||||||
writes the report body to the prepared path. Extra Markdown copies are handled
|
|
||||||
by app orchestration. Distributor notification debug artifacts are written
|
|
||||||
atomically when notification is attempted and include rendered distributor
|
|
||||||
pipeline ID, bundle ID, idempotency key, bundle paths, upload status, latest
|
|
||||||
run status, and redacted errors.
|
|
||||||
|
|
||||||
Single-report notification artifacts use schema version
|
|
||||||
`weatherreporter.distributor_notification.v1` and record one managed source
|
|
||||||
path plus that source's bundle paths. Batch notification artifacts use schema
|
|
||||||
version `weatherreporter.batch_distributor_notification.v1` and record:
|
|
||||||
|
|
||||||
- `batch`
|
|
||||||
- `batchRunId`
|
|
||||||
- `attemptedAt`
|
|
||||||
- `endpoint`
|
|
||||||
- `pipelineId`
|
|
||||||
- `bundleId`
|
|
||||||
- `idempotencyKey`
|
|
||||||
- `bundleCreated`
|
|
||||||
- `includedReports`, each with `reportId`, `runId`, `sourcePath`, and
|
|
||||||
`bundlePaths`
|
|
||||||
- `status`
|
|
||||||
- `upload`
|
|
||||||
- `runStatus`
|
|
||||||
- `statusError`
|
|
||||||
- `error`
|
|
||||||
|
|
||||||
Inspection helpers read existing metadata, module snapshot, data package,
|
|
||||||
generated text, generated-text run result, and render context files. Missing
|
|
||||||
metadata directories return no inspection records or no prior snapshot rather
|
|
||||||
than creating state.
|
|
||||||
|
|
||||||
## Failure Behavior
|
|
||||||
|
|
||||||
- Invalid workspace paths return validation errors.
|
|
||||||
- Missing required metadata fields prevent metadata writes.
|
|
||||||
- JSON writes use a temporary file followed by rename where practical.
|
|
||||||
- Read and decode failures include path context.
|
|
||||||
- Unknown RunIDs produce an actionable lookup error.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Inspect:
|
|
||||||
|
|
||||||
- `internal/state/filesystem_test.go`
|
|
||||||
- `internal/app/app_test.go`
|
|
||||||
|
|
||||||
## Invariants
|
|
||||||
|
|
||||||
- Managed paths stay under the configured workspace root.
|
|
||||||
- Artifact grouping comes from report definitions.
|
|
||||||
- Metadata links artifacts produced for a run.
|
|
||||||
- Generated-text artifacts live under the snapshots tree beside module
|
|
||||||
snapshots and metadata.
|
|
||||||
- Batch notification artifacts live under `notifications/batches` and are not
|
|
||||||
linked from report metadata.
|
|
||||||
- Prior lookup is based on structured metadata, not rendered report text.
|
|
||||||
|
|||||||
@@ -1,101 +1,69 @@
|
|||||||
# Weather Data Internals
|
# Weather Data Internals
|
||||||
|
|
||||||
This document describes Weather API ingestion into `weatherdata.Bundle`.
|
`internal/weatherdata` owns the normalized, wire-independent weather bundle
|
||||||
|
that passes from collection through rendering and persistence. The Weather API
|
||||||
|
adapter translates provider responses into these types; its request, response,
|
||||||
|
and availability contract is documented in the
|
||||||
|
[Weather API integration guide](../integrations/weatherapi.md).
|
||||||
|
|
||||||
## Purpose
|
## Bundle contract
|
||||||
|
|
||||||
`internal/adapters/weatherapi` fetches normalized weather data from the
|
`Bundle` has a collection timestamp (`FetchedAt`), source provenance
|
||||||
configured Weather API and assembles the bundle consumed by forecast derivation
|
(`Sources`), and collection-level warnings (`Warnings`). Its product fields are
|
||||||
and module builders. Module builders expose normalized current conditions and
|
optional so an allowed missing source can be represented without manufacturing
|
||||||
weather story context when those sources are available.
|
weather data.
|
||||||
|
|
||||||
## Inputs And Outputs
|
| Field | Normalized product |
|
||||||
|
| --- | --- |
|
||||||
|
| `Observation` | Station observation |
|
||||||
|
| `Current` | Current conditions |
|
||||||
|
| `Hourly` | Hourly forecast periods |
|
||||||
|
| `Narrative` | Narrative forecast |
|
||||||
|
| `Alerts` | Active-alert check, including an explicitly empty result |
|
||||||
|
| `Discussion` | Forecast discussion and its time-range sections |
|
||||||
|
| `Daily` | Daily forecast periods when supplied |
|
||||||
|
| `WeatherStory` | Latest weather story |
|
||||||
|
| `SPCConvectiveOutlooks` | Convective outlook run, discussions, and GeoJSON geometry |
|
||||||
|
|
||||||
Inputs:
|
The bundle carries values rather than provider request details. Consumers use
|
||||||
|
it to construct report facts and data packages; they should not infer a
|
||||||
|
provider endpoint or retry policy from the normalized types. See
|
||||||
|
[collection](collect.md) for assembly and
|
||||||
|
[report templates](../templates.md) for the values exposed to authors.
|
||||||
|
|
||||||
- `config.Config` with Weather API URL, timeout, format, units, timezone,
|
## Source provenance
|
||||||
precision, and missing-source policy
|
|
||||||
- HTTP responses using the Weather API `data` envelope
|
|
||||||
|
|
||||||
Outputs:
|
Every checked source is represented by a `Source` entry. The record identifies
|
||||||
|
the source (`Name`), request location and query (`Endpoint`, `Query`), fetch
|
||||||
|
time, provider issue and update times when available, a SHA-256 digest of the
|
||||||
|
source data, and whether the source was unavailable (`Missing`). Its warnings
|
||||||
|
stay with that source in addition to the bundle-level warning list.
|
||||||
|
|
||||||
- `weatherdata.Bundle` with observation, current conditions, hourly forecast,
|
An empty product can be meaningful checked data. For example, an explicit
|
||||||
narrative forecast, active alerts, discussion, latest weather story, source
|
empty alerts result is not missing and retains its source hash. A source is
|
||||||
records, source warnings, and typed SPC convective outlook data when that
|
marked missing only when the adapter's missing-source policy treats the
|
||||||
optional source is available
|
response or parsing failure as unavailable. The policy itself belongs to the
|
||||||
- optional saved bundle JSON through app fetch helpers
|
[configuration reference](../config.md).
|
||||||
|
|
||||||
## Boundaries
|
## Warning semantics
|
||||||
|
|
||||||
- The adapter owns HTTP calls, response-envelope handling, source hashing, and
|
`SourceWarning` has a source name, stable code, severity, explanatory message,
|
||||||
decoding into internal bundle types.
|
endpoint, and `CompletenessImpact`. When collection proceeds with a warning,
|
||||||
- It does not derive dayparts, resolve report periods, build module values, compare
|
the same warning appears in `Source.Warnings` and `Bundle.Warnings` so both
|
||||||
snapshots, write report state, or invoke Scriptorium.
|
local provenance and whole-run consumers see it. A policy that treats a missing
|
||||||
|
source as an error returns no partial bundle.
|
||||||
|
|
||||||
## Config Fields Used
|
Warnings describe data completeness, not rendering or delivery failures.
|
||||||
|
Those failures are recorded by the application and state layers; see
|
||||||
|
[application orchestration](app-orchestration.md) and [state internals](state.md).
|
||||||
|
|
||||||
- `weather_api.base_url`
|
## Boundaries and verification
|
||||||
- `weather_api.timeout`
|
|
||||||
- `weather_api.format`
|
|
||||||
- `weather_api.units`
|
|
||||||
- `weather_api.timezone`
|
|
||||||
- `weather_api.precision`
|
|
||||||
- `missing_source.default`
|
|
||||||
- `missing_source.sources`
|
|
||||||
|
|
||||||
## External Adapters Used
|
This package defines data shapes and has no HTTP client, configuration loader,
|
||||||
|
filesystem access, or template behavior. Focused tests cover the normalized
|
||||||
|
types and the Weather API adapter verifies translation into them:
|
||||||
|
|
||||||
- Weather API HTTP service
|
```sh
|
||||||
|
go test ./internal/weatherdata
|
||||||
See [Weather API integration](../integrations/weatherapi.md) for the external
|
go test ./internal/adapters/weatherapi
|
||||||
contract used by this project.
|
```
|
||||||
|
|
||||||
## State Or Manifest Behavior
|
|
||||||
|
|
||||||
The adapter records source name, endpoint, query, fetch time, source timestamps
|
|
||||||
when available, SHA-256 hash over compact raw `data` JSON, missing status, and
|
|
||||||
source warnings. Successful `data: null` responses from `/alerts/active`
|
|
||||||
represent a checked empty active-alert list, not a missing source. Successful
|
|
||||||
non-null `/outlooks/convective` responses with empty outlook and discussion
|
|
||||||
arrays represent checked empty outlook data.
|
|
||||||
`app.FetchAndSaveBundle` can write bundle JSON atomically for inspection.
|
|
||||||
|
|
||||||
SPC convective outlook data is stored on
|
|
||||||
`weatherdata.Bundle.SPCConvectiveOutlooks`. The collected run keeps upstream
|
|
||||||
run metadata, location identifiers, ordered outlook records, discussion
|
|
||||||
records, and each outlook's raw GeoJSON geometry. Source provenance for this
|
|
||||||
payload uses the `spc_convective_outlooks` source name, endpoint
|
|
||||||
`/outlooks/convective`, the query sent by the adapter, timestamps, and a hash
|
|
||||||
of the raw `data` object.
|
|
||||||
|
|
||||||
## Skip And Resume Behavior
|
|
||||||
|
|
||||||
No resume behavior. Optional missing or malformed sources may be omitted,
|
|
||||||
warned, or treated as errors according to missing-source policy. Hourly forecast
|
|
||||||
data is required and cannot be skipped.
|
|
||||||
|
|
||||||
## Failure Behavior
|
|
||||||
|
|
||||||
- Missing or invalid `weather_api.base_url` prevents client construction.
|
|
||||||
- HTTP errors, response read failures, and envelope decode failures include
|
|
||||||
endpoint context.
|
|
||||||
- Missing hourly data or hourly forecasts with no periods fail bundle fetch.
|
|
||||||
- Optional sources follow missing-source policy.
|
|
||||||
- Explicit `data: null` from `/alerts/active` produces an empty, non-missing
|
|
||||||
alert run.
|
|
||||||
- Explicit `data: null` from `/outlooks/convective` follows optional
|
|
||||||
missing-source policy.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Inspect:
|
|
||||||
|
|
||||||
- `internal/adapters/weatherapi/client_test.go`
|
|
||||||
- `internal/app/app_test.go`
|
|
||||||
|
|
||||||
## Invariants
|
|
||||||
|
|
||||||
- Weather facts come from normalized source data.
|
|
||||||
- Full hourly and narrative products are fetched; Go owns report-period
|
|
||||||
selection.
|
|
||||||
- Source provenance and warnings remain inspectable downstream.
|
|
||||||
|
|||||||
@@ -1,77 +1,54 @@
|
|||||||
# Weatherreporter Operations
|
# Weatherreporter Operations
|
||||||
|
|
||||||
This guide covers normal operation, generated artifacts, inspection, recovery,
|
This guide covers normal operation, managed workspace state, inspection,
|
||||||
and operational caveats. For symptom-specific diagnosis, see
|
recovery, and operational caveats. See the [CLI reference](cli.md) for complete
|
||||||
|
command syntax and the [configuration reference](config.md) for fields,
|
||||||
|
defaults, and notification templates. For symptom-based diagnosis, see
|
||||||
[Troubleshooting](troubleshooting.md).
|
[Troubleshooting](troubleshooting.md).
|
||||||
|
|
||||||
## Normal Workflow
|
## Normal Operation
|
||||||
|
|
||||||
Generation commands:
|
After configuring a Weather API endpoint, generate one report:
|
||||||
|
|
||||||
```text
|
```sh
|
||||||
weatherreporter generate daily --date 2026-05-29
|
weatherreporter generate today --out ./today.md
|
||||||
weatherreporter generate today
|
|
||||||
weatherreporter generate tomorrow
|
|
||||||
weatherreporter generate hourly
|
|
||||||
weatherreporter generate three-day
|
|
||||||
weatherreporter generate weekend
|
|
||||||
weatherreporter generate storm --start 2026-05-29T18:00 --end 2026-05-30T06:00
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Generation commands resolve a report period, collect a Weather API bundle,
|
A generation collects weather data, resolves the report period, builds and
|
||||||
build a rich JSON module snapshot, build a curated YAML prompt input data
|
persists the module snapshot and prompt data package, runs Scriptorium
|
||||||
package, run `scriptorium render`, and write managed artifacts under the
|
preflight, then produces the managed Markdown report. Daily, Today, Tomorrow,
|
||||||
configured workspace. Markdown-path reports then run `scriptorium run` directly
|
and Hourly reports additionally persist generated-text artifacts, validate the
|
||||||
to the managed Markdown report path.
|
structured generated text, and render Markdown from the validated text and
|
||||||
|
deterministic values.
|
||||||
|
|
||||||
`generate daily`, `generate today`, `generate tomorrow`, and `generate hourly`
|
The managed report and its final metadata are saved before single-report
|
||||||
use the generated-text-template workflow. They run structured `scriptorium run`
|
Distributor notification is attempted. `--out` writes an extra operator copy;
|
||||||
to raw GeneratedText JSON, validate the structured text, save a render context,
|
it never changes the managed report or upload source. A successful generate
|
||||||
and render the managed Markdown report from embedded templates. `generate
|
command prints its summary to stdout unless `--quiet` is used.
|
||||||
daily` requires `--date YYYY-MM-DD` for the selected local civil day.
|
|
||||||
`generate today` covers the selected or current local civil day. `generate
|
|
||||||
hourly` covers the six-hour rolling period from generation time in the
|
|
||||||
effective report timezone and is not included in `run morning` or
|
|
||||||
`run evening`.
|
|
||||||
|
|
||||||
When distributor notification is enabled, weatherreporter uploads the managed
|
Run a scheduled batch with the same configured collection:
|
||||||
Markdown report after report rendering succeeds and final metadata is saved.
|
|
||||||
`--out PATH` writes an extra Markdown copy for generated reports; it is not used
|
|
||||||
as the distributor upload source. Generate commands emit a compact JSON summary
|
|
||||||
to stdout by default. Use `--quiet` to suppress successful stdout for cron jobs
|
|
||||||
or other schedulers that only need nonzero exits and external logs.
|
|
||||||
|
|
||||||
Batch commands:
|
```sh
|
||||||
|
weatherreporter run morning --out-dir ./reports
|
||||||
```text
|
|
||||||
weatherreporter run morning
|
|
||||||
weatherreporter run evening
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`run morning` generates Today Report, Tomorrow Report, and a dated Daily Report
|
Each batch collects once before it plans reports. Morning runs Today, Tomorrow,
|
||||||
for each later future local civil day with complete hourly forecast coverage.
|
and every eligible dated Daily Report; evening runs Tomorrow and the same
|
||||||
`run evening` generates Tomorrow Report and the same eligible future Daily
|
eligible Daily Reports. Eligible Daily dates begin after tomorrow and require
|
||||||
reports. Future Daily expansion starts with the day after tomorrow. A Daily
|
complete hourly coverage for their entire local civil day. A batch continues
|
||||||
report is eligible only when the collected hourly forecast contains every
|
after an individual report fails and returns an aggregate failure when any
|
||||||
hourly period for that local civil day; partial days are skipped. Batch commands
|
report or batch notification fails.
|
||||||
collect weather data once before planning, and a collection failure stops the
|
|
||||||
batch before any report is generated.
|
|
||||||
|
|
||||||
After planning succeeds, batch commands print a JSON summary to stdout, write
|
`--out-dir` writes extra copies such as `today.md`, `tomorrow.md`, and
|
||||||
compact per-report status lines to stderr, continue independent reports after
|
`daily-YYYY-MM-DD.md`. These copies are never upload sources. Batch report
|
||||||
one report fails, and return nonzero when any report failed. Batch commands do
|
copies and notification behavior are summarized in the CLI result; use the
|
||||||
not upload each report independently. When distributor notification and batch
|
[CLI reference](cli.md) for its exact JSON and stderr contract.
|
||||||
notification are enabled, weatherreporter uploads one distributor bundle only
|
|
||||||
after every planned report succeeds. If any report fails, the batch upload is
|
|
||||||
skipped for the whole batch. `--out-dir PATH` writes extra Markdown copies
|
|
||||||
using report default filenames such as `today.md` and `tomorrow.md`; dynamic
|
|
||||||
Daily copies use `daily-YYYY-MM-DD.md`. These copies are not used as
|
|
||||||
distributor upload sources. Use `--quiet` to suppress successful batch summary
|
|
||||||
and status output; failures still return nonzero.
|
|
||||||
|
|
||||||
## Filesystem Layout
|
## Managed Workspace
|
||||||
|
|
||||||
The default workspace root is `workspace`.
|
The default workspace root is `workspace`. Artifact paths use the report
|
||||||
|
definition's artifact group, the valid-period start date in the effective
|
||||||
|
timezone, and the RunID:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
workspace/
|
workspace/
|
||||||
@@ -91,195 +68,89 @@ workspace/
|
|||||||
notifications/batches/<batch>/<YYYY-MM-DD>/distributor.<batch_run_id>.json
|
notifications/batches/<batch>/<YYYY-MM-DD>/distributor.<batch_run_id>.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Managed artifact filenames use the artifact kind and RunID, so repeated runs
|
The generated-text and render-context artifacts are written only by Daily,
|
||||||
for the same valid period do not overwrite each other. The date directory is
|
Today, Tomorrow, and Hourly reports. A report's metadata links the module
|
||||||
the valid-period start date in the effective report timezone. Generated-text
|
snapshot, data package, preflight artifact, managed report, and any available
|
||||||
artifacts are written only for Daily, Today, Tomorrow, and Hourly reports.
|
generated-text or single-report notification artifact. Batch notification
|
||||||
|
artifacts are separate batch-level records under `notifications/batches`.
|
||||||
|
|
||||||
## RunID And Metadata
|
RunIDs begin with the UTC generation timestamp and report ID. A Daily RunID
|
||||||
|
also contains its local valid date so multiple Daily reports in one batch have
|
||||||
RunIDs are based on generation time plus report ID. Reports that can be
|
different managed paths. Batch notification RunIDs contain the UTC batch start
|
||||||
generated more than once in a single command may append a report-specific
|
timestamp and batch name.
|
||||||
disambiguator. Daily appends the local valid date so multiple dynamic Daily
|
|
||||||
reports in one batch have distinct managed artifacts:
|
|
||||||
|
|
||||||
```text
|
|
||||||
20260529T100000.123456789Z_daily_2026-05-31
|
|
||||||
20260529T100000.123456789Z_today
|
|
||||||
```
|
|
||||||
|
|
||||||
Batch notification RunIDs use the batch start timestamp plus the batch command
|
|
||||||
name:
|
|
||||||
|
|
||||||
```text
|
|
||||||
20260529T100000.123456789Z_morning
|
|
||||||
20260529T220000.123456789Z_evening
|
|
||||||
```
|
|
||||||
|
|
||||||
Each generated report writes metadata that links:
|
|
||||||
|
|
||||||
- RunID, report ID, variant, and prompt ID
|
|
||||||
- generation time, timezone, and valid period
|
|
||||||
- source location, source hashes, and source warnings
|
|
||||||
- module snapshot path
|
|
||||||
- prompt input data package path
|
|
||||||
- preflight output path
|
|
||||||
- managed Markdown report path
|
|
||||||
- generated text schema ID and generated-text artifact paths for
|
|
||||||
generated-text-template reports
|
|
||||||
- distributor notification debug artifact path, when notification is attempted
|
|
||||||
|
|
||||||
Batch summaries include report status, error text when applicable, valid
|
|
||||||
period, and known artifact paths for each attempted report. Single-report
|
|
||||||
notification fields on report items are empty for batch commands. When a batch
|
|
||||||
notification is attempted, skipped, or fails, the summary includes one
|
|
||||||
top-level `notification` object with fields such as `status`, `reason`,
|
|
||||||
`runId`, `pipelineId`, `bundleId`, `idempotencyKey`, `path`,
|
|
||||||
`includedReports`, and `error`.
|
|
||||||
|
|
||||||
## Distributor Notification
|
## Distributor Notification
|
||||||
|
|
||||||
Distributor notification is configured with `notify.distributor` and is
|
When `notify.distributor.enabled` is enabled, a successful `generate`
|
||||||
disabled by default. For `generate <report>`, weatherreporter uploads the
|
uploads only the managed Markdown report after final metadata has been saved.
|
||||||
managed Markdown report path recorded in the report result and metadata. That
|
The extra copy from `--out` is never uploaded. A notification attempt writes
|
||||||
single source file is mapped to report-specific bundle paths. Extra copies
|
a redacted debug artifact at
|
||||||
written by `--out` or `--out-dir` are operator conveniences only.
|
`notifications/<artifact_group>/<YYYY-MM-DD>/distributor.<run_id>.json`; its
|
||||||
|
path is then recorded in report metadata.
|
||||||
|
|
||||||
For `run morning` and `run evening`, per-report notification is suppressed. If
|
Batches suppress per-report notification. When both Distributor and its batch
|
||||||
`notify.distributor.enabled` and `notify.distributor.batch.enabled` are both
|
notification are enabled, Weatherreporter submits one multi-report upload after
|
||||||
true, the batch uploads once after all reports finish successfully. The upload
|
every planned report succeeds. If any report fails, it records a top-level
|
||||||
contains one file mapping set per included report. Each mapping uses the
|
`skipped` notification with reason `one or more reports failed` and does not
|
||||||
managed Markdown report as the source and report-specific path templates for
|
call Distributor. If batch notification is disabled, a batch does not fall back
|
||||||
that report. All rendered bundle paths across the batch must be unique. If any
|
to individual uploads.
|
||||||
report fails, weatherreporter records a top-level
|
|
||||||
notification status of `skipped` with reason `one or more reports failed` and
|
|
||||||
does not call distributor. If batch notification is disabled, run commands do
|
|
||||||
not fall back to per-report uploads.
|
|
||||||
|
|
||||||
The rendered pipeline ID selects the configured distributor `http_upload`
|
A batch notification attempt writes
|
||||||
workflow. The default bundle ID is a stable logical source identity derived from
|
`notifications/batches/<batch>/<YYYY-MM-DD>/distributor.<batch_run_id>.json`.
|
||||||
producer name, location ID, and report ID:
|
A notification failure makes the batch fail but does not change successful
|
||||||
|
individual report items into failed items. The debug artifacts contain rendered
|
||||||
|
identifiers, managed source and bundle paths, upload and status results, and
|
||||||
|
redacted errors; they do not contain tokens.
|
||||||
|
|
||||||
```text
|
## Inspecting Stored Runs
|
||||||
weatherreporter.{location_id}.{report_id}
|
|
||||||
```
|
|
||||||
|
|
||||||
The default single-report idempotency key appends RunID to the rendered bundle
|
Inspection is read-only: it neither collects weather data nor invokes
|
||||||
ID so each report generation has a distinct retry identity. The default bundle
|
Scriptorium or Distributor. Start by finding a RunID:
|
||||||
path uses the valid-period start date, artifact group, and RunID. Batch bundle
|
|
||||||
IDs default to `weatherreporter.{location_id}.{batch}`, and batch idempotency
|
|
||||||
keys default to `{bundle_id}.{batch_run_id}`. Distributor owns destination
|
|
||||||
merge, retention, and derived snapshot behavior such as `latest`. For Daily,
|
|
||||||
the default report ID and artifact group values are both `daily`, and the
|
|
||||||
default output filename value is `daily.md`. For Today, the default report ID
|
|
||||||
and artifact group values are both `today`, and the batch output filename value
|
|
||||||
is `today.md`.
|
|
||||||
|
|
||||||
Single-report notification happens after final metadata save for generated
|
```sh
|
||||||
reports. Batch notification happens after all planned reports have finished and
|
|
||||||
only when all report generations succeeded. Collection, module snapshot,
|
|
||||||
data-package, render preflight, Scriptorium run, generated-text validation,
|
|
||||||
template rendering, and metadata-save failures do not trigger notification. A
|
|
||||||
single-report notification failure fails that report. A batch notification
|
|
||||||
failure makes the batch return nonzero and increments the aggregate failure
|
|
||||||
count, but individual report items remain succeeded.
|
|
||||||
|
|
||||||
Each notification attempt writes a debug artifact under `notifications/`.
|
|
||||||
Single-report artifacts live under
|
|
||||||
`notifications/<artifact_group>/<YYYY-MM-DD>/distributor.<run_id>.json`. Batch
|
|
||||||
artifacts live under
|
|
||||||
`notifications/batches/<batch>/<YYYY-MM-DD>/distributor.<batch_run_id>.json`,
|
|
||||||
where the date directory is the batch start date in the effective report
|
|
||||||
timezone. The artifact records the rendered pipeline ID, bundle ID,
|
|
||||||
idempotency key, managed source paths, bundle-relative paths, bundle created
|
|
||||||
timestamp, accepted upload response, and the latest distributor run status
|
|
||||||
response when available. Weatherreporter polls status until distributor reports
|
|
||||||
`succeeded` or `failed`, or until the configured notification timeout expires.
|
|
||||||
The run status includes the distributor status, error text, and raw run report
|
|
||||||
JSON, which can show actions such as `replace_older`, `skip_same`,
|
|
||||||
`skip_destination_newer`, or `failed`. Token values are not written.
|
|
||||||
|
|
||||||
Weatherreporter is responsible for selecting the managed Markdown report,
|
|
||||||
constructing a source bundle, and submitting it to the configured distributor
|
|
||||||
HTTP endpoint. Distributor remains responsible for destination routing,
|
|
||||||
publication, and any downstream Markdown-to-HTML transformation. Distributor
|
|
||||||
leaves destination files alone when they are not tracked by a newly uploaded
|
|
||||||
bundle, so existing uploaded dated report paths can remain available.
|
|
||||||
|
|
||||||
## Inspection
|
|
||||||
|
|
||||||
Inspection commands read existing workspace artifacts and emit JSON to stdout.
|
|
||||||
They do not collect weather data or run `scriptorium`.
|
|
||||||
|
|
||||||
```text
|
|
||||||
weatherreporter inspect reports --limit 10
|
weatherreporter inspect reports --limit 10
|
||||||
weatherreporter inspect metadata RUN_ID
|
weatherreporter inspect metadata RUN_ID
|
||||||
weatherreporter inspect modules RUN_ID
|
|
||||||
weatherreporter inspect data-package RUN_ID
|
|
||||||
weatherreporter inspect prior RUN_ID
|
|
||||||
weatherreporter inspect sources RUN_ID
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `inspect reports` to find RunIDs and artifact paths. Use
|
| Command | Reads |
|
||||||
`inspect metadata` to see the artifact links recorded for a run. Use
|
| --- | --- |
|
||||||
`inspect modules` to review the persisted ordered module snapshot with rich
|
| `inspect reports` | Metadata files under the workspace snapshots tree. |
|
||||||
template-facing values, and `inspect data-package` to review the curated prompt
|
| `inspect metadata RUN_ID` | Metadata located by RunID. |
|
||||||
package passed to Scriptorium. Use `inspect prior` to see the prior comparable
|
| `inspect modules RUN_ID` | The module snapshot path recorded in metadata. |
|
||||||
snapshot selected for Recent Changes, or `null` when none exists. Use
|
| `inspect data-package RUN_ID` | The data-package path recorded in metadata. |
|
||||||
`inspect sources` to review source provenance and warnings without dumping full
|
| `inspect prior RUN_ID` | The run metadata, then compatible earlier metadata for its comparison policy. |
|
||||||
weather payloads.
|
| `inspect sources RUN_ID` | Source provenance and warnings in the run metadata. |
|
||||||
|
|
||||||
## Recent Changes
|
A missing snapshots directory produces no listed reports. An unknown or empty
|
||||||
|
RunID is an error; use `inspect reports` to obtain a valid value.
|
||||||
Recent Changes are computed from structured module snapshots, not rendered
|
|
||||||
Markdown or YAML text.
|
|
||||||
|
|
||||||
Daily Report compares with prior Daily Report snapshots for the same valid
|
|
||||||
local date. Today Report compares with prior Today Report snapshots for the
|
|
||||||
same valid local date. Tomorrow Report compares with prior Tomorrow Report
|
|
||||||
snapshots for the same valid local date. 3-Day Outlook compares with prior
|
|
||||||
compatible 3-Day snapshots for the same valid local date. Weekend Outlook
|
|
||||||
compares with prior compatible Weekend snapshots for the same weekend window.
|
|
||||||
Hourly Report and Storm Report leave Recent Changes empty.
|
|
||||||
|
|
||||||
When no prior comparable snapshot exists, or no configured threshold is crossed,
|
|
||||||
`recentChanges.items` is empty.
|
|
||||||
|
|
||||||
## Recovery
|
## Recovery
|
||||||
|
|
||||||
A failed generation run may still leave useful artifacts:
|
Keep the workspace when a run fails: artifacts reached before the failure
|
||||||
|
remain available where they can be safely persisted.
|
||||||
|
|
||||||
- If `scriptorium render` returns a result with a nonzero exit code, the
|
- A preflight failure can leave the preflight artifact and metadata.
|
||||||
preflight JSON and metadata are written for inspection.
|
- A report-generation failure can leave the managed report, module snapshot,
|
||||||
- If `scriptorium run` exits nonzero after writing a report, the managed report
|
data package, and metadata.
|
||||||
and metadata remain available.
|
- A generated-text failure can leave raw text, the structured run result, or a
|
||||||
- Generated-text failures for Daily, Today, Tomorrow, and Hourly reports preserve
|
validated generated-text and render-context artifact, depending on where it
|
||||||
available intermediate artifacts, such as the structured run result, raw
|
stopped.
|
||||||
generated-text JSON, validated generated text, and render context. Metadata
|
- A single-report notification failure preserves the report and final metadata,
|
||||||
links those paths when it can be safely written.
|
including its notification artifact when it was written.
|
||||||
- If single-report distributor notification fails, report artifacts and final
|
- A batch notification failure preserves each report's artifacts and adds the
|
||||||
metadata remain available, but the report command returns nonzero.
|
top-level batch notification artifact.
|
||||||
- If batch distributor notification fails, report artifacts and final metadata
|
|
||||||
remain available, the top-level batch notification links the debug artifact,
|
|
||||||
and the batch command returns nonzero.
|
|
||||||
- For batch commands, inspect the stdout JSON summary first, then inspect the
|
|
||||||
artifact paths for each failed report or the top-level notification path.
|
|
||||||
|
|
||||||
For a bad report, start with:
|
Use the RunID from the action summary with the inspection commands above. For
|
||||||
|
a batch failure, inspect the summary first, then inspect the affected report
|
||||||
```text
|
RunIDs or the batch notification path. Do not remove the whole workspace as a
|
||||||
weatherreporter inspect metadata RUN_ID
|
first response; retain it until the failure is understood.
|
||||||
weatherreporter inspect sources RUN_ID
|
|
||||||
weatherreporter inspect modules RUN_ID
|
|
||||||
weatherreporter inspect data-package RUN_ID
|
|
||||||
weatherreporter inspect prior RUN_ID
|
|
||||||
```
|
|
||||||
|
|
||||||
## Operational Caveats
|
## Operational Caveats
|
||||||
|
|
||||||
- The application uses one configured Weather API endpoint.
|
- Workspace files, generated reports, and Scriptorium stderr can contain
|
||||||
- The application writes local filesystem state only.
|
sensitive operational context. Set appropriate filesystem permissions and do
|
||||||
- The application does not implement resume, cleanup, archive, remote storage,
|
not publish them unintentionally.
|
||||||
daemon operation, or automatic storm monitoring.
|
- Weatherreporter uses one configured Weather API endpoint and local workspace
|
||||||
- Generated reports and Scriptorium stderr can contain sensitive operational
|
state.
|
||||||
context. Store workspace artifacts with appropriate filesystem permissions.
|
- It does not provide automatic resume, cleanup, archival, remote state, daemon
|
||||||
|
operation, or automatic storm monitoring.
|
||||||
|
|||||||
@@ -1,125 +1,218 @@
|
|||||||
# Architecture
|
# Architecture Policy
|
||||||
|
|
||||||
This document defines the development principles for this Go project. It is inward-facing: developers and LLM coding agents should use it to preserve the project’s shape, boundaries, and invariants as the code evolves.
|
## Purpose
|
||||||
|
|
||||||
## weatherreporter
|
This policy defines Weatherreporter's system shape, normative ownership,
|
||||||
`weatherreporter` is a deterministic weather briefing and report-preparation application. It consumes normalized weather data from the internal weatherfeeder-backed API, derives report-specific module snapshots and prompt packages, compares module snapshots against prior runs, and invokes an external prompt runner to produce human-facing reports.
|
dependency direction, architectural invariants, safety properties, and
|
||||||
|
non-goals. Developers and coding agents should use it to preserve the
|
||||||
|
application's boundaries as the implementation evolves.
|
||||||
|
|
||||||
The application should keep meteorological data selection, daypart grouping, threshold detection, forecast-period resolution, and recent-change comparison inside Go domain packages. LLM prompts should receive curated module-based prompt packages rather than raw unbounded source payloads wherever practical.
|
The [development guide](../development.md) owns the current package inventory
|
||||||
|
and contributor workflow. Focused documents under `docs/internal/` own
|
||||||
|
implemented subsystem mechanics. This policy owns the rules those packages and
|
||||||
|
mechanics must preserve.
|
||||||
|
|
||||||
Report types must be defined through a registry or equivalent mechanism. Each report definition should declare its report ID, prompt ID, valid-period resolver, module composition, comparison strategy, and output naming behavior. Avoid scattering report-type conditionals across CLI and orchestration code.
|
## System Shape
|
||||||
|
|
||||||
Generated reports must be associated with explicit metadata, including report type, location, generation time, valid period, source product timestamps or hashes, module snapshot path, and output path. Recent Changes must be based on structured snapshot comparison rather than comparison of rendered Markdown report text.
|
Weatherreporter is a deterministic weather briefing and report-preparation CLI.
|
||||||
|
It consumes normalized weather data, derives report facts and module snapshots,
|
||||||
|
builds curated prompt packages, compares structured snapshots with prior runs,
|
||||||
|
and invokes Scriptorium either to produce managed Markdown directly or to
|
||||||
|
produce bounded generated-text prose for repository-owned templates. It
|
||||||
|
persists inspectable artifacts and can upload completed reports through
|
||||||
|
Distributor.
|
||||||
|
|
||||||
`scriptorium` is an external adapter, not domain logic. Subprocess execution must be isolated under `internal/adapters/scriptorium`, use context-aware execution, avoid shell interpolation, capture actionable stderr, and keep scriptorium-specific flags from leaking into domain packages.
|
The application is intentionally a small, explicit, dependency-light Go
|
||||||
|
program. Add abstraction only when it protects a real boundary, makes an
|
||||||
|
important invariant testable, or supports an implemented extension point.
|
||||||
|
|
||||||
`distributor` is also an external adapter. Upload behavior must be isolated
|
The primary flow is:
|
||||||
under `internal/adapters/distributor`, dependency types from the distributor
|
|
||||||
module must not leak outside that adapter, and the selected upload source must
|
|
||||||
be the managed Markdown report rather than optional output copies or broad
|
|
||||||
workspace scans.
|
|
||||||
|
|
||||||
## Project Shape
|
1. CLI parsing and configuration resolution;
|
||||||
|
2. report or batch resolution;
|
||||||
|
3. normalized weather collection;
|
||||||
|
4. deterministic fact derivation and module construction;
|
||||||
|
5. structured prior-snapshot comparison;
|
||||||
|
6. curated prompt input and report-mode-specific Scriptorium processing;
|
||||||
|
7. generated-text validation when applicable, managed Markdown production,
|
||||||
|
and metadata persistence; and
|
||||||
|
8. optional notification using managed report artifacts.
|
||||||
|
|
||||||
Default to a small, explicit, dependency-light Go application. Keep the design modular enough to test and change safely, but do not add abstraction unless it protects a real boundary or enables a real extension point.
|
Inspection is a separate read-only flow over persisted state. It must not
|
||||||
|
collect weather data, invoke Scriptorium, or upload reports.
|
||||||
|
|
||||||
Business/domain logic should live outside CLI, transport, and external-adapter packages.
|
## Ownership And Dependency Direction
|
||||||
|
|
||||||
|
### Entry Point And CLI
|
||||||
|
|
||||||
|
The binary entry point should do no business work beyond constructing and
|
||||||
|
running the CLI. CLI code owns commands, arguments, flags, help, output
|
||||||
|
formatting, and conversion into application requests.
|
||||||
|
|
||||||
|
CLI packages must not own meteorological decisions, report composition,
|
||||||
|
artifact layout, Recent Changes comparison, external transport, or subprocess
|
||||||
|
construction.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Configuration loading, built-in defaults, overrides, secret loading, and
|
||||||
|
validation belong to `internal/config`. Operational values shared across
|
||||||
|
packages must be explicit configuration or constants owned by the responsible
|
||||||
|
package, not hidden in CLI or adapter code.
|
||||||
|
|
||||||
|
The exact configuration contract belongs in the
|
||||||
|
[configuration reference](../config.md). Other architecture documents should
|
||||||
|
state ownership and safety rules rather than repeat fields, defaults, or
|
||||||
|
precedence.
|
||||||
|
|
||||||
|
### Application Orchestration
|
||||||
|
|
||||||
|
`internal/app` owns top-level use cases and workflow order. It composes report
|
||||||
|
resolution, collection, domain transformations, state, rendering, and optional
|
||||||
|
notification through narrow project-owned contracts.
|
||||||
|
|
||||||
|
The application layer may coordinate components and convert between their
|
||||||
|
contracts. It must not absorb CLI parsing, HTTP transport, subprocess argument
|
||||||
|
construction, filesystem layout, weather derivation algorithms, template
|
||||||
|
execution, or adapter-specific dependency types.
|
||||||
|
|
||||||
|
### Domain And Report Logic
|
||||||
|
|
||||||
|
Meteorological selection, forecast-period resolution, daypart grouping,
|
||||||
|
threshold detection, fact derivation, report composition, module construction,
|
||||||
|
generated-text validation, and Recent Changes comparison belong in deterministic
|
||||||
|
Go domain packages.
|
||||||
|
|
||||||
|
Domain packages must not depend on CLI parsing, process execution, remote
|
||||||
|
transport, or concrete external-library types. Given the same normalized
|
||||||
|
inputs, configuration, valid period, prior snapshot, and clock, domain behavior
|
||||||
|
should be reproducible.
|
||||||
|
|
||||||
|
Report selection must go through the report registry or an equivalent
|
||||||
|
centralized mechanism. A report definition owns its identity, prompt and
|
||||||
|
rendering mode, valid-period resolver, module composition, comparison strategy,
|
||||||
|
artifact grouping, and output naming. Do not scatter report-ID conditionals
|
||||||
|
through CLI, orchestration, or adapters.
|
||||||
|
|
||||||
|
### External Adapters
|
||||||
|
|
||||||
|
External integrations use adapter boundaries under `internal/adapters`.
|
||||||
|
Adapters own transport and protocol mechanics; application and domain packages
|
||||||
|
own decisions.
|
||||||
|
|
||||||
|
- The Weather API adapter owns HTTP request construction, timeouts, retries,
|
||||||
|
response-envelope handling, decoding, and endpoint compatibility.
|
||||||
|
- The Scriptorium adapter owns argument construction, context-aware subprocess
|
||||||
|
execution, stdout and stderr capture, exit interpretation, and result
|
||||||
|
decoding. It must avoid shell interpolation.
|
||||||
|
- The Distributor adapter owns dependency-specific bundle and upload types,
|
||||||
|
client construction, request execution, status handling, and redaction.
|
||||||
|
|
||||||
|
External dependency types must not leak beyond the adapter that integrates
|
||||||
|
them. Adapters should expose narrow project-owned inputs and outputs so an
|
||||||
|
integration can be tested or replaced without changing domain logic.
|
||||||
|
|
||||||
|
### State And Embedded Assets
|
||||||
|
|
||||||
|
`internal/state` owns managed workspace paths, durable metadata, atomic
|
||||||
|
artifact persistence, prior lookup, and inspection reads. Other packages should
|
||||||
|
request state operations rather than reconstruct managed paths independently.
|
||||||
|
|
||||||
|
Schemas, prompts, Markdown templates, and partials should live as separate
|
||||||
|
repository assets and be embedded by the package that owns their execution or
|
||||||
|
lookup. Keep weather derivation and path construction out of templates.
|
||||||
|
|
||||||
|
## Architectural Invariants
|
||||||
|
|
||||||
|
### Weather Truth And Generated Text
|
||||||
|
|
||||||
|
- Normalized source data and deterministic Go derivation are authoritative for
|
||||||
|
weather facts.
|
||||||
|
- LLM prompts receive curated module-based packages rather than raw,
|
||||||
|
unbounded source payloads.
|
||||||
|
- For generated-text-template reports, generated text is limited to defined
|
||||||
|
prose slots, validated before use, and rendered through typed or otherwise
|
||||||
|
explicit contexts.
|
||||||
|
- Direct-Markdown reports receive the same curated prompt-package boundary but
|
||||||
|
produce managed Markdown directly through Scriptorium rather than the
|
||||||
|
generated-text schema and repository-template workflow.
|
||||||
|
- Repository-owned templates arrange validated prose and deterministic facts;
|
||||||
|
they do not perform meteorological derivation.
|
||||||
|
|
||||||
|
### Reports And Comparison
|
||||||
|
|
||||||
|
- Report behavior is resolved through centralized definitions.
|
||||||
|
- Recent Changes is computed from structured module snapshots, never by
|
||||||
|
comparing rendered Markdown.
|
||||||
|
- Batch workflows collect normalized weather data once and reuse that
|
||||||
|
collection for planning and report generation.
|
||||||
|
- Report metadata links identity, generation time, valid period, source
|
||||||
|
provenance, and the managed artifacts produced for the run.
|
||||||
|
|
||||||
|
### Managed State And Notification
|
||||||
|
|
||||||
|
- Durable structured writes are atomic where practical.
|
||||||
|
- Managed paths remain beneath the configured workspace root.
|
||||||
|
- Operations that delete, move, overwrite, or copy files use narrow, explicit
|
||||||
|
paths; destructive cleanup is opt-in.
|
||||||
|
- Intermediate artifacts reached before a later failure remain inspectable
|
||||||
|
where practical.
|
||||||
|
- Distributor uploads use managed Markdown reports, never optional output
|
||||||
|
copies or broad workspace scans.
|
||||||
|
- Notification occurs only after the managed report and required metadata have
|
||||||
|
been successfully produced.
|
||||||
|
|
||||||
|
### Security, Errors, And Cancellation
|
||||||
|
|
||||||
|
- Secrets must not appear in logs, errors, persisted artifacts, examples, or
|
||||||
|
user-facing output.
|
||||||
|
- Errors preserve actionable operation, report, RunID, path, endpoint, or
|
||||||
|
subprocess context without exposing secrets or unnecessarily large payloads.
|
||||||
|
- External calls, subprocesses, storage operations, and multi-step workflows
|
||||||
|
accept or propagate `context.Context` where cancellation or timeout is
|
||||||
|
meaningful.
|
||||||
|
- Adapter failures preserve useful status, stderr, or response context at the
|
||||||
|
boundary and are translated into project-owned errors before crossing into
|
||||||
|
unrelated packages.
|
||||||
|
|
||||||
## Dependency Policy
|
## Dependency Policy
|
||||||
|
|
||||||
Prefer the Go standard library where practical.
|
Prefer the Go standard library. Add an external dependency only when it
|
||||||
|
materially improves correctness, security, interoperability, or
|
||||||
|
maintainability. A dependency used for a small convenience does not justify its
|
||||||
|
lifetime upgrade and compatibility cost.
|
||||||
|
|
||||||
Use external dependencies only when justified by correctness, security, interoperability, or substantial complexity reduction. Good reasons include complex security-sensitive behavior, such as HTML sanitization, or widely used de facto standards, such as YAML parsing.
|
Keep dependency-specific types inside the package that intentionally adopts
|
||||||
|
the dependency. The application should remain understandable and testable
|
||||||
|
without requiring framework-wide abstractions or live external services.
|
||||||
|
|
||||||
Avoid dependencies for small conveniences. Do not let external dependency types leak across internal package boundaries unless the dependency is itself the explicit public contract of that package.
|
## Verification And Documentation
|
||||||
|
|
||||||
## Package Layout
|
Core behavior must be testable without live Weather API, Scriptorium, or
|
||||||
|
Distributor services. The [testing policy](testing.md) owns test philosophy,
|
||||||
|
sufficiency, boundaries, and test-double guidance.
|
||||||
|
|
||||||
Use this layout unless the project has a documented reason to differ:
|
Documentation must follow the
|
||||||
|
[documentation policy](documentation.md). Update the canonical user,
|
||||||
|
operator, integration, internal, and example documentation in the same change
|
||||||
|
as the behavior it describes. Future or proposed behavior belongs under
|
||||||
|
`docs/roadmap/`; significant durable decisions may be recorded as ADRs.
|
||||||
|
|
||||||
- `internal/app`: application orchestration and top-level use cases.
|
## Non-Goals
|
||||||
- `internal/cli`: CLI command definitions, flags, argument parsing, and command wiring.
|
|
||||||
- `internal/config`: configuration structs, defaults, loading, precedence, and validation.
|
|
||||||
- `internal/adapters/<name>`: adapters for external CLIs, APIs, databases, object stores, or libraries.
|
|
||||||
- `internal/api`: HTTP API handlers and request/response types, when the application exposes an HTTP API.
|
|
||||||
- `internal/transport/http`: HTTP client code, when the application calls HTTP services.
|
|
||||||
|
|
||||||
Package-private implementation constants may live near the package that owns them, preferably in `constants.go` when useful.
|
Weatherreporter is not:
|
||||||
|
|
||||||
## Configuration
|
- a source weather-data ingestion or normalization service;
|
||||||
|
- a general-purpose LLM orchestration framework;
|
||||||
|
- an application in which an LLM selects authoritative weather facts or report
|
||||||
|
policy;
|
||||||
|
- a plugin framework with dynamically discovered report or module behavior;
|
||||||
|
- an HTTP service or multi-user distributed job system;
|
||||||
|
- a replacement for Scriptorium or Distributor protocol ownership; or
|
||||||
|
- a system that hides operational state exclusively inside opaque logs or
|
||||||
|
remote services.
|
||||||
|
|
||||||
Centralize configuration loading, processing, precedence, defaults, and validation in `internal/config`.
|
New requirements may justify revisiting a non-goal. A change that alters system
|
||||||
|
shape, dependency direction, a safety property, or another architectural
|
||||||
The goal is to make configuration discoverable and avoid implicit or hidden operational values. User-visible defaults and cross-package operational defaults should be defined in `internal/config/defaults.go`.
|
invariant should be recorded deliberately in this policy or an ADR rather than
|
||||||
|
introduced implicitly.
|
||||||
Configuration precedence is:
|
|
||||||
|
|
||||||
1. CLI flags
|
|
||||||
2. configuration file
|
|
||||||
3. built-in defaults
|
|
||||||
|
|
||||||
Prefer YAML configuration unless the project has a strong reason to use another format. Config files should be discovered at `/usr/local/etc/<app_name>/config.yml`, with a CLI override via `--config`.
|
|
||||||
|
|
||||||
Configuration files should not contain raw secrets unless the application is explicitly designed for that. Prefer environment variables or secret files for secrets. File-backed secrets are loaded through `secrets.directory`; secret values must not be logged, persisted, or included in user-facing output.
|
|
||||||
|
|
||||||
## Adapters and External Integrations
|
|
||||||
|
|
||||||
Use a hexagonal architecture style for external integrations.
|
|
||||||
|
|
||||||
External adapters belong under `internal/adapters/<name>`. If an adapter uses an external dependency, that dependency’s interface must not leak outside the adapter package. Other packages should interact only with the adapter’s API, so the dependency can be swapped, upgraded, or removed without touching unrelated code.
|
|
||||||
|
|
||||||
Adapters should be thin. Domain decisions belong in application/domain packages, not inside adapter glue.
|
|
||||||
|
|
||||||
## Components and Registries
|
|
||||||
|
|
||||||
When the application has major workflow components, each component should live
|
|
||||||
near the package that owns its contract and have explicit inputs and outputs.
|
|
||||||
|
|
||||||
The orchestrator should compose components in an explicit order using a default
|
|
||||||
sequence, dependency graph, or documented orchestration rule.
|
|
||||||
|
|
||||||
If users can select components, validators, renderers, or adapters, selection
|
|
||||||
should go through a registry or equivalent mechanism rather than scattered
|
|
||||||
conditionals.
|
|
||||||
|
|
||||||
## Embedded Assets
|
|
||||||
|
|
||||||
Store embedded JSON schemas, Markdown prompts, templates, and similar assets as separate files, not inline string literals, unless there is a strong reason otherwise.
|
|
||||||
|
|
||||||
## Errors and Logging
|
|
||||||
|
|
||||||
Errors should be actionable and preserve context. Wrap errors with operation and path/resource context. CLI code should convert internal errors into concise user-facing messages.
|
|
||||||
|
|
||||||
Errors and logs must not expose secrets.
|
|
||||||
|
|
||||||
Use structured logging where practical. Logs should describe operations, paths, external calls, retries, and failure causes, but should not include large user data by default.
|
|
||||||
|
|
||||||
## Context, Timeouts, and Cancellation
|
|
||||||
|
|
||||||
Long-running operations should accept `context.Context`. External calls,
|
|
||||||
subprocesses, HTTP requests, storage operations, and multi-step workflows should
|
|
||||||
respect cancellation and timeouts.
|
|
||||||
|
|
||||||
## State, Files, and Safety
|
|
||||||
|
|
||||||
If the application writes durable state, writes should be atomic where
|
|
||||||
practical. Multi-step workflows should preserve enough state to support
|
|
||||||
inspection and retry diagnosis after failure.
|
|
||||||
|
|
||||||
Code that deletes, moves, or overwrites files must use narrow, explicit paths. Avoid broad parent-directory operations. Cleanup that can cause data loss must be opt-in.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Core logic should be testable without real external services. Use fakes, fixtures, or local test doubles for adapters where practical.
|
|
||||||
|
|
||||||
Config examples should be load-tested. Important CLI workflows should have
|
|
||||||
parser or command tests. Component contracts should have focused tests that do
|
|
||||||
not require running the full application unless end-to-end coverage is
|
|
||||||
intentional.
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
Documentation should follow the project documentation policy. Keep user docs focused on implemented behavior. Put future, planned, or aspirational work only under `docs/roadmap/`.
|
|
||||||
|
|
||||||
When changing architecture, config, CLI behavior, adapters, or component
|
|
||||||
contracts, update the relevant docs and examples in the same change.
|
|
||||||
|
|||||||
@@ -1,208 +0,0 @@
|
|||||||
# Development Policy
|
|
||||||
|
|
||||||
This document is the contributor workflow policy for `weatherreporter`.
|
|
||||||
Developers and LLM coding agents should use it with
|
|
||||||
`docs/policy/architecture.md` and `docs/policy/documentation.md`.
|
|
||||||
|
|
||||||
## Repository Layout
|
|
||||||
|
|
||||||
- `cmd/weatherreporter`: binary entry point.
|
|
||||||
- `internal/app`: orchestration for generation, batches, fetch helpers, and
|
|
||||||
inspection.
|
|
||||||
- `internal/cli`: command parsing, flag handling, help text, and JSON output.
|
|
||||||
- `internal/config`: configuration structs, defaults, loading, overrides, and
|
|
||||||
validation.
|
|
||||||
- `internal/fileutil`: shared atomic filesystem write and copy helpers.
|
|
||||||
- `internal/adapters/distributor`: Distributor upload adapter.
|
|
||||||
- `internal/adapters/weatherapi`: Weather API HTTP adapter.
|
|
||||||
- `internal/adapters/scriptorium`: Scriptorium subprocess adapter.
|
|
||||||
- `internal/weatherdata`: normalized weather source facts, source metadata, and
|
|
||||||
source warnings.
|
|
||||||
- `internal/forecast`: deterministic forecast derivation.
|
|
||||||
- `internal/facts`: collected and derived report fact contracts.
|
|
||||||
- `internal/module`: module IDs, config items, output envelopes, and snapshots.
|
|
||||||
- `internal/report`: report definitions, valid periods, batches, output names,
|
|
||||||
and comparison declarations.
|
|
||||||
- `internal/briefing`: prompt-facing module value builders and module registry.
|
|
||||||
- `internal/changes`: structured Recent Changes comparison.
|
|
||||||
- `internal/promptinput`: Scriptorium `data_package` construction and
|
|
||||||
validation.
|
|
||||||
- `internal/state`: filesystem paths, atomic JSON writes, metadata, lookup, and
|
|
||||||
inspection support.
|
|
||||||
- `internal/timeutil`: clock, date, timezone, and period helpers.
|
|
||||||
- `docs`: user, operator, developer, integration, internal, policy, and roadmap
|
|
||||||
documentation.
|
|
||||||
- `examples`: maintained copyable examples.
|
|
||||||
|
|
||||||
## Local Validation
|
|
||||||
|
|
||||||
Use focused checks while editing and broader checks before committing:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go test ./...
|
|
||||||
go run ./cmd/weatherreporter --help
|
|
||||||
git diff --check
|
|
||||||
```
|
|
||||||
|
|
||||||
Useful focused checks:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go test ./internal/cli ./internal/config
|
|
||||||
go test ./internal/app ./internal/state
|
|
||||||
go test ./internal/adapters/distributor ./internal/adapters/weatherapi ./internal/adapters/scriptorium
|
|
||||||
go test ./internal/forecast ./internal/report ./internal/briefing ./internal/changes ./internal/promptinput
|
|
||||||
```
|
|
||||||
|
|
||||||
Run `gofmt -w` on changed Go files before committing.
|
|
||||||
|
|
||||||
## Coding Conventions
|
|
||||||
|
|
||||||
- Keep domain logic out of `cmd`, `internal/cli`, and adapter packages.
|
|
||||||
- Prefer small explicit structs and functions over broad framework-style
|
|
||||||
abstractions.
|
|
||||||
- Keep package APIs narrow and named around implemented behavior.
|
|
||||||
- Return errors with operation, path, endpoint, report, or RunID context.
|
|
||||||
- Do not log or expose secrets.
|
|
||||||
- Use `context.Context` for external calls, subprocesses, and orchestrated
|
|
||||||
workflows that may be canceled.
|
|
||||||
- Use atomic writes for durable JSON artifacts where practical.
|
|
||||||
- Keep report selection and prompt IDs centralized in `internal/report`.
|
|
||||||
- Keep Scriptorium argv construction inside `internal/adapters/scriptorium`.
|
|
||||||
- Keep distributor package types and upload-client construction inside
|
|
||||||
`internal/adapters/distributor`.
|
|
||||||
- Keep Weather API transport and envelope handling inside
|
|
||||||
`internal/adapters/weatherapi`.
|
|
||||||
|
|
||||||
## Dependency Policy
|
|
||||||
|
|
||||||
Prefer the Go standard library. Add dependencies only when they materially
|
|
||||||
improve correctness, interoperability, security, or maintainability.
|
|
||||||
|
|
||||||
Current external dependencies:
|
|
||||||
|
|
||||||
- `gitea.maximumdirect.net/eric/distributor` for distributor source bundle
|
|
||||||
construction and HTTP upload client behavior.
|
|
||||||
- `gopkg.in/yaml.v3` for YAML configuration parsing.
|
|
||||||
|
|
||||||
When adding a dependency:
|
|
||||||
|
|
||||||
- explain why the standard library is not enough;
|
|
||||||
- keep dependency types from leaking across unrelated package boundaries;
|
|
||||||
- add tests for the behavior the dependency supports;
|
|
||||||
- update this policy if the dependency becomes part of contributor workflow.
|
|
||||||
|
|
||||||
## Configuration Changes
|
|
||||||
|
|
||||||
Configuration is owned by `internal/config`.
|
|
||||||
|
|
||||||
When adding or changing a field:
|
|
||||||
|
|
||||||
- update `Config` and the nested config struct in `config.go`;
|
|
||||||
- add or adjust defaults in `defaults.go` when the field has a safe default;
|
|
||||||
- update loading or CLI override behavior in `load.go` only when needed;
|
|
||||||
- validate required values and accepted ranges in `validate.go`;
|
|
||||||
- add or update config tests;
|
|
||||||
- update `docs/config.md` and maintained examples when the field is user
|
|
||||||
visible;
|
|
||||||
- keep secrets out of example config files.
|
|
||||||
|
|
||||||
Configuration precedence is:
|
|
||||||
|
|
||||||
1. CLI overrides supported by `config.LoadOptions`;
|
|
||||||
2. configuration file values;
|
|
||||||
3. built-in defaults.
|
|
||||||
|
|
||||||
The default config path is `/usr/local/etc/weatherreporter/config.yml`.
|
|
||||||
|
|
||||||
## CLI Changes
|
|
||||||
|
|
||||||
The CLI is owned by `internal/cli`.
|
|
||||||
|
|
||||||
When adding or changing a command or flag:
|
|
||||||
|
|
||||||
- update help text and parser behavior together;
|
|
||||||
- declare whether the command is an action command or an inspection/data-output
|
|
||||||
command;
|
|
||||||
- convert parsed values into app-layer request structs;
|
|
||||||
- keep domain decisions in `internal/app` or domain packages;
|
|
||||||
- use the centralized output helpers in `internal/cli/output.go`;
|
|
||||||
- keep action-command summary conversion in `internal/cli/result.go`;
|
|
||||||
- add parser or command tests in `internal/cli`;
|
|
||||||
- update `docs/cli.md`;
|
|
||||||
- update `docs/operations.md` or `docs/troubleshooting.md` when behavior affects
|
|
||||||
operators.
|
|
||||||
|
|
||||||
CLI commands should return concise actionable errors and avoid printing partial
|
|
||||||
JSON when command construction fails.
|
|
||||||
|
|
||||||
## Components And Adapters
|
|
||||||
|
|
||||||
Use existing package boundaries before adding a package.
|
|
||||||
|
|
||||||
Add a new internal component only when it owns a distinct implemented contract.
|
|
||||||
Define its inputs, outputs, state behavior, failure behavior, tests, and
|
|
||||||
invariants in `docs/internal/`.
|
|
||||||
|
|
||||||
Adapters should stay thin:
|
|
||||||
|
|
||||||
- HTTP adapters own transport, request construction, envelope handling, and
|
|
||||||
decode boundaries.
|
|
||||||
- subprocess adapters own argv construction, timeout handling, stdout/stderr
|
|
||||||
capture, and exit-code interpretation.
|
|
||||||
- adapter packages should not own report selection, forecast summarization,
|
|
||||||
Recent Changes, or prompt input schema decisions.
|
|
||||||
|
|
||||||
When an external contract changes, update the matching file under
|
|
||||||
`docs/integrations/`.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Core tests must not require live Weather API, Scriptorium, or distributor
|
|
||||||
services.
|
|
||||||
|
|
||||||
Preferred test patterns:
|
|
||||||
|
|
||||||
- fake command runners for subprocess behavior;
|
|
||||||
- `httptest.Server` for Weather API behavior;
|
|
||||||
- fake distributor upload clients for notification behavior;
|
|
||||||
- filesystem temp directories for state behavior;
|
|
||||||
- deterministic clocks for report periods and RunIDs;
|
|
||||||
- table tests for config validation, CLI parsing, period resolution, and
|
|
||||||
threshold behavior.
|
|
||||||
|
|
||||||
Add focused tests near the package that owns the behavior. Use app-level tests
|
|
||||||
for workflow ordering, persistence, and cross-package contracts.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
Examples under `examples/` must be real, maintained, and free of secrets.
|
|
||||||
|
|
||||||
When updating examples:
|
|
||||||
|
|
||||||
- use implemented config fields only;
|
|
||||||
- avoid private endpoints and credentials;
|
|
||||||
- keep comments short and operationally useful;
|
|
||||||
- add or update validation coverage when a new example file is introduced;
|
|
||||||
- link maintained examples from `docs/config.md`.
|
|
||||||
|
|
||||||
Do not add generated report examples unless they can be kept current without
|
|
||||||
live external services.
|
|
||||||
|
|
||||||
## Documentation Checklist
|
|
||||||
|
|
||||||
Documentation updates are part of behavior changes.
|
|
||||||
|
|
||||||
Update:
|
|
||||||
|
|
||||||
- `README.md` for project orientation or quickstart changes;
|
|
||||||
- `docs/cli.md` for command and flag changes;
|
|
||||||
- `docs/config.md` for config fields, defaults, and precedence changes;
|
|
||||||
- `docs/operations.md` for state, artifact, batch, inspection, and recovery
|
|
||||||
behavior;
|
|
||||||
- `docs/troubleshooting.md` for recurring operator-facing failure modes;
|
|
||||||
- `docs/internal/` for component contracts and invariants;
|
|
||||||
- `docs/integrations/` for external Weather API, Scriptorium, or distributor
|
|
||||||
contract changes;
|
|
||||||
- `docs/roadmap/` only for unimplemented or deferred work.
|
|
||||||
|
|
||||||
Non-roadmap docs must describe implemented behavior only.
|
|
||||||
@@ -1,356 +1,207 @@
|
|||||||
# Go Project Documentation Policy
|
# Documentation Policy
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
Project documentation must help four audiences:
|
This policy assigns each Weatherreporter documentation topic to one canonical
|
||||||
|
owner. Its goal is to keep documentation accurate, concise, discoverable, and
|
||||||
1. users who need to run the application;
|
resistant to drift for users, operators, developers, integrators, maintainers,
|
||||||
2. administrators/operators who need to configure and operate it;
|
and coding agents.
|
||||||
3. developers who need to understand and change it safely;
|
|
||||||
4. LLM coding agents that need clear scope, boundaries, and invariants.
|
|
||||||
|
|
||||||
Docs should be accurate, concise, task-oriented, and organized by audience. Prefer links to canonical docs over repetition.
|
|
||||||
|
|
||||||
## Core Rules
|
## Core Rules
|
||||||
|
|
||||||
### 1. Keep docs concise
|
### One Canonical Documentation Owner
|
||||||
|
|
||||||
Each document should cover a defined scope and only the essentials for that scope.
|
Each authoritative fact belongs in one canonical document or documentation
|
||||||
|
area. A non-owning document may give a short, stable summary for orientation,
|
||||||
Avoid:
|
but it must link to the canonical owner instead of maintaining a second
|
||||||
- long background explanations;
|
definition.
|
||||||
- repeated reference material;
|
|
||||||
- implementation detail in user-facing docs;
|
Volatile details include commands, flags, configuration fields and defaults,
|
||||||
- aspirational language outside roadmap docs;
|
report and module IDs, schemas, file names, paths, status and exit behavior,
|
||||||
- verbose examples where one minimal example is clearer.
|
retry behavior, and runtime guarantees. If readers could reasonably treat a
|
||||||
|
statement as a contract, its exact documentation belongs with the owner named
|
||||||
### 2. Document only implemented behavior outside roadmap files
|
in this policy.
|
||||||
|
|
||||||
Unimplemented, planned, aspirational, experimental, or future work may be described only under:
|
Executable sources of truth and documentation owners serve different purposes.
|
||||||
|
Code, schemas, and embedded assets determine runtime behavior. The canonical
|
||||||
- `docs/roadmap/`
|
document owns the corresponding explanation or reference for readers. Both may
|
||||||
|
necessarily express the same contract, but other documentation should summarize
|
||||||
No other documentation file, including `README.md`, should describe code, features, modules, stages, commands, config fields, or behaviors that do not currently exist.
|
and link rather than create another complete reference. When implementation and
|
||||||
|
documentation disagree, verify the intended behavior and update them together.
|
||||||
If a feature is partial, non-roadmap docs may describe only the implemented portion and its current boundary.
|
|
||||||
|
### Current State, Decisions, And Future Work
|
||||||
### 3. Use canonical homes
|
|
||||||
|
Outside `docs/roadmap/`, documentation describes implemented behavior only.
|
||||||
Each type of information should have one canonical location.
|
Partial features may be described only to their implemented boundary.
|
||||||
|
|
||||||
Canonical homes:
|
An accepted architecture decision may describe an approved direction before it
|
||||||
|
is implemented, but acceptance is not evidence that the behavior exists.
|
||||||
- project purpose and quickstart: `README.md`
|
Current-state documents change when the implementation lands. Temporary
|
||||||
- development principles: `docs/policy/architecture.md`
|
roadmaps own future work, sequencing, and implementation status; they do not
|
||||||
- configuration reference: `docs/config.md`
|
replace durable policies, decisions, or current contracts.
|
||||||
- CLI reference: `docs/cli.md`
|
|
||||||
- operations and recovery: `docs/operations.md`
|
### Audience And Detail
|
||||||
- troubleshooting: `docs/troubleshooting.md`
|
|
||||||
- implemented internals: `docs/internal/`
|
Write for the document's stated audience and include only the detail needed for
|
||||||
- future work: `docs/roadmap/`
|
its owned topic. User and operator documentation should not expose incidental
|
||||||
- contributor workflow: `docs/policy/development.md`
|
implementation detail. Developer documentation should link to user-facing and
|
||||||
- copyable examples: `examples/`
|
external contracts instead of restating them.
|
||||||
|
|
||||||
Other files should summarize briefly and link to the canonical source.
|
### Links
|
||||||
|
|
||||||
### 4. Keep examples real
|
Use descriptive link text and repository-relative links for repository
|
||||||
|
documents. Link to the canonical owner rather than to a duplicate summary.
|
||||||
Examples should be valid, maintained, and free of secrets.
|
Check every added or changed link, and repair or remove links when their target
|
||||||
|
moves or is retired.
|
||||||
Where practical:
|
|
||||||
- example configs should load successfully;
|
### Examples And Code Fences
|
||||||
- example commands should match real CLI syntax;
|
|
||||||
- important examples should be covered by tests.
|
Complete copyable files belong in `examples/` when maintained examples exist.
|
||||||
|
Documentation may use the smallest illustrative snippet needed for its owned
|
||||||
## Documentation Profiles
|
topic, but should link to a maintained example instead of embedding a second
|
||||||
|
complete copy.
|
||||||
All projects require:
|
|
||||||
|
Examples must be valid, secret-free, and tested where practical. Commands,
|
||||||
- `README.md`
|
flags, configuration, imports, and Go snippets must match implemented behavior.
|
||||||
- `docs/policy/architecture.md`
|
Use a language tag on fenced code blocks, and identify fragments that are
|
||||||
|
illustrative rather than directly runnable.
|
||||||
Additional docs depend on the project.
|
|
||||||
|
### Security And Privacy
|
||||||
### Small library
|
|
||||||
|
Documentation and examples must not contain real credentials, private keys,
|
||||||
Recommended:
|
private environment dumps, sensitive source material, or private
|
||||||
- `docs/policy/development.md`, if contributor conventions are non-obvious
|
infrastructure details unless intentionally public. Document secret-handling
|
||||||
|
mechanisms, not secret values.
|
||||||
### Simple CLI
|
|
||||||
|
## Canonical Ownership
|
||||||
Required:
|
|
||||||
- `docs/cli.md`
|
| Topic | Canonical owner | Owned content | Content owned elsewhere |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
Recommended:
|
| Product orientation and minimal quickstart | `README.md` | What Weatherreporter is, why it is useful, one shortest successful invocation, and links onward. | Complete command reference, configuration reference, operational procedures, architecture, and implementation detail. |
|
||||||
- `docs/policy/development.md`
|
| Contributor workflow and package inventory | `docs/development.md` | Repository layout, local workflow, validation commands, coding conventions, task-specific change guidance, dependency workflow, and repository hygiene. | Architectural invariants, user-facing contracts, detailed subsystem behavior, and future work. |
|
||||||
|
| Current application architecture | `docs/policy/architecture.md` | System shape, normative ownership, dependency direction, package boundaries, invariants, safety properties, and non-goals. | Concrete implementation mechanics, contributor procedures, decision history, and future work. |
|
||||||
### Config-driven CLI
|
| Documentation organization | `docs/policy/documentation.md` | Documentation ownership, audience boundaries, maintenance rules, and document lifecycle. | Application architecture and runtime behavior. |
|
||||||
|
| Testing policy | `docs/policy/testing.md` | Test philosophy, risk-based sufficiency, stable test boundaries, doubles, coverage guidance, regression policy, and criteria for adding, rewriting, or deleting tests. | Subsystem behavior, application contracts, subsystem-specific test inventories, and implementation plans. |
|
||||||
Required:
|
| CLI contract | `docs/cli.md` | Commands, arguments, flags, invocation semantics, stdout and stderr behavior, summaries, and exit behavior. | Configuration field definitions, complete operating procedures, runtime filesystem layout, and command implementation. |
|
||||||
- `docs/cli.md`
|
| Configuration contract | `docs/config.md` | Discovery and precedence, fields, defaults, secrets, validation rules, and user-selectable values. | Complete example files, CLI syntax, runtime state lifecycle, and loading implementation. |
|
||||||
- `docs/config.md`
|
| Operations | `docs/operations.md` | Normal workflows, physical workspace layout, artifacts and metadata, inspection, notification behavior, recovery, cleanup, permissions, and operational caveats. | Complete CLI syntax, configuration field definitions, logical external contracts, and implementation mechanics. |
|
||||||
|
| Troubleshooting | `docs/troubleshooting.md` | Recurring symptoms, likely causes, diagnostic steps, safe fixes, and links to normal-operation references. | Complete command and configuration references, routine operating procedures, and implementation detail. |
|
||||||
Recommended:
|
| Report template surface | `docs/templates.md` | Implemented template files and partials, render-context fields, editing rules, and maintainer-facing template examples. | Weather derivation, module implementation, generated-text validation internals, and operator procedures. |
|
||||||
- `examples/`
|
| External and durable integration contracts | `docs/integrations/` | Weather API, Scriptorium, Distributor, external formats and protocols, durable logical paths and schemas, compatibility behavior, and upstream or downstream responsibilities. | Physical runtime placement and lifecycle, internal transformations, CLI syntax, and configuration defaults. |
|
||||||
- `docs/policy/development.md`
|
| Internal subsystem behavior | `docs/internal/` | Implementation flow, internal collaborators and state transitions, package-local guarantees and failures, and relevant tests. | Global architecture invariants, user-facing contracts, external schemas, operator procedures, and future package plans. |
|
||||||
|
| Architectural decision history | `docs/adr/`, when repository-local decisions require records | Significant decisions, context, alternatives, rationale, consequences, and supersession history. | Current behavior reference, implementation status, and task sequencing. |
|
||||||
### Stateful or operator-facing application
|
| Temporary feature roadmaps | `docs/roadmap/`, while planned work needs coordination | Proposed, accepted, deferred, or rejected work; sequencing; gates; implementation status; and task breakdowns. | Implemented behavior reference and durable decision rationale. |
|
||||||
|
| Complete copyable artifacts | `examples/` | Maintained configuration and other files intended to be copied or run. | Field-by-field reference, command reference, and prose explanation. |
|
||||||
Required:
|
|
||||||
- `docs/cli.md`, if CLI-based
|
Conditional owners do not require placeholder files or directories. If
|
||||||
- `docs/config.md`, if config-driven
|
Weatherreporter introduces a new public API, consumer interface, release
|
||||||
- `docs/operations.md`
|
process, or other durable documentation responsibility, update this policy to
|
||||||
|
assign its canonical owner when that responsibility is introduced.
|
||||||
Recommended:
|
|
||||||
- `docs/troubleshooting.md`
|
## Boundary Rules
|
||||||
- `examples/`
|
|
||||||
- `docs/policy/development.md`
|
### Orientation, Architecture, And Internals
|
||||||
|
|
||||||
### Modular, staged, service-oriented, or orchestration application
|
The README owns product orientation. The development policy routes contributors
|
||||||
|
and owns the concise current package inventory. Architecture owns normative
|
||||||
Required:
|
structure and invariants. Focused internal documents own implementation
|
||||||
- `docs/cli.md`, if CLI-based
|
behavior. These documents may link to one another but must not maintain
|
||||||
- `docs/config.md`, if config-driven
|
parallel package or behavior references.
|
||||||
- `docs/operations.md`
|
|
||||||
- `docs/internal/`
|
### Commands, Configuration, Operations, And Troubleshooting
|
||||||
- `docs/policy/development.md`
|
|
||||||
|
CLI documentation answers how to invoke Weatherreporter and what its command
|
||||||
Recommended:
|
interface does. Configuration documentation answers what settings mean.
|
||||||
- `docs/troubleshooting.md`
|
Operations answers what happens to runtime state and how to operate or recover
|
||||||
- validated examples under `examples/`
|
the application. Troubleshooting starts from a symptom and leads to diagnosis
|
||||||
|
and a safe fix.
|
||||||
## Required Documents
|
|
||||||
|
When a workflow crosses these topics, place the complete procedure with the
|
||||||
### README.md
|
document that owns the task and link to the other contracts. Do not duplicate
|
||||||
|
complete flag, field, or path references to make a workflow self-contained.
|
||||||
**Audience:** users, administrators, operators
|
|
||||||
|
### Templates, Integrations, And Implementation
|
||||||
The README is the outward-facing project orientation page.
|
|
||||||
|
Template documentation defines the maintainer-facing rendering surface.
|
||||||
It should include, in order:
|
Integration documentation defines externally observable shapes, logical paths,
|
||||||
|
protocols, and compatibility behavior. Internal documentation explains how
|
||||||
1. concise description;
|
Weatherreporter produces, transforms, or consumes those contracts.
|
||||||
2. elevator pitch;
|
|
||||||
3. shortest useful command or usage example;
|
Internal documents may name a command, field, template value, path, or protocol
|
||||||
4. links to targeted docs.
|
to identify a dependency, but must link to its canonical documentation for the
|
||||||
|
complete definition.
|
||||||
The README should be short. It is not a manual.
|
|
||||||
|
### Executable Authority
|
||||||
The “shortest useful command” means the simplest command that performs the project’s core use case. (It does not mean `app --help`.)
|
|
||||||
|
CLI parsing and help generation are the executable authority for accepted
|
||||||
### docs/policy/architecture.md
|
commands and flags. Configuration structs, defaults, loading, and validation
|
||||||
|
are the executable authority for configuration behavior. Schemas and embedded
|
||||||
**Audience:** developers, LLM coding agents
|
assets are the executable authority for validated formats and template
|
||||||
|
execution. Tests protect selected contracts and invariants but do not become a
|
||||||
`docs/policy/architecture.md` is required for every project.
|
second documentation reference merely by asserting them.
|
||||||
|
|
||||||
It is an inward-facing development policy document. It should describe how the project is intended to be built and changed.
|
Canonical documentation must be checked against these authorities whenever the
|
||||||
|
corresponding behavior changes.
|
||||||
It should include:
|
|
||||||
|
### Security Topics
|
||||||
- project shape;
|
|
||||||
- core design principles;
|
This policy owns what documentation and examples may contain. Architecture owns
|
||||||
- package and boundary philosophy;
|
application security boundaries and invariants. Configuration owns
|
||||||
- state/persistence philosophy, if applicable;
|
credential-supply mechanisms. Operations owns permissions and handling of
|
||||||
- external integration philosophy, if applicable;
|
sensitive runtime artifacts. Integration documents own consumer-visible
|
||||||
- error-handling and logging principles;
|
security contracts. Internal documents own implementation mechanisms only.
|
||||||
- testing expectations;
|
|
||||||
- documentation expectations;
|
## Architecture Decision Records
|
||||||
- architectural invariants;
|
|
||||||
- explicit non-goals, if useful.
|
Use sequentially numbered ADR filenames such as
|
||||||
|
`0001-record-architecture-decisions.md`. Follow the lightweight Nygard format:
|
||||||
For small projects, this file may be brief. It may simply state that the project is intentionally narrow, monolithic, and dependency-light.
|
|
||||||
|
1. title;
|
||||||
### docs/policy/development.md
|
2. status;
|
||||||
|
3. date;
|
||||||
**Audience:** developers, LLM coding agents
|
4. context;
|
||||||
|
5. decision;
|
||||||
Required for projects maintained by humans and LLM coding agents.
|
6. alternatives considered;
|
||||||
|
7. consequences.
|
||||||
It should include:
|
|
||||||
|
Use one of these statuses:
|
||||||
- repository layout;
|
|
||||||
- build/test commands;
|
- **Proposed:** the decision is under consideration and may change;
|
||||||
- coding conventions;
|
- **Accepted:** the decision is approved, whether or not implementation is
|
||||||
- dependency policy;
|
complete;
|
||||||
- how to add config fields;
|
- **Rejected:** the proposed decision was considered and not adopted;
|
||||||
- how to add CLI flags;
|
- **Superseded:** a later accepted ADR replaces the accepted decision.
|
||||||
- how to add stages/modules/adapters, if applicable;
|
|
||||||
- how to update examples;
|
A proposed ADR transitions to Accepted or Rejected. An Accepted ADR transitions
|
||||||
- documentation update expectations.
|
to Superseded only when a later Accepted ADR replaces it. An ADR may be created
|
||||||
|
as Accepted when the decision has already been made.
|
||||||
### docs/config.md
|
|
||||||
|
Treat the decision content of an Accepted ADR as immutable. A changed decision
|
||||||
**Audience:** administrators, operators, advanced users
|
requires a later ADR rather than a rewrite of the accepted record. A Superseded
|
||||||
|
ADR must link to its replacement, and the replacement must link back. Rejected
|
||||||
Required for applications with configuration files.
|
architectural alternatives belong in the ADR; rejected feature ideas belong in
|
||||||
|
a roadmap when they need to be retained.
|
||||||
It should include, in order:
|
|
||||||
|
## Document Lifecycle
|
||||||
1. config file locations and discovery precedence;
|
|
||||||
2. minimal working config;
|
Create durable current-state documentation with the implementation it
|
||||||
3. production-oriented config;
|
describes. Update its canonical owner in the same change when behavior changes.
|
||||||
4. full configuration reference;
|
If ownership moves, remove the old definition and leave a link where navigation
|
||||||
5. secrets handling, if applicable;
|
remains useful.
|
||||||
6. links to maintained examples.
|
|
||||||
|
Roadmaps are temporary coordination documents. When their work is complete,
|
||||||
The full configuration reference should be canonical.
|
record completion, move any still-useful decisions or contracts to their
|
||||||
|
durable owners, update incoming links, and archive or remove the roadmap
|
||||||
### docs/cli.md
|
according to repository practice. Do not preserve completed roadmaps as a
|
||||||
|
second current-state reference.
|
||||||
**Audience:** users, administrators, operators
|
|
||||||
|
Before completing documentation work:
|
||||||
Required for CLI applications.
|
|
||||||
|
- verify affected behavior and examples;
|
||||||
It should include, in order:
|
- check commands, flags, fields, defaults, schemas, paths, and identifiers
|
||||||
|
against their implementation;
|
||||||
1. shortest useful command;
|
- keep unimplemented behavior in a roadmap, subject to the ADR exception;
|
||||||
2. command overview;
|
- validate links and fenced examples;
|
||||||
3. complete flag reference;
|
- confirm non-owning documents summarize and link rather than redefine;
|
||||||
4. common workflows;
|
- remove stale or unsupported claims; and
|
||||||
5. diagnostic or recovery commands, if applicable.
|
- confirm that no secrets or sensitive private data were added.
|
||||||
|
|
||||||
Explain when commands are useful, not just their syntax.
|
|
||||||
|
|
||||||
### docs/operations.md
|
|
||||||
|
|
||||||
**Audience:** administrators, operators
|
|
||||||
|
|
||||||
Required for applications that maintain state, support resume behavior, run multiple stages, write durable artifacts, use remote storage, or require recovery procedures.
|
|
||||||
|
|
||||||
It should cover:
|
|
||||||
|
|
||||||
- normal workflow;
|
|
||||||
- filesystem layout;
|
|
||||||
- remote storage layout, if applicable;
|
|
||||||
- logs and manifests;
|
|
||||||
- resume/retry behavior;
|
|
||||||
- cleanup behavior;
|
|
||||||
- archive/backup behavior;
|
|
||||||
- safe recovery procedures;
|
|
||||||
- operational caveats.
|
|
||||||
|
|
||||||
### docs/troubleshooting.md
|
|
||||||
|
|
||||||
**Audience:** administrators, operators
|
|
||||||
|
|
||||||
Recommended once recurring failure modes exist.
|
|
||||||
|
|
||||||
Each entry should include:
|
|
||||||
|
|
||||||
- symptom;
|
|
||||||
- likely cause;
|
|
||||||
- diagnostic command or inspection step;
|
|
||||||
- safe fix;
|
|
||||||
- relevant links.
|
|
||||||
|
|
||||||
### docs/internal/
|
|
||||||
|
|
||||||
**Audience:** developers, LLM coding agents
|
|
||||||
|
|
||||||
Required for modular, staged, service-oriented, or orchestration projects.
|
|
||||||
|
|
||||||
This directory describes implemented internal components. It is not the roadmap.
|
|
||||||
|
|
||||||
Use one file per major component where useful.
|
|
||||||
|
|
||||||
Each component doc should include:
|
|
||||||
|
|
||||||
1. purpose;
|
|
||||||
2. inputs and outputs;
|
|
||||||
3. boundaries;
|
|
||||||
4. config fields used;
|
|
||||||
5. external adapters used;
|
|
||||||
6. state or manifest behavior, if applicable;
|
|
||||||
7. skip/resume behavior, if applicable;
|
|
||||||
8. failure behavior;
|
|
||||||
9. tests to inspect before changing;
|
|
||||||
10. architectural invariants.
|
|
||||||
|
|
||||||
### docs/roadmap/
|
|
||||||
|
|
||||||
**Audience:** maintainers, developers, LLM coding agents
|
|
||||||
|
|
||||||
This is the only place for planned, future, aspirational, experimental, or unimplemented work.
|
|
||||||
|
|
||||||
Roadmap docs should clearly distinguish:
|
|
||||||
|
|
||||||
- proposed work;
|
|
||||||
- accepted plans;
|
|
||||||
- deferred ideas;
|
|
||||||
- rejected ideas;
|
|
||||||
- implementation prompts or task breakdowns, if useful.
|
|
||||||
|
|
||||||
Roadmap docs should not be confused with current behavior.
|
|
||||||
|
|
||||||
### docs/integrations/
|
|
||||||
|
|
||||||
**Audience:** developers, LLM coding agents
|
|
||||||
|
|
||||||
Required for projects that depend on external CLIs, APIs, services, protocols, or file formats where the integration contract is important to maintain.
|
|
||||||
|
|
||||||
This directory contains concise, versioned reference notes for external integration contracts. It should document only the parts of the external system that this project actually uses.
|
|
||||||
|
|
||||||
Use one file per integration where useful.
|
|
||||||
|
|
||||||
## Examples Directory
|
|
||||||
|
|
||||||
Projects with non-trivial configuration or workflows should include `examples/`.
|
|
||||||
|
|
||||||
Useful examples include:
|
|
||||||
|
|
||||||
- minimal working config;
|
|
||||||
- production-oriented config;
|
|
||||||
- full annotated config;
|
|
||||||
- local development config;
|
|
||||||
- remote/object-storage config;
|
|
||||||
- minimal session/input file.
|
|
||||||
|
|
||||||
Examples should be valid, maintained, tested when practical, and linked from relevant docs.
|
|
||||||
|
|
||||||
## Security and Privacy
|
|
||||||
|
|
||||||
Docs and examples must not include:
|
|
||||||
|
|
||||||
- real API keys;
|
|
||||||
- tokens;
|
|
||||||
- passwords;
|
|
||||||
- private keys;
|
|
||||||
- private environment dumps;
|
|
||||||
- sensitive user data;
|
|
||||||
- raw private transcripts;
|
|
||||||
- private infrastructure details unless intentionally public.
|
|
||||||
|
|
||||||
Document secret-handling mechanisms, not actual secret values.
|
|
||||||
|
|
||||||
## Maintenance Rules
|
|
||||||
|
|
||||||
When docs change, verify the affected behavior.
|
|
||||||
|
|
||||||
Where practical:
|
|
||||||
|
|
||||||
- load example config files in tests;
|
|
||||||
- test CLI examples or command parser behavior;
|
|
||||||
- validate documented flags against real flags;
|
|
||||||
- remove stale references;
|
|
||||||
- update links after renames;
|
|
||||||
- keep roadmap content out of non-roadmap docs.
|
|
||||||
|
|
||||||
If documentation and code disagree, fix the documentation and/or open a roadmap item; do not leave aspirational behavior in current-behavior docs.
|
|
||||||
|
|
||||||
Documentation is complete only when it matches the current code.
|
|
||||||
|
|
||||||
## Documentation Change Checklist
|
|
||||||
|
|
||||||
Before merging documentation changes, verify:
|
|
||||||
|
|
||||||
- README is concise and orientation-focused.
|
|
||||||
- `docs/policy/architecture.md` describes development principles.
|
|
||||||
- Future work appears only under `docs/roadmap/`.
|
|
||||||
- User-facing docs avoid unnecessary internals.
|
|
||||||
- Developer-facing docs preserve boundaries and invariants.
|
|
||||||
- Config examples match the schema.
|
|
||||||
- CLI examples match real commands and flags.
|
|
||||||
- Defaults appear in the canonical config reference.
|
|
||||||
- No secrets or private data are included.
|
|
||||||
- Links are accurate.
|
|
||||||
|
|||||||
337
docs/policy/testing.md
Normal file
337
docs/policy/testing.md
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
# Testing Policy
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Our tests exist to make **incorrect changes expensive and correct changes
|
||||||
|
cheap**.
|
||||||
|
|
||||||
|
We do not optimize for test count, line coverage, exhaustive isolation, or the
|
||||||
|
fewest possible tests. We optimize for sufficient confidence in important
|
||||||
|
behavior while imposing as little unnecessary friction as possible on future
|
||||||
|
development.
|
||||||
|
|
||||||
|
## Every Test Has A Cost
|
||||||
|
|
||||||
|
Every test has an immediate cost and a continuing lifetime cost. It must be
|
||||||
|
written, reviewed, executed, understood, diagnosed when it fails, updated when
|
||||||
|
legitimate behavior changes, and maintained as fixtures and dependencies
|
||||||
|
evolve.
|
||||||
|
|
||||||
|
Tests also create cognitive and architectural friction. They can constrain
|
||||||
|
refactoring, duplicate policy, slow feedback, add noise to failures, and cause
|
||||||
|
harmless implementation changes to require unrelated suite edits.
|
||||||
|
|
||||||
|
A test is warranted when the confidence it provides justifies those costs.
|
||||||
|
Apply that judgment at two levels:
|
||||||
|
|
||||||
|
1. **Per test:** What realistic defect does this test detect, how consequential
|
||||||
|
would it be, and is that protection worth the test's lifetime cost?
|
||||||
|
2. **Across the suite:** Does this collection provide materially more
|
||||||
|
confidence than a smaller, simpler suite would?
|
||||||
|
|
||||||
|
Prefer a lean suite that provides sufficient confidence in the risks that
|
||||||
|
matter without redundant or low-value tests. Some friction is intentional:
|
||||||
|
tests should make dangerous changes, such as corrupting state, breaking
|
||||||
|
compatibility, violating security boundaries, or reintroducing subtle defects,
|
||||||
|
require deliberate review. They should not make ordinary internal changes
|
||||||
|
needlessly expensive.
|
||||||
|
|
||||||
|
Maintenance cost is not a reason to omit testing by default. When omitting a
|
||||||
|
plausible test, be able to explain why the protected failure is low-risk,
|
||||||
|
already covered, obvious, reversible, or cheaper to detect elsewhere. Favor
|
||||||
|
testing when failure would be consequential, subtle, or difficult to observe.
|
||||||
|
|
||||||
|
## Default Testing Style
|
||||||
|
|
||||||
|
Use a classical or Detroit-style approach:
|
||||||
|
|
||||||
|
- Test observable behavior, resulting state, contracts, and invariants.
|
||||||
|
- Use real internal collaborators when they are fast and deterministic.
|
||||||
|
- Use fakes, stubs, or mocks primarily at expensive, nondeterministic,
|
||||||
|
destructive, or external boundaries.
|
||||||
|
- Prefer package-level behavioral tests over tests coupled to private helpers
|
||||||
|
or internal call sequences.
|
||||||
|
- Test exact collaborator interactions only when the interaction itself is a
|
||||||
|
requirement.
|
||||||
|
|
||||||
|
Weatherreporter's important seams include clocks, subprocesses, HTTP services,
|
||||||
|
Distributor uploads, filesystem roots, environment-backed secrets, and any
|
||||||
|
future source of randomness or nondeterminism.
|
||||||
|
|
||||||
|
## Execution Requirements
|
||||||
|
|
||||||
|
The [development guide](../development.md) owns baseline repository validation.
|
||||||
|
The default test suite is:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
Run race-enabled tests when a change affects concurrent execution, goroutine
|
||||||
|
lifecycle, shared mutable state, or cancellation coordination. Use a focused
|
||||||
|
package command while iterating and `go test -race ./...` when the risk crosses
|
||||||
|
package boundaries.
|
||||||
|
|
||||||
|
Tests in the default suite must be deterministic, offline, and independent of
|
||||||
|
real credentials. They must not invoke live Weather API, Scriptorium, or
|
||||||
|
Distributor services or depend on other mutable external infrastructure.
|
||||||
|
Tests that require live infrastructure must be explicitly opt-in and clearly
|
||||||
|
separated from the default suite.
|
||||||
|
|
||||||
|
Control clocks, environment variables, filesystem roots, and machine-specific
|
||||||
|
state when they affect behavior. Tests must be safe to repeat and must not
|
||||||
|
depend on execution order or state left by an earlier test. Tests that modify
|
||||||
|
process-global state may remain serial; use `t.Parallel()` only when the test
|
||||||
|
and its collaborators are actually safe to run concurrently.
|
||||||
|
|
||||||
|
## Test Types And Assets
|
||||||
|
|
||||||
|
Use each test type where it protects a distinct risk:
|
||||||
|
|
||||||
|
- Unit and package tests protect focused domain behavior and invariants through
|
||||||
|
the narrowest stable boundary.
|
||||||
|
- Contract tests protect CLI behavior, configuration, durable artifacts,
|
||||||
|
schemas, templates, integration formats, compatibility, and stable error
|
||||||
|
identity.
|
||||||
|
- Integration tests use real deterministic collaborators when correctness
|
||||||
|
depends on their interaction, while replacing live or nondeterministic
|
||||||
|
external boundaries.
|
||||||
|
- App and CLI tests protect representative assembled generation, batch,
|
||||||
|
inspection, persistence, and notification workflows.
|
||||||
|
- Fixtures must be minimal, synthetic, versioned with the behavior they
|
||||||
|
exercise, and free of credentials or private data.
|
||||||
|
- Golden files are appropriate only when the complete output is intentionally
|
||||||
|
stable and semantic review of updates is practical.
|
||||||
|
- Failure-path tests should cover consequential malformed input, dependency
|
||||||
|
failure, cancellation, partial results, and recovery behavior.
|
||||||
|
|
||||||
|
## What Deserves Tests
|
||||||
|
|
||||||
|
Prioritize tests for:
|
||||||
|
|
||||||
|
1. CLI, configuration, artifact, template, integration, and package contracts.
|
||||||
|
2. Meteorological domain rules and important invariants.
|
||||||
|
3. Boundary conditions and malformed input.
|
||||||
|
4. Failure handling, cancellation, retries, recovery, and partial success.
|
||||||
|
5. Serialization, schemas, compatibility, and round trips.
|
||||||
|
6. Previously observed or plausible regressions.
|
||||||
|
7. Representative app and CLI workflows.
|
||||||
|
|
||||||
|
A package-level contract is behavior relied upon by another package or major
|
||||||
|
collaborator, not every observable implementation detail.
|
||||||
|
|
||||||
|
For data integrity, destructive operations, compatibility, security,
|
||||||
|
concurrency, idempotency, or recovery, presume that durable tests are required
|
||||||
|
unless the behavior is already credibly protected at another layer.
|
||||||
|
|
||||||
|
Do not add tests merely because a function, branch, or line exists. Do not add
|
||||||
|
a test when the same meaningful risk is already adequately protected
|
||||||
|
elsewhere.
|
||||||
|
|
||||||
|
## Choose The Right Boundary
|
||||||
|
|
||||||
|
Test through the narrowest stable boundary that expresses the behavior clearly.
|
||||||
|
That may be:
|
||||||
|
|
||||||
|
- a small pure function when dense domain logic is clearest there;
|
||||||
|
- a package operation when several internal collaborators jointly produce the
|
||||||
|
behavior; or
|
||||||
|
- a larger integration or app boundary when correctness emerges from
|
||||||
|
interaction.
|
||||||
|
|
||||||
|
Do not force every behavior through oversized workflow tests. Do not test every
|
||||||
|
private helper merely because it exists. Choose the boundary that provides
|
||||||
|
durable confidence with the least incidental coupling.
|
||||||
|
|
||||||
|
## Test Behavior, Not Implementation
|
||||||
|
|
||||||
|
A test should protect a decision, contract, or invariant, not memorialize the
|
||||||
|
current implementation. Before adding or retaining a test, ask:
|
||||||
|
|
||||||
|
> What realistic defect would this test catch?
|
||||||
|
|
||||||
|
A test is suspect when its main purpose is to detect that someone changed a
|
||||||
|
private constant, renamed or split a helper, reordered equivalent operations,
|
||||||
|
changed incidental formatting, replaced one correct algorithm with another, or
|
||||||
|
refactored private structure without changing behavior.
|
||||||
|
|
||||||
|
Refactoring should normally require no test edits unless the changed structure
|
||||||
|
is itself contractual. A test can be factually correct and still have negative
|
||||||
|
value when the behavior it protects is too incidental to justify its future
|
||||||
|
cost.
|
||||||
|
|
||||||
|
Use these expectations when evaluating failures:
|
||||||
|
|
||||||
|
| Change | Expected effect on tests |
|
||||||
|
| --- | --- |
|
||||||
|
| Internal refactor that preserves behavior | Existing tests should normally remain unchanged and pass. |
|
||||||
|
| Internal default change with no contractual significance | Tests should normally derive expectations from configuration or relationships rather than duplicate the old value. |
|
||||||
|
| Intentional change to user-visible behavior, policy, schema, or compatibility | Relevant tests should be reviewed and changed deliberately. |
|
||||||
|
| Accidental contract or invariant violation | Tests should fail; fix production code rather than rewriting tests to accept the defect. |
|
||||||
|
|
||||||
|
A failing test is not necessarily a test that should be edited. Many tests may
|
||||||
|
correctly fail because of one production defect. The maintenance smell is a
|
||||||
|
correct internal change that requires unrelated expectation changes throughout
|
||||||
|
the suite.
|
||||||
|
|
||||||
|
## Separate Mechanism From Policy
|
||||||
|
|
||||||
|
Do not duplicate configurable thresholds and defaults throughout the suite.
|
||||||
|
Test mechanisms relationally: a configured valid value is accepted, a value
|
||||||
|
outside the permitted relationship is rejected, and runtime behavior respects
|
||||||
|
the configured value.
|
||||||
|
|
||||||
|
Test an exact default when its literal value is itself a documented user,
|
||||||
|
operational, safety, protocol, or compatibility contract. The same distinction
|
||||||
|
applies to timeouts, capacities, retry counts, ranges, thresholds, and output
|
||||||
|
limits.
|
||||||
|
|
||||||
|
When concurrency limits are introduced, distinguish configuration enforcement
|
||||||
|
from runtime enforcement. Validate accepted and rejected settings separately
|
||||||
|
from measuring whether observed peak concurrency respects the configured
|
||||||
|
limit.
|
||||||
|
|
||||||
|
## Avoid Semantic Duplication
|
||||||
|
|
||||||
|
Each behavior should have a clear test owner:
|
||||||
|
|
||||||
|
- CLI parser tests own arguments, flags, and command construction.
|
||||||
|
- Config tests own loading, precedence, defaults, secrets, and validation.
|
||||||
|
- Domain tests own weather transformations and invariants.
|
||||||
|
- Adapter tests own HTTP, subprocess, and upload boundaries.
|
||||||
|
- Orchestrator tests own workflow ordering, persistence, partial success, and
|
||||||
|
failure propagation.
|
||||||
|
- State tests own path derivation, atomic artifacts, lookup, and round trips.
|
||||||
|
- Template and generated-text tests own schemas, render contexts, and rendered
|
||||||
|
output contracts.
|
||||||
|
|
||||||
|
Higher-level tests should not repeat every lower-level case. Tests that are
|
||||||
|
individually reasonable may still be collectively redundant; assess the
|
||||||
|
marginal protection of each additional test.
|
||||||
|
|
||||||
|
## Use Test Doubles Deliberately
|
||||||
|
|
||||||
|
Choose the least elaborate double that provides the required control or
|
||||||
|
observation:
|
||||||
|
|
||||||
|
1. Prefer real collaborators when they are fast and deterministic.
|
||||||
|
2. Use small in-memory fakes when realistic stateful behavior helps.
|
||||||
|
3. Use stubs when a dependency only needs controlled responses.
|
||||||
|
4. Use mocks when the interaction itself is contractual.
|
||||||
|
|
||||||
|
Mocks are appropriate for requirements such as uploading exactly once, saving
|
||||||
|
metadata before notification, propagating cancellation to Scriptorium, or
|
||||||
|
avoiding an external call after an earlier workflow failure. Do not use mocks
|
||||||
|
merely to isolate every object or reproduce the implementation's call graph.
|
||||||
|
|
||||||
|
## Go-Specific Guidance
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
- table-driven tests for meaningful behavioral categories and boundaries;
|
||||||
|
- `t.TempDir()` for real filesystem behavior;
|
||||||
|
- `httptest.Server` for realistic Weather API interactions;
|
||||||
|
- test-controlled clocks for periods and RunIDs;
|
||||||
|
- fake command runners for Scriptorium behavior;
|
||||||
|
- fake upload clients for Distributor behavior;
|
||||||
|
- fuzz tests when parsers, normalization, or path handling have a broad and
|
||||||
|
consequential input space;
|
||||||
|
- golden files only when complete output stability is intentional; and
|
||||||
|
- a small number of representative app and CLI workflow tests.
|
||||||
|
|
||||||
|
Avoid exact error-string assertions unless wording is contractual. Prefer
|
||||||
|
`errors.Is`, `errors.As`, typed errors, structured fields, or the smallest
|
||||||
|
stable semantic fragment that identifies the failure. At CLI boundaries,
|
||||||
|
prefer structured summaries, exit behavior, and stable classifications over
|
||||||
|
snapshots of complete diagnostic wording.
|
||||||
|
|
||||||
|
Golden-file updates must require an explicit local flag. Ordinary validation
|
||||||
|
must never update golden files automatically, and maintainers must inspect the
|
||||||
|
semantic diff before accepting an update.
|
||||||
|
|
||||||
|
Keep tests readable and direct. Helpers and fixture frameworks must earn their
|
||||||
|
maintenance cost; do not build elaborate infrastructure for small or isolated
|
||||||
|
needs.
|
||||||
|
|
||||||
|
## Coverage
|
||||||
|
|
||||||
|
Coverage is a diagnostic, not a target. Use it to find untested critical
|
||||||
|
branches and unexpectedly weak packages. Do not write low-value tests solely
|
||||||
|
to increase a percentage or infer quality from coverage alone.
|
||||||
|
|
||||||
|
Pure domain logic will often warrant higher coverage than CLI wiring or thin
|
||||||
|
external adapters. Uneven coverage is acceptable when it reflects risk.
|
||||||
|
|
||||||
|
## Regression Tests
|
||||||
|
|
||||||
|
A bug fix should normally include a regression test that fails before the fix
|
||||||
|
and passes afterward. Prefer the narrowest durable test of the violated
|
||||||
|
contract or invariant.
|
||||||
|
|
||||||
|
Retain the test when the defect could realistically recur and its consequences
|
||||||
|
justify the ongoing cost. Remove or consolidate it if the design makes
|
||||||
|
recurrence implausible or a stronger invariant test subsumes it.
|
||||||
|
|
||||||
|
## Deleting Or Rewriting Tests
|
||||||
|
|
||||||
|
Tests are maintained code, not permanent historical artifacts. Delete or
|
||||||
|
rewrite a test when its maintenance cost exceeds the confidence it provides.
|
||||||
|
Candidates include tests that:
|
||||||
|
|
||||||
|
- require edits after harmless internal changes;
|
||||||
|
- assert private constants without protecting a real contract;
|
||||||
|
- duplicate the same policy across several layers;
|
||||||
|
- verify mock choreography rather than outcomes;
|
||||||
|
- snapshot large amounts of incidental output;
|
||||||
|
- protect risks already covered more effectively elsewhere; or
|
||||||
|
- are flaky, misleading, obsolete, or no longer correspond to a plausible
|
||||||
|
failure.
|
||||||
|
|
||||||
|
Test removal must be deliberate and within the scope of the change. Identify
|
||||||
|
the behavior the test protected and show that the behavior is covered more
|
||||||
|
effectively elsewhere or that the failure is no longer plausible enough to
|
||||||
|
justify durable coverage. Replace several brittle tests with one stronger
|
||||||
|
behavior or invariant test when appropriate.
|
||||||
|
|
||||||
|
Do not delete or weaken a test merely because it fails after a production
|
||||||
|
change. First determine whether the failure exposes an accidental regression,
|
||||||
|
an intentional contract change, or an implementation-coupled assertion.
|
||||||
|
|
||||||
|
## Reviewing A Proposed Test
|
||||||
|
|
||||||
|
When a proposed test's value or durability is not self-evident, ask:
|
||||||
|
|
||||||
|
1. What realistic defect would it catch, and how consequential is that defect?
|
||||||
|
2. Is the behavior already protected elsewhere?
|
||||||
|
3. Which layer should own the test?
|
||||||
|
4. Does it assert a durable contract or incidental implementation detail?
|
||||||
|
5. What should cause it to fail, and what legitimate changes should not?
|
||||||
|
6. Could a smaller or more direct test protect the same risk?
|
||||||
|
7. What ongoing maintenance, execution, and diagnostic cost will it impose?
|
||||||
|
|
||||||
|
Written answers are not required for every routine test. Do not add a test when
|
||||||
|
its expected lifetime cost exceeds its expected protective value.
|
||||||
|
|
||||||
|
## Definition Of Sufficient
|
||||||
|
|
||||||
|
A suite is sufficient when:
|
||||||
|
|
||||||
|
- important contracts and invariants are protected;
|
||||||
|
- meaningful boundaries and failure modes are exercised;
|
||||||
|
- consequential regressions are credibly protected against silent recurrence;
|
||||||
|
- data integrity, destructive operations, compatibility, security,
|
||||||
|
concurrency, idempotency, and recovery receive risk-appropriate protection;
|
||||||
|
- external boundaries have realistic local integration coverage;
|
||||||
|
- representative complete workflows are tested;
|
||||||
|
- failures provide useful signal rather than redundant noise; and
|
||||||
|
- legitimate internal changes usually do not require test edits.
|
||||||
|
|
||||||
|
Sufficiency is a risk judgment, not a coverage percentage or test count.
|
||||||
|
Reassess it as Weatherreporter, its users, and the consequences of failure
|
||||||
|
evolve.
|
||||||
|
|
||||||
|
The governing rule is:
|
||||||
|
|
||||||
|
> Test heavily where failure is consequential, subtle, or difficult to detect
|
||||||
|
> after the fact. Test lightly where failure is obvious, reversible, and
|
||||||
|
> inexpensive.
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
# Future Roadmap
|
# Future Roadmap
|
||||||
|
|
||||||
This roadmap contains project work that is not implemented. Current behavior is
|
This roadmap contains future work only. Each section identifies its planning
|
||||||
documented outside `docs/roadmap/`.
|
status; current behavior is documented outside `docs/roadmap/`.
|
||||||
|
|
||||||
## Automatic Storm Monitoring
|
## Automatic Storm Monitoring
|
||||||
|
|
||||||
Manual Storm Report generation is available through:
|
Status: Proposed and unimplemented.
|
||||||
|
|
||||||
```sh
|
Manual Storm Report generation is implemented; see the [CLI reference](../cli.md).
|
||||||
weatherreporter generate storm --start TIME --end TIME
|
Automatic storm-event evaluation remains unimplemented.
|
||||||
```
|
|
||||||
|
|
||||||
Automatic storm-event evaluation is not implemented.
|
|
||||||
|
|
||||||
Possible direction:
|
Possible direction:
|
||||||
|
|
||||||
@@ -37,6 +34,8 @@ coverage for deterministic candidate detection.
|
|||||||
|
|
||||||
## Future Report Types
|
## Future Report Types
|
||||||
|
|
||||||
|
Status: Proposed and unimplemented.
|
||||||
|
|
||||||
Possible future report types:
|
Possible future report types:
|
||||||
|
|
||||||
- a short-fuse planning report distinct from the implemented Hourly Report, if
|
- a short-fuse planning report distinct from the implemented Hourly Report, if
|
||||||
@@ -46,12 +45,13 @@ Possible future report types:
|
|||||||
- archive-focused report variants if generated report history becomes a
|
- archive-focused report variants if generated report history becomes a
|
||||||
first-class product
|
first-class product
|
||||||
|
|
||||||
New reports should keep report identity, prompt IDs, templates, valid-period
|
New reports should preserve the boundaries documented in the [report registry
|
||||||
resolution, artifact grouping, batch output names, and comparison policy inside
|
internals](../internal/report-registry.md).
|
||||||
`internal/report`.
|
|
||||||
|
|
||||||
## Future Modules
|
## Future Modules
|
||||||
|
|
||||||
|
Status: Proposed and unimplemented.
|
||||||
|
|
||||||
Possible future modules:
|
Possible future modules:
|
||||||
|
|
||||||
- `hourly_table` for compact valid-period hourly facts
|
- `hourly_table` for compact valid-period hourly facts
|
||||||
@@ -71,7 +71,9 @@ QPF fields such as `measurable_qpf_total_in` and `max_hourly_qpf_in` should
|
|||||||
remain omitted until a real upstream quantitative precipitation source is
|
remain omitted until a real upstream quantitative precipitation source is
|
||||||
represented in `CollectedFacts`.
|
represented in `CollectedFacts`.
|
||||||
|
|
||||||
Future module work should preserve these boundaries:
|
Future module work should preserve the boundaries documented in [fact
|
||||||
|
contracts](../internal/facts.md), [module internals](../internal/module.md), and
|
||||||
|
[briefing internals](../internal/briefing.md):
|
||||||
|
|
||||||
- keep upstream collection in app orchestration
|
- keep upstream collection in app orchestration
|
||||||
- keep upstream collection out of modules
|
- keep upstream collection out of modules
|
||||||
@@ -82,9 +84,13 @@ Future module work should preserve these boundaries:
|
|||||||
|
|
||||||
## Distributor Notification Enhancements
|
## Distributor Notification Enhancements
|
||||||
|
|
||||||
Distributor notification uploads one managed Markdown report per successful
|
Status: Proposed and unimplemented.
|
||||||
generated report through the configured HTTP upload pipeline. The following
|
|
||||||
enhancements are not implemented:
|
Single-report and batch Distributor notification are implemented. Current
|
||||||
|
behavior is documented in the [Distributor adapter guide](../internal/distributor-adapter.md),
|
||||||
|
[Distributor integration guides](../integrations/distributor/), and
|
||||||
|
[operations guide](../operations.md). The following enhancements remain
|
||||||
|
unimplemented:
|
||||||
|
|
||||||
- `failure_policy: warn`
|
- `failure_policy: warn`
|
||||||
- uploading metadata, module snapshots, data packages, or preflight artifacts
|
- uploading metadata, module snapshots, data packages, or preflight artifacts
|
||||||
@@ -100,7 +106,9 @@ while distributor owns destination routing and publication behavior.
|
|||||||
|
|
||||||
## Alternate Runtime Integrations
|
## Alternate Runtime Integrations
|
||||||
|
|
||||||
These ideas are not implemented:
|
Status: Proposed and unimplemented.
|
||||||
|
|
||||||
|
These ideas remain unimplemented:
|
||||||
|
|
||||||
- native LLM client inside `weatherreporter`
|
- native LLM client inside `weatherreporter`
|
||||||
- database-backed state
|
- database-backed state
|
||||||
@@ -119,6 +127,8 @@ must not describe these as available behavior.
|
|||||||
|
|
||||||
## Deferred Refactors
|
## Deferred Refactors
|
||||||
|
|
||||||
|
Status: Deferred.
|
||||||
|
|
||||||
These refactors should remain deferred until new requirements or recurring
|
These refactors should remain deferred until new requirements or recurring
|
||||||
maintenance costs make the added abstraction worthwhile:
|
maintenance costs make the added abstraction worthwhile:
|
||||||
|
|
||||||
|
|||||||
327
docs/roadmap/promptkit.md
Normal file
327
docs/roadmap/promptkit.md
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
# Promptkit Migration Roadmap
|
||||||
|
|
||||||
|
Status: Accepted migration policy; the migration itself is unimplemented.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This roadmap defines the scope and desired end state for replacing the
|
||||||
|
external Scriptorium CLI integration with the Promptkit Go library. The
|
||||||
|
migration is not yet implemented. Current Scriptorium behavior remains
|
||||||
|
documented in the [Scriptorium integration guide](../integrations/scriptorium.md)
|
||||||
|
until the replacement is complete.
|
||||||
|
|
||||||
|
A separate staged implementation plan will describe how to move from the
|
||||||
|
current code to this target state. That plan should reference this roadmap
|
||||||
|
rather than redefine its architectural decisions or scope.
|
||||||
|
|
||||||
|
## Desired End State
|
||||||
|
|
||||||
|
Status: Accepted target state; unimplemented.
|
||||||
|
|
||||||
|
Weatherreporter uses a pinned released version of
|
||||||
|
`gitea.maximumdirect.net/eric/promptkit` as its in-process prompt preparation
|
||||||
|
and LLM execution engine. The `scriptorium` executable, subprocess adapter,
|
||||||
|
configuration, runtime dependency, and integration documentation have been
|
||||||
|
removed.
|
||||||
|
|
||||||
|
The migration does not change weatherreporter's fundamental product behavior.
|
||||||
|
Weather selection, forecast derivation, report periods, module construction,
|
||||||
|
Recent Changes, generated-text interpretation, Markdown templates, durable
|
||||||
|
state, inspection, output copies, and distributor notification remain owned by
|
||||||
|
weatherreporter.
|
||||||
|
|
||||||
|
All report prompts and private response schemas are versioned application
|
||||||
|
assets. Operators may configure Promptkit execution profiles without replacing
|
||||||
|
the report-owned prompt and schema corpus. One Promptkit engine is constructed
|
||||||
|
per CLI invocation and shared by every report in that invocation, including
|
||||||
|
all reports in a morning or evening batch.
|
||||||
|
|
||||||
|
Promptkit is isolated behind a weatherreporter-owned prompt execution contract.
|
||||||
|
Promptkit request, result, validation, error, profile, backend, and provider
|
||||||
|
types do not leak into application orchestration, report definitions, domain
|
||||||
|
packages, CLI summaries, state contracts, or distributor behavior.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
Status: Accepted migration scope; unimplemented.
|
||||||
|
|
||||||
|
- Remove the runtime dependency on the `scriptorium` executable.
|
||||||
|
- Replace shell-free subprocess orchestration with typed in-process Promptkit
|
||||||
|
preparation and execution.
|
||||||
|
- Preserve the seven report definitions and their existing prompt IDs.
|
||||||
|
- Preserve both direct-Markdown and generated-text-template report workflows.
|
||||||
|
- Preserve deterministic module snapshots and structured Recent Changes.
|
||||||
|
- Preserve context cancellation, actionable errors, secret redaction, and
|
||||||
|
inspectable failures.
|
||||||
|
- Improve durable prompt provenance with prompt, input, profile, model,
|
||||||
|
validation, usage, and timing metadata.
|
||||||
|
- Keep content-rich prompt and response diagnostics separate from routine
|
||||||
|
metadata and CLI output.
|
||||||
|
- Keep tests offline and deterministic through injected Promptkit model
|
||||||
|
clients and fixtures.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
Status: Accepted migration scope; unimplemented.
|
||||||
|
|
||||||
|
The migration will not:
|
||||||
|
|
||||||
|
- move meteorological selection, derivation, thresholds, or comparison logic
|
||||||
|
into prompts or Promptkit;
|
||||||
|
- send raw unbounded Weather API responses to the model;
|
||||||
|
- replace weatherreporter's generated-text domain validation or Markdown
|
||||||
|
template rendering;
|
||||||
|
- add a general workflow engine, provider plugin system, or arbitrary backend
|
||||||
|
registry to weatherreporter;
|
||||||
|
- add automatic provider, validation, or capacity retries;
|
||||||
|
- add concurrent report generation to the existing sequential batch workflow;
|
||||||
|
- expose Promptkit types as a weatherreporter component contract;
|
||||||
|
- keep a production-selectable Scriptorium/Promptkit dual-run mode; or
|
||||||
|
- use an unpublished Promptkit commit, committed Go workspace, or committed
|
||||||
|
local module replacement.
|
||||||
|
|
||||||
|
## Locked Decisions
|
||||||
|
|
||||||
|
Status: Accepted decisions for the unimplemented migration.
|
||||||
|
|
||||||
|
### Dependency And Versioning
|
||||||
|
|
||||||
|
- The initial integration will pin Promptkit `v0.3.0`.
|
||||||
|
- Coordinated local development may temporarily use the sibling Promptkit
|
||||||
|
checkout, but committed module metadata must reference the tagged release.
|
||||||
|
- A future Promptkit upgrade requires an explicit review of the public engine,
|
||||||
|
prompt/profile/schema formats, error identities, validation behavior, and
|
||||||
|
outbound provider contract used by weatherreporter.
|
||||||
|
|
||||||
|
### Application Boundary
|
||||||
|
|
||||||
|
- Promptkit remains an adapter boundary even though it runs in process.
|
||||||
|
- A weatherreporter-owned contract will represent preparation, execution,
|
||||||
|
output formats, validation, usage, provenance, and neutral error categories.
|
||||||
|
- The Promptkit adapter will map public Promptkit values into that contract at
|
||||||
|
the boundary.
|
||||||
|
- App orchestration and test fakes will depend on the weatherreporter contract,
|
||||||
|
not on Promptkit.
|
||||||
|
- Existing Scriptorium-specific generation mode names will be replaced with
|
||||||
|
provider-neutral names.
|
||||||
|
|
||||||
|
### Prompt And Schema Ownership
|
||||||
|
|
||||||
|
- Weatherreporter will embed all report prompt definitions, prompt content,
|
||||||
|
and private response schemas.
|
||||||
|
- Prompt assets will remain separate files rather than inline Go strings.
|
||||||
|
- The current Scriptorium prompt corpus will be retrieved before the
|
||||||
|
implementation stage that establishes the embedded Promptkit assets.
|
||||||
|
- The retrieved corpus will be reviewed and converted to the pinned Promptkit
|
||||||
|
format without changing report intent or prompt IDs.
|
||||||
|
- The four existing generated-text prompt fragments and schemas under
|
||||||
|
`internal/reporttemplate` will be reconciled with that corpus rather than
|
||||||
|
duplicated.
|
||||||
|
- Direct-Markdown prompt assets for the three-day, weekend, and storm reports
|
||||||
|
will become weatherreporter-owned assets.
|
||||||
|
- Weatherreporter needs one centralized embedded prompt/schema source; it does
|
||||||
|
not need Notarius's multi-module asset-flattening registry.
|
||||||
|
|
||||||
|
### Profiles, Backends, And Credentials
|
||||||
|
|
||||||
|
- Execution profiles remain operator-configurable rather than embedded report
|
||||||
|
policy.
|
||||||
|
- Configuration will support at most one external profile source: a profile
|
||||||
|
directory or a single profile file.
|
||||||
|
- Prompt definitions may provide their normal default profile, while
|
||||||
|
weatherreporter may support an explicit configured profile selection.
|
||||||
|
- Credential values remain in environment variables or file-backed
|
||||||
|
environment secrets. Configuration contains only credential source names.
|
||||||
|
- Provider credentials must not appear in logs, errors, CLI output, durable
|
||||||
|
metadata, preparation artifacts, execution artifacts, or debug summaries.
|
||||||
|
- Weatherreporter will not expose Promptkit's general backend registry as
|
||||||
|
arbitrary application configuration.
|
||||||
|
|
||||||
|
### Engine Lifetime
|
||||||
|
|
||||||
|
- One Promptkit engine will be constructed per CLI invocation at the
|
||||||
|
application composition boundary.
|
||||||
|
- Single-report generation will use that engine for preparation and execution.
|
||||||
|
- Morning and evening batches will share the same engine across every planned
|
||||||
|
report.
|
||||||
|
- Per-report orchestration will not construct its own default Promptkit engine.
|
||||||
|
- Promptkit backend capacity state and HTTP transport will therefore be shared
|
||||||
|
consistently for the invocation.
|
||||||
|
|
||||||
|
### Prompt Input
|
||||||
|
|
||||||
|
- Promptkit will continue to receive the curated `data_package` produced by
|
||||||
|
`internal/promptinput`.
|
||||||
|
- Weatherreporter will serialize the data package once, atomically persist
|
||||||
|
those exact bytes, and supply the same bytes as a Promptkit inline artifact.
|
||||||
|
- The managed data-package path may be supplied as non-secret artifact
|
||||||
|
provenance.
|
||||||
|
- Weatherreporter will not delegate unrestricted path loading to Promptkit's
|
||||||
|
default file artifact reader.
|
||||||
|
- The same immutable Promptkit request will be used for preparation and
|
||||||
|
execution so the preflight and run inputs cannot diverge.
|
||||||
|
|
||||||
|
### Preparation And Execution
|
||||||
|
|
||||||
|
- Promptkit `Prepare` replaces the current Scriptorium render preflight.
|
||||||
|
- Promptkit `Run` performs both Markdown and structured generated-text
|
||||||
|
execution.
|
||||||
|
- Promptkit basic validation will be used where appropriate for direct
|
||||||
|
Markdown output.
|
||||||
|
- Promptkit JSON Schema validation provides the provider-facing and first
|
||||||
|
structured-output check for generated-text reports.
|
||||||
|
- Weatherreporter's `internal/generatedtext` validation remains the final
|
||||||
|
report-specific domain boundary.
|
||||||
|
- Weatherreporter's `internal/reporttemplate` remains responsible for
|
||||||
|
generated-text Markdown rendering.
|
||||||
|
- Weatherreporter will atomically persist Promptkit output rather than asking
|
||||||
|
the dependency to write managed report files.
|
||||||
|
- The migration will not rely on Promptkit output repair. Promptkit v0.3.0's
|
||||||
|
public engine validates in a single pass even when a prompt declares repair
|
||||||
|
attempts.
|
||||||
|
|
||||||
|
## Durable Artifacts And Observability
|
||||||
|
|
||||||
|
Status: Accepted design constraints; unimplemented.
|
||||||
|
|
||||||
|
Routine durable artifacts should retain useful non-secret provenance without
|
||||||
|
persisting full rendered prompts by default.
|
||||||
|
|
||||||
|
The preparation record should contain:
|
||||||
|
|
||||||
|
- prompt ID and version;
|
||||||
|
- prompt definition hash;
|
||||||
|
- rendered prompt hash;
|
||||||
|
- input hashes;
|
||||||
|
- selected profile and backend identity;
|
||||||
|
- effective model identity;
|
||||||
|
- output contract summary; and
|
||||||
|
- preparation timing.
|
||||||
|
|
||||||
|
The execution record and run metadata should contain, when available:
|
||||||
|
|
||||||
|
- Promptkit run ID;
|
||||||
|
- prompt ID, version, and hashes;
|
||||||
|
- input hashes;
|
||||||
|
- selected profile, backend, and model identity;
|
||||||
|
- generated-content hash;
|
||||||
|
- token usage;
|
||||||
|
- start, end, and duration;
|
||||||
|
- validation status and bounded diagnostics; and
|
||||||
|
- the path of any separately persisted raw generated output.
|
||||||
|
|
||||||
|
Provider endpoints, full effective model parameter maps, rendered messages,
|
||||||
|
schema bodies, data-package contents, and generated content do not belong in
|
||||||
|
routine metadata or CLI summaries.
|
||||||
|
|
||||||
|
Rendered messages and other content-rich preparation or response diagnostics
|
||||||
|
will be available only through an explicitly enabled debug mechanism. Debug
|
||||||
|
artifacts must be documented as potentially sensitive, must not contain
|
||||||
|
credentials, and must have a clear operator-owned retention policy.
|
||||||
|
|
||||||
|
## Failure Contract
|
||||||
|
|
||||||
|
Status: Accepted design constraints; unimplemented.
|
||||||
|
|
||||||
|
Promptkit returns a completed `RunResult` for output-validation failure but no
|
||||||
|
partial result for operational preparation or execution errors. Weatherreporter
|
||||||
|
will preserve that distinction.
|
||||||
|
|
||||||
|
- A preparation failure produces a redacted weatherreporter-owned failure
|
||||||
|
receipt with report, RunID, prompt, stage, timing, and classified error
|
||||||
|
context. It does not fabricate a Promptkit preparation result.
|
||||||
|
- An operational execution failure retains the successful preparation record
|
||||||
|
and adds a redacted execution failure receipt. No partial Promptkit result or
|
||||||
|
model output is invented.
|
||||||
|
- A Promptkit validation failure retains the returned result, raw generated
|
||||||
|
output, validation details, and safe provenance before the report fails.
|
||||||
|
- A later weatherreporter generated-text decode, domain-validation, or template
|
||||||
|
failure retains every raw and validated artifact reached before that stage.
|
||||||
|
- Context cancellation takes precedence when the caller context is canceled.
|
||||||
|
- Promptkit capacity rejection maps to a weatherreporter-owned error category.
|
||||||
|
It is an operational report failure, not invalid model output.
|
||||||
|
- Single-report commands return the classified failure with available
|
||||||
|
inspectable paths.
|
||||||
|
- Batch runs continue independent later reports under the existing batch
|
||||||
|
failure policy.
|
||||||
|
- The migration adds no automatic retries. Any future retry policy belongs to
|
||||||
|
app orchestration, not the Promptkit adapter.
|
||||||
|
|
||||||
|
## Compatibility Requirements
|
||||||
|
|
||||||
|
Status: Accepted design constraints; unimplemented.
|
||||||
|
|
||||||
|
- Report IDs, prompt IDs, report selection, valid periods, artifact grouping,
|
||||||
|
output names, and distributor bundle behavior remain stable.
|
||||||
|
- Module snapshot and Recent Changes behavior remains deterministic.
|
||||||
|
- Promptkit receives only the existing curated prompt-input boundary.
|
||||||
|
- Generated reports continue to use the managed Markdown path as the
|
||||||
|
distributor upload source.
|
||||||
|
- RunID lookup and inspection remain available for successful and failed runs.
|
||||||
|
- Existing managed state paths remain stable where their meaning is unchanged.
|
||||||
|
Scriptorium-specific artifact names or schemas may change when retaining
|
||||||
|
them would misrepresent the new contract.
|
||||||
|
- Any artifact or metadata schema change is explicit, documented, and covered
|
||||||
|
by state and inspection tests.
|
||||||
|
- Prompt or generated content is not added to routine logs or CLI summaries.
|
||||||
|
- Tests do not require live Promptkit providers or credentials.
|
||||||
|
|
||||||
|
## Verification And Completion Criteria
|
||||||
|
|
||||||
|
Status: Proposed completion criteria for the unimplemented migration.
|
||||||
|
|
||||||
|
The migration is complete when:
|
||||||
|
|
||||||
|
- all seven reports prepare and execute through Promptkit using embedded
|
||||||
|
report-owned assets;
|
||||||
|
- direct-Markdown and generated-text-template paths have deterministic offline
|
||||||
|
adapter and app-level coverage;
|
||||||
|
- preparation, provider failure, capacity rejection, cancellation, timeout,
|
||||||
|
Promptkit validation failure, generated-text validation failure, template
|
||||||
|
failure, and successful generation preserve their specified artifacts;
|
||||||
|
- morning and evening batches construct one shared engine and preserve current
|
||||||
|
collection, planning, ordering, continuation, output, and notification
|
||||||
|
behavior;
|
||||||
|
- configuration examples load and contain no Scriptorium fields;
|
||||||
|
- CLI summaries and inspection commands expose the new artifact contract
|
||||||
|
without Promptkit dependency types;
|
||||||
|
- Scriptorium code, configuration, tests, and runtime documentation have been
|
||||||
|
removed;
|
||||||
|
- non-roadmap documentation describes only the implemented Promptkit
|
||||||
|
integration;
|
||||||
|
- `go test ./...`, CLI help validation, and `git diff --check` pass; and
|
||||||
|
- no committed `go.work`, local `replace`, live-provider test, or secret-bearing
|
||||||
|
fixture remains.
|
||||||
|
|
||||||
|
Fixture-based comparison with the current Scriptorium behavior is sufficient
|
||||||
|
for migration verification. A production-selectable dual-run period is not
|
||||||
|
required because model calls are nondeterministic, costly, and difficult to
|
||||||
|
compare meaningfully.
|
||||||
|
|
||||||
|
## External Prerequisite
|
||||||
|
|
||||||
|
Status: Required and unimplemented.
|
||||||
|
|
||||||
|
Before implementing the embedded asset stage, the current Scriptorium prompt
|
||||||
|
corpus must be made available in this repository. It should include the seven
|
||||||
|
prompt definitions, referenced content files, private response schemas,
|
||||||
|
relevant default-profile declarations, and any shared prompt fragments needed
|
||||||
|
to reproduce current report behavior.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
Status: Open; these require decisions before implementation.
|
||||||
|
|
||||||
|
- What exact `promptkit.*` configuration fields should replace the current
|
||||||
|
Scriptorium fields, including the name and precedence of an optional explicit
|
||||||
|
profile override?
|
||||||
|
- Should weatherreporter expose Promptkit's conventional `local` backend
|
||||||
|
registration as a narrow configuration feature, or rely initially on
|
||||||
|
built-in and endpoint-only profiles?
|
||||||
|
- Should report definitions store an explicit Promptkit prompt version, or
|
||||||
|
should each embedded prompt ID be required to have exactly one version?
|
||||||
|
- What CLI or configuration control enables sensitive prompt/response debug
|
||||||
|
artifacts, and where should those artifacts live?
|
||||||
|
- What final names and schema versions should replace the
|
||||||
|
Scriptorium-specific preflight and run-result artifacts while balancing
|
||||||
|
semantic clarity with existing state-path compatibility?
|
||||||
107
docs/roadmap/scriptorium/prompts/common/data_package.user.md
Normal file
107
docs/roadmap/scriptorium/prompts/common/data_package.user.md
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
Your task is to generate a local weather forecast analysis from the following YAML data package, which is prepared by the weatherreporter application.
|
||||||
|
|
||||||
|
Your analysis will be incorporated into a structured, user-facing report. The report may be for today, tomorrow, or a future date. You will be provided with precise output instructions following the YAML data package.
|
||||||
|
|
||||||
|
# SOURCE ROLES AND WEIGHTING
|
||||||
|
|
||||||
|
Use `report` and `briefing.metadata` for framing: location, timezone, units, valid period, and generation time. Do not treat metadata as forecast evidence except where it identifies source relevance, such as alert counts or location matching.
|
||||||
|
|
||||||
|
For weather interpretation, think in four source layers, in this order:
|
||||||
|
|
||||||
|
## 1. Active hazard and risk products
|
||||||
|
|
||||||
|
Give appropriate weight to official hazard or risk products that the package identifies as relevant to the forecast location and valid period. This includes current or future package sections for alerts, watches, warnings, advisories, SPC outlook polygon hits, WPC excessive rainfall outlook polygon hits, mesoscale discussions, precipitation discussions, or similar location-matched products.
|
||||||
|
|
||||||
|
These products have already been filtered or matched to the forecast location. Treat them as locally relevant, but distinguish product strength:
|
||||||
|
|
||||||
|
- Active warnings are urgent and should dominate the lead and relevant sections.
|
||||||
|
- Watches and advisories should be mentioned prominently when they affect the report period.
|
||||||
|
- Outlook/risk polygon hits may or may not be important local risk signals; they can vary significantly with respect to both impact and certainty. Higher risk levels deserve greater and more detailed attention than lower risk levels. Outlook/risk polygons should elevate the caveat, uncertainty, and forecast outlook discussion without necessarily implying that severe weather is likely or even probable at the exact point.
|
||||||
|
- Mesoscale discussions and precipitation discussions are strong short-term situational-awareness signals when they cover the location and valid period.
|
||||||
|
|
||||||
|
For the current schema, use `briefing.applicable_risk_products.alert_digest` and `briefing.metadata.alerts` to determine whether relevant local alerts exist. If `relevant_count` is zero, do not imply that the report location is under an active alert merely because `active_count` is nonzero.
|
||||||
|
|
||||||
|
## 2. Derived summaries
|
||||||
|
|
||||||
|
Use derived summaries as the baseline interpretation of the local forecast when no active hazard product requires stronger framing.
|
||||||
|
|
||||||
|
For the current schema:
|
||||||
|
|
||||||
|
- Use `briefing.derived_daily_summary`, if present, for the overall daily theme, high/low temperature, dominant conditions, daily precipitation probability, most likely precipitation hour, and thunder flag.
|
||||||
|
- Use `briefing.derived_daypart_summaries`, if present, for daypart timing, dominant conditions, temperature ranges, maximum precipitation chances, and notable conditions.
|
||||||
|
- Use `briefing.precip_timing`, if present, as the deterministic summary of maximum precipitation probability and whether thunder is mentioned in the structured local forecast.
|
||||||
|
- Use `briefing.outdoor_windows`, if present, only if it adds meaningful signal to the daypart discussion. Do not turn the report into outdoor-planning advice.
|
||||||
|
|
||||||
|
## 3. Narrative products
|
||||||
|
|
||||||
|
Use `briefing.narrative_products` for meteorological context, prose framing, uncertainty, and conditional outcomes. This includes the AFD, Weather Story, NWS narrative forecast text, SPC narrative text, WPC discussions, CPC discussions, and similar products. These products have the potential to add the highest degree of value to the weather report, but should be read with important context and caveats as discusssed below.
|
||||||
|
|
||||||
|
For the current schema:
|
||||||
|
|
||||||
|
- Use `briefing.narrative_products.narrative_forecast.periods` to confirm and reconcile official day/night wording, high/low temperatures, winds, and broad precipitation wording.
|
||||||
|
- Use `briefing.narrative_products.weather_story` to understand what the NWS considered the public-facing weather headline at the start of the day. Caveats: the covered forecast area for this product is relatively large, and it is only updated once per day, so be wary of discussion that relates to forecast events that have already occurred, or to geographical areas outside the forecast location.
|
||||||
|
- Use `briefing.narrative_products.area_forecast_discussion.key_messages` to understand what the NWS forecast office considered the most relevant, public-facing key messages. This product is updated somewhat more frequently than `briefing.narrative_products.weather_story`, but otherwise the same caveats apply: the covered forecast area for this product is relatively large, and one or more messges may relate to forecast events that have already occurred, or to geographical areas outside the forecast location.
|
||||||
|
- Use `briefing.narrative_products.area_forecast_discussion.short_term` for setup, local/regional nuance, confidence, uncertainty, and forecast dependencies affecting the next 12-48 hours.
|
||||||
|
- Use `briefing.narrative_products.area_forecast_discussion.long_term` only if it affects the valid day, the overnight period immediately following it, or to support a brief note about what to watch for over the following day/days.
|
||||||
|
- If `briefing.narrative_products.spc_convective_discussion.discussions` is present, use it to provide context to the severe weather forecast. Because the covered forecast area for this product is relatively large, be wary of discussion that relates to geographical areas far from the forecast location, except as a discussion of the broader synoptic pattern. Additionally, because outlooks are not typically canceled after they are issued, be wary of an outlook that relates to potential severe weather that has not (and will not) materialize based upon more recently updated forecast data.
|
||||||
|
|
||||||
|
Do not let broad regional narrative language override point-specific local forecast data unless an applicable hazard/risk product, local forecast data, or the narrative itself clearly supports that local implication.
|
||||||
|
|
||||||
|
## 4. Raw underlying data
|
||||||
|
|
||||||
|
Use `briefing.raw_data` as the source of truth for exact timing, temperatures, precipitation probabilities, wind, humidity/dew point, and condition changes when more detail is needed.
|
||||||
|
|
||||||
|
For the current schema, `briefing.raw_data.hourly_forecast.periods` is the most granular local forecast source.
|
||||||
|
|
||||||
|
Use `briefing.raw_data.current_conditions` only as generation-time context.
|
||||||
|
|
||||||
|
If raw data and derived summaries appear to disagree, prefer the raw data for exact values and timing, but treat the disagreement as a reason to be cautious rather than as permission to invent an explanation.
|
||||||
|
|
||||||
|
# CONFLICT RESOLUTION
|
||||||
|
|
||||||
|
When sources differ, ask:
|
||||||
|
|
||||||
|
1. Which source is most local to the forecast point?
|
||||||
|
2. Which source is valid for the report period or near-term window?
|
||||||
|
3. Which source is most authoritative for the type of claim being made?
|
||||||
|
4. Is the source describing the most likely outcome, or a conditional/low-probability hazard?
|
||||||
|
|
||||||
|
Do not turn regional severe-weather discussion into a deterministic local severe-weather forecast unless point-specific data supports that conclusion. Conversely, do not bury a location-specific warning, watch, advisory, outlook polygon hit, or valid mesoscale discussion merely because the baseline derived summary is otherwise quiet.
|
||||||
|
|
||||||
|
# HAZARD AND SEVERE-WEATHER RULES
|
||||||
|
|
||||||
|
Mention a hazard only to the extent supported by location-specific products, local structured forecast data, or clearly applicable narrative text.
|
||||||
|
|
||||||
|
Preserve product strength and uncertainty. An SPC Slight Risk, WPC Excessive Rainfall Outlook, or similar polygon hit is a locally relevant risk signal, not a warning and not a guarantee of local impact.
|
||||||
|
|
||||||
|
Preserve geography. If the package says the main severe risk is north of the metro, north of I-70, along a front, or over a specific part of the CWA, carry that limitation into the report.
|
||||||
|
|
||||||
|
Preserve timing. Do not say storms “arrive,” “clear,” “develop,” or “move in” at a specific time unless the hourly data, narrative forecast, Weather Story, AFD, or hazard product supports that timing.
|
||||||
|
|
||||||
|
# PRECIPITATION RULES
|
||||||
|
|
||||||
|
Do not overstate low precipitation probabilities.
|
||||||
|
|
||||||
|
Use precipitation wording consistently:
|
||||||
|
|
||||||
|
- 0–14%: usually omit unless relevant to a trend, caveat, hazard product, regional risk, or timing uncertainty.
|
||||||
|
- 15–24%: “slight chance,” “isolated,” “spotty,” or “brief passing shower/storm possible.”
|
||||||
|
- 25–39%: “chance,” “scattered,” or “some showers/storms possible.”
|
||||||
|
- 40–59%: “good chance” or “showers/storms likely enough to plan around.”
|
||||||
|
- 60%+: “likely,” “wet,” or “unsettled,” if consistent with the narrative forecast.
|
||||||
|
|
||||||
|
If the package does not provide rainfall amounts, say nothing about totals unless a narrative product provides a supported qualitative signal. Do not invent QPF.
|
||||||
|
|
||||||
|
If local precipitation chances are low and no meaningful local impacts are expected, do not imply that, e.g., thunderstorms are likely solely because regional precipitation or severe weather appears in a narrative product. Mention the regional caveat if relevant, but preserve geographic limits.
|
||||||
|
|
||||||
|
# STYLE RULES
|
||||||
|
|
||||||
|
- Plainspoken, precise, and weather-literate.
|
||||||
|
- Compact, but not shallow.
|
||||||
|
- No generic public-safety filler.
|
||||||
|
- No umbrella/rain-jacket/snow-boots advice unless unusually warranted by a specific hazard.
|
||||||
|
- No commute or outdoor-plan boilerplate.
|
||||||
|
- No unsupported precision.
|
||||||
|
- No apologies for missing data.
|
||||||
|
- Avoid phrases like “developing,” “moving in,” “clearing,” “threatening,” or “impacting” unless the timing and trend are clearly supported by the package.
|
||||||
|
- Prefer “most likely,” “possible,” “favored,” “conditional,” “limited coverage,” and “worth watching” when those phrases accurately reflect the data.
|
||||||
9
docs/roadmap/scriptorium/prompts/common/system.md
Normal file
9
docs/roadmap/scriptorium/prompts/common/system.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
You are WeatherReporter, a concise personal weather briefing writer.
|
||||||
|
|
||||||
|
You generate local weather forecast analysis from structured data packages prepared by the weatherreporter application.
|
||||||
|
|
||||||
|
Use only the provided data package as your source of truth. Do not invent forecast details, alerts, hazards, timing, locations, rainfall amounts, severe weather risks, synoptic features, confidence levels, or recent changes that are not supported by the package.
|
||||||
|
|
||||||
|
The reader is intelligent and weather-literate, but not a professional meteorologist. If asked to provide narrative analysis or commentary, write in plain, precise, meteorologically informed language. Avoid hype, filler, generic safety advice, and TV-weather style. Provide polished prose that avoids highly technical meteorological jargon or shorthand.
|
||||||
|
|
||||||
|
Do not mention that you are an AI model.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
TASK: You are writing structured prose slots for a daily weather report.
|
||||||
|
|
||||||
|
The calling application will render the final Markdown report. Your job is not to write the full report. Return only a JSON object matching the configured schema.
|
||||||
|
|
||||||
|
Use only the supplied `data_package`. Do not invent weather details, times, hazards, probabilities, or impacts that are not supported by the data.
|
||||||
|
|
||||||
|
The report focuses on the valid period in `report.valid_period`, which corresponds to an upcoming civil day for the configured location.
|
||||||
|
|
||||||
|
Return these fields:
|
||||||
|
|
||||||
|
- `summary`: required. 1-2 sentences summarizing the main weather story for the valid period.
|
||||||
|
- `forecast_discussion`: required. 3 paragraphs explaining the broader setup, trend, and/or forecast reasoning most relevant to the valid period.
|
||||||
|
- `precipitation_timing`: optional. Include only when the deterministic `precip_timing` module contains precipitation windows.
|
||||||
|
|
||||||
|
Return JSON only.
|
||||||
|
|
||||||
|
# summary
|
||||||
|
|
||||||
|
The summary should typically consist of two sentences.
|
||||||
|
|
||||||
|
If an active warning is relevant during the report period, lead with the hazard. Otherwise, the first sentence should state the most likely local weather outcome for the valid period, including the overall character of the weather and expected temperature/temperature range.
|
||||||
|
|
||||||
|
The second sentence should state the most important active hazard, caveat, uncertainty, or alternate outcome, if one exists. If there is no meaningful caveat, the second sentence may be omitted.
|
||||||
|
|
||||||
|
In the lead, distinguish the main weather outcome from the caveat. If showers and thunderstorms have different timing, state that difference rather than combining them as a single risk throughout the valid period. If the main caveat is a regional severe-weather or precipitation risk displaced from the report location, state that limitation clearly.
|
||||||
|
|
||||||
|
Example style:
|
||||||
|
|
||||||
|
- “Today is expected to be warm and dry, with mostly clear skies. There is a slight chance of isolated showers and thunderstorms developing from late afternoon into early evening.”
|
||||||
|
|
||||||
|
# forecast_discussion
|
||||||
|
|
||||||
|
Use narrative products to explain the “why” behind the local forecast when useful. Useful context may include:
|
||||||
|
|
||||||
|
- synoptic pattern
|
||||||
|
- fronts or boundaries
|
||||||
|
- shortwaves, troughs, or ridges
|
||||||
|
- instability, moisture, shear, forcing, or capping
|
||||||
|
- regional placement of precipitation or severe-weather chances
|
||||||
|
- hazard types and timing windows
|
||||||
|
- confidence or uncertainty
|
||||||
|
- conditional outcomes
|
||||||
|
- relevant notes about the following day or days
|
||||||
|
|
||||||
|
In most cases, the `forecast_discussion` should include three paragraphs:
|
||||||
|
|
||||||
|
1. 2–4 sentences summarizing the relevant local/regional setup.
|
||||||
|
2. 2-4 sentences describing the main forecast uncertainty or conditional factor, if present.
|
||||||
|
3. 2-4 sentences about the next day or broader pattern if supported.
|
||||||
|
|
||||||
|
# precipitation_timing
|
||||||
|
|
||||||
|
Optional. Return only if precipitation is forecast. If present, provide 1 to 4 sentences to add practical context, including:
|
||||||
|
|
||||||
|
- Whether the precipitation is associated with a moving frontal boundary, convective initiation, or wide stratiform rain (if this can be determined from the data package);
|
||||||
|
- The expected type, intensity, and duration of the precipitation; and
|
||||||
|
- Any caveats or uncertainty with respect to the onset, duration, or occurrance of the precipitation.
|
||||||
|
|
||||||
|
# Narrative Source Selection
|
||||||
|
|
||||||
|
As previously noted, use `briefing.derived_daily_summary`, `briefing.derived_daypart_summaries`, `briefing.narrative_products.narrative_forecast.periods`, and `briefing.raw_data.hourly_forecast.periods` as your primary reference sources for forecast.
|
||||||
|
|
||||||
|
As previously noted, narrative sources can provide significant added value, but you must think carefully about whether information from the available narrative sources is relevant to the valid period. If the valid period relates to a civil day that is several days in the future, then products such as `briefing.narrative_products.weather_story`, `briefing.narrative_products.area_forecast_discussion.key_messages`, and `briefing.narrative_products.area_forecast_discussion.short_term` may have limited relevance. On the other hand, `briefing.narrative_products.area_forecast_discussion.long_term` may have relatively more relevance.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
id: weather.daily_generated_text
|
||||||
|
version: "1.0.0"
|
||||||
|
default_profile: local-heavy
|
||||||
|
#default_profile: gemini-3-flash-lite
|
||||||
|
description: Daily weather report analysis prompt.
|
||||||
|
inputs:
|
||||||
|
- name: data_package
|
||||||
|
required: true
|
||||||
|
content_type: application/json
|
||||||
|
description: Structured weather data package
|
||||||
|
messages:
|
||||||
|
- role: system
|
||||||
|
content_file: ../common/system.md
|
||||||
|
- role: user
|
||||||
|
content_file: ../common/data_package.user.md
|
||||||
|
- role: user
|
||||||
|
content: |
|
||||||
|
{{input "data_package"}}
|
||||||
|
- role: user
|
||||||
|
content_file: ./daily_generated_text.user.md
|
||||||
|
output:
|
||||||
|
format: json
|
||||||
|
validation_mode: json_schema
|
||||||
|
schema_path: pipeline-weather/daily/daily.generated_text.schema.json
|
||||||
|
repair_attempts: 2
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
You are WeatherReporter, a concise personal weather briefing writer.
|
||||||
|
|
||||||
|
You generate local daily weather briefings from structured data packages prepared by the weatherreporter application.
|
||||||
|
|
||||||
|
The reader is weather-literate and interested in meteorology. Do not write a generic public weather report. Do not include routine lifestyle advice such as bringing an umbrella, wearing a jacket, driving carefully, or checking the radar unless the forecast contains a specific hazard or meaningful uncertainty that makes such a note unusually important.
|
||||||
|
|
||||||
|
Your job is to identify the most likely weather outcome, state meaningful caveats or uncertainty, summarize the daypart forecast, and explain the meteorological setup when useful.
|
||||||
|
|
||||||
|
Use only the provided data package as your source of truth. Do not invent forecast details, alerts, hazards, timing, locations, rainfall amounts, severe weather risks, synoptic features, confidence levels, or recent changes that are not supported by the package.
|
||||||
|
|
||||||
|
Write in plain, precise, meteorologically informed language. Avoid hype, filler, generic safety advice, and TV-weather style. Do not mention that you are an AI model. Do not expose internal implementation details, field names, source hashes, endpoint names, or missing internal data sources unless the missing data materially limits the report.
|
||||||
|
|
||||||
|
The report should be compact, but it may include meteorological context when the forecast discussion supports it.
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
Generate a Daily Weather Report from the following weatherreporter YAML data package.
|
||||||
|
|
||||||
|
The report may be for today, tomorrow, or a future date. Determine the correct framing from report, briefing.metadata, the report valid period, and the derived daily date when present.
|
||||||
|
|
||||||
|
Use Markdown.
|
||||||
|
|
||||||
|
# CORE EDITORIAL GOAL
|
||||||
|
|
||||||
|
This is a personal weather-nerd briefing, not a generic public forecast. The report should answer:
|
||||||
|
|
||||||
|
1. What is the most likely local weather outcome for the day?
|
||||||
|
2. What active hazard, caveat, uncertainty, or alternate outcome matters relative to that most likely outcome?
|
||||||
|
3. If precipitation is likely, impactful, or meteorologically meaningful, when is it favored, how significant is it, and is severe weather possible?
|
||||||
|
4. What should each daypart generally look and feel like?
|
||||||
|
5. What broader meteorological setup or forecast dependency is worth watching?
|
||||||
|
|
||||||
|
# SOURCE ROLES AND WEIGHTING
|
||||||
|
|
||||||
|
Use report and briefing.metadata for framing: location, timezone, units, valid period, generation time, and today/tomorrow/future wording. Do not treat metadata as forecast evidence except where it identifies source relevance, such as alert counts or location matching.
|
||||||
|
|
||||||
|
For weather interpretation, think in four source layers, in this order:
|
||||||
|
|
||||||
|
## 1. Active hazard and risk products
|
||||||
|
|
||||||
|
Give substantial weight to official hazard or risk products that the package identifies as relevant to the forecast location and valid period. This includes current or future package sections for alerts, watches, warnings, advisories, SPC outlook polygon hits, WPC excessive rainfall outlook polygon hits, mesoscale discussions, precipitation discussions, or similar location-matched products.
|
||||||
|
|
||||||
|
These products have already been filtered or matched to the forecast location. Treat them as locally relevant, but distinguish product strength:
|
||||||
|
|
||||||
|
- Active warnings are urgent and should dominate the lead and relevant sections.
|
||||||
|
- Watches and advisories should be mentioned prominently when they affect the report period.
|
||||||
|
- Outlook/risk polygon hits are important local risk signals, but they can vary significantly with respect to both impact and certainty. Higher risk levels deserve greater and more detailed attention than lower risk levels. Outlook/risk polygons should elevate the caveat, uncertainty, and ## What to Watch discussion without necessarily implying that severe weather is certain at the exact point.
|
||||||
|
- Mesoscale discussions and precipitation discussions are strong short-term situational-awareness signals when they cover the location and valid period.
|
||||||
|
|
||||||
|
For the current schema, use `briefing.applicable_risk_products.alert_digest` and `briefing.metadata.alerts` to determine whether relevant local alerts exist. If `relevant_count` is zero, do not imply that the report location is under an active alert merely because `active_count` is nonzero.
|
||||||
|
|
||||||
|
## 2. Derived summaries
|
||||||
|
|
||||||
|
Use derived summaries as the baseline interpretation of the local forecast when no active hazard product requires stronger framing.
|
||||||
|
|
||||||
|
For the current schema:
|
||||||
|
|
||||||
|
- Use briefing.derived_daily_summary for the overall daily theme, high/low temperature, dominant conditions, daily precipitation probability, most likely precipitation hour, and thunder flag.
|
||||||
|
- Use briefing.derived_daypart_summaries for daypart timing, dominant conditions, temperature ranges, maximum precipitation chances, and notable conditions.
|
||||||
|
- Use briefing.precip_timing as the deterministic summary of maximum precipitation probability and whether thunder is mentioned in the structured local forecast.
|
||||||
|
- Use briefing.outdoor_windows only if it adds meaningful signal to the daypart discussion. Do not turn the report into outdoor-planning advice.
|
||||||
|
|
||||||
|
## 3. Narrative products
|
||||||
|
|
||||||
|
Use `briefing.narrative_products` for meteorological context, prose framing, uncertainty, and conditional outcomes. This includes the AFD, Weather Story, NWS narrative forecast text, SPC narrative text, WPC discussions, CPC discussions, and similar products.
|
||||||
|
|
||||||
|
For the current schema:
|
||||||
|
|
||||||
|
- Use `briefing.narrative_products.narrative_forecast.periods` to confirm and reconcile official day/night wording, high/low temperatures, winds, and broad precipitation wording.
|
||||||
|
- Use `briefing.narrative_products.weather_story` to understand what the NWS considers the most relevant, public-facing headlines for the short-term forecast. Because the covered forecast area for this product is relatively large, be wary of discussion that relates to geographical areas outside the forecast location, and preserve spatial limits such as “north of I-70.”
|
||||||
|
- Use `briefing.narrative_products.area_forecast_discussion.key_messages` and `briefing.narrative_products.area_forecast_discussion.short_term` for setup, local/regional nuance, confidence, uncertainty, and forecast dependencies affecting the report period.
|
||||||
|
- Use `briefing.narrative_products.area_forecast_discussion.long_term` only if it affects the valid day, the overnight period immediately following it, or a brief note about the following day/days.
|
||||||
|
- If `briefing.narrative_products.spc_convective_discussion.discussions` is present, use it to understand and to provide context to the severe weather forecast. Because the covered forecast area for this product is relatively large, be wary of discussion that relates to geographical areas far from the forecast location, except as a discussion of the broader synoptic pattern.
|
||||||
|
|
||||||
|
Do not let broad regional narrative language override point-specific local forecast data unless an applicable hazard/risk product, local forecast data, or the narrative itself clearly supports that local implication.
|
||||||
|
|
||||||
|
## 4. Raw underlying data
|
||||||
|
|
||||||
|
Use `briefing.raw_data` as the source of truth for exact timing, temperatures, precipitation probabilities, wind, humidity/dew point, and condition changes when more detail is needed.
|
||||||
|
|
||||||
|
For the current schema, `briefing.raw_data.hourly_forecast.periods` is the most granular local forecast source. Use it to verify daypart summaries, refine timing, identify trends, and resolve ambiguity.
|
||||||
|
|
||||||
|
Use `briefing.raw_data.current_conditions` only as generation-time context. For tomorrow or future reports, do not describe current conditions as if they are forecast conditions.
|
||||||
|
|
||||||
|
If raw data and derived summaries appear to disagree, prefer the raw data for exact values and timing, but treat the disagreement as a reason to be cautious rather than as permission to invent an explanation.
|
||||||
|
|
||||||
|
# CONFLICT RESOLUTION
|
||||||
|
|
||||||
|
When sources differ, ask:
|
||||||
|
|
||||||
|
1. Which source is most local to the forecast point?
|
||||||
|
2. Which source is valid for the report period or near-term window?
|
||||||
|
3. Which source is most authoritative for the type of claim being made?
|
||||||
|
4. Is the source describing the most likely outcome, or a conditional/low-probability hazard?
|
||||||
|
|
||||||
|
Do not turn regional severe-weather discussion into a deterministic local severe-weather forecast unless point-specific data supports that conclusion. Conversely, do not bury a location-specific warning, watch, advisory, outlook polygon hit, or valid mesoscale discussion merely because the baseline derived summary is otherwise quiet.
|
||||||
|
|
||||||
|
# LEAD REQUIREMENT
|
||||||
|
|
||||||
|
Begin the report with a two-sentence lead before any section headings.
|
||||||
|
|
||||||
|
If an active warning is relevant during the report period, lead with the hazard. Otherwise, the first sentence should state the most likely local weather outcome for the day, including the overall character of the weather and expected high temperature.
|
||||||
|
|
||||||
|
The second sentence should state the most important active hazard, caveat, uncertainty, or alternate outcome, if one exists. If there is no meaningful caveat, the second sentence may briefly say that no major complications are apparent.
|
||||||
|
|
||||||
|
In the lead, distinguish the main weather outcome from the caveat. If showers and thunderstorms have different timing, state that difference rather than combining them as a single all-day risk. If the main caveat is a regional severe-weather or precipitation risk displaced from the report location, state that limitation clearly.
|
||||||
|
|
||||||
|
Example style:
|
||||||
|
|
||||||
|
- “Tomorrow is expected to be warm, mostly cloudy, and mostly dry, with a high near 72. There is a slight chance of isolated showers and thunderstorms from late afternoon into early evening.”
|
||||||
|
|
||||||
|
Do not open with generic planning advice.
|
||||||
|
|
||||||
|
# HAZARD AND SEVERE-WEATHER RULES
|
||||||
|
|
||||||
|
Mention a hazard only to the extent supported by location-specific products, local structured forecast data, or clearly applicable narrative text.
|
||||||
|
|
||||||
|
Preserve product strength and uncertainty. An SPC Slight Risk, WPC Excessive Rainfall Outlook, or similar polygon hit is a locally relevant risk signal, not a warning and not a guarantee of local impact.
|
||||||
|
|
||||||
|
Preserve geography. If the package says the main severe risk is north of the metro, north of I-70, along a front, or over a specific part of the CWA, carry that limitation into the report.
|
||||||
|
|
||||||
|
Preserve timing. Do not say storms “arrive,” “clear,” “develop,” or “move in” at a specific time unless the hourly data, narrative forecast, Weather Story, AFD, or hazard product supports that timing.
|
||||||
|
|
||||||
|
# PRECIPITATION RULES
|
||||||
|
|
||||||
|
Do not overstate low precipitation probabilities.
|
||||||
|
|
||||||
|
Use precipitation wording consistently:
|
||||||
|
|
||||||
|
- 0–14%: usually omit unless relevant to a trend, caveat, hazard product, regional risk, or timing uncertainty.
|
||||||
|
- 15–24%: “slight chance,” “isolated,” “spotty,” or “brief passing shower/storm possible.”
|
||||||
|
- 25–39%: “chance,” “scattered,” or “some showers/storms possible.”
|
||||||
|
- 40–59%: “good chance” or “showers/storms likely enough to plan around.”
|
||||||
|
- 60%+: “likely,” “wet,” or “unsettled,” if consistent with the narrative forecast.
|
||||||
|
|
||||||
|
Include ## Precipitation Details only when precipitation is likely, potentially impactful, or meteorologically interesting. In that section, address as many of the following as the data supports:
|
||||||
|
|
||||||
|
- likely or favored start/end timing
|
||||||
|
- most likely precipitation window
|
||||||
|
- expected intensity
|
||||||
|
- expected rainfall amount
|
||||||
|
- thunderstorm potential
|
||||||
|
- severe-weather potential
|
||||||
|
- uncertainty in timing, coverage, or placement
|
||||||
|
|
||||||
|
If the package does not provide rainfall amounts, say nothing about totals unless a narrative product provides a supported qualitative signal. Do not invent QPF.
|
||||||
|
|
||||||
|
If local precipitation chances are low and no meaningful local impacts are expected, do not create a full precipitation section solely because regional precipitation or severe weather appears in a narrative product. Mention the regional caveat in the lead or ## What to Watch instead, preserving geographic limits.
|
||||||
|
|
||||||
|
# DAYPART RULES
|
||||||
|
|
||||||
|
Use dayparts from briefing.derived_daypart_summaries. If a daypart is present but incomplete, use raw hourly data and narrative forecast periods to fill in only what is supported. Only include dayparts present in the package.
|
||||||
|
|
||||||
|
In ## Daypart Forecast, each bullet should usually follow this pattern:
|
||||||
|
|
||||||
|
- **Daypart:** [Sky/general condition] with [temperature trend or approximate temperature]. [Precipitation/storm/hazard sentence only if relevant.] [Wind sentence only if meaningful.]
|
||||||
|
|
||||||
|
Always include the expected sky or general condition when supported, such as mostly cloudy, partly cloudy, sunny, overcast, rainy, snowy, foggy, or stormy.
|
||||||
|
|
||||||
|
Prefer natural temperature phrasing:
|
||||||
|
|
||||||
|
- “temperatures around 82”
|
||||||
|
- “temperatures rising from the upper 60s into the low 70s”
|
||||||
|
- “temperatures near 80”
|
||||||
|
- “cooling from the low 80s into the low 70s”
|
||||||
|
- “holding in the upper 60s”
|
||||||
|
- “peaking near 83 late in the day”
|
||||||
|
|
||||||
|
For quiet or mostly dry dayparts, keep the bullet to one sentence. For dayparts with meaningful precipitation, thunder, snow, ice, fog, high wind, heat, or other weather impacts, add a second sentence with timing and caveat details.
|
||||||
|
|
||||||
|
Keep sky/general condition separate from precipitation probability. Do not write only “slight chance of showers” when the broader condition is “mostly cloudy with a slight chance of showers.”
|
||||||
|
|
||||||
|
When precipitation or hazards are likely during only part of a daypart, describe that timing first, then describe the sky/temperature trend. Do not lead with a benign sky condition if showers, storms, snow, ice, fog, or other impacts are likely during that same daypart.
|
||||||
|
|
||||||
|
Avoid “throughout the day” unless the same weather risk is meaningfully present across most dayparts.
|
||||||
|
|
||||||
|
# METEOROLOGICAL CONTEXT RULES
|
||||||
|
|
||||||
|
Use narrative products to explain the “why” behind the local forecast when useful.
|
||||||
|
|
||||||
|
Useful context may include:
|
||||||
|
|
||||||
|
- synoptic pattern
|
||||||
|
- fronts or boundaries
|
||||||
|
- shortwaves, troughs, or ridges
|
||||||
|
- instability, moisture, shear, forcing, or capping
|
||||||
|
- regional placement of precipitation or severe-weather chances
|
||||||
|
- hazard types and timing windows
|
||||||
|
- confidence or uncertainty
|
||||||
|
- conditional outcomes
|
||||||
|
- relevant notes about the following day or days
|
||||||
|
|
||||||
|
Do not simply quote or summarize narrative products at length. Translate them into concise, plainspoken, weather-literate context.
|
||||||
|
|
||||||
|
# OUTPUT FORMAT
|
||||||
|
|
||||||
|
Use this structure:
|
||||||
|
|
||||||
|
# [Today’s/Tomorrow’s/DOW's] Weather — [Location Name]
|
||||||
|
|
||||||
|
[Valid date]
|
||||||
|
|
||||||
|
[Two-sentence lead.]
|
||||||
|
|
||||||
|
## Daypart Forecast
|
||||||
|
|
||||||
|
- Morning: ...
|
||||||
|
- Midday: ...
|
||||||
|
- Afternoon: ...
|
||||||
|
- Evening: ...
|
||||||
|
- Overnight: ...
|
||||||
|
|
||||||
|
Only include dayparts present in the package. Use natural language timing where helpful.
|
||||||
|
|
||||||
|
## Precipitation Details
|
||||||
|
|
||||||
|
Include this section only if:
|
||||||
|
|
||||||
|
- local precipitation probability reaches at least 30% during the valid period;
|
||||||
|
- thunder is mentioned in the structured local forecast and the timing/coverage is meteorologically interesting;
|
||||||
|
- a relevant hazard/risk product discusses flooding, severe weather, winter weather, high wind, or another meaningful precipitation-related hazard;
|
||||||
|
- narrative products discuss intensity, rainfall rates, flooding, severe potential, or meaningful uncertainty that plausibly affects the report location or is important regional context;
|
||||||
|
- recent changes materially affect precipitation timing, coverage, or intensity.
|
||||||
|
|
||||||
|
## Recent Changes
|
||||||
|
|
||||||
|
Include this section only if recent_changes.items contains meaningful changes. Summarize changes in plain English. Do not fabricate changes.
|
||||||
|
|
||||||
|
## What to Watch
|
||||||
|
|
||||||
|
Include meteorological context, uncertainty, conditional forecast factors, and any relevant non-warning hazard/risk signals.
|
||||||
|
|
||||||
|
In most cases:
|
||||||
|
|
||||||
|
- Provide 2–3 sentences summarizing the relevant local/regional setup.
|
||||||
|
- Provide 1–2 sentences describing the main forecast uncertainty or conditional factor, if present.
|
||||||
|
- Optionally include 1–2 sentences about the next day or broader pattern if supported.
|
||||||
|
|
||||||
|
# STYLE RULES
|
||||||
|
|
||||||
|
- Plainspoken, precise, and weather-literate.
|
||||||
|
- Compact, but not shallow.
|
||||||
|
- No generic public-safety filler.
|
||||||
|
- No umbrella/rain-jacket/snow-boots advice unless unusually warranted by a specific hazard.
|
||||||
|
- No commute or outdoor-plan boilerplate.
|
||||||
|
- No unsupported precision.
|
||||||
|
- No raw YAML, raw JSON, internal field names, source hashes, endpoint names, URLs, implementation details, or debugging notes.
|
||||||
|
- No apologies for missing data.
|
||||||
|
- Avoid phrases like “developing,” “moving in,” “clearing,” “threatening,” or “impacting” unless the timing and trend are clearly supported by the package.
|
||||||
|
- Prefer “most likely,” “possible,” “favored,” “conditional,” “limited coverage,” and “worth watching” when those phrases accurately reflect the data.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
id: weather.daily_report
|
||||||
|
version: "1.0.0"
|
||||||
|
#default_profile: local-heavy
|
||||||
|
default_profile: gemini-3-flash-lite
|
||||||
|
description: Daily weather report prompt.
|
||||||
|
inputs:
|
||||||
|
- name: data_package
|
||||||
|
required: true
|
||||||
|
content_type: application/json
|
||||||
|
description: Structured weather data package
|
||||||
|
messages:
|
||||||
|
- role: system
|
||||||
|
content_file: ./daily_report.system.md
|
||||||
|
- role: user
|
||||||
|
content_file: ./daily_report.user.md
|
||||||
|
- role: user
|
||||||
|
content: |
|
||||||
|
<<<CURRENT_SESSION_TRANSCRIPT
|
||||||
|
{{input "data_package"}}
|
||||||
|
CURRENT_SESSION_TRANSCRIPT>>>
|
||||||
|
output:
|
||||||
|
format: markdown
|
||||||
|
validation_mode: basic
|
||||||
|
repair_attempts: 0
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
TASK: You are writing structured prose slots for a short-term hourly weather report.
|
||||||
|
|
||||||
|
The calling application will render the final Markdown report. Your job is not to write the full report. Return only a JSON object matching the configured schema.
|
||||||
|
|
||||||
|
Use only the supplied `data_package`. Do not invent weather details, times, hazards, probabilities, or impacts that are not supported by the data.
|
||||||
|
|
||||||
|
The report focuses on the valid period in `report.valid_period`, typically the next several hours for the configured location.
|
||||||
|
|
||||||
|
Return these fields:
|
||||||
|
|
||||||
|
- `summary`: required. 1-2 sentences summarizing the main weather story for the valid period.
|
||||||
|
- `forecast_discussion`: required. 2-3 sentences explaining the broader setup, trend, or forecast reasoning most relevant to the valid period.
|
||||||
|
- `precipitation_timing`: optional. Include only when the deterministic `precip_timing` module contains precipitation windows.
|
||||||
|
|
||||||
|
Return JSON only.
|
||||||
|
|
||||||
|
# summary
|
||||||
|
|
||||||
|
The summary should typically consist of two sentences.
|
||||||
|
|
||||||
|
If an active warning is relevant during the report period, lead with the hazard. Otherwise, the first sentence should state the most likely local weather outcome for the valid period, including the overall character of the weather and expected temperature/temperature range.
|
||||||
|
|
||||||
|
If the forecast indicates a significant shift in conditions over time (e.g., from sunny to overcast), then identify the hour when the shift is most likely to occur. If the conditions are generally similar or stable across the forecast period, then pick a single descriptor (e.g., mostly clear) that best captures the character of the weather.
|
||||||
|
|
||||||
|
The second sentence should state the most important active hazard, caveat, uncertainty, or alternate outcome, if one exists. If there is no meaningful caveat, the second sentence may be omitted, or may briefly say that no major complications are apparent.
|
||||||
|
|
||||||
|
In the lead, distinguish the main weather outcome from the caveat. If showers and thunderstorms have different timing, state that difference rather than combining them as a single risk throughout the valid period. If the main caveat is a regional severe-weather or precipitation risk displaced from the report location, state that limitation clearly.
|
||||||
|
|
||||||
|
Example style:
|
||||||
|
|
||||||
|
- “The rest of the afternoon is expected to be warm and dry, with mostly clear skies. There is a slight chance of isolated showers and thunderstorms developing from late afternoon into early evening.”
|
||||||
|
|
||||||
|
# forecast_discussion
|
||||||
|
|
||||||
|
Use narrative products to explain the “why” behind the local forecast when useful.
|
||||||
|
|
||||||
|
Useful context may include:
|
||||||
|
|
||||||
|
- synoptic pattern
|
||||||
|
- fronts or boundaries
|
||||||
|
- shortwaves, troughs, or ridges
|
||||||
|
- instability, moisture, shear, forcing, or capping
|
||||||
|
- regional placement of precipitation or severe-weather chances
|
||||||
|
- hazard types and timing windows
|
||||||
|
- confidence or uncertainty
|
||||||
|
- conditional outcomes
|
||||||
|
- relevant notes about the following day or days
|
||||||
|
|
||||||
|
# precipitation_timing
|
||||||
|
|
||||||
|
Optional. Return only if precipitation is forecast. If present, provide 1 to 4 sentences to add practical context, including:
|
||||||
|
|
||||||
|
- Whether the precipitation is associated with a moving frontal boundary, convective initiation, or wide stratiform rain (if this can be determined from the data package);
|
||||||
|
- The expected type, intensity, and duration of the precipitation; and
|
||||||
|
- Any caveats or uncertainty with respect to the onset, duration, or occurrance of the precipitation.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
id: weather.hourly_generated_text
|
||||||
|
version: "1.0.0"
|
||||||
|
default_profile: local-heavy
|
||||||
|
#default_profile: gemini-3-flash-lite
|
||||||
|
description: Hourly weather report analysis prompt.
|
||||||
|
inputs:
|
||||||
|
- name: data_package
|
||||||
|
required: true
|
||||||
|
content_type: application/json
|
||||||
|
description: Structured weather data package
|
||||||
|
messages:
|
||||||
|
- role: system
|
||||||
|
content_file: ../common/system.md
|
||||||
|
- role: user
|
||||||
|
content_file: ../common/data_package.user.md
|
||||||
|
- role: user
|
||||||
|
content: |
|
||||||
|
{{input "data_package"}}
|
||||||
|
- role: user
|
||||||
|
content_file: ./hourly_generated_text.user.md
|
||||||
|
output:
|
||||||
|
format: json
|
||||||
|
validation_mode: json_schema
|
||||||
|
schema_path: pipeline-weather/hourly/hourly.generated_text.schema.json
|
||||||
|
repair_attempts: 2
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
TASK: You are writing structured prose slots for a daily weather report.
|
||||||
|
|
||||||
|
The calling application will render the final Markdown report. Your job is not to write the full report. Return only a JSON object matching the configured schema.
|
||||||
|
|
||||||
|
Use only the supplied `data_package`. Do not invent weather details, times, hazards, probabilities, or impacts that are not supported by the data.
|
||||||
|
|
||||||
|
The report focuses on the valid period in `report.valid_period`, which corresponds to the current civil day (today) for the configured location.
|
||||||
|
|
||||||
|
Return these fields:
|
||||||
|
|
||||||
|
- `summary`: required. 1-2 sentences summarizing the main weather story for the valid period.
|
||||||
|
- `forecast_discussion`: required. 3 paragraphs explaining the broader setup, trend, and/or forecast reasoning most relevant to the valid period.
|
||||||
|
- `precipitation_timing`: optional. Include only when the deterministic `precip_timing` module contains precipitation windows.
|
||||||
|
|
||||||
|
Return JSON only.
|
||||||
|
|
||||||
|
# summary
|
||||||
|
|
||||||
|
The summary should typically consist of two sentences.
|
||||||
|
|
||||||
|
If an active warning is relevant during the report period, lead with the hazard. Otherwise, the first sentence should state the most likely local weather outcome for the valid period, including the overall character of the weather and expected temperature/temperature range.
|
||||||
|
|
||||||
|
The second sentence should state the most important active hazard, caveat, uncertainty, or alternate outcome, if one exists. If there is no meaningful caveat, the second sentence may be omitted.
|
||||||
|
|
||||||
|
In the lead, distinguish the main weather outcome from the caveat. If showers and thunderstorms have different timing, state that difference rather than combining them as a single risk throughout the valid period. If the main caveat is a regional severe-weather or precipitation risk displaced from the report location, state that limitation clearly.
|
||||||
|
|
||||||
|
Example style:
|
||||||
|
|
||||||
|
- “Today is expected to be warm and dry, with mostly clear skies. There is a slight chance of isolated showers and thunderstorms developing from late afternoon into early evening.”
|
||||||
|
|
||||||
|
# forecast_discussion
|
||||||
|
|
||||||
|
Use narrative products to explain the “why” behind the local forecast when useful. Useful context may include:
|
||||||
|
|
||||||
|
- synoptic pattern
|
||||||
|
- fronts or boundaries
|
||||||
|
- shortwaves, troughs, or ridges
|
||||||
|
- instability, moisture, shear, forcing, or capping
|
||||||
|
- regional placement of precipitation or severe-weather chances
|
||||||
|
- hazard types and timing windows
|
||||||
|
- confidence or uncertainty
|
||||||
|
- conditional outcomes
|
||||||
|
- relevant notes about the following day or days
|
||||||
|
|
||||||
|
In most cases, the `forecast_discussion` should include three paragraphs:
|
||||||
|
|
||||||
|
1. 2–4 sentences summarizing the relevant local/regional setup.
|
||||||
|
2. 2-4 sentences describing the main forecast uncertainty or conditional factor, if present.
|
||||||
|
3. 2-4 sentences about the next day or broader pattern if supported.
|
||||||
|
|
||||||
|
# precipitation_timing
|
||||||
|
|
||||||
|
Optional. Return only if precipitation is forecast. If present, provide 1 to 4 sentences to add practical context, including:
|
||||||
|
|
||||||
|
- Whether the precipitation is associated with a moving frontal boundary, convective initiation, or wide stratiform rain (if this can be determined from the data package);
|
||||||
|
- The expected type, intensity, and duration of the precipitation; and
|
||||||
|
- Any caveats or uncertainty with respect to the onset, duration, or occurrance of the precipitation.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
id: weather.today_generated_text
|
||||||
|
version: "1.0.0"
|
||||||
|
default_profile: local-heavy
|
||||||
|
#default_profile: gemini-3-flash-lite
|
||||||
|
description: Today's weather report analysis prompt.
|
||||||
|
inputs:
|
||||||
|
- name: data_package
|
||||||
|
required: true
|
||||||
|
content_type: application/json
|
||||||
|
description: Structured weather data package
|
||||||
|
messages:
|
||||||
|
- role: system
|
||||||
|
content_file: ../common/system.md
|
||||||
|
- role: user
|
||||||
|
content_file: ../common/data_package.user.md
|
||||||
|
- role: user
|
||||||
|
content: |
|
||||||
|
{{input "data_package"}}
|
||||||
|
- role: user
|
||||||
|
content_file: ./today_generated_text.user.md
|
||||||
|
output:
|
||||||
|
format: json
|
||||||
|
validation_mode: json_schema
|
||||||
|
schema_path: pipeline-weather/today/today.generated_text.schema.json
|
||||||
|
repair_attempts: 2
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
TASK: You are writing structured prose slots for a daily weather report.
|
||||||
|
|
||||||
|
The calling application will render the final Markdown report. Your job is not to write the full report. Return only a JSON object matching the configured schema.
|
||||||
|
|
||||||
|
Use only the supplied `data_package`. Do not invent weather details, times, hazards, probabilities, or impacts that are not supported by the data.
|
||||||
|
|
||||||
|
The report focuses on the valid period in `report.valid_period`, which corresponds to the next civil day (tomorrow) for the configured location.
|
||||||
|
|
||||||
|
Return these fields:
|
||||||
|
|
||||||
|
- `summary`: required. 1-2 sentences summarizing the main weather story for the valid period.
|
||||||
|
- `forecast_discussion`: required. 3 paragraphs explaining the broader setup, trend, and/or forecast reasoning most relevant to the valid period.
|
||||||
|
- `precipitation_timing`: optional. Include only when the deterministic `precip_timing` module contains precipitation windows.
|
||||||
|
- `confidence`: optional. Include only if uncertainty, timing spread, or conflicting signals materially affect how the reader should interpret the forecast.
|
||||||
|
|
||||||
|
Return JSON only.
|
||||||
|
|
||||||
|
# summary
|
||||||
|
|
||||||
|
The summary should typically consist of two sentences.
|
||||||
|
|
||||||
|
If an active warning is relevant during the report period, lead with the hazard. Otherwise, the first sentence should state the most likely local weather outcome for the valid period, including the overall character of the weather and expected temperature/temperature range.
|
||||||
|
|
||||||
|
The second sentence should state the most important active hazard, caveat, uncertainty, or alternate outcome, if one exists. If there is no meaningful caveat, the second sentence may be omitted.
|
||||||
|
|
||||||
|
In the lead, distinguish the main weather outcome from the caveat. If showers and thunderstorms have different timing, state that difference rather than combining them as a single risk throughout the valid period. If the main caveat is a regional severe-weather or precipitation risk displaced from the report location, state that limitation clearly.
|
||||||
|
|
||||||
|
Example style:
|
||||||
|
|
||||||
|
- “Sunday is expected to be warm and dry, with mostly clear skies. There is a slight chance of isolated showers and thunderstorms developing from late afternoon into early evening.”
|
||||||
|
|
||||||
|
# forecast_discussion
|
||||||
|
|
||||||
|
Use narrative products to explain the “why” behind the local forecast when useful. Useful context may include:
|
||||||
|
|
||||||
|
- synoptic pattern
|
||||||
|
- fronts or boundaries
|
||||||
|
- shortwaves, troughs, or ridges
|
||||||
|
- instability, moisture, shear, forcing, or capping
|
||||||
|
- regional placement of precipitation or severe-weather chances
|
||||||
|
- hazard types and timing windows
|
||||||
|
- confidence or uncertainty
|
||||||
|
- conditional outcomes
|
||||||
|
- relevant notes about the following day or days
|
||||||
|
|
||||||
|
In most cases, the `forecast_discussion` should include three paragraphs:
|
||||||
|
|
||||||
|
1. 2–4 sentences summarizing the relevant local/regional setup.
|
||||||
|
2. 2-4 sentences describing the main forecast uncertainty or conditional factor, if present.
|
||||||
|
3. 2-4 sentences about the next day or broader pattern if supported.
|
||||||
|
|
||||||
|
# precipitation_timing
|
||||||
|
|
||||||
|
Use 1-2 sentences to add practical context, including:
|
||||||
|
|
||||||
|
- Whether the precipitation is associated with a moving frontal boundary, convective initiation, or wide stratiform rain (if this can be determined from the data package);
|
||||||
|
- The expected type, intensity, and duration of the precipitation; and
|
||||||
|
- Any caveats or uncertainty with respect to the onset, duration, or occurrance of the precipitation.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
id: weather.tomorrow_generated_text
|
||||||
|
version: "1.0.0"
|
||||||
|
default_profile: local-heavy
|
||||||
|
#default_profile: gemini-3-flash-lite
|
||||||
|
description: Tomorrow's weather report analysis prompt.
|
||||||
|
inputs:
|
||||||
|
- name: data_package
|
||||||
|
required: true
|
||||||
|
content_type: application/json
|
||||||
|
description: Structured weather data package
|
||||||
|
messages:
|
||||||
|
- role: system
|
||||||
|
content_file: ../common/system.md
|
||||||
|
- role: user
|
||||||
|
content_file: ../common/data_package.user.md
|
||||||
|
- role: user
|
||||||
|
content: |
|
||||||
|
{{input "data_package"}}
|
||||||
|
- role: user
|
||||||
|
content_file: ./tomorrow_generated_text.user.md
|
||||||
|
output:
|
||||||
|
format: json
|
||||||
|
validation_mode: json_schema
|
||||||
|
schema_path: pipeline-weather/tomorrow/tomorrow.generated_text.schema.json
|
||||||
|
repair_attempts: 2
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "weatherreporter.today.generated_text.schema.json",
|
||||||
|
"title": "Today GeneratedText",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"summary",
|
||||||
|
"forecast_discussion"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"summary": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"forecast_discussion": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"minItems": 1
|
||||||
|
},
|
||||||
|
"precipitation_timing": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "weatherreporter.hourly.generated_text.schema.json",
|
||||||
|
"title": "Hourly GeneratedText",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"summary",
|
||||||
|
"forecast_discussion"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"summary": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"forecast_discussion": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"precipitation_timing": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "weatherreporter.today.generated_text.schema.json",
|
||||||
|
"title": "Today GeneratedText",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"summary",
|
||||||
|
"forecast_discussion"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"summary": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"forecast_discussion": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"minItems": 1
|
||||||
|
},
|
||||||
|
"precipitation_timing": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "weatherreporter.tomorrow.generated_text.schema.json",
|
||||||
|
"title": "Tomorrow GeneratedText",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"summary",
|
||||||
|
"forecast_discussion"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"summary": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"forecast_discussion": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"minItems": 1
|
||||||
|
},
|
||||||
|
"precipitation_timing": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,442 +1,179 @@
|
|||||||
# Report Templates
|
# Report Templates
|
||||||
|
|
||||||
## Purpose
|
This guide is for maintainers editing Weatherreporter's embedded Markdown
|
||||||
|
templates. Templates format already validated report inputs; they do not select
|
||||||
|
sources, derive weather facts, or validate generated prose. For those details,
|
||||||
|
see [Generated Text internals](internal/generatedtext.md) and [Report Template
|
||||||
|
internals](internal/reporttemplate.md).
|
||||||
|
|
||||||
This guide describes the implemented Markdown report template surface for
|
## Template Assets
|
||||||
`weatherreporter`. It is for maintainers editing embedded report templates,
|
|
||||||
especially generated-text-template reports.
|
|
||||||
|
|
||||||
Templates are Go `text/template` files. The implemented top-level templates
|
Only the generated-text reports use repository-native Markdown templates.
|
||||||
are:
|
Each report has one matching template ID, generated-text schema ID, and prompt
|
||||||
|
source:
|
||||||
|
|
||||||
- `internal/reporttemplate/templates/daily.md.tmpl`
|
| Report | Template | Schema | Prompt ID and source |
|
||||||
- `internal/reporttemplate/templates/today.md.tmpl`
|
| --- | --- | --- | --- |
|
||||||
- `internal/reporttemplate/templates/tomorrow.md.tmpl`
|
| Daily | `templates/daily.md.tmpl` (`daily`) | `daily` | `weather.daily_generated_text`; `prompts/daily.generated_text.md` |
|
||||||
- `internal/reporttemplate/templates/hourly.md.tmpl`
|
| Today | `templates/today.md.tmpl` (`today`) | `today` | `weather.today_generated_text`; `prompts/today.generated_text.md` |
|
||||||
|
| Tomorrow | `templates/tomorrow.md.tmpl` (`tomorrow`) | `tomorrow` | `weather.tomorrow_generated_text`; `prompts/tomorrow.generated_text.md` |
|
||||||
|
| Hourly | `templates/hourly.md.tmpl` (`hourly`) | `hourly` | `weather.hourly_generated_text`; `prompts/hourly.generated_text.md` |
|
||||||
|
|
||||||
Shared named partials live under `internal/reporttemplate/templates/partials/`:
|
The matching schema files are under `internal/reporttemplate/schemas/`. The
|
||||||
|
generated-text catalog pairs each schema ID with its template ID; keep the
|
||||||
|
matching report prompt source aligned with that pair.
|
||||||
|
|
||||||
- `alert_digest.md.tmpl`, used by Daily, Today, Tomorrow, and Hourly for the
|
Shared partials are under `internal/reporttemplate/templates/partials/`:
|
||||||
combined Alerts and Risk Products section
|
|
||||||
- `daypart_forecast.md.tmpl`, used by Daily and Tomorrow
|
|
||||||
- `today_daypart_forecast.md.tmpl`, used by Today
|
|
||||||
- `precipitation_timing.md.tmpl`, used by Daily, Today, Tomorrow, and Hourly
|
|
||||||
|
|
||||||
Templates are rendered from structured contexts such as `DailyRenderContext`,
|
| Partial | Used by |
|
||||||
`TodayRenderContext`, `TomorrowRenderContext`, and `HourlyRenderContext`.
|
| --- | --- |
|
||||||
Weather data collection, derivation, module execution, generated text
|
| `alert_digest.md.tmpl` | Daily, Today, Tomorrow, and Hourly |
|
||||||
validation, and artifact paths are handled before template rendering.
|
| `precipitation_timing.md.tmpl` | Daily, Today, Tomorrow, and Hourly |
|
||||||
|
| `daypart_forecast.md.tmpl` | Daily and Tomorrow |
|
||||||
|
| `today_daypart_forecast.md.tmpl` | Today |
|
||||||
|
|
||||||
|
All shared partials are parsed whenever any top-level template is rendered. A
|
||||||
|
syntax error in a partial can therefore prevent every generated-text report
|
||||||
|
from rendering.
|
||||||
|
|
||||||
## Editing Rules
|
## Editing Rules
|
||||||
|
|
||||||
- Use Go `text/template` syntax.
|
- Use Go `text/template` syntax and keep changes to Markdown structure,
|
||||||
- Keep templates focused on Markdown layout, headings, ordering, and simple
|
ordering, and display conditions.
|
||||||
conditional display.
|
- Templates use `missingkey=error`; reference only documented fields and guard
|
||||||
- Do not put weather derivation, source selection, or path construction logic in
|
optional module pointers with `with` or `if`.
|
||||||
templates.
|
- Prefer `.Modules` for deterministic display values. Do not add weather
|
||||||
- Missing template keys are errors. A misspelled variable will fail rendering.
|
calculations, source selection, or prompt-input shaping to a template.
|
||||||
- No custom template functions are registered.
|
- Keep generated prose in `.GeneratedText`; do not restate deterministic facts
|
||||||
- Named partials are invoked with `{{ template "name" . }}`. Pass the current
|
in generated prose merely to compensate for a template change.
|
||||||
render context (`.`) unless the partial is intentionally designed for a
|
- When changing the generated-prose contract, update the matching prompt,
|
||||||
narrower value.
|
schema, validator, render context, and template together. The validation and
|
||||||
- Optional module stanzas are pointers and should be guarded with
|
catalog rules are owned by [Generated Text internals](internal/generatedtext.md).
|
||||||
`{{ with .Modules.WeatherStory }}...{{ end }}`.
|
- Use `.Modules.Dayparts` for ordered daypart output. Do not range over
|
||||||
- Slices can be rendered with `{{ range .Items }}...{{ else }}...{{ end }}`.
|
`.Modules.DerivedDaypartSummaries`, which is a map.
|
||||||
|
|
||||||
## Hourly Context
|
Minimal optional-value pattern:
|
||||||
|
|
||||||
The hourly template receives five top-level values:
|
|
||||||
|
|
||||||
| Variable | Type | Description |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `.Report` | HourlyReportContext | Display metadata and friendly labels for the rendered report. |
|
|
||||||
| `.GeneratedText` | Hourly | Structured text returned by Scriptorium. |
|
|
||||||
| `.Modules` | HourlyTemplateModules | Preferred deterministic template surface, keyed by module purpose. |
|
|
||||||
| `.Collected` | facts.CollectedFacts | Normalized upstream facts for advanced template use. |
|
|
||||||
| `.Derived` | facts.DerivedFacts | Shared derived facts for advanced template use. |
|
|
||||||
|
|
||||||
Prefer `.Modules` for normal template edits. `.Collected` and `.Derived` are
|
|
||||||
available when a template needs lower-level facts, but templates should still
|
|
||||||
avoid nontrivial derivation.
|
|
||||||
|
|
||||||
## Report
|
|
||||||
|
|
||||||
| Variable | Type | Description |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `.Report.Title` | string | Display title. Currently `Hourly Report`. |
|
|
||||||
| `.Report.LocationName` | string | Prompt/report location label, such as `Brentwood, MO`. |
|
|
||||||
| `.Report.GeneratedAt` | time.Time | Canonical generation timestamp. |
|
|
||||||
| `.Report.GeneratedAtLabel` | string | Friendly local generation time label. |
|
|
||||||
| `.Report.ValidPeriod` | timeutil.Period | Canonical valid period. |
|
|
||||||
| `.Report.ValidPeriodLabel` | string | Friendly local valid period label, such as `2026-05-29 at 8:30 AM to 2026-05-29 at 2:30 PM`. |
|
|
||||||
| `.Report.Timezone` | string | Effective report timezone. |
|
|
||||||
|
|
||||||
## GeneratedText
|
|
||||||
|
|
||||||
These fields are written by Scriptorium as structured JSON, validated by
|
|
||||||
weatherreporter, and then inserted into the render context.
|
|
||||||
|
|
||||||
| Variable | Type | Description |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `.GeneratedText.Summary` | string | Required short prose summary. |
|
|
||||||
| `.GeneratedText.ForecastDiscussion` | string | Required prose for the Forecast Discussion section. |
|
|
||||||
| `.GeneratedText.PrecipitationTiming` | string | Optional prose rendered after deterministic precipitation windows. |
|
|
||||||
| `.GeneratedText.Confidence` | string | Optional confidence or uncertainty note. Empty when omitted by the LLM; not rendered by the current hourly template. |
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```gotemplate
|
|
||||||
{{ .GeneratedText.Summary }}
|
|
||||||
|
|
||||||
## Forecast Discussion
|
|
||||||
|
|
||||||
{{ .GeneratedText.ForecastDiscussion }}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tomorrow Context
|
|
||||||
|
|
||||||
The Tomorrow template receives five top-level values:
|
|
||||||
|
|
||||||
| Variable | Type | Description |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `.Report` | TomorrowReportContext | Display metadata and friendly labels for the rendered report. |
|
|
||||||
| `.GeneratedText` | Tomorrow | Structured text returned by Scriptorium. |
|
|
||||||
| `.Modules` | TomorrowTemplateModules | Preferred deterministic template surface, keyed by module purpose. |
|
|
||||||
| `.Collected` | facts.CollectedFacts | Normalized upstream facts for advanced template use. |
|
|
||||||
| `.Derived` | facts.DerivedFacts | Shared derived facts for advanced template use. |
|
|
||||||
|
|
||||||
Tomorrow report metadata includes `.Report.Title`, `.Report.ForecastDate`,
|
|
||||||
`.Report.ForecastDateLabel`, `.Report.ForecastDayName`,
|
|
||||||
`.Report.GeneratedAt`, `.Report.GeneratedAtLabel`, `.Report.ValidPeriod`, and
|
|
||||||
`.Report.Timezone`.
|
|
||||||
|
|
||||||
Tomorrow generated text uses the same `.GeneratedText.Summary`,
|
|
||||||
`.GeneratedText.PrecipitationTiming`, and `.GeneratedText.Confidence` fields as
|
|
||||||
Hourly. `.GeneratedText.ForecastDiscussion` is a slice of paragraphs and should
|
|
||||||
be rendered with `range`.
|
|
||||||
|
|
||||||
Tomorrow uses the shared `alert_digest`, `daypart_forecast`, and
|
|
||||||
`precipitation_timing` partials.
|
|
||||||
|
|
||||||
Tomorrow modules include the Hourly module fields plus:
|
|
||||||
|
|
||||||
| Variable | Type | Description |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `.Modules.DerivedDailySummary` | *briefing.DerivedDailySummaryModule | Daily summary facts for the forecast date. |
|
|
||||||
| `.Modules.DerivedDaypartSummaries` | *map[string]briefing.DerivedDaypartSummaryModule | Raw daypart summary map, when direct keyed access is needed. |
|
|
||||||
| `.Modules.Dayparts` | []generatedtext.TomorrowDaypartContext | Ordered daypart summaries for deterministic template rendering. |
|
|
||||||
| `.Modules.TomorrowPlanning` | *briefing.TomorrowPlanningModule | Planning facts for the next local civil day. |
|
|
||||||
|
|
||||||
Prefer `.Modules.Dayparts` over ranging through
|
|
||||||
`.Modules.DerivedDaypartSummaries`; it follows configured daypart order and
|
|
||||||
falls back to sorted keys for any unmatched entries.
|
|
||||||
|
|
||||||
## Daily Context
|
|
||||||
|
|
||||||
The Daily template receives the same five top-level values as Tomorrow, using
|
|
||||||
`DailyReportContext`, `Daily`, and `DailyTemplateModules`.
|
|
||||||
|
|
||||||
Daily report metadata includes `.Report.Title`, `.Report.ForecastDate`,
|
|
||||||
`.Report.ForecastDateLabel`, `.Report.ForecastDayName`,
|
|
||||||
`.Report.GeneratedAt`, `.Report.GeneratedAtLabel`, `.Report.ValidPeriod`, and
|
|
||||||
`.Report.Timezone`.
|
|
||||||
|
|
||||||
Daily generated text uses `.GeneratedText.Summary`,
|
|
||||||
`.GeneratedText.ForecastDiscussion`, `.GeneratedText.PrecipitationTiming`, and
|
|
||||||
`.GeneratedText.Confidence`. Forecast discussion is a slice of paragraphs and
|
|
||||||
should be rendered with `range`.
|
|
||||||
|
|
||||||
Daily uses the shared `alert_digest`, `daypart_forecast`, and
|
|
||||||
`precipitation_timing` partials.
|
|
||||||
|
|
||||||
Daily uses template ID `daily`, generated-text schema ID `daily`, and prompt
|
|
||||||
source `internal/reporttemplate/prompts/daily.generated_text.md`.
|
|
||||||
|
|
||||||
Daily modules include the Hourly module fields plus:
|
|
||||||
|
|
||||||
| Variable | Type | Description |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `.Modules.DerivedDailySummary` | *briefing.DerivedDailySummaryModule | Daily summary facts for the forecast date. |
|
|
||||||
| `.Modules.DerivedDaypartSummaries` | *map[string]briefing.DerivedDaypartSummaryModule | Raw daypart summary map, when direct keyed access is needed. |
|
|
||||||
| `.Modules.Dayparts` | []generatedtext.DailyDaypartContext | Ordered daypart summaries for deterministic template rendering. |
|
|
||||||
| `.Modules.DailyPlanning` | *briefing.DailyPlanningModule | Planning facts for the selected local civil day. |
|
|
||||||
|
|
||||||
Prefer `.Modules.Dayparts` over ranging through
|
|
||||||
`.Modules.DerivedDaypartSummaries`; it follows configured daypart order and
|
|
||||||
falls back to sorted keys for any unmatched entries.
|
|
||||||
|
|
||||||
## Today Context
|
|
||||||
|
|
||||||
The Today template receives the same five top-level values as Tomorrow, using
|
|
||||||
`TodayReportContext`, `Today`, and `TodayTemplateModules`.
|
|
||||||
|
|
||||||
Today report metadata includes `.Report.Title`, `.Report.ForecastDate`,
|
|
||||||
`.Report.ForecastDateLabel`, `.Report.ForecastDayName`,
|
|
||||||
`.Report.GeneratedAt`, `.Report.GeneratedAtLabel`, `.Report.ValidPeriod`, and
|
|
||||||
`.Report.Timezone`.
|
|
||||||
|
|
||||||
Today generated text uses `.GeneratedText.Summary`,
|
|
||||||
`.GeneratedText.ForecastDiscussion`, `.GeneratedText.PrecipitationTiming`, and
|
|
||||||
`.GeneratedText.Confidence`. Forecast discussion is a slice of paragraphs and
|
|
||||||
should be rendered with `range`.
|
|
||||||
|
|
||||||
Today uses the `today_daypart_forecast` partial so elapsed or missing dayparts
|
|
||||||
can be omitted while Daily and Tomorrow keep their fallback row. It also uses
|
|
||||||
the shared `alert_digest` and `precipitation_timing` partials.
|
|
||||||
|
|
||||||
Today uses template ID `today`, generated-text schema ID `today`, and prompt
|
|
||||||
source `internal/reporttemplate/prompts/today.generated_text.md`.
|
|
||||||
|
|
||||||
Today modules include the Hourly module fields plus:
|
|
||||||
|
|
||||||
| Variable | Type | Description |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `.Modules.DerivedDailySummary` | *briefing.DerivedDailySummaryModule | Daily summary facts for the forecast date. |
|
|
||||||
| `.Modules.DerivedDaypartSummaries` | *map[string]briefing.DerivedDaypartSummaryModule | Raw daypart summary map, when direct keyed access is needed. |
|
|
||||||
| `.Modules.Dayparts` | []generatedtext.TodayDaypartContext | Ordered daypart summaries for deterministic template rendering. |
|
|
||||||
| `.Modules.TodayPlanning` | *briefing.TodayPlanningModule | Planning facts for the current local civil day. |
|
|
||||||
|
|
||||||
Prefer `.Modules.Dayparts` over ranging through
|
|
||||||
`.Modules.DerivedDaypartSummaries`; it follows configured daypart order and
|
|
||||||
falls back to sorted keys for any unmatched entries.
|
|
||||||
|
|
||||||
## Modules
|
|
||||||
|
|
||||||
`.Modules` exposes typed outputs from the same module pipeline used for the
|
|
||||||
prompt data package. Module fields are pointers because missing-data policy may
|
|
||||||
omit a stanza.
|
|
||||||
|
|
||||||
Templates render from rich module values, not from the curated YAML data
|
|
||||||
package. Some fields documented below are deterministic wording helpers for
|
|
||||||
Markdown templates and are intentionally omitted from data packages passed to
|
|
||||||
Scriptorium. The data package is a prompt input, while the render context is the
|
|
||||||
template surface.
|
|
||||||
|
|
||||||
| Variable | Type | Description |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `.Modules.Metadata` | *briefing.MetadataModule | Report metadata module output, when present. |
|
|
||||||
| `.Modules.CurrentConditions` | *briefing.CurrentConditionsModule | Current conditions from `/conditions/current`. |
|
|
||||||
| `.Modules.HourlyForecast` | *briefing.HourlyForecastModule | Hourly forecast periods overlapping the report valid period. |
|
|
||||||
| `.Modules.PrecipTiming` | *briefing.PrecipTimingModule | Derived precipitation timing facts and threshold windows. |
|
|
||||||
| `.Modules.AlertDigest` | *briefing.AlertDigestModule | Active alert status and relevant alert overlaps. |
|
|
||||||
| `.Modules.SPCConvectiveOutlooks` | *briefing.SPCConvectiveOutlooksModule | SPC outlooks that overlap the report valid period. |
|
|
||||||
| `.Modules.AreaForecastDiscussion` | *briefing.AreaForecastDiscussionModule | AFD key messages and configured discussion sections. |
|
|
||||||
| `.Modules.SPCConvectiveDiscussion` | *briefing.SPCConvectiveDiscussionModule | SPC discussions retained for qualifying overlapping categorical risk days. |
|
|
||||||
| `.Modules.WeatherStory` | *briefing.WeatherStoryModule | Latest NWS weather story, when available. |
|
|
||||||
|
|
||||||
### Current Conditions
|
|
||||||
|
|
||||||
Common fields:
|
|
||||||
|
|
||||||
| Variable | Type | Description |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `.Modules.CurrentConditions.ConditionText` | string | Current condition text. |
|
|
||||||
| `.Modules.CurrentConditions.ConditionTextLower` | string | Lower-case current condition text for inline sentences. |
|
|
||||||
| `.Modules.CurrentConditions.TemperatureF` | *int | Rounded current temperature. |
|
|
||||||
| `.Modules.CurrentConditions.ApparentTemperatureF` | *int | Rounded apparent temperature. |
|
|
||||||
| `.Modules.CurrentConditions.RelativeHumidityPercent` | *int | Rounded relative humidity. |
|
|
||||||
| `.Modules.CurrentConditions.WindDirection` | string | 16-point compass wind direction. |
|
|
||||||
| `.Modules.CurrentConditions.WindDirectionText` | string | Lower-case full wind direction text, such as `northwest`. |
|
|
||||||
| `.Modules.CurrentConditions.WindSpeedMph` | *int | Rounded wind speed. |
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```gotemplate
|
```gotemplate
|
||||||
{{ with .Modules.CurrentConditions }}
|
{{ with .Modules.CurrentConditions }}
|
||||||
{{ .ConditionText }}{{ with .TemperatureF }}; {{ . }} F{{ end }}{{ with .WindDirection }}; wind {{ . }}{{ end }}{{ with .WindSpeedMph }} {{ . }} mph{{ end }}
|
Currently, it is {{ with .TemperatureF }}{{ . }}°F{{ end }}.
|
||||||
{{ else }}
|
{{ else }}
|
||||||
No current conditions available.
|
Current conditions are unavailable.
|
||||||
{{ end }}
|
{{ end }}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Hourly Forecast
|
Minimal list pattern:
|
||||||
|
|
||||||
Common period fields:
|
|
||||||
|
|
||||||
| Variable | Type | Description |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `.Modules.HourlyForecast.Periods` | []briefing.HourlyForecastPeriod | Ordered periods for the hourly report valid period. |
|
|
||||||
| `.Modules.HourlyForecast.Periods[].HourLabel` | string | Friendly hour label such as `4:00 PM`. |
|
|
||||||
| `.Modules.HourlyForecast.Periods[].PeriodBegins` | string | Friendly local period start label. |
|
|
||||||
| `.Modules.HourlyForecast.Periods[].PeriodEnds` | string | Friendly local period end label. |
|
|
||||||
| `.Modules.HourlyForecast.Periods[].Name` | string | Source period name. |
|
|
||||||
| `.Modules.HourlyForecast.Periods[].TextDescription` | string | Hourly forecast text. |
|
|
||||||
| `.Modules.HourlyForecast.Periods[].TextDescriptionLower` | string | Lower-case hourly forecast text for inline sentences. |
|
|
||||||
| `.Modules.HourlyForecast.Periods[].TemperatureF` | *float64 | Forecast temperature. |
|
|
||||||
| `.Modules.HourlyForecast.Periods[].ProbabilityOfPrecipitationPercent` | *float64 | Forecast precipitation probability. |
|
|
||||||
| `.Modules.HourlyForecast.Periods[].MentionPrecipitation` | bool | True when precipitation probability meets the hourly mention threshold. |
|
|
||||||
| `.Modules.HourlyForecast.Periods[].WindDirection` | string | 16-point compass wind direction. |
|
|
||||||
| `.Modules.HourlyForecast.Periods[].WindSpeedMph` | *float64 | Wind speed. |
|
|
||||||
| `.Modules.HourlyForecast.Periods[].WindGustMph` | *float64 | Wind gust. |
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```gotemplate
|
```gotemplate
|
||||||
{{ with .Modules.HourlyForecast }}{{ range .Periods }}
|
{{ range .GeneratedText.ForecastDiscussion }}
|
||||||
- **{{ .HourLabel }}:**{{ with .TemperatureF }} {{ . }}°F{{ end }} and {{ .TextDescriptionLower }}.{{ if .MentionPrecipitation }}{{ with .ProbabilityOfPrecipitationPercent }} Probability of precipitation is {{ . }}%.{{ end }}{{ end }}
|
{{ . }}
|
||||||
{{ else }}
|
{{ end }}
|
||||||
- No hourly forecast rows available.
|
|
||||||
{{ end }}{{ end }}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Precipitation Timing
|
## Registered Functions
|
||||||
|
|
||||||
Common fields:
|
Templates have these helpers in addition to Go template built-ins:
|
||||||
|
|
||||||
| Variable | Type | Description |
|
| Function | Accepts | Returns true when |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `.Modules.PrecipTiming.MaxPopPercent` | *int | Highest hourly precipitation probability in the valid period. |
|
| `hasRelevantAlerts` | an alert-digest value or pointer | its `Relevant` slice is nonempty |
|
||||||
| `.Modules.PrecipTiming.MaxPopTime` | string | Friendly local time for the highest hourly precipitation probability. |
|
| `hasEnhancedOrHigherSPCRisk` | an SPC outlook value or pointer | its `RiskDigest` contains an Enhanced, Moderate, or High Risk entry |
|
||||||
| `.Modules.PrecipTiming.ProbabilityThreshold` | float64 | Threshold used to define precipitation windows. |
|
| `isEnhancedOrHigherSPCRisk` | one SPC risk-digest entry | its `LabelText`, or fallback `RiskLabel`, is Enhanced, Moderate, or High Risk |
|
||||||
| `.Modules.PrecipTiming.PrecipitationWindows` | []briefing.PrecipitationWindowModule | One or more threshold precipitation windows. |
|
|
||||||
| `.Modules.PrecipTiming.PrecipitationWindows[].PeriodBegins` | string | Friendly local window start. |
|
|
||||||
| `.Modules.PrecipTiming.PrecipitationWindows[].PeriodBeginsHourLabel` | string | Friendly window start hour, such as `4:00 PM`. |
|
|
||||||
| `.Modules.PrecipTiming.PrecipitationWindows[].PeriodEnds` | string | Friendly local window end; omitted for open windows. |
|
|
||||||
| `.Modules.PrecipTiming.PrecipitationWindows[].PeriodEndsHourLabel` | string | Friendly window end hour; omitted for open windows. |
|
|
||||||
| `.Modules.PrecipTiming.PrecipitationWindows[].MaxPopPercent` | *int | Highest precipitation probability inside the window. |
|
|
||||||
| `.Modules.PrecipTiming.PrecipitationWindows[].MaxPopTime` | string | Friendly local time for the window maximum. |
|
|
||||||
| `.Modules.PrecipTiming.PrecipitationWindows[].MaxPopHourLabel` | string | Friendly hour label for the window maximum. |
|
|
||||||
| `.Modules.PrecipTiming.PrecipitationWindows[].PrecipitationType` | string | Conservatively inferred precipitation type, such as `showers and thunderstorms`. |
|
|
||||||
| `.Modules.PrecipTiming.PrecipitationWindows[].ExpectationPhrase` | string | Probability-based sentence used by precipitation timing templates. |
|
|
||||||
| `.Modules.PrecipTiming.ThunderMentioned` | bool | Whether thunder is mentioned in the forecast text. |
|
|
||||||
|
|
||||||
### Daypart Summaries
|
For example, the alert partial uses the first two functions to decide whether
|
||||||
|
to render the section:
|
||||||
|
|
||||||
Daily, Today, and Tomorrow templates should use `.Modules.Dayparts` for
|
```gotemplate
|
||||||
ordered daypart rendering. Each item has `Key` and `Summary`; `Summary` is a
|
{{ if hasRelevantAlerts .Modules.AlertDigest }}
|
||||||
rich `briefing.DerivedDaypartSummaryModule`.
|
## Alert Digest
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
The shared daypart partials render from these same `.Modules.Dayparts` values.
|
## Render Context
|
||||||
Edit `daypart_forecast.md.tmpl` for common Daily/Tomorrow wording, and edit
|
|
||||||
`today_daypart_forecast.md.tmpl` for Today-specific omission behavior.
|
|
||||||
|
|
||||||
Common rich daypart fields:
|
Every rendered template receives one typed context with these five top-level
|
||||||
|
fields:
|
||||||
|
|
||||||
| Variable | Type | Description |
|
| Field | Purpose |
|
||||||
| --- | --- | --- |
|
| --- | --- |
|
||||||
| `.Modules.Dayparts[].Summary.DisplayName` | string | Human-readable daypart label. |
|
| `.Report` | Display labels and canonical report timing metadata. |
|
||||||
| `.Modules.Dayparts[].Summary.PeriodBegins` | string | Friendly local daypart start. |
|
| `.GeneratedText` | Validated prose supplied by Scriptorium. |
|
||||||
| `.Modules.Dayparts[].Summary.PeriodEnds` | string | Friendly local daypart end. |
|
| `.Modules` | Deterministic, typed values prepared for Markdown rendering. |
|
||||||
| `.Modules.Dayparts[].Summary.TempRangeF` | string | Rounded temperature range or single temperature. |
|
| `.Collected` | Normalized upstream facts for advanced use. |
|
||||||
| `.Modules.Dayparts[].Summary.TemperaturePhraseF` | string | Temperature phrase used for steady template wording. |
|
| `.Derived` | Shared calculated facts for advanced use. |
|
||||||
| `.Modules.Dayparts[].Summary.TemperatureTrend` | string | Trend category such as `rising`, `falling`, `peaking`, or `steady`. |
|
|
||||||
| `.Modules.Dayparts[].Summary.TemperatureStartPhraseF` | string | Starting temperature phrase for rising/falling wording. |
|
|
||||||
| `.Modules.Dayparts[].Summary.TemperatureEndPhraseF` | string | Ending temperature phrase for rising/falling wording. |
|
|
||||||
| `.Modules.Dayparts[].Summary.TemperaturePeakPhraseF` | string | Peak temperature phrase for peaking wording. |
|
|
||||||
| `.Modules.Dayparts[].Summary.TemperatureSteadyPhraseF` | string | Steady temperature phrase. |
|
|
||||||
| `.Modules.Dayparts[].Summary.MaxPopPercent` | *int | Highest precipitation probability in the daypart. |
|
|
||||||
| `.Modules.Dayparts[].Summary.MaxPopTime` | string | Friendly local time for the highest precipitation probability. |
|
|
||||||
| `.Modules.Dayparts[].Summary.MaxPopTimeLabel` | string | Clock-style label for deterministic precipitation timing text. |
|
|
||||||
| `.Modules.Dayparts[].Summary.MentionPrecipitation` | bool | True when precipitation probability should be mentioned by the template. |
|
|
||||||
| `.Modules.Dayparts[].Summary.DominantCondition` | string | Dominant condition text. |
|
|
||||||
| `.Modules.Dayparts[].Summary.DominantConditionLower` | string | Lower-case condition text for inline sentences. |
|
|
||||||
| `.Modules.Dayparts[].Summary.DominantConditionDisplay` | string | Display-case condition text for bullet starts. |
|
|
||||||
| `.Modules.Dayparts[].Summary.NotableConditions` | []string | Notable condition labels retained for the daypart. |
|
|
||||||
|
|
||||||
Template-only daypart helpers such as `TemperaturePhraseF`,
|
`.Collected` and `.Derived` are available for an exceptional display need, but
|
||||||
`DominantConditionLower`, `DominantConditionDisplay`, and `MaxPopTimeLabel`
|
they are lower-level contracts. Keep reusable weather derivation in Go and use
|
||||||
remain available here even though they are not serialized into data-package
|
the module surface for normal template work.
|
||||||
YAML.
|
|
||||||
|
|
||||||
### Alert Digest
|
### Report Metadata
|
||||||
|
|
||||||
| Variable | Type | Description |
|
All contexts provide `.Report.Title`, `.Report.GeneratedAt`,
|
||||||
| --- | --- | --- |
|
`.Report.GeneratedAtLabel`, `.Report.ValidPeriod`, and `.Report.Timezone`.
|
||||||
| `.Modules.AlertDigest.Checked` | bool | Whether alert data was checked successfully. |
|
|
||||||
| `.Modules.AlertDigest.ActiveCount` | int | Active alert count from the source. |
|
|
||||||
| `.Modules.AlertDigest.RelevantCount` | int | Alert count overlapping the report period. |
|
|
||||||
| `.Modules.AlertDigest.Missing` | bool | True when alert data is unavailable. |
|
|
||||||
| `.Modules.AlertDigest.Relevant` | []briefing.AlertSummary | Relevant alert summaries. |
|
|
||||||
| `.Modules.AlertDigest.Relevant[].Event` | string | Alert event name. |
|
|
||||||
| `.Modules.AlertDigest.Relevant[].Headline` | string | Alert headline. |
|
|
||||||
| `.Modules.AlertDigest.Relevant[].Severity` | string | Alert severity. |
|
|
||||||
| `.Modules.AlertDigest.Relevant[].PeriodBegins` | string | Friendly local alert applicability start. |
|
|
||||||
| `.Modules.AlertDigest.Relevant[].PeriodEnds` | string | Friendly local alert applicability end. |
|
|
||||||
| `.Modules.AlertDigest.Relevant[].Instruction` | string | Alert instruction text, when provided. |
|
|
||||||
| `.Modules.AlertDigest.Relevant[].Description` | string | Alert description text, when provided. |
|
|
||||||
|
|
||||||
### SPC Outlooks And Discussion
|
Hourly additionally provides `.Report.LocationName` and
|
||||||
|
`.Report.ValidPeriodLabel`.
|
||||||
|
|
||||||
| Variable | Type | Description |
|
Daily, Today, and Tomorrow additionally provide `.Report.ForecastDate`,
|
||||||
| --- | --- | --- |
|
`.Report.ForecastDateLabel`, and `.Report.ForecastDayName`. Their valid-period
|
||||||
| `.Modules.SPCConvectiveOutlooks.Checked` | bool | Whether SPC outlook data was checked successfully. |
|
field remains canonical timing data; use the supplied display labels instead
|
||||||
| `.Modules.SPCConvectiveOutlooks.AsOf` | string | Friendly source as-of time. |
|
of formatting timestamps in a template.
|
||||||
| `.Modules.SPCConvectiveOutlooks.IssuedAt` | string | Friendly source issue time. |
|
|
||||||
| `.Modules.SPCConvectiveOutlooks.Outlooks` | []briefing.SPCConvectiveOutlookRecord | Overlapping outlook records. |
|
|
||||||
| `.Modules.SPCConvectiveOutlooks.Outlooks[].Day` | int | SPC day number. |
|
|
||||||
| `.Modules.SPCConvectiveOutlooks.Outlooks[].OutlookType` | string | Outlook type, such as `categorical`. |
|
|
||||||
| `.Modules.SPCConvectiveOutlooks.Outlooks[].Label` | string | Short outlook label. |
|
|
||||||
| `.Modules.SPCConvectiveOutlooks.Outlooks[].LabelText` | string | Human-readable outlook label. |
|
|
||||||
| `.Modules.SPCConvectiveOutlooks.Outlooks[].PeriodBegins` | string | Friendly outlook period start. |
|
|
||||||
| `.Modules.SPCConvectiveOutlooks.Outlooks[].PeriodEnds` | string | Friendly outlook period end. |
|
|
||||||
| `.Modules.SPCConvectiveOutlooks.Outlooks[].ImageURL` | string | Source image URL. |
|
|
||||||
| `.Modules.SPCConvectiveOutlooks.RiskDigest` | []briefing.SPCConvectiveOutlookDigest | Curated categorical outlooks for the shared Alerts and Risk Products section. |
|
|
||||||
| `.Modules.SPCConvectiveOutlooks.RiskDigest[].LabelText` | string | Human-readable outlook label. |
|
|
||||||
| `.Modules.SPCConvectiveOutlooks.RiskDigest[].RiskLabel` | string | Sentence-style risk label for report rendering. |
|
|
||||||
| `.Modules.SPCConvectiveOutlooks.RiskDigest[].PeriodBegins` | string | Friendly outlook period start. |
|
|
||||||
| `.Modules.SPCConvectiveOutlooks.RiskDigest[].PeriodEnds` | string | Friendly outlook period end. |
|
|
||||||
| `.Modules.SPCConvectiveDiscussion.IncludedBecause` | string | Criterion used to include discussions. |
|
|
||||||
| `.Modules.SPCConvectiveDiscussion.Discussions` | []briefing.SPCConvectiveDiscussionRecord | Retained discussion records. |
|
|
||||||
| `.Modules.SPCConvectiveDiscussion.Discussions[].Headline` | string | Discussion headline. |
|
|
||||||
| `.Modules.SPCConvectiveDiscussion.Discussions[].Summary` | string | Discussion summary. |
|
|
||||||
| `.Modules.SPCConvectiveDiscussion.Discussions[].Discussion` | string | Full discussion text. |
|
|
||||||
|
|
||||||
### Area Forecast Discussion
|
### Validated GeneratedText Prose
|
||||||
|
|
||||||
| Variable | Type | Description |
|
GeneratedText is prose returned by Scriptorium and validated before rendering.
|
||||||
| --- | --- | --- |
|
It is not a source for deterministic weather facts.
|
||||||
| `.Modules.AreaForecastDiscussion.Product` | string | Source product identifier. |
|
|
||||||
| `.Modules.AreaForecastDiscussion.KeyMessages` | []string | AFD key messages. |
|
|
||||||
| `.Modules.AreaForecastDiscussion.ShortTerm` | string | AFD short-term section text. |
|
|
||||||
| `.Modules.AreaForecastDiscussion.LongTerm` | string | AFD long-term section text. |
|
|
||||||
|
|
||||||
### Weather Story
|
| Field | Hourly type | Daily, Today, and Tomorrow type | Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `.GeneratedText.Summary` | `string` | `string` | Required. |
|
||||||
|
| `.GeneratedText.ForecastDiscussion` | `string` | `[]string` | Required; range over the day-style paragraph slice. |
|
||||||
|
| `.GeneratedText.PrecipitationTiming` | `string` | `string` | Optional prose used by the precipitation partial when deterministic windows exist. |
|
||||||
|
| `.GeneratedText.Confidence` | `string` | `string` | Optional validated prose; the current templates do not render it. |
|
||||||
|
|
||||||
| Variable | Type | Description |
|
The JSON schema rejects unknown properties and defines the required fields, but
|
||||||
| --- | --- | --- |
|
the schema body and validation behavior are documented in [Generated Text
|
||||||
| `.Modules.WeatherStory.Available` | bool | True when a story is available. |
|
internals](internal/generatedtext.md).
|
||||||
| `.Modules.WeatherStory.OfficeID` | string | Source office ID. |
|
|
||||||
| `.Modules.WeatherStory.PeriodBegins` | string | Friendly story period start. |
|
|
||||||
| `.Modules.WeatherStory.PeriodEnds` | string | Friendly story period end. |
|
|
||||||
| `.Modules.WeatherStory.UpdatedAt` | *time.Time | Canonical update timestamp. |
|
|
||||||
| `.Modules.WeatherStory.Title` | string | Story title. |
|
|
||||||
| `.Modules.WeatherStory.Description` | string | Story description. |
|
|
||||||
| `.Modules.WeatherStory.AltText` | string | Story image alt text. |
|
|
||||||
| `.Modules.WeatherStory.Priority` | bool | Source priority flag. |
|
|
||||||
| `.Modules.WeatherStory.Order` | int | Source order. |
|
|
||||||
| `.Modules.WeatherStory.DownloadURL` | string | Source download URL. |
|
|
||||||
|
|
||||||
## Collected And Derived Facts
|
### Deterministic Module Values
|
||||||
|
|
||||||
The template also receives the full `facts.CollectedFacts` and
|
Module values are deterministic outputs built from collected and derived facts.
|
||||||
`facts.DerivedFacts` structs:
|
Module pointers can be nil when their source or policy permits omission.
|
||||||
|
|
||||||
- `.Collected` contains normalized source data and provenance from upstream
|
| Module field | Available in |
|
||||||
Weather API fetches.
|
| --- | --- |
|
||||||
- `.Derived` contains shared slices and calculations used across modules, such
|
| `.Modules.Metadata`, `.Modules.CurrentConditions`, `.Modules.HourlyForecast`, `.Modules.PrecipTiming`, `.Modules.AlertDigest`, `.Modules.SPCConvectiveOutlooks`, `.Modules.AreaForecastDiscussion`, `.Modules.SPCConvectiveDiscussion`, `.Modules.WeatherStory` | All four contexts |
|
||||||
as valid-period hourly periods, precipitation timing, alert overlaps, and SPC
|
| `.Modules.DerivedDailySummary`, `.Modules.DerivedDaypartSummaries`, `.Modules.Dayparts` | Daily, Today, Tomorrow |
|
||||||
filtering inputs.
|
| `.Modules.OutdoorWindows`, `.Modules.DailyPlanning` | Daily |
|
||||||
|
| `.Modules.TodayPlanning` | Today |
|
||||||
|
| `.Modules.TomorrowPlanning` | Tomorrow |
|
||||||
|
|
||||||
These values are intentionally lower-level than `.Modules`. Use them when a
|
The repository templates currently use the following nested display values.
|
||||||
template needs a specific field that is not exposed by a module, but keep
|
They are the preferred surface for comparable edits:
|
||||||
calculation-heavy changes in Go.
|
|
||||||
|
|
||||||
## Validation
|
| Area | Values |
|
||||||
|
| --- | --- |
|
||||||
|
| Current conditions | `.TemperatureF`, `.ConditionText`, `.ConditionTextLower`, `.ApparentTemperatureF`, `.RelativeHumidityPercent`, `.WindDirectionText`, `.WindSpeedMph` |
|
||||||
|
| Hourly periods | `.Periods`, `.HourLabel`, `.Name`, `.TemperatureF`, `.TextDescription`, `.TextDescriptionLower`, `.MentionPrecipitation`, `.ProbabilityOfPrecipitationPercent` |
|
||||||
|
| Dayparts | `.Dayparts[].Key` and `.Dayparts[].Summary` fields `DisplayName`, `DominantCondition`, `DominantConditionDisplay`, `TemperatureTrend`, `TemperatureStartPhraseF`, `TemperatureEndPhraseF`, `TemperaturePeakPhraseF`, `TemperatureSteadyPhraseF`, `TemperaturePhraseF`, `MentionPrecipitation`, and `MaxPopPercent` |
|
||||||
|
| Precipitation timing | `.PrecipitationWindows`, plus each window's `PeriodBegins`, `PeriodBeginsHourLabel`, `PeriodEnds`, `PeriodEndsHourLabel`, `ExpectationPhrase`, `MaxPopPercent`, `MaxPopTime`, and `MaxPopHourLabel` |
|
||||||
|
| Alert digest | `.AlertDigest.Relevant` entries' `Event`, `Headline`, `PeriodBegins`, and `PeriodEnds` |
|
||||||
|
| SPC risk digest | `.SPCConvectiveOutlooks.RiskDigest` entries' `LabelText`, `RiskLabel`, `PeriodBegins`, and `PeriodEnds` |
|
||||||
|
|
||||||
After editing a template, run:
|
Other fields on these typed modules remain available when a template has a
|
||||||
|
well-defined display need. Their module contracts and weather derivation belong
|
||||||
|
to [Module contract internals](internal/module.md), [Module builder
|
||||||
|
internals](internal/briefing.md), and [Forecast derivation
|
||||||
|
internals](internal/forecast-derivation.md).
|
||||||
|
|
||||||
```bash
|
## Validate Changes
|
||||||
|
|
||||||
|
Run the focused checks after editing templates, partials, prompts, or schemas:
|
||||||
|
|
||||||
|
```sh
|
||||||
go test ./internal/reporttemplate ./internal/generatedtext ./internal/app
|
go test ./internal/reporttemplate ./internal/generatedtext ./internal/app
|
||||||
```
|
|
||||||
|
|
||||||
For a full check, run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go test ./...
|
|
||||||
go run ./cmd/weatherreporter --help
|
|
||||||
git diff --check
|
git diff --check
|
||||||
```
|
```
|
||||||
|
|
||||||
Template render tests exercise the Daily, Today, Tomorrow, and Hourly
|
The render-context and template tests cover Daily, Today, Tomorrow, and Hourly
|
||||||
templates through `internal/generatedtext/render_context_test.go` and
|
contexts. Run the repository-wide test suite before merging a broader change.
|
||||||
`internal/reporttemplate/reporttemplate_test.go`.
|
|
||||||
|
|||||||
@@ -1,481 +1,271 @@
|
|||||||
# Weatherreporter Troubleshooting
|
# Troubleshooting
|
||||||
|
|
||||||
This guide lists recurring failures with likely causes, diagnostics, and safe
|
Use the error from the command together with the run artifacts when a run ID is
|
||||||
fixes. See [CLI reference](cli.md), [Configuration reference](config.md), and
|
available. Start with [`inspect metadata`](cli.md#inspection-commands) to identify the
|
||||||
[Operations guide](operations.md) for normal usage.
|
report and artifact paths, then use the narrower inspection command named
|
||||||
|
below. Do not remove a workspace to diagnose a failure: it contains the
|
||||||
|
evidence needed to correct it safely.
|
||||||
|
|
||||||
## `weather_api.base_url is required`
|
## A command or configuration is rejected before work starts
|
||||||
|
|
||||||
Symptom: a generation command fails before collecting weather data.
|
Symptom: The command exits before it creates a run, with an unknown-flag,
|
||||||
|
missing-argument, invalid date or time bound, invalid timezone, or
|
||||||
|
`weather_api.base_url` message.
|
||||||
|
|
||||||
Likely cause: no Weather API base URL is configured.
|
Likely cause: The command does not accept that option for the requested report,
|
||||||
|
or required command and configuration values are absent or malformed.
|
||||||
|
|
||||||
Diagnostic:
|
Diagnostic: Compare the command with [`generate` and `run`](cli.md#commands-and-usage)
|
||||||
|
and review the configured value named in the error. `generate daily` requires
|
||||||
|
`--date`; `generate storm` requires both `--start` and `--end`.
|
||||||
|
|
||||||
```sh
|
Safe fix: Correct only the reported option or configuration value. Use an
|
||||||
weatherreporter generate daily --config ./config.yml --date 2026-05-29
|
absolute Weather API URL and a valid IANA timezone; do not change unrelated
|
||||||
```
|
workspace data.
|
||||||
|
|
||||||
Safe fix: add `weather_api.base_url` to the config file, or pass the intended
|
See also: [Configuration](config.md) and [Weather API integration](integrations/weatherapi.md).
|
||||||
config path with `--config`.
|
|
||||||
|
|
||||||
Relevant docs: [Configuration reference](config.md).
|
## Weather data cannot be collected
|
||||||
|
|
||||||
## `weather_api.base_url must be an absolute URL`
|
Symptom: A generation command fails while fetching weather data, or reports
|
||||||
|
`hourly forecast data is missing` or `contains no periods`.
|
||||||
|
|
||||||
Symptom: config loading fails with a base URL validation error.
|
Likely cause: The Weather API is unavailable, its configured endpoint or
|
||||||
|
credentials are unsuitable, or the response lacks the hourly forecast required
|
||||||
|
by the selected report.
|
||||||
|
|
||||||
Likely cause: `weather_api.base_url` is missing a scheme or host.
|
Diagnostic: Check the service status and the configured base URL, then retry
|
||||||
|
the same report. If a run ID was produced, run `weatherreporter inspect sources
|
||||||
|
RUN_ID` to see the recorded source result.
|
||||||
|
|
||||||
Diagnostic: inspect the configured value in the file passed to `--config`.
|
Safe fix: Restore access to the configured Weather API or choose a reporting
|
||||||
|
period supported by the returned forecast. Do not invent missing hourly values
|
||||||
|
in local artifacts.
|
||||||
|
|
||||||
Safe fix: use an absolute URL such as `https://weather.api.example.com/`.
|
See also: [Configuration](config.md) and [Weather API integration](integrations/weatherapi.md).
|
||||||
|
|
||||||
Relevant docs: [Configuration reference](config.md).
|
## Optional source warnings appear
|
||||||
|
|
||||||
## Invalid Timezone
|
Symptom: The report succeeds but its output says that a source supplied a
|
||||||
|
warning or degraded result.
|
||||||
|
|
||||||
Symptom: config loading fails with `weather_api.timezone` context, or a CLI
|
Likely cause: An optional source did not return usable data; mandatory weather
|
||||||
timezone override fails.
|
collection still completed.
|
||||||
|
|
||||||
Likely cause: `weather_api.timezone` or `--tz` is not recognized.
|
Diagnostic: Run `weatherreporter inspect sources RUN_ID` and identify the
|
||||||
|
source and warning recorded for that run.
|
||||||
|
|
||||||
Diagnostic:
|
Safe fix: Correct the affected source configuration or service issue, then
|
||||||
|
generate a new report if the missing optional information is needed. Keep the
|
||||||
|
existing run for comparison.
|
||||||
|
|
||||||
```sh
|
See also: [Inspecting a run](cli.md#inspection-commands) and [Operations](operations.md).
|
||||||
weatherreporter generate daily --tz America/Chicago --date 2026-05-29
|
|
||||||
```
|
|
||||||
|
|
||||||
Safe fix: use an accepted timezone value, such as an IANA timezone name,
|
## Scriptorium cannot be prepared
|
||||||
`Chicago`, `Stl`, a US timezone abbreviation, or a UTC offset.
|
|
||||||
|
|
||||||
Relevant docs: [Configuration reference](config.md).
|
Symptom: The report fails with a fragment such as `run scriptorium render`, or
|
||||||
|
the Scriptorium executable cannot be started.
|
||||||
|
|
||||||
## Storm Command Rejects Time Bounds
|
Likely cause: The configured executable, profile, prompt, or its local runtime
|
||||||
|
environment is unavailable to Weatherreporter.
|
||||||
|
|
||||||
Symptom: `generate storm` fails with `requires --start`, `requires --end`, or
|
Diagnostic: Confirm that the configured executable can be run by the same user
|
||||||
`requires --end after --start`.
|
and inspect `weatherreporter inspect metadata RUN_ID` when a run ID is shown.
|
||||||
|
|
||||||
Likely cause: the manual event window is missing or invalid.
|
Safe fix: Repair the executable path or the Scriptorium configuration and retry
|
||||||
|
the report. Do not edit generated artifacts to bypass preparation.
|
||||||
|
|
||||||
Diagnostic:
|
See also: [Configuration](config.md) and [Operations](operations.md).
|
||||||
|
|
||||||
```sh
|
## Scriptorium preflight fails
|
||||||
weatherreporter generate storm --start 2026-05-29T18:00 --end 2026-05-30T06:00
|
|
||||||
```
|
|
||||||
|
|
||||||
Safe fix: provide both bounds. Use `YYYY-MM-DDTHH:MM` in the configured
|
Symptom: A Scriptorium-backed report stops before text generation, often with
|
||||||
timezone, or RFC3339 timestamps with explicit offsets.
|
a `scriptorium render exited with code` fragment.
|
||||||
|
|
||||||
Relevant docs: [CLI reference](cli.md).
|
Likely cause: Scriptorium rejected the render request, prompt, profile, or data
|
||||||
|
package before it could run the report.
|
||||||
|
|
||||||
## Weather API Fetch Fails
|
Diagnostic: Inspect the run metadata and the saved preflight artifact path it
|
||||||
|
references. Compare the reported Scriptorium diagnostic with its configuration.
|
||||||
|
|
||||||
Symptom: generation fails with `fetch /...`, an HTTP status, or request context.
|
Safe fix: Correct the reported Scriptorium input or configuration, then create
|
||||||
|
a new run. Preserve the failed preflight artifact for support or comparison.
|
||||||
|
|
||||||
Likely cause: the configured Weather API endpoint is unreachable, returned a
|
See also: [Inspecting a run](cli.md#inspection-commands) and [Operations](operations.md).
|
||||||
non-2xx response, or returned an invalid response envelope.
|
|
||||||
|
|
||||||
Diagnostic:
|
## Scriptorium report execution fails
|
||||||
|
|
||||||
```sh
|
Symptom: Preparation succeeded, but generation stops with a
|
||||||
weatherreporter generate daily --config ./config.yml --date 2026-05-29
|
`scriptorium run exited with code` fragment.
|
||||||
```
|
|
||||||
|
|
||||||
Safe fix: verify `weather_api.base_url`, network access, and the Weather API
|
Likely cause: The Scriptorium run failed after preflight, for example because
|
||||||
service response. The adapter fetches `/observations`, `/conditions/current`,
|
its prompt execution or runtime dependency failed.
|
||||||
`/forecast/hourly`, `/forecast/narrative`, `/alerts/active`, and `/discussion`.
|
|
||||||
|
|
||||||
Relevant docs: [Configuration reference](config.md).
|
Diagnostic: Inspect the run metadata and preflight artifact, then review the
|
||||||
|
exit diagnostic from the command. This distinguishes a run failure from a
|
||||||
|
preflight failure.
|
||||||
|
|
||||||
## Hourly Forecast Is Missing
|
Safe fix: Correct the Scriptorium issue identified by that diagnostic and run
|
||||||
|
the report again; leave the failed run artifacts in place.
|
||||||
|
|
||||||
Symptom: generation fails with hourly forecast context, such as missing hourly
|
See also: [Operations](operations.md).
|
||||||
data or an hourly forecast containing no periods.
|
|
||||||
|
|
||||||
Likely cause: hourly forecast data is required for generated reports.
|
## Generated text fails validation
|
||||||
|
|
||||||
Diagnostic: check the Weather API response for `/forecast/hourly`.
|
Symptom: A generated-text report fails after Scriptorium returns text, with a
|
||||||
|
message about generated text or required report content.
|
||||||
|
|
||||||
Safe fix: restore hourly forecast data at the Weather API. Missing-source
|
Likely cause: Returned text does not meet the report's validation rules.
|
||||||
policy cannot make hourly optional.
|
|
||||||
|
|
||||||
Relevant docs: [Configuration reference](config.md), [Operations guide](operations.md).
|
Diagnostic: Use `weatherreporter inspect metadata RUN_ID` to find the saved raw
|
||||||
|
generated-text artifact, and inspect it alongside the reported validation
|
||||||
|
message.
|
||||||
|
|
||||||
## Source Warnings Appear
|
Safe fix: Correct the upstream prompt or generation configuration that caused
|
||||||
|
the invalid output, then create a new run. Do not hand-edit saved raw text and
|
||||||
|
present it as a validated report.
|
||||||
|
|
||||||
Symptom: generation succeeds, but metadata or `inspect sources` shows source
|
See also: [Operations](operations.md).
|
||||||
warnings.
|
|
||||||
|
|
||||||
Likely cause: an optional source was missing or malformed under a warning
|
## Report template rendering fails
|
||||||
missing-source policy.
|
|
||||||
|
|
||||||
Diagnostic:
|
Symptom: Scriptorium output is available, but the report fails while building
|
||||||
|
the final Markdown document.
|
||||||
|
|
||||||
```sh
|
Likely cause: The selected report template or the render context is
|
||||||
weatherreporter inspect sources RUN_ID
|
incompatible with the generated or collected data.
|
||||||
weatherreporter inspect metadata RUN_ID
|
|
||||||
```
|
|
||||||
|
|
||||||
Safe fix: inspect the warning `source`, `code`, `message`, and `endpoint`. Fix
|
Diagnostic: Inspect the metadata, generated-text result, and render-context
|
||||||
the upstream optional source, or intentionally change the relevant
|
artifacts for the run. Note the template or missing-field fragment in the
|
||||||
`missing_source` policy.
|
error rather than relying on a complete error string.
|
||||||
|
|
||||||
Relevant docs: [Configuration reference](config.md), [Operations guide](operations.md).
|
Safe fix: Correct the template or its supported inputs in source control, test
|
||||||
|
the change, and create a new report. Do not alter the saved context merely to
|
||||||
|
make one historical run render.
|
||||||
|
|
||||||
## `scriptorium` Is Not Found Or Cannot Start
|
See also: [Operations](operations.md).
|
||||||
|
|
||||||
Symptom: generation fails with `run scriptorium render` or `run scriptorium`
|
## A report fails after artifacts are saved
|
||||||
and an executable or OS error.
|
|
||||||
|
|
||||||
Likely cause: the configured Scriptorium binary is unavailable or not
|
Symptom: A generation command reports an error after showing a run ID, such as
|
||||||
executable.
|
an error writing the managed report, copying `--out`, saving metadata, or
|
||||||
|
notifying Distributor.
|
||||||
|
|
||||||
Diagnostic: check `scriptorium.binary` in config and run the same binary outside
|
Likely cause: A local filesystem permission or path problem, an unavailable
|
||||||
`weatherreporter`.
|
destination for `--out`, or a later report-delivery failure occurred after
|
||||||
|
earlier steps succeeded.
|
||||||
|
|
||||||
Safe fix: install Scriptorium, update `scriptorium.binary`, or fix executable
|
Diagnostic: Run `weatherreporter inspect metadata RUN_ID` and check the exact
|
||||||
|
path and operation named in the error. For an `--out` failure, verify only the
|
||||||
|
specified destination directory and filename.
|
||||||
|
|
||||||
|
Safe fix: Repair access to that exact path or disable the optional delivery
|
||||||
|
step only when appropriate, then generate a new report. Keep the existing
|
||||||
|
managed artifacts untouched.
|
||||||
|
|
||||||
|
See also: [Operations](operations.md) and [Distributor integration](integrations/distributor/pkg-upload.md).
|
||||||
|
|
||||||
|
## A batch has partial report failures
|
||||||
|
|
||||||
|
Symptom: `run morning` or `run evening` returns nonzero and reports both
|
||||||
|
succeeded and failed report items.
|
||||||
|
|
||||||
|
Likely cause: A report-level collection, generation, rendering, or local
|
||||||
|
output failure affected one or more planned reports; the remaining reports
|
||||||
|
continue independently.
|
||||||
|
|
||||||
|
Diagnostic: Read the per-report status lines, then inspect the run ID for each
|
||||||
|
failed item with `weatherreporter inspect metadata RUN_ID`.
|
||||||
|
|
||||||
|
Safe fix: Correct the specific failure and rerun the batch or affected report.
|
||||||
|
Do not delete successful reports simply because another item failed.
|
||||||
|
|
||||||
|
See also: [Batch commands](cli.md#commands-and-usage) and [Operations](operations.md).
|
||||||
|
|
||||||
|
## A batch upload is skipped
|
||||||
|
|
||||||
|
Symptom: The batch result says Distributor notification was skipped because
|
||||||
|
one or more reports failed.
|
||||||
|
|
||||||
|
Likely cause: Batch notification intentionally runs only after every planned
|
||||||
|
report succeeds.
|
||||||
|
|
||||||
|
Diagnostic: Review the failed report items and their metadata; a skipped batch
|
||||||
|
notification is expected while any item is failed.
|
||||||
|
|
||||||
|
Safe fix: Resolve the report failures and rerun the batch. Do not upload a
|
||||||
|
partial bundle by manually reusing batch artifacts.
|
||||||
|
|
||||||
|
See also: [Batch commands](cli.md#commands-and-usage) and [Operations](operations.md).
|
||||||
|
|
||||||
|
## Distributor notification fails
|
||||||
|
|
||||||
|
Symptom: A completed report or otherwise successful batch reports a Distributor
|
||||||
|
error, including a rejected upload, source or idempotency conflict, or service
|
||||||
|
unavailability.
|
||||||
|
|
||||||
|
Likely cause: Distributor rejected the request identity or bundle, required
|
||||||
|
credentials are unavailable, or the remote service cannot be reached.
|
||||||
|
|
||||||
|
Diagnostic: Inspect the report metadata or batch result for the notification
|
||||||
|
artifact and the error fragment. Verify the configured Distributor endpoint and
|
||||||
|
request identity without exposing credentials.
|
||||||
|
|
||||||
|
Safe fix: Resolve the reported remote conflict, configuration, or availability
|
||||||
|
issue and create a new report or rerun the batch. Do not modify recorded bundle
|
||||||
|
or idempotency artifacts to force an upload.
|
||||||
|
|
||||||
|
See also: [Configuration](config.md), [Distributor integration](integrations/distributor/pkg-upload.md), and [Operations](operations.md).
|
||||||
|
|
||||||
|
## Secrets cannot be loaded
|
||||||
|
|
||||||
|
Symptom: Startup reports `read secrets directory`, `secret file`, or a token
|
||||||
|
environment-variable error before the affected service can be used.
|
||||||
|
|
||||||
|
Likely cause: The configured secrets directory cannot be read, contains a
|
||||||
|
non-regular file, or does not supply the environment variable required by an
|
||||||
|
enabled integration.
|
||||||
|
|
||||||
|
Diagnostic: Check the configured secrets directory path, ownership, and that
|
||||||
|
each intended secret is a regular file. Confirm the variable name from
|
||||||
|
configuration only; never print or paste its value.
|
||||||
|
|
||||||
|
Safe fix: Correct permissions, file type, or the missing secret file, then
|
||||||
|
retry. Keep secret values out of commands, logs, tickets, and artifacts.
|
||||||
|
|
||||||
|
See also: [Configuration](config.md) and [Operations](operations.md).
|
||||||
|
|
||||||
|
## A run ID or saved state cannot be found
|
||||||
|
|
||||||
|
Symptom: An inspection command reports that metadata for a run ID was not
|
||||||
|
found, or a report cannot use a prior snapshot.
|
||||||
|
|
||||||
|
Likely cause: The run ID is wrong, the configured workspace is different from
|
||||||
|
the one that created the run, or no compatible prior snapshot exists.
|
||||||
|
|
||||||
|
Diagnostic: Use `weatherreporter inspect reports` to list available reports in
|
||||||
|
the current workspace, then copy the run ID from that output. Confirm the
|
||||||
|
workspace configuration before retrying a prior-snapshot operation.
|
||||||
|
|
||||||
|
Safe fix: Use an existing run ID and its original workspace, or generate a new
|
||||||
|
compatible report when no prior snapshot is available. Do not fabricate state
|
||||||
|
files or run IDs.
|
||||||
|
|
||||||
|
See also: [Inspecting a run](cli.md#inspection-commands) and [Operations](operations.md).
|
||||||
|
|
||||||
|
## Workspace paths cannot be read or written
|
||||||
|
|
||||||
|
Symptom: Startup or report persistence reports a workspace-path, permission,
|
||||||
|
or "must be relative to workspace root" error.
|
||||||
|
|
||||||
|
Likely cause: A configured artifact directory escapes the workspace, or the
|
||||||
|
current user lacks access to the specific workspace location.
|
||||||
|
|
||||||
|
Diagnostic: Check the named configuration path against the configured workspace
|
||||||
|
root and inspect ownership and permissions of that exact directory.
|
||||||
|
|
||||||
|
Safe fix: Set the path to a location within the workspace or repair access to
|
||||||
|
the named directory, then rerun. Do not remove the workspace or broadly relax
|
||||||
permissions.
|
permissions.
|
||||||
|
|
||||||
Relevant docs: [Configuration reference](config.md),
|
See also: [Configuration](config.md) and [Operations](operations.md).
|
||||||
[Scriptorium integration](integrations/scriptorium.md).
|
|
||||||
|
|
||||||
## Render Preflight Fails
|
|
||||||
|
|
||||||
Symptom: generation fails with `scriptorium render exited with code ...`.
|
|
||||||
|
|
||||||
Likely cause: Scriptorium rejected the prompt, config, profile, or
|
|
||||||
`data_package` input before report generation.
|
|
||||||
|
|
||||||
Diagnostic:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
weatherreporter inspect metadata RUN_ID
|
|
||||||
weatherreporter inspect data-package RUN_ID
|
|
||||||
```
|
|
||||||
|
|
||||||
Then read the preflight path from metadata. It contains captured stdout, stderr,
|
|
||||||
exit code, and command.
|
|
||||||
|
|
||||||
Safe fix: fix the Scriptorium configuration, prompt ID, profile, or data package
|
|
||||||
input indicated by stderr.
|
|
||||||
|
|
||||||
Relevant docs: [Operations guide](operations.md),
|
|
||||||
[Scriptorium integration](integrations/scriptorium.md).
|
|
||||||
|
|
||||||
## Scriptorium Run Fails
|
|
||||||
|
|
||||||
Symptom: generation fails with `scriptorium run exited with code ...`.
|
|
||||||
|
|
||||||
Likely cause: Scriptorium failed during report generation or validation.
|
|
||||||
|
|
||||||
Diagnostic:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
weatherreporter inspect metadata RUN_ID
|
|
||||||
weatherreporter inspect data-package RUN_ID
|
|
||||||
```
|
|
||||||
|
|
||||||
If metadata includes a rendered report path, inspect that report as well. A
|
|
||||||
nonzero run can still leave a managed report artifact.
|
|
||||||
|
|
||||||
Safe fix: use the captured stderr and data package to fix the Scriptorium
|
|
||||||
prompt, profile, model configuration, or validation issue.
|
|
||||||
|
|
||||||
Relevant docs: [Operations guide](operations.md),
|
|
||||||
[Scriptorium integration](integrations/scriptorium.md).
|
|
||||||
|
|
||||||
## Generated Text Validation Fails
|
|
||||||
|
|
||||||
Symptom: Daily, Today, Tomorrow, or Hourly generation fails with generated-text
|
|
||||||
decode, unknown-field, required-field, or multiple-JSON-values context.
|
|
||||||
|
|
||||||
Likely cause: Scriptorium wrote structured JSON that does not match the
|
|
||||||
GeneratedText contract for the selected report.
|
|
||||||
|
|
||||||
Diagnostic:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
weatherreporter inspect metadata RUN_ID
|
|
||||||
```
|
|
||||||
|
|
||||||
Then inspect the generated-text raw path recorded in metadata, if present.
|
|
||||||
|
|
||||||
Safe fix: update the Scriptorium prompt or schema configuration so the prompt
|
|
||||||
writes the expected structured JSON for the report.
|
|
||||||
|
|
||||||
Relevant docs: [Operations guide](operations.md),
|
|
||||||
[Generated Text internals](internal/generatedtext.md),
|
|
||||||
[Scriptorium integration](integrations/scriptorium.md).
|
|
||||||
|
|
||||||
## Template Rendering Fails
|
|
||||||
|
|
||||||
Symptom: Daily, Today, Tomorrow, or Hourly generation fails with report template
|
|
||||||
parsing or execution context after generated text validation succeeds.
|
|
||||||
|
|
||||||
Likely cause: an embedded template references a missing context field or
|
|
||||||
receives a value shape that does not match its typed render context.
|
|
||||||
|
|
||||||
Diagnostic:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
weatherreporter inspect metadata RUN_ID
|
|
||||||
```
|
|
||||||
|
|
||||||
If metadata records generated-text and render-context paths, inspect those
|
|
||||||
artifacts along with the template named by the report definition.
|
|
||||||
|
|
||||||
Safe fix: update the embedded template or render-context builder so the
|
|
||||||
template uses the implemented typed context.
|
|
||||||
|
|
||||||
Relevant docs: [Report Templates](templates.md),
|
|
||||||
[Report Template internals](internal/reporttemplate.md).
|
|
||||||
|
|
||||||
## Batch Command Returns Nonzero
|
|
||||||
|
|
||||||
Symptom: `run morning` or `run evening` returns nonzero.
|
|
||||||
|
|
||||||
Likely cause: weather collection failed before planning, or at least one
|
|
||||||
planned report failed after planning succeeded, or every report succeeded but
|
|
||||||
the top-level batch distributor notification failed.
|
|
||||||
|
|
||||||
Diagnostic: if stdout contains a JSON summary, inspect each failed report item
|
|
||||||
and the top-level `notification` object. Stderr includes one
|
|
||||||
`batchNotification` line when batch notification is attempted, skipped, or
|
|
||||||
fails. If no summary was emitted, inspect the command error; configuration,
|
|
||||||
Weather API collection, or batch validation failed before any report artifacts
|
|
||||||
were created.
|
|
||||||
|
|
||||||
Safe fix: for collection failures, fix the configuration or upstream Weather
|
|
||||||
API availability and rerun the batch. For report failures, use the failed
|
|
||||||
report's artifact paths from the summary, then inspect metadata, sources,
|
|
||||||
module snapshot, and data package for that RunID. For a batch notification
|
|
||||||
failure, inspect the notification artifact path from the top-level
|
|
||||||
`notification.path`.
|
|
||||||
|
|
||||||
Relevant docs: [CLI reference](cli.md), [Operations guide](operations.md).
|
|
||||||
|
|
||||||
## Batch Upload Skipped
|
|
||||||
|
|
||||||
Symptom: a batch JSON summary contains
|
|
||||||
`"notification":{"status":"skipped","reason":"one or more reports failed"}`.
|
|
||||||
|
|
||||||
Likely cause: at least one planned report failed, so weatherreporter did not
|
|
||||||
call distributor for the batch.
|
|
||||||
|
|
||||||
Diagnostic: inspect the failed report items in the batch JSON summary and the
|
|
||||||
matching stderr report lines. A skipped batch notification has no distributor
|
|
||||||
run ID and no notification artifact path.
|
|
||||||
|
|
||||||
Safe fix: fix the report-generation failure first, then rerun the batch. The
|
|
||||||
batch upload is all-or-nothing.
|
|
||||||
|
|
||||||
Relevant docs: [Operations guide](operations.md).
|
|
||||||
|
|
||||||
## Batch Upload Fails
|
|
||||||
|
|
||||||
Symptom: every report item in a batch summary is succeeded, but the batch
|
|
||||||
returns nonzero and the top-level notification has `status: "failed"`.
|
|
||||||
|
|
||||||
Likely cause: the distributor upload was rejected, the distributor service was
|
|
||||||
unavailable, status polling reached a terminal distributor failure, or
|
|
||||||
weatherreporter rejected the batch file mapping before upload.
|
|
||||||
|
|
||||||
Diagnostic: inspect `notification.error`, `notification.pipelineId`,
|
|
||||||
`notification.bundleId`, `notification.idempotencyKey`, and
|
|
||||||
`notification.path` in stdout. Then inspect the notification artifact; it
|
|
||||||
records included report source paths, bundle paths, upload status, distributor
|
|
||||||
run status, status lookup error, and raw status report JSON when available.
|
|
||||||
|
|
||||||
Safe fix: fix the endpoint, token, distributor pipeline, batch identity
|
|
||||||
templates, or report path templates indicated by the error, then rerun the
|
|
||||||
batch. Individual report artifacts from the failed batch notification remain
|
|
||||||
available and do not need to be regenerated for diagnosis.
|
|
||||||
|
|
||||||
Relevant docs: [Configuration reference](config.md),
|
|
||||||
[Operations guide](operations.md).
|
|
||||||
|
|
||||||
## Duplicate Batch Bundle Path
|
|
||||||
|
|
||||||
Symptom: a batch returns nonzero with duplicate bundle path context before a
|
|
||||||
distributor run ID is accepted.
|
|
||||||
|
|
||||||
Likely cause: report-specific distributor path templates rendered the same
|
|
||||||
bundle-relative path for two included reports in the same batch.
|
|
||||||
|
|
||||||
Diagnostic: inspect the error in stdout or stderr. The validation error
|
|
||||||
includes the duplicate bundle path plus the report IDs, RunIDs, and managed
|
|
||||||
source paths involved.
|
|
||||||
|
|
||||||
Safe fix: configure a per-report distributor path override so every report in a
|
|
||||||
batch renders a unique path. Include values such as `{artifact_group}`,
|
|
||||||
`{valid_start_date}`, `{batch_output_name}`, or `{run_id}` when needed.
|
|
||||||
|
|
||||||
Relevant docs: [Configuration reference](config.md),
|
|
||||||
[Operations guide](operations.md).
|
|
||||||
|
|
||||||
## Distributor Source Conflict
|
|
||||||
|
|
||||||
Symptom: distributor accepts or rejects an upload with conflict context for a
|
|
||||||
source, destination, digest, or idempotency key.
|
|
||||||
|
|
||||||
Likely cause: the rendered bundle ID or idempotency key does not match the
|
|
||||||
intended producer identity. A bundle ID identifies the logical source stream;
|
|
||||||
an idempotency key identifies a retry of the same upload request.
|
|
||||||
|
|
||||||
Diagnostic: inspect the report notification artifact linked from metadata or
|
|
||||||
the batch notification artifact linked from the top-level notification path.
|
|
||||||
Compare the rendered pipeline ID, bundle ID, idempotency key, included source
|
|
||||||
paths, and bundle paths with `notify.distributor.*` templates and distributor
|
|
||||||
pipeline state.
|
|
||||||
|
|
||||||
Safe fix: keep bundle ID templates stable for the source stream that should be
|
|
||||||
updated, and keep idempotency keys stable only for retries of the same generated
|
|
||||||
content. Do not reuse one idempotency key for different report or batch
|
|
||||||
content.
|
|
||||||
|
|
||||||
Relevant docs: [Operations guide](operations.md),
|
|
||||||
[Distributor adapter internals](internal/distributor-adapter.md).
|
|
||||||
|
|
||||||
## Invalid Secrets Directory
|
|
||||||
|
|
||||||
Symptom: config loading fails with `read secrets directory`, `secret file`, or
|
|
||||||
environment variable name context.
|
|
||||||
|
|
||||||
Likely cause: `secrets.directory` points to a missing directory or contains an
|
|
||||||
invalid entry. Secret entries must be regular files directly under the
|
|
||||||
configured directory, and file basenames must match
|
|
||||||
`[A-Za-z_][A-Za-z0-9_]*`.
|
|
||||||
|
|
||||||
Diagnostic: list the configured directory and inspect entry names and file
|
|
||||||
types. Do not print secret file contents.
|
|
||||||
|
|
||||||
Safe fix: create the directory, remove subdirectories or symlinks, fix invalid
|
|
||||||
filenames, and ensure the weatherreporter process can read each secret file.
|
|
||||||
|
|
||||||
Relevant docs: [Configuration reference](config.md).
|
|
||||||
|
|
||||||
## Distributor Token Is Missing
|
|
||||||
|
|
||||||
Symptom: notification fails with a message that the distributor token
|
|
||||||
environment variable is not set.
|
|
||||||
|
|
||||||
Likely cause: `notify.distributor.enabled` is true, but the environment
|
|
||||||
variable named by `notify.distributor.token_env` was not populated directly or
|
|
||||||
through `secrets.directory`.
|
|
||||||
|
|
||||||
Diagnostic: check `notify.distributor.token_env`, then verify a matching secret
|
|
||||||
file exists under `secrets.directory` or that the process environment includes
|
|
||||||
the variable. Do not print the token value.
|
|
||||||
|
|
||||||
Safe fix: create a readable secret file whose basename matches `token_env`, or
|
|
||||||
set the environment variable through the service manager.
|
|
||||||
|
|
||||||
Relevant docs: [Configuration reference](config.md),
|
|
||||||
[Operations guide](operations.md).
|
|
||||||
|
|
||||||
## Distributor Upload Conflict
|
|
||||||
|
|
||||||
Symptom: notification fails with idempotency conflict context.
|
|
||||||
|
|
||||||
Likely cause: the same idempotency key was reused for different bundle content
|
|
||||||
within the same distributor token and pipeline. By default the bundle ID is a
|
|
||||||
stable report-stream identity and the idempotency key appends RunID.
|
|
||||||
|
|
||||||
Diagnostic: inspect the failed batch JSON or stderr line for pipeline, bundle,
|
|
||||||
and idempotency context. For batch commands, use the top-level notification
|
|
||||||
object rather than per-report notification fields. Compare the configured
|
|
||||||
templates with the report RunID or batch RunID and report path.
|
|
||||||
|
|
||||||
Also inspect the notification artifact linked from metadata or from the
|
|
||||||
top-level batch notification path. It records the rendered pipeline ID, bundle
|
|
||||||
ID, idempotency key, upload result, distributor run status, status error, and
|
|
||||||
raw run report JSON when available.
|
|
||||||
|
|
||||||
Safe fix: keep idempotency templates stable for retries of the same generated
|
|
||||||
report, but do not reuse the same rendered key for different generated report
|
|
||||||
content.
|
|
||||||
|
|
||||||
Relevant docs: [Operations guide](operations.md),
|
|
||||||
[Distributor adapter internals](internal/distributor-adapter.md).
|
|
||||||
|
|
||||||
## Distributor Upload Rejected
|
|
||||||
|
|
||||||
Symptom: notification fails with distributor upload rejection, HTTP status, or
|
|
||||||
bundle validation context.
|
|
||||||
|
|
||||||
Likely cause: the distributor endpoint rejected the token, pipeline ID, bundle
|
|
||||||
ID, idempotency key, source file, or one of the rendered bundle paths.
|
|
||||||
|
|
||||||
Diagnostic: inspect stdout JSON or stderr status lines for
|
|
||||||
`notificationError` or the top-level batch notification `error`. Confirm
|
|
||||||
`notify.distributor.endpoint`,
|
|
||||||
`notify.distributor.pipeline_id_template`,
|
|
||||||
report-specific distributor paths, and token configuration. Token
|
|
||||||
values are redacted from weatherreporter errors.
|
|
||||||
|
|
||||||
If the upload was accepted but destination output did not change, inspect the
|
|
||||||
notification artifact's `runStatus.report`. Distributor actions such as
|
|
||||||
`replace_older`, `skip_same`, `skip_destination_newer`, or `failed` explain how
|
|
||||||
the destination handled the uploaded bundle.
|
|
||||||
|
|
||||||
Safe fix: fix the endpoint, token, templates, or distributor-side upload
|
|
||||||
configuration. The weatherreporter upload source is the managed Markdown report,
|
|
||||||
not `--out` or `--out-dir` copies.
|
|
||||||
|
|
||||||
Relevant docs: [Configuration reference](config.md),
|
|
||||||
[Operations guide](operations.md),
|
|
||||||
[Distributor adapter internals](internal/distributor-adapter.md).
|
|
||||||
|
|
||||||
## Distributor Unavailable
|
|
||||||
|
|
||||||
Symptom: notification fails with network, timeout, or service unavailable
|
|
||||||
context.
|
|
||||||
|
|
||||||
Likely cause: the configured distributor endpoint is unreachable, slow, or
|
|
||||||
temporarily unavailable.
|
|
||||||
|
|
||||||
Diagnostic: check network access from the weatherreporter host to
|
|
||||||
`notify.distributor.endpoint`. For batch runs, inspect the top-level
|
|
||||||
notification object and the artifact linked by `notification.path`.
|
|
||||||
|
|
||||||
Safe fix: restore distributor service availability and rerun the affected
|
|
||||||
report or batch. Stable idempotency keys make retrying the same generated report
|
|
||||||
safe unless the distributor reports a conflict.
|
|
||||||
|
|
||||||
Relevant docs: [Operations guide](operations.md).
|
|
||||||
|
|
||||||
## Unknown RunID
|
|
||||||
|
|
||||||
Symptom: an inspect command fails with `metadata for run id ... was not found`.
|
|
||||||
|
|
||||||
Likely cause: the RunID is mistyped or the command is reading a different
|
|
||||||
workspace.
|
|
||||||
|
|
||||||
Diagnostic:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
weatherreporter inspect reports --config ./config.yml --limit 20
|
|
||||||
```
|
|
||||||
|
|
||||||
Safe fix: copy a RunID from `inspect reports`, or use the same `--config` and
|
|
||||||
workspace that generated the report.
|
|
||||||
|
|
||||||
Relevant docs: [Operations guide](operations.md).
|
|
||||||
|
|
||||||
## Workspace Path Error
|
|
||||||
|
|
||||||
Symptom: startup or inspection fails with workspace path validation or
|
|
||||||
filesystem read/write context.
|
|
||||||
|
|
||||||
Likely cause: a workspace subdirectory is absolute, escapes `workspace.root`, or
|
|
||||||
the process cannot read or write the configured path.
|
|
||||||
|
|
||||||
Diagnostic: review `workspace.root`, `workspace.snapshots_dir`,
|
|
||||||
`workspace.reports_dir`, `workspace.data_packages_dir`, and
|
|
||||||
`workspace.preflight_dir`.
|
|
||||||
|
|
||||||
Safe fix: keep workspace subdirectories relative to `workspace.root`, and grant
|
|
||||||
the process appropriate filesystem permissions.
|
|
||||||
|
|
||||||
Relevant docs: [Configuration reference](config.md), [Operations guide](operations.md).
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
weather_api:
|
weather_api:
|
||||||
base_url: https://weather.api.example.com/
|
base_url: https://weather.api.example.com/
|
||||||
timeout: 15s
|
timeout: 15s
|
||||||
precision: 1
|
precision: 0
|
||||||
units: us
|
units: us
|
||||||
timezone: "America/Chicago"
|
timezone: "America/Chicago"
|
||||||
format: json
|
format: json
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ import (
|
|||||||
const (
|
const (
|
||||||
convectiveOutlooksEndpoint = "/outlooks/convective"
|
convectiveOutlooksEndpoint = "/outlooks/convective"
|
||||||
sourceSPCConvectiveOutlooks = "spc_convective_outlooks"
|
sourceSPCConvectiveOutlooks = "spc_convective_outlooks"
|
||||||
|
|
||||||
|
defaultWarmupEndpoint = "/conditions/current"
|
||||||
|
defaultWarmupAttempts = 3
|
||||||
|
defaultWarmupDelay = time.Second
|
||||||
|
defaultFetchAttempts = 2
|
||||||
|
defaultFetchRetryDelay = time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
@@ -35,6 +41,12 @@ type Client struct {
|
|||||||
precision int
|
precision int
|
||||||
missingSource config.MissingSourceConfig
|
missingSource config.MissingSourceConfig
|
||||||
now func() time.Time
|
now func() time.Time
|
||||||
|
|
||||||
|
warmupEndpoint string
|
||||||
|
warmupAttempts int
|
||||||
|
warmupDelay time.Duration
|
||||||
|
fetchAttempts int
|
||||||
|
fetchRetryDelay time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type Option func(*Client)
|
type Option func(*Client)
|
||||||
@@ -81,6 +93,11 @@ func New(cfg config.Config, opts ...Option) (*Client, error) {
|
|||||||
Sources: cfg.MissingSource.Sources,
|
Sources: cfg.MissingSource.Sources,
|
||||||
},
|
},
|
||||||
now: time.Now,
|
now: time.Now,
|
||||||
|
warmupEndpoint: defaultWarmupEndpoint,
|
||||||
|
warmupAttempts: defaultWarmupAttempts,
|
||||||
|
warmupDelay: defaultWarmupDelay,
|
||||||
|
fetchAttempts: defaultFetchAttempts,
|
||||||
|
fetchRetryDelay: defaultFetchRetryDelay,
|
||||||
}
|
}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(client)
|
opt(client)
|
||||||
@@ -89,6 +106,10 @@ func New(cfg config.Config, opts ...Option) (*Client, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) FetchBundle(ctx context.Context) (*weatherdata.Bundle, error) {
|
func (c *Client) FetchBundle(ctx context.Context) (*weatherdata.Bundle, error) {
|
||||||
|
if err := c.warmup(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
fetchedAt := c.now()
|
fetchedAt := c.now()
|
||||||
builder := bundleBuilder{
|
builder := bundleBuilder{
|
||||||
client: c,
|
client: c,
|
||||||
@@ -397,24 +418,9 @@ type envelope struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) fetch(ctx context.Context, sourceName string, endpoint string, opts queryOptions) (json.RawMessage, weatherdata.Source, error) {
|
func (c *Client) fetch(ctx context.Context, sourceName string, endpoint string, opts queryOptions) (json.RawMessage, weatherdata.Source, error) {
|
||||||
reqURL := c.endpointURL(endpoint, opts)
|
reqURL, body, err := c.fetchHTTP(ctx, endpoint, opts)
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL.String(), nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, weatherdata.Source{}, fmt.Errorf("create request for %s: %w", endpoint, err)
|
return nil, weatherdata.Source{}, err
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, weatherdata.Source{}, fmt.Errorf("fetch %s: %w", endpoint, err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20))
|
|
||||||
if err != nil {
|
|
||||||
return nil, weatherdata.Source{}, fmt.Errorf("read %s response: %w", endpoint, err)
|
|
||||||
}
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
||||||
return nil, weatherdata.Source{}, fmt.Errorf("fetch %s: unexpected HTTP status %d: %s", endpoint, resp.StatusCode, strings.TrimSpace(string(body)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var env envelope
|
var env envelope
|
||||||
@@ -440,6 +446,169 @@ func (c *Client) fetch(ctx context.Context, sourceName string, endpoint string,
|
|||||||
return env.Data, source, nil
|
return env.Data, source, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) warmup(ctx context.Context) error {
|
||||||
|
endpoint := c.warmupEndpoint
|
||||||
|
if strings.TrimSpace(endpoint) == "" {
|
||||||
|
endpoint = defaultWarmupEndpoint
|
||||||
|
}
|
||||||
|
attempts := positiveAttemptCount(c.warmupAttempts)
|
||||||
|
var lastErr error
|
||||||
|
for attempt := 1; attempt <= attempts; attempt++ {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return fmt.Errorf("warm up weather API via %s: %w", endpoint, err)
|
||||||
|
}
|
||||||
|
if err := c.warmupOnce(ctx, endpoint); err != nil {
|
||||||
|
lastErr = err
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if attempt == attempts {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err := waitForRetry(ctx, c.warmupDelay); err != nil {
|
||||||
|
return fmt.Errorf("warm up weather API via %s after %d attempt(s): %w", endpoint, attempt, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("warm up weather API via %s failed after %d attempts: %w", endpoint, attempts, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) warmupOnce(ctx context.Context, endpoint string) error {
|
||||||
|
reqURL := c.endpointURL(endpoint, queryOptions{precision: true})
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create request for %s: %w", endpoint, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fetch %s: %w", endpoint, err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read %s response: %w", endpoint, err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return fmt.Errorf("fetch %s: unexpected HTTP status %d: %s", endpoint, resp.StatusCode, strings.TrimSpace(string(body)))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) fetchHTTP(ctx context.Context, endpoint string, opts queryOptions) (*url.URL, []byte, error) {
|
||||||
|
attempts := positiveAttemptCount(c.fetchAttempts)
|
||||||
|
var lastErr error
|
||||||
|
var lastRetryable bool
|
||||||
|
for attempt := 1; attempt <= attempts; attempt++ {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("fetch %s: %w", endpoint, err)
|
||||||
|
}
|
||||||
|
reqURL, body, err := c.fetchHTTPOnce(ctx, endpoint, opts)
|
||||||
|
if err == nil {
|
||||||
|
return reqURL, body, nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
lastRetryable = isRetryableRequestError(err)
|
||||||
|
if !lastRetryable || attempt == attempts {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err := waitForRetry(ctx, c.fetchRetryDelay); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("fetch %s retry delay after attempt %d: %w", endpoint, attempt, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lastRetryable {
|
||||||
|
return nil, nil, fmt.Errorf("fetch %s failed after %d attempts: %w", endpoint, attempts, lastErr)
|
||||||
|
}
|
||||||
|
return nil, nil, lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) fetchHTTPOnce(ctx context.Context, endpoint string, opts queryOptions) (*url.URL, []byte, error) {
|
||||||
|
reqURL := c.endpointURL(endpoint, opts)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("create request for %s: %w", endpoint, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("fetch %s: %w", endpoint, err)
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return reqURL, nil, err
|
||||||
|
}
|
||||||
|
return reqURL, nil, retryableRequestError{err: err}
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20))
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("read %s response: %w", endpoint, err)
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return reqURL, nil, err
|
||||||
|
}
|
||||||
|
return reqURL, nil, retryableRequestError{err: err}
|
||||||
|
}
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
err := fmt.Errorf("fetch %s: unexpected HTTP status %d: %s", endpoint, resp.StatusCode, strings.TrimSpace(string(body)))
|
||||||
|
if isRetryableHTTPStatus(resp.StatusCode) {
|
||||||
|
return reqURL, nil, retryableRequestError{err: err}
|
||||||
|
}
|
||||||
|
return reqURL, nil, err
|
||||||
|
}
|
||||||
|
return reqURL, body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type retryableRequestError struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e retryableRequestError) Error() string {
|
||||||
|
return e.err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e retryableRequestError) Unwrap() error {
|
||||||
|
return e.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRetryableRequestError(err error) bool {
|
||||||
|
_, ok := err.(retryableRequestError)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRetryableHTTPStatus(status int) bool {
|
||||||
|
switch status {
|
||||||
|
case http.StatusRequestTimeout,
|
||||||
|
http.StatusTooManyRequests,
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
http.StatusBadGateway,
|
||||||
|
http.StatusServiceUnavailable,
|
||||||
|
http.StatusGatewayTimeout:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForRetry(ctx context.Context, delay time.Duration) error {
|
||||||
|
if delay <= 0 {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
timer := time.NewTimer(delay)
|
||||||
|
defer timer.Stop()
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-timer.C:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func positiveAttemptCount(attempts int) int {
|
||||||
|
if attempts < 1 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return attempts
|
||||||
|
}
|
||||||
|
|
||||||
func isJSONNull(raw json.RawMessage) bool {
|
func isJSONNull(raw json.RawMessage) bool {
|
||||||
return bytes.Equal(bytes.TrimSpace(raw), []byte("null"))
|
return bytes.Equal(bytes.TrimSpace(raw), []byte("null"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,8 +80,11 @@ func TestFetchBundleFromFixtures(t *testing.T) {
|
|||||||
"/weatherstories/latest",
|
"/weatherstories/latest",
|
||||||
convectiveOutlooksEndpoint,
|
convectiveOutlooksEndpoint,
|
||||||
}
|
}
|
||||||
if len(requested) != len(wantPaths) {
|
if len(requested) != len(wantPaths)+1 {
|
||||||
t.Fatalf("requested paths = %v, want %d source endpoints", requested, len(wantPaths))
|
t.Fatalf("requested paths = %v, want warmup plus %d source endpoints", requested, len(wantPaths))
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(requested[0], defaultWarmupEndpoint+"?") && requested[0] != defaultWarmupEndpoint {
|
||||||
|
t.Fatalf("first requested path = %q, want warmup endpoint %s", requested[0], defaultWarmupEndpoint)
|
||||||
}
|
}
|
||||||
for _, want := range wantPaths {
|
for _, want := range wantPaths {
|
||||||
if !containsPath(requested, want) {
|
if !containsPath(requested, want) {
|
||||||
@@ -132,9 +135,15 @@ func TestFetchBundleBuildsExpectedQueries(t *testing.T) {
|
|||||||
t.Fatalf("request %q missing units=us", rawURL)
|
t.Fatalf("request %q missing units=us", rawURL)
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(rawURL, "/forecast/") {
|
if strings.HasPrefix(rawURL, "/forecast/") {
|
||||||
if !strings.Contains(rawURL, "precision=1") || !strings.Contains(rawURL, "tz=America%2FChicago") {
|
if !strings.Contains(rawURL, "precision=0") || !strings.Contains(rawURL, "tz=America%2FChicago") {
|
||||||
t.Fatalf("forecast request %q missing precision or tz", rawURL)
|
t.Fatalf("forecast request %q missing precision or tz", rawURL)
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rawURL == defaultWarmupEndpoint || strings.HasPrefix(rawURL, defaultWarmupEndpoint+"?") || strings.HasPrefix(rawURL, "/observations?") {
|
||||||
|
if !strings.Contains(rawURL, "precision=0") {
|
||||||
|
t.Fatalf("request %q missing precision=0", rawURL)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,7 +195,7 @@ func TestFetchBundleRecordsSourceHash(t *testing.T) {
|
|||||||
|
|
||||||
func TestHTTPErrorIsActionable(t *testing.T) {
|
func TestHTTPErrorIsActionable(t *testing.T) {
|
||||||
server := fixtureServer(t, map[string]handlerOverride{
|
server := fixtureServer(t, map[string]handlerOverride{
|
||||||
"/conditions/current": {status: http.StatusBadGateway, body: `upstream failed`},
|
"/forecast/hourly": {status: http.StatusBadGateway, body: `upstream failed`},
|
||||||
}, nil)
|
}, nil)
|
||||||
client := newTestClient(t, server.URL+"/", nil)
|
client := newTestClient(t, server.URL+"/", nil)
|
||||||
|
|
||||||
@@ -194,15 +203,140 @@ func TestHTTPErrorIsActionable(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("FetchBundle() error = nil, want HTTP error")
|
t.Fatal("FetchBundle() error = nil, want HTTP error")
|
||||||
}
|
}
|
||||||
if !strings.Contains(err.Error(), "/conditions/current") || !strings.Contains(err.Error(), "502") {
|
if !strings.Contains(err.Error(), "/forecast/hourly") || !strings.Contains(err.Error(), "502") {
|
||||||
t.Fatalf("error = %q, want endpoint and status", err.Error())
|
t.Fatalf("error = %q, want endpoint and status", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWarmupRetriesBeforeFetchBundle(t *testing.T) {
|
||||||
|
var requested []string
|
||||||
|
var warmupCalls int
|
||||||
|
server := fixtureServer(t, map[string]handlerOverride{
|
||||||
|
defaultWarmupEndpoint: {handler: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
warmupCalls++
|
||||||
|
if warmupCalls == 1 {
|
||||||
|
w.WriteHeader(http.StatusBadGateway)
|
||||||
|
_, _ = w.Write([]byte("vpn waking up"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.ServeFile(w, r, filepath.Join("testdata", "current.json"))
|
||||||
|
}},
|
||||||
|
}, &requested)
|
||||||
|
client := newTestClient(t, server.URL+"/", nil)
|
||||||
|
|
||||||
|
bundle, err := client.FetchBundle(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FetchBundle() error = %v", err)
|
||||||
|
}
|
||||||
|
if bundle.Current == nil {
|
||||||
|
t.Fatal("Current = nil, want successful fetch after warmup retry")
|
||||||
|
}
|
||||||
|
if warmupCalls != 3 {
|
||||||
|
t.Fatalf("conditions/current calls = %d, want failed warmup, successful warmup, and current source fetch", warmupCalls)
|
||||||
|
}
|
||||||
|
if len(requested) < 2 || !containsPath(requested[:2], defaultWarmupEndpoint) {
|
||||||
|
t.Fatalf("initial requests = %v, want warmup endpoint retries", requested)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWarmupFailureStopsBeforeSourceFetches(t *testing.T) {
|
||||||
|
var requested []string
|
||||||
|
server := fixtureServer(t, map[string]handlerOverride{
|
||||||
|
defaultWarmupEndpoint: {status: http.StatusBadGateway, body: `vpn unavailable`},
|
||||||
|
}, &requested)
|
||||||
|
client := newTestClient(t, server.URL+"/", nil)
|
||||||
|
client.warmupAttempts = 2
|
||||||
|
|
||||||
|
_, err := client.FetchBundle(context.Background())
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("FetchBundle() error = nil, want warmup failure")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "warm up weather API") ||
|
||||||
|
!strings.Contains(err.Error(), defaultWarmupEndpoint) ||
|
||||||
|
!strings.Contains(err.Error(), "2 attempts") ||
|
||||||
|
!strings.Contains(err.Error(), "502") {
|
||||||
|
t.Fatalf("error = %q, want warmup endpoint, attempts, and status", err.Error())
|
||||||
|
}
|
||||||
|
if got := countPath(requested, defaultWarmupEndpoint); got != 2 {
|
||||||
|
t.Fatalf("warmup requests = %d, want 2; all requests = %v", got, requested)
|
||||||
|
}
|
||||||
|
if containsPath(requested, "/observations") {
|
||||||
|
t.Fatalf("requested paths = %v, want warmup failure before source fetches", requested)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchRetriesRetryableStatus(t *testing.T) {
|
||||||
|
var hourlyCalls int
|
||||||
|
server := fixtureServer(t, map[string]handlerOverride{
|
||||||
|
"/forecast/hourly": {handler: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
hourlyCalls++
|
||||||
|
if hourlyCalls == 1 {
|
||||||
|
w.WriteHeader(http.StatusBadGateway)
|
||||||
|
_, _ = w.Write([]byte("temporary upstream failure"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.ServeFile(w, r, filepath.Join("testdata", "hourly.json"))
|
||||||
|
}},
|
||||||
|
}, nil)
|
||||||
|
client := newTestClient(t, server.URL+"/", nil)
|
||||||
|
|
||||||
|
bundle, err := client.FetchBundle(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FetchBundle() error = %v", err)
|
||||||
|
}
|
||||||
|
if bundle.Hourly == nil {
|
||||||
|
t.Fatal("Hourly = nil, want successful fetch after retry")
|
||||||
|
}
|
||||||
|
if hourlyCalls != 2 {
|
||||||
|
t.Fatalf("hourly calls = %d, want 2", hourlyCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchDoesNotRetryNonRetryableStatus(t *testing.T) {
|
||||||
|
var hourlyCalls int
|
||||||
|
server := fixtureServer(t, map[string]handlerOverride{
|
||||||
|
"/forecast/hourly": {handler: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
hourlyCalls++
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
_, _ = w.Write([]byte("not found"))
|
||||||
|
}},
|
||||||
|
}, nil)
|
||||||
|
client := newTestClient(t, server.URL+"/", nil)
|
||||||
|
|
||||||
|
_, err := client.FetchBundle(context.Background())
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("FetchBundle() error = nil, want non-retryable status error")
|
||||||
|
}
|
||||||
|
if hourlyCalls != 1 {
|
||||||
|
t.Fatalf("hourly calls = %d, want no retry", hourlyCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchDoesNotRetryMalformedEnvelope(t *testing.T) {
|
||||||
|
var hourlyCalls int
|
||||||
|
server := fixtureServer(t, map[string]handlerOverride{
|
||||||
|
"/forecast/hourly": {handler: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
hourlyCalls++
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte(`not-json`))
|
||||||
|
}},
|
||||||
|
}, nil)
|
||||||
|
client := newTestClient(t, server.URL+"/", nil)
|
||||||
|
|
||||||
|
_, err := client.FetchBundle(context.Background())
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("FetchBundle() error = nil, want envelope decode error")
|
||||||
|
}
|
||||||
|
if hourlyCalls != 1 {
|
||||||
|
t.Fatalf("hourly calls = %d, want no retry", hourlyCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRequiredHourlyForecast(t *testing.T) {
|
func TestRequiredHourlyForecast(t *testing.T) {
|
||||||
|
var requested []string
|
||||||
server := fixtureServer(t, map[string]handlerOverride{
|
server := fixtureServer(t, map[string]handlerOverride{
|
||||||
"/forecast/hourly": {status: http.StatusOK, body: `{"data": null}`},
|
"/forecast/hourly": {status: http.StatusOK, body: `{"data": null}`},
|
||||||
}, nil)
|
}, &requested)
|
||||||
client := newTestClient(t, server.URL+"/", nil)
|
client := newTestClient(t, server.URL+"/", nil)
|
||||||
|
|
||||||
_, err := client.FetchBundle(context.Background())
|
_, err := client.FetchBundle(context.Background())
|
||||||
@@ -212,6 +346,9 @@ func TestRequiredHourlyForecast(t *testing.T) {
|
|||||||
if !strings.Contains(err.Error(), "hourly forecast data") {
|
if !strings.Contains(err.Error(), "hourly forecast data") {
|
||||||
t.Fatalf("error = %q, want hourly context", err.Error())
|
t.Fatalf("error = %q, want hourly context", err.Error())
|
||||||
}
|
}
|
||||||
|
if got := countPath(requested, "/forecast/hourly"); got != 1 {
|
||||||
|
t.Fatalf("hourly requests = %d, want no retry; all requests = %v", got, requested)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNullAlertsMeansNoActiveAlerts(t *testing.T) {
|
func TestNullAlertsMeansNoActiveAlerts(t *testing.T) {
|
||||||
@@ -420,6 +557,43 @@ func TestContextCancellation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRetryDelayRespectsContextCancellation(t *testing.T) {
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
var hourlyCalls int
|
||||||
|
server := fixtureServer(t, map[string]handlerOverride{
|
||||||
|
"/forecast/hourly": {handler: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
hourlyCalls++
|
||||||
|
if cancel != nil {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusBadGateway)
|
||||||
|
_, _ = w.Write([]byte("temporary upstream failure"))
|
||||||
|
}},
|
||||||
|
}, nil)
|
||||||
|
client := newTestClient(t, server.URL+"/", nil)
|
||||||
|
client.fetchRetryDelay = time.Hour
|
||||||
|
|
||||||
|
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||||
|
cancel = cancelFunc
|
||||||
|
defer cancelFunc()
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
_, err := client.FetchBundle(ctx)
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("FetchBundle() error = nil, want cancellation during retry delay")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), context.Canceled.Error()) {
|
||||||
|
t.Fatalf("error = %q, want context cancellation", err.Error())
|
||||||
|
}
|
||||||
|
if elapsed > time.Second {
|
||||||
|
t.Fatalf("FetchBundle() elapsed = %s, want prompt cancellation", elapsed)
|
||||||
|
}
|
||||||
|
if hourlyCalls != 1 {
|
||||||
|
t.Fatalf("hourly calls = %d, want retry delay cancellation before second attempt", hourlyCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHTTPTimeout(t *testing.T) {
|
func TestHTTPTimeout(t *testing.T) {
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
@@ -432,12 +606,14 @@ func TestHTTPTimeout(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("New() error = %v", err)
|
t.Fatalf("New() error = %v", err)
|
||||||
}
|
}
|
||||||
|
client.warmupDelay = 0
|
||||||
|
client.fetchRetryDelay = 0
|
||||||
|
|
||||||
_, err = client.FetchBundle(context.Background())
|
_, err = client.FetchBundle(context.Background())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("FetchBundle() error = nil, want timeout error")
|
t.Fatal("FetchBundle() error = nil, want timeout error")
|
||||||
}
|
}
|
||||||
if !strings.Contains(err.Error(), "/observations") {
|
if !strings.Contains(err.Error(), defaultWarmupEndpoint) {
|
||||||
t.Fatalf("error = %q, want endpoint context", err.Error())
|
t.Fatalf("error = %q, want endpoint context", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -466,6 +642,7 @@ func TestSaveBundle(t *testing.T) {
|
|||||||
type handlerOverride struct {
|
type handlerOverride struct {
|
||||||
status int
|
status int
|
||||||
body string
|
body string
|
||||||
|
handler http.HandlerFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func fixtureServer(t *testing.T, overrides map[string]handlerOverride, requested *[]string) *httptest.Server {
|
func fixtureServer(t *testing.T, overrides map[string]handlerOverride, requested *[]string) *httptest.Server {
|
||||||
@@ -485,6 +662,10 @@ func fixtureServer(t *testing.T, overrides map[string]handlerOverride, requested
|
|||||||
*requested = append(*requested, r.URL.String())
|
*requested = append(*requested, r.URL.String())
|
||||||
}
|
}
|
||||||
if override, ok := overrides[r.URL.Path]; ok {
|
if override, ok := overrides[r.URL.Path]; ok {
|
||||||
|
if override.handler != nil {
|
||||||
|
override.handler(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
w.WriteHeader(override.status)
|
w.WriteHeader(override.status)
|
||||||
_, _ = w.Write([]byte(override.body))
|
_, _ = w.Write([]byte(override.body))
|
||||||
return
|
return
|
||||||
@@ -510,6 +691,8 @@ func newTestClient(t *testing.T, baseURL string, sourcePolicies map[string]confi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("New() error = %v", err)
|
t.Fatalf("New() error = %v", err)
|
||||||
}
|
}
|
||||||
|
client.warmupDelay = 0
|
||||||
|
client.fetchRetryDelay = 0
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,6 +715,16 @@ func containsPath(requested []string, path string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func countPath(requested []string, path string) int {
|
||||||
|
var count int
|
||||||
|
for _, rawURL := range requested {
|
||||||
|
if strings.HasPrefix(rawURL, path+"?") || rawURL == path {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
func sourceByName(t *testing.T, sources []weatherdata.Source, name string) weatherdata.Source {
|
func sourceByName(t *testing.T, sources []weatherdata.Source, name string) weatherdata.Source {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
for _, source := range sources {
|
for _, source := range sources {
|
||||||
|
|||||||
@@ -390,8 +390,12 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
|
|||||||
if savedDataPackage.Report.CurrentLocalDate != "2026-05-29" {
|
if savedDataPackage.Report.CurrentLocalDate != "2026-05-29" {
|
||||||
t.Fatalf("data package currentLocalDate = %q, want 2026-05-29", savedDataPackage.Report.CurrentLocalDate)
|
t.Fatalf("data package currentLocalDate = %q, want 2026-05-29", savedDataPackage.Report.CurrentLocalDate)
|
||||||
}
|
}
|
||||||
if _, ok := savedDataPackage.Briefing.Values["metadata"]; !ok {
|
metadataStanza, ok := savedDataPackage.Briefing.Values["metadata"].(map[string]any)
|
||||||
t.Fatal("data package metadata stanza missing")
|
if !ok {
|
||||||
|
t.Fatalf("data package metadata stanza = %#v, want metadata map", savedDataPackage.Briefing.Values["metadata"])
|
||||||
|
}
|
||||||
|
if _, ok := metadataStanza["alerts"]; ok {
|
||||||
|
t.Fatalf("data package metadata contains alerts, want alert details only in alert_digest: %#v", metadataStanza)
|
||||||
}
|
}
|
||||||
assertNoStaleModuleIntervalKeys(t, savedDataPackage.Briefing.Values)
|
assertNoStaleModuleIntervalKeys(t, savedDataPackage.Briefing.Values)
|
||||||
spcOutlooks, ok := savedDataPackage.Briefing.Values["spc_convective_outlooks"].(map[string]any)
|
spcOutlooks, ok := savedDataPackage.Briefing.Values["spc_convective_outlooks"].(map[string]any)
|
||||||
@@ -572,6 +576,10 @@ func TestGenerateReportIncludesSPCConvectivePromptStanzas(t *testing.T) {
|
|||||||
" spc_convective_discussion:",
|
" spc_convective_discussion:",
|
||||||
" included_because: categorical severity_rank >= 3",
|
" included_because: categorical severity_rank >= 3",
|
||||||
" label_text: Slight Risk",
|
" label_text: Slight Risk",
|
||||||
|
"background_definition:",
|
||||||
|
"plain_language: Scattered severe storms possible.",
|
||||||
|
"official_description: Isolated intense storms are possible within the risk area, but severe weather is generally expected to be short-lived and/or not widespread.",
|
||||||
|
"relative_level: 2 of 5",
|
||||||
" period_begins:",
|
" period_begins:",
|
||||||
" period_ends:",
|
" period_ends:",
|
||||||
" discussion: Severe thunderstorms may produce damaging winds during the afternoon.",
|
" discussion: Severe thunderstorms may produce damaging winds during the afternoon.",
|
||||||
@@ -753,7 +761,7 @@ func TestGenerateHourlyReportUsesGeneratedTextTemplateWorkflow(t *testing.T) {
|
|||||||
"# Hourly Report",
|
"# Hourly Report",
|
||||||
"Storm chances increase through late morning.",
|
"Storm chances increase through late morning.",
|
||||||
"## Alert Digest",
|
"## Alert Digest",
|
||||||
"- **Flood Watch**: Flood Watch in effect from May 29 at 11:00 AM to May 29 at 3:00 PM. Avoid low-water crossings.",
|
"- **Flood Watch**: Flood Watch in effect from May 29 at 11:00 AM to May 29 at 3:00 PM.",
|
||||||
"## Precipitation Timing",
|
"## Precipitation Timing",
|
||||||
"A cold front is moving into the region.",
|
"A cold front is moving into the region.",
|
||||||
"A front will keep the region unsettled.",
|
"A front will keep the region unsettled.",
|
||||||
@@ -762,6 +770,9 @@ func TestGenerateHourlyReportUsesGeneratedTextTemplateWorkflow(t *testing.T) {
|
|||||||
t.Fatalf("rendered hourly report missing %q:\n%s", want, reportText)
|
t.Fatalf("rendered hourly report missing %q:\n%s", want, reportText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if strings.Contains(reportText, "Avoid low-water crossings.") {
|
||||||
|
t.Fatalf("rendered hourly report includes alert instruction:\n%s", reportText)
|
||||||
|
}
|
||||||
if result.OutputPath != result.ReportPath {
|
if result.OutputPath != result.ReportPath {
|
||||||
t.Fatalf("OutputPath = %q, want managed report path %q", result.OutputPath, result.ReportPath)
|
t.Fatalf("OutputPath = %q, want managed report path %q", result.OutputPath, result.ReportPath)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"categorical:TSTM": {
|
||||||
|
"plain_language": "General or non-severe thunderstorms.",
|
||||||
|
"official_description": "No severe thunderstorms expected.",
|
||||||
|
"relative_level": "0 of 5"
|
||||||
|
},
|
||||||
|
"categorical:MRGL": {
|
||||||
|
"plain_language": "Isolated severe storms possible.",
|
||||||
|
"official_description": "Isolated severe storms may occur within the risk area, but they are expected to be limited in duration, coverage, and intensity.",
|
||||||
|
"relative_level": "1 of 5"
|
||||||
|
},
|
||||||
|
"categorical:SLGT": {
|
||||||
|
"plain_language": "Scattered severe storms possible.",
|
||||||
|
"official_description": "Isolated intense storms are possible within the risk area, but severe weather is generally expected to be short-lived and/or not widespread.",
|
||||||
|
"relative_level": "2 of 5"
|
||||||
|
},
|
||||||
|
"categorical:ENH": {
|
||||||
|
"plain_language": "Numerous severe storms possible.",
|
||||||
|
"official_description": "Numerous severe storms are possible within the risk area, some of which may be intense.",
|
||||||
|
"relative_level": "3 of 5"
|
||||||
|
},
|
||||||
|
"categorical:MDT": {
|
||||||
|
"plain_language": "Widespread severe storms likely.",
|
||||||
|
"official_description": "Widespread severe storms are likely within the risk area. Storms may be long-lived, widespread, and intense. This risk is usually reserved for days with several supercells producing intense tornadoes and/or very large hail, or an intense squall line with widespread damaging winds.",
|
||||||
|
"relative_level": "4 of 5"
|
||||||
|
},
|
||||||
|
"categorical:HIGH": {
|
||||||
|
"plain_language": "Major severe outbreak expected.",
|
||||||
|
"official_description": "A major severe weather outbreak is expected, with long-lived, very widespread, and particularly intense severe storms. This risk is reserved for when high confidence exists in widespread coverage of severe weather with embedded instances of extreme severity (i.e., violent tornadoes or very damaging convective wind events).",
|
||||||
|
"relative_level": "5 of 5"
|
||||||
|
},
|
||||||
|
"tornado:CIG1": {
|
||||||
|
"plain_language": "Conditional potential for significant tornadoes.",
|
||||||
|
"official_description": "Intensity Level 1: Reasonable Max EF2. Note that this product describes the reasonable maximum intensity of a hazard if that hazard occurs. It does not by itself indicate the probability that the hazard will occur.",
|
||||||
|
"relative_level": "1 of 3"
|
||||||
|
},
|
||||||
|
"tornado:CIG2": {
|
||||||
|
"plain_language": "Conditional potential for strong tornadoes.",
|
||||||
|
"official_description": "Intensity Level 2: Reasonable Max EF3. Note that this product describes the reasonable maximum intensity of a hazard if that hazard occurs. It does not by itself indicate the probability that the hazard will occur.",
|
||||||
|
"relative_level": "2 of 3"
|
||||||
|
},
|
||||||
|
"tornado:CIG3": {
|
||||||
|
"plain_language": "Conditional potential for violent tornadoes.",
|
||||||
|
"official_description": "Intensity Level 3: Reasonable Max EF4 or higher. Note that this product describes the reasonable maximum intensity of a hazard if that hazard occurs. It does not by itself indicate the probability that the hazard will occur.",
|
||||||
|
"relative_level": "3 of 3"
|
||||||
|
},
|
||||||
|
"wind:CIG1": {
|
||||||
|
"plain_language": "Conditional potential for significant severe wind.",
|
||||||
|
"official_description": "Intensity Level 1: Reasonable Max wind gusts around 65 kt / 75 mph or higher. Note that this product describes the reasonable maximum intensity of a hazard if that hazard occurs. It does not by itself indicate the probability that the hazard will occur.",
|
||||||
|
"relative_level": "1 of 3"
|
||||||
|
},
|
||||||
|
"wind:CIG2": {
|
||||||
|
"plain_language": "Conditional potential for intense severe wind.",
|
||||||
|
"official_description": "Intensity Level 2: Reasonable Max wind gusts around 75 kt / 85 mph or higher. Note that this product describes the reasonable maximum intensity of a hazard if that hazard occurs. It does not by itself indicate the probability that the hazard will occur.",
|
||||||
|
"relative_level": "2 of 3"
|
||||||
|
},
|
||||||
|
"wind:CIG3": {
|
||||||
|
"plain_language": "Conditional potential for extreme severe wind.",
|
||||||
|
"official_description": "Intensity Level 3: Reasonable Max wind gusts around 100 kt / 115 mph or higher. Note that this product describes the reasonable maximum intensity of a hazard if that hazard occurs. It does not by itself indicate the probability that the hazard will occur.",
|
||||||
|
"relative_level": "3 of 3"
|
||||||
|
},
|
||||||
|
"hail:CIG1": {
|
||||||
|
"plain_language": "Conditional potential for significant hail.",
|
||||||
|
"official_description": "Intensity Level 1: Reasonable Max hail size around 2.00 to 3.75 inches. Note that this product describes the reasonable maximum intensity of a hazard if that hazard occurs. It does not by itself indicate the probability that the hazard will occur.",
|
||||||
|
"relative_level": "1 of 2"
|
||||||
|
},
|
||||||
|
"hail:CIG2": {
|
||||||
|
"plain_language": "Conditional potential for giant hail.",
|
||||||
|
"official_description": "Intensity Level 2: Reasonable Max hail size greater than 3.75 inches. Note that this product describes the reasonable maximum intensity of a hazard if that hazard occurs. It does not by itself indicate the probability that the hazard will occur.",
|
||||||
|
"relative_level": "2 of 2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -253,9 +253,6 @@ func TestMetadataModuleUsesPromptSafeSourceWarningSummary(t *testing.T) {
|
|||||||
if len(value.SourceWarnings) != 1 || value.SourceWarnings[0].CompletenessImpact != "source omitted" {
|
if len(value.SourceWarnings) != 1 || value.SourceWarnings[0].CompletenessImpact != "source omitted" {
|
||||||
t.Fatalf("SourceWarnings = %#v, want warning summary", value.SourceWarnings)
|
t.Fatalf("SourceWarnings = %#v, want warning summary", value.SourceWarnings)
|
||||||
}
|
}
|
||||||
if value.Alerts == nil || !value.Alerts.Checked || value.Alerts.ActiveCount != 1 || value.Alerts.RelevantCount != 1 {
|
|
||||||
t.Fatalf("Alerts = %#v, want checked alert status", value.Alerts)
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(output.Value)
|
data, err := json.Marshal(output.Value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Marshal metadata: %v", err)
|
t.Fatalf("Marshal metadata: %v", err)
|
||||||
@@ -264,6 +261,9 @@ func TestMetadataModuleUsesPromptSafeSourceWarningSummary(t *testing.T) {
|
|||||||
if !strings.Contains(jsonText, "source_warnings") || strings.Contains(jsonText, "endpoint") || strings.Contains(jsonText, "dataSha256") {
|
if !strings.Contains(jsonText, "source_warnings") || strings.Contains(jsonText, "endpoint") || strings.Contains(jsonText, "dataSha256") {
|
||||||
t.Fatalf("metadata json = %s, want source warning summary without transport provenance", jsonText)
|
t.Fatalf("metadata json = %s, want source warning summary without transport provenance", jsonText)
|
||||||
}
|
}
|
||||||
|
if strings.Contains(jsonText, `"alerts"`) {
|
||||||
|
t.Fatalf("metadata json = %s, want alert details only in alert_digest", jsonText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCurrentConditionsModuleUsesSnakeCaseUnitFields(t *testing.T) {
|
func TestCurrentConditionsModuleUsesSnakeCaseUnitFields(t *testing.T) {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ type MetadataModule struct {
|
|||||||
ValidPeriod timeutil.Period `json:"valid_period"`
|
ValidPeriod timeutil.Period `json:"valid_period"`
|
||||||
Location *LocationContext `json:"location,omitempty"`
|
Location *LocationContext `json:"location,omitempty"`
|
||||||
SourceWarnings []SourceWarningSummary `json:"source_warnings,omitempty"`
|
SourceWarnings []SourceWarningSummary `json:"source_warnings,omitempty"`
|
||||||
Alerts *AlertDigestModule `json:"alerts,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SourceWarningSummary struct {
|
type SourceWarningSummary struct {
|
||||||
@@ -44,7 +43,6 @@ func buildMetadataModule(ctx ModuleContext, _ any) (*module.Output, error) {
|
|||||||
ValidPeriod: metadata.ValidPeriod,
|
ValidPeriod: metadata.ValidPeriod,
|
||||||
Location: copyLocation(ctx.Location),
|
Location: copyLocation(ctx.Location),
|
||||||
SourceWarnings: sourceWarningSummaries(ctx.Collected.SourceWarnings),
|
SourceWarnings: sourceWarningSummaries(ctx.Collected.SourceWarnings),
|
||||||
Alerts: alertDigest(ctx.Collected, ctx.Derived.AlertOverlaps, ctx.Timezone),
|
|
||||||
}
|
}
|
||||||
return &module.Output{ID: module.Metadata, StanzaName: "metadata", Value: value}, nil
|
return &module.Output{ID: module.Metadata, StanzaName: "metadata", Value: value}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
37
internal/briefing/spc_convective_outlook_definitions.go
Normal file
37
internal/briefing/spc_convective_outlook_definitions.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package briefing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed assets/spc_convective_outlook_definitions.json
|
||||||
|
var spcConvectiveOutlookDefinitionAssets embed.FS
|
||||||
|
|
||||||
|
var spcOutlookBackgroundDefinitions = mustLoadSPCOutlookBackgroundDefinitions()
|
||||||
|
|
||||||
|
func mustLoadSPCOutlookBackgroundDefinitions() map[string]SPCOutlookBackgroundDefinition {
|
||||||
|
data, err := spcConvectiveOutlookDefinitionAssets.ReadFile("assets/spc_convective_outlook_definitions.json")
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("read embedded SPC outlook definitions: %v", err))
|
||||||
|
}
|
||||||
|
var definitions map[string]SPCOutlookBackgroundDefinition
|
||||||
|
if err := json.Unmarshal(data, &definitions); err != nil {
|
||||||
|
panic(fmt.Sprintf("decode embedded SPC outlook definitions: %v", err))
|
||||||
|
}
|
||||||
|
return definitions
|
||||||
|
}
|
||||||
|
|
||||||
|
func spcOutlookBackgroundDefinition(outlookType string, label string) *SPCOutlookBackgroundDefinition {
|
||||||
|
definition, ok := spcOutlookBackgroundDefinitions[spcOutlookDefinitionKey(outlookType, label)]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &definition
|
||||||
|
}
|
||||||
|
|
||||||
|
func spcOutlookDefinitionKey(outlookType string, label string) string {
|
||||||
|
return strings.ToLower(strings.TrimSpace(outlookType)) + ":" + strings.ToUpper(strings.TrimSpace(label))
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ type SPCConvectiveOutlookRecord struct {
|
|||||||
OutlookType string `json:"outlook_type,omitempty"`
|
OutlookType string `json:"outlook_type,omitempty"`
|
||||||
Label string `json:"label,omitempty"`
|
Label string `json:"label,omitempty"`
|
||||||
LabelText string `json:"label_text,omitempty"`
|
LabelText string `json:"label_text,omitempty"`
|
||||||
|
BackgroundDefinition *SPCOutlookBackgroundDefinition `json:"background_definition,omitempty"`
|
||||||
PeriodBegins string `json:"period_begins,omitempty"`
|
PeriodBegins string `json:"period_begins,omitempty"`
|
||||||
PeriodEnds string `json:"period_ends,omitempty"`
|
PeriodEnds string `json:"period_ends,omitempty"`
|
||||||
IssuedAt string `json:"issued_at,omitempty"`
|
IssuedAt string `json:"issued_at,omitempty"`
|
||||||
@@ -36,6 +37,12 @@ type SPCConvectiveOutlookRecord struct {
|
|||||||
ImageURL string `json:"image_url,omitempty"`
|
ImageURL string `json:"image_url,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SPCOutlookBackgroundDefinition struct {
|
||||||
|
PlainLanguage string `json:"plain_language,omitempty"`
|
||||||
|
OfficialDescription string `json:"official_description,omitempty"`
|
||||||
|
RelativeLevel string `json:"relative_level,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type SPCConvectiveOutlookDigest struct {
|
type SPCConvectiveOutlookDigest struct {
|
||||||
LabelText string `json:"label_text,omitempty"`
|
LabelText string `json:"label_text,omitempty"`
|
||||||
RiskLabel string `json:"risk_label,omitempty"`
|
RiskLabel string `json:"risk_label,omitempty"`
|
||||||
@@ -91,6 +98,7 @@ func spcConvectiveOutlookRecords(outlooks []weatherdata.ConvectiveOutlook, repor
|
|||||||
OutlookType: outlook.OutlookType,
|
OutlookType: outlook.OutlookType,
|
||||||
Label: outlook.Label,
|
Label: outlook.Label,
|
||||||
LabelText: outlook.LabelText,
|
LabelText: outlook.LabelText,
|
||||||
|
BackgroundDefinition: spcOutlookBackgroundDefinition(outlook.OutlookType, outlook.Label),
|
||||||
PeriodBegins: friendlyPeriodBeginsLabel(outlookPeriod, timezone),
|
PeriodBegins: friendlyPeriodBeginsLabel(outlookPeriod, timezone),
|
||||||
PeriodEnds: friendlyPeriodEndsLabel(outlookPeriod, timezone),
|
PeriodEnds: friendlyPeriodEndsLabel(outlookPeriod, timezone),
|
||||||
IssuedAt: friendlyOptionalTime(outlook.IssuedAt, timezone),
|
IssuedAt: friendlyOptionalTime(outlook.IssuedAt, timezone),
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ func TestSPCConvectiveOutlooksModuleBuildsPromptSafeRiskProduct(t *testing.T) {
|
|||||||
if got.Day != 1 || got.OutlookType != "categorical" || got.Label != "SLGT" || got.LabelText != "Slight Risk" {
|
if got.Day != 1 || got.OutlookType != "categorical" || got.Label != "SLGT" || got.LabelText != "Slight Risk" {
|
||||||
t.Fatalf("outlook = %#v, want categorical slight risk fields", got)
|
t.Fatalf("outlook = %#v, want categorical slight risk fields", got)
|
||||||
}
|
}
|
||||||
|
if got.BackgroundDefinition == nil ||
|
||||||
|
got.BackgroundDefinition.PlainLanguage != "Scattered severe storms possible." ||
|
||||||
|
got.BackgroundDefinition.OfficialDescription != "Isolated intense storms are possible within the risk area, but severe weather is generally expected to be short-lived and/or not widespread." ||
|
||||||
|
got.BackgroundDefinition.RelativeLevel != "2 of 5" {
|
||||||
|
t.Fatalf("background definition = %#v, want Slight Risk helper", got.BackgroundDefinition)
|
||||||
|
}
|
||||||
if got.PeriodBegins != "2026-05-29 at 11:00 AM" || got.PeriodEnds != "2026-05-30 at 7:00 AM" || got.IssuedAt != "2026-05-29 at 8:45 AM" {
|
if got.PeriodBegins != "2026-05-29 at 11:00 AM" || got.PeriodEnds != "2026-05-30 at 7:00 AM" || got.IssuedAt != "2026-05-29 at 8:45 AM" {
|
||||||
t.Fatalf("outlook times = %#v, want friendly local labels", got)
|
t.Fatalf("outlook times = %#v, want friendly local labels", got)
|
||||||
}
|
}
|
||||||
@@ -83,7 +89,7 @@ func TestSPCConvectiveOutlooksModuleBuildsPromptSafeRiskProduct(t *testing.T) {
|
|||||||
t.Fatalf("Marshal() error = %v", err)
|
t.Fatalf("Marshal() error = %v", err)
|
||||||
}
|
}
|
||||||
text := string(data)
|
text := string(data)
|
||||||
for _, field := range []string{"checked", "as_of", "issued_at", "location_id", "location_name", "outlook_count", "outlooks", "risk_digest", "period_begins", "period_ends", "contains_location", "image_url"} {
|
for _, field := range []string{"checked", "as_of", "issued_at", "location_id", "location_name", "outlook_count", "outlooks", "risk_digest", "background_definition", "plain_language", "official_description", "relative_level", "period_begins", "period_ends", "contains_location", "image_url"} {
|
||||||
if !strings.Contains(text, field) {
|
if !strings.Contains(text, field) {
|
||||||
t.Fatalf("json = %s, want field %s", text, field)
|
t.Fatalf("json = %s, want field %s", text, field)
|
||||||
}
|
}
|
||||||
@@ -95,6 +101,98 @@ func TestSPCConvectiveOutlooksModuleBuildsPromptSafeRiskProduct(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSPCOutlookBackgroundDefinitionLookup(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
outlookType string
|
||||||
|
label string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "exact slight risk", outlookType: "categorical", label: "SLGT", want: true},
|
||||||
|
{name: "normalized slight risk", outlookType: " Categorical ", label: "slgt", want: true},
|
||||||
|
{name: "expanded marginal risk", outlookType: "categorical", label: "MRGL", want: true},
|
||||||
|
{name: "expanded conditional tornado risk", outlookType: "tornado", label: "CIG3", want: true},
|
||||||
|
{name: "unknown risk", outlookType: "categorical", label: "FOO"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
definition := spcOutlookBackgroundDefinition(tt.outlookType, tt.label)
|
||||||
|
if tt.want && definition == nil {
|
||||||
|
t.Fatalf("spcOutlookBackgroundDefinition(%q, %q) = nil, want definition", tt.outlookType, tt.label)
|
||||||
|
}
|
||||||
|
if !tt.want && definition != nil {
|
||||||
|
t.Fatalf("spcOutlookBackgroundDefinition(%q, %q) = %#v, want nil", tt.outlookType, tt.label, definition)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSPCConvectiveOutlooksModuleOmitsUnknownBackgroundDefinition(t *testing.T) {
|
||||||
|
registry := MustDefaultModuleRegistry()
|
||||||
|
ctx := testModuleContext()
|
||||||
|
outlook := spcRiskDigestTestOutlook("categorical", "Unknown Risk", 2, true,
|
||||||
|
"2026-05-29T11:00:00-05:00", "2026-05-30T07:00:00-05:00")
|
||||||
|
outlook.Label = "FOO"
|
||||||
|
ctx.Collected.SPCConvectiveOutlooks = &weatherdata.ConvectiveOutlookRun{
|
||||||
|
Outlooks: []weatherdata.ConvectiveOutlook{outlook},
|
||||||
|
}
|
||||||
|
ctx.Derived.SPCConvectiveOutlooks = []weatherdata.ConvectiveOutlook{outlook}
|
||||||
|
|
||||||
|
output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.SPCConvectiveOutlooks})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildModule() error = %v", err)
|
||||||
|
}
|
||||||
|
value := moduleValue[SPCConvectiveOutlooksModule](t, output)
|
||||||
|
if len(value.Outlooks) != 1 {
|
||||||
|
t.Fatalf("Outlooks length = %d, want 1", len(value.Outlooks))
|
||||||
|
}
|
||||||
|
if value.Outlooks[0].BackgroundDefinition != nil {
|
||||||
|
t.Fatalf("BackgroundDefinition = %#v, want nil for undefined risk", value.Outlooks[0].BackgroundDefinition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSPCOutlookBackgroundDefinitionsAssetHasUsableEntries(t *testing.T) {
|
||||||
|
wantKeys := []string{
|
||||||
|
"categorical:TSTM",
|
||||||
|
"categorical:MRGL",
|
||||||
|
"categorical:SLGT",
|
||||||
|
"categorical:ENH",
|
||||||
|
"categorical:MDT",
|
||||||
|
"categorical:HIGH",
|
||||||
|
"tornado:CIG1",
|
||||||
|
"tornado:CIG2",
|
||||||
|
"tornado:CIG3",
|
||||||
|
"wind:CIG1",
|
||||||
|
"wind:CIG2",
|
||||||
|
"wind:CIG3",
|
||||||
|
"hail:CIG1",
|
||||||
|
"hail:CIG2",
|
||||||
|
}
|
||||||
|
if len(spcOutlookBackgroundDefinitions) != len(wantKeys) {
|
||||||
|
t.Fatalf("embedded SPC outlook background definitions length = %d, want %d", len(spcOutlookBackgroundDefinitions), len(wantKeys))
|
||||||
|
}
|
||||||
|
for _, key := range wantKeys {
|
||||||
|
if _, ok := spcOutlookBackgroundDefinitions[key]; !ok {
|
||||||
|
t.Fatalf("embedded SPC outlook background definitions missing %q", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key, definition := range spcOutlookBackgroundDefinitions {
|
||||||
|
if strings.TrimSpace(key) == "" {
|
||||||
|
t.Fatal("embedded SPC outlook background definitions contain empty key")
|
||||||
|
}
|
||||||
|
if definition.PlainLanguage == "" || definition.OfficialDescription == "" || definition.RelativeLevel == "" {
|
||||||
|
t.Fatalf("embedded SPC outlook background definition %q is incomplete: %#v", key, definition)
|
||||||
|
}
|
||||||
|
if strings.Contains(definition.OfficialDescription, ".Note") || strings.Contains(definition.OfficialDescription, "higher.Note") {
|
||||||
|
t.Fatalf("embedded SPC outlook background definition %q has missing sentence spacing: %q", key, definition.OfficialDescription)
|
||||||
|
}
|
||||||
|
if strings.Contains(definition.OfficialDescription, "by themselves") {
|
||||||
|
t.Fatalf("embedded SPC outlook background definition %q has singular grammar issue: %q", key, definition.OfficialDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSPCRiskDigestDefaultPolicyConstants(t *testing.T) {
|
func TestSPCRiskDigestDefaultPolicyConstants(t *testing.T) {
|
||||||
if defaultSPCRiskDigestOutlookType != "categorical" {
|
if defaultSPCRiskDigestOutlookType != "categorical" {
|
||||||
t.Fatalf("defaultSPCRiskDigestOutlookType = %q, want categorical", defaultSPCRiskDigestOutlookType)
|
t.Fatalf("defaultSPCRiskDigestOutlookType = %q, want categorical", defaultSPCRiskDigestOutlookType)
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ func TestDefaults(t *testing.T) {
|
|||||||
if cfg.WeatherAPI.Format != "json" {
|
if cfg.WeatherAPI.Format != "json" {
|
||||||
t.Fatalf("Format = %q, want json", cfg.WeatherAPI.Format)
|
t.Fatalf("Format = %q, want json", cfg.WeatherAPI.Format)
|
||||||
}
|
}
|
||||||
|
if cfg.WeatherAPI.Precision != 0 {
|
||||||
|
t.Fatalf("Precision = %d, want 0", cfg.WeatherAPI.Precision)
|
||||||
|
}
|
||||||
if cfg.Location.ID != "home" || cfg.Location.Name != "Brentwood" || cfg.Location.Region != "St. Louis Metro" {
|
if cfg.Location.ID != "home" || cfg.Location.Name != "Brentwood" || cfg.Location.Region != "St. Louis Metro" {
|
||||||
t.Fatalf("Location = %#v, want home/Brentwood/St. Louis Metro", cfg.Location)
|
t.Fatalf("Location = %#v, want home/Brentwood/St. Louis Metro", cfg.Location)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ func Defaults() Config {
|
|||||||
return Config{
|
return Config{
|
||||||
WeatherAPI: WeatherAPIConfig{
|
WeatherAPI: WeatherAPIConfig{
|
||||||
Timeout: 10 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
Precision: 1,
|
Precision: 0,
|
||||||
Units: "us",
|
Units: "us",
|
||||||
Timezone: "America/Chicago",
|
Timezone: "America/Chicago",
|
||||||
Format: "json",
|
Format: "json",
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func TestBuildHourlyRenderContext(t *testing.T) {
|
|||||||
"**Updated:** Friday, May 29, 2026 at 8:30 AM",
|
"**Updated:** Friday, May 29, 2026 at 8:30 AM",
|
||||||
"Storm chances increase through late morning.",
|
"Storm chances increase through late morning.",
|
||||||
"- **10:00 AM:** 75°F and showers. Probability of precipitation is 70%.",
|
"- **10:00 AM:** 75°F and showers. Probability of precipitation is 70%.",
|
||||||
"- **Flood Watch**: Flood Watch in effect from May 29 at 10:00 AM to May 29 at 2:30 PM. Avoid low-water crossings.",
|
"- **Flood Watch**: Flood Watch in effect from May 29 at 10:00 AM to May 29 at 2:30 PM.",
|
||||||
"A cold front is moving into the region.",
|
"A cold front is moving into the region.",
|
||||||
"A front will keep the region unsettled.",
|
"A front will keep the region unsettled.",
|
||||||
} {
|
} {
|
||||||
@@ -99,6 +99,9 @@ func TestBuildHourlyRenderContext(t *testing.T) {
|
|||||||
t.Fatalf("rendered template missing %q:\n%s", want, text)
|
t.Fatalf("rendered template missing %q:\n%s", want, text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if strings.Contains(text, "Avoid low-water crossings.") {
|
||||||
|
t.Fatalf("rendered template includes alert instruction:\n%s", text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildHourlyRenderContextAllowsOmittedOptionalModules(t *testing.T) {
|
func TestBuildHourlyRenderContextAllowsOmittedOptionalModules(t *testing.T) {
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func Render(id string, data any) ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tmpl, err := template.New(id).Option("missingkey=error").Parse(source)
|
tmpl, err := template.New(id).Funcs(templateFuncs()).Option("missingkey=error").Parse(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parse report template %q: %w", id, err)
|
return nil, fmt.Errorf("parse report template %q: %w", id, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -270,8 +270,7 @@ func TestRenderHourly(t *testing.T) {
|
|||||||
"Storm chances increase through late morning.",
|
"Storm chances increase through late morning.",
|
||||||
"Currently, it is 74°F and partly cloudy. It feels like 76°F, with a relative humidity of 71% and winds from the south at 8 mph.",
|
"Currently, it is 74°F and partly cloudy. It feels like 76°F, with a relative humidity of 71% and winds from the south at 8 mph.",
|
||||||
"## Alert Digest",
|
"## Alert Digest",
|
||||||
"- **Flood Watch**: Flood Watch in effect from May 29 at 10:00 AM to May 29 at 2:30 PM. Avoid low-water crossings.",
|
"- **Flood Watch**: Flood Watch in effect from May 29 at 10:00 AM to May 29 at 2:30 PM.",
|
||||||
"- **SPC Convective Outlook**: Slight risk for severe thunderstorms in effect from May 29 at 8:00 AM to May 29 at 2:00 PM.",
|
|
||||||
"- **9:00 AM:** 74°F and cloudy.",
|
"- **9:00 AM:** 74°F and cloudy.",
|
||||||
"- **10:00 AM:** 75°F and showers. Probability of precipitation is 70%.",
|
"- **10:00 AM:** 75°F and showers. Probability of precipitation is 70%.",
|
||||||
"- **10:00 AM** to **12:00 PM**: Expect showers. The peak precipitation chance is 70% at 10:00 AM.",
|
"- **10:00 AM** to **12:00 PM**: Expect showers. The peak precipitation chance is 70% at 10:00 AM.",
|
||||||
@@ -285,6 +284,11 @@ func TestRenderHourly(t *testing.T) {
|
|||||||
if strings.Contains(text, "19%") || strings.Contains(text, "wind S") || strings.Contains(text, "## Confidence") {
|
if strings.Contains(text, "19%") || strings.Contains(text, "wind S") || strings.Contains(text, "## Confidence") {
|
||||||
t.Fatalf("rendered template included omitted details:\n%s", text)
|
t.Fatalf("rendered template included omitted details:\n%s", text)
|
||||||
}
|
}
|
||||||
|
for _, unwanted := range []string{"Avoid low-water crossings.", "Slight risk for severe thunderstorms"} {
|
||||||
|
if strings.Contains(text, unwanted) {
|
||||||
|
t.Fatalf("rendered template included %q:\n%s", unwanted, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
assertOrderedText(t, text, []string{
|
assertOrderedText(t, text, []string{
|
||||||
"# Hourly Report",
|
"# Hourly Report",
|
||||||
"## Alert Digest",
|
"## Alert Digest",
|
||||||
@@ -371,8 +375,7 @@ func TestRenderTomorrow(t *testing.T) {
|
|||||||
"**Updated:** Sunday, June 14, 2026 at 9:14 AM",
|
"**Updated:** Sunday, June 14, 2026 at 9:14 AM",
|
||||||
"Tomorrow starts dry before showers return later in the day.",
|
"Tomorrow starts dry before showers return later in the day.",
|
||||||
"## Alert Digest",
|
"## Alert Digest",
|
||||||
"- **Wind Advisory**: Wind Advisory in effect from June 15 at 1:00 PM to June 15 at 8:00 PM. Secure outdoor objects.",
|
"- **Wind Advisory**: Wind Advisory in effect from June 15 at 1:00 PM to June 15 at 8:00 PM.",
|
||||||
"- **SPC Convective Outlook**: Slight risk for severe thunderstorms in effect from June 15 at 7:00 AM to June 16 at 7:00 AM.",
|
|
||||||
"- **Overnight:** Partly cloudy, with temperatures falling from the mid 60s to the upper 50s.",
|
"- **Overnight:** Partly cloudy, with temperatures falling from the mid 60s to the upper 50s.",
|
||||||
"- **Morning:** Sunny, with temperatures rising from the upper 50s to the upper 60s.",
|
"- **Morning:** Sunny, with temperatures rising from the upper 50s to the upper 60s.",
|
||||||
"- **Afternoon:** Sunny, with temperatures in the upper 70s. Chance of precipitation is 70%.",
|
"- **Afternoon:** Sunny, with temperatures in the upper 70s. Chance of precipitation is 70%.",
|
||||||
@@ -388,6 +391,8 @@ func TestRenderTomorrow(t *testing.T) {
|
|||||||
for _, unwanted := range []string{
|
for _, unwanted := range []string{
|
||||||
"upper 50s.\n\n- **Morning:**",
|
"upper 50s.\n\n- **Morning:**",
|
||||||
"upper 60s.\n\n- **Afternoon:**",
|
"upper 60s.\n\n- **Afternoon:**",
|
||||||
|
"Secure outdoor objects.",
|
||||||
|
"Slight risk for severe thunderstorms",
|
||||||
} {
|
} {
|
||||||
if strings.Contains(text, unwanted) {
|
if strings.Contains(text, unwanted) {
|
||||||
t.Fatalf("rendered template includes blank lines between daypart bullets:\n%s", text)
|
t.Fatalf("rendered template includes blank lines between daypart bullets:\n%s", text)
|
||||||
@@ -473,7 +478,7 @@ func TestRenderDaily(t *testing.T) {
|
|||||||
"**Updated:** Sunday, June 14, 2026 at 9:14 AM",
|
"**Updated:** Sunday, June 14, 2026 at 9:14 AM",
|
||||||
"The selected day starts dry before showers return later in the day.",
|
"The selected day starts dry before showers return later in the day.",
|
||||||
"## Alert Digest",
|
"## Alert Digest",
|
||||||
"- **Flood Watch**: Flood Watch in effect from June 15 at 3:00 PM to June 15 at 6:00 PM. Monitor creek levels.",
|
"- **Flood Watch**: Flood Watch in effect from June 15 at 3:00 PM to June 15 at 6:00 PM.",
|
||||||
"- **SPC Convective Outlook**: Enhanced risk for severe thunderstorms in effect from June 15 at 7:00 AM to June 16 at 7:00 AM.",
|
"- **SPC Convective Outlook**: Enhanced risk for severe thunderstorms in effect from June 15 at 7:00 AM to June 16 at 7:00 AM.",
|
||||||
"- **Morning:** Sunny, with temperatures rising from the upper 50s to the upper 60s.",
|
"- **Morning:** Sunny, with temperatures rising from the upper 50s to the upper 60s.",
|
||||||
"- **Afternoon:** Showers, with temperatures in the upper 70s. Chance of precipitation is 70%.",
|
"- **Afternoon:** Showers, with temperatures in the upper 70s. Chance of precipitation is 70%.",
|
||||||
@@ -486,6 +491,9 @@ func TestRenderDaily(t *testing.T) {
|
|||||||
t.Fatalf("rendered template missing %q:\n%s", want, text)
|
t.Fatalf("rendered template missing %q:\n%s", want, text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if strings.Contains(text, "Monitor creek levels.") {
|
||||||
|
t.Fatalf("rendered template included alert description:\n%s", text)
|
||||||
|
}
|
||||||
assertOrderedText(t, text, []string{
|
assertOrderedText(t, text, []string{
|
||||||
"# Monday's Weather",
|
"# Monday's Weather",
|
||||||
"The selected day starts dry before showers return later in the day.",
|
"The selected day starts dry before showers return later in the day.",
|
||||||
@@ -581,8 +589,7 @@ func TestRenderToday(t *testing.T) {
|
|||||||
"**Updated:** Monday, June 15, 2026 at 7:14 AM",
|
"**Updated:** Monday, June 15, 2026 at 7:14 AM",
|
||||||
"Today starts dry before showers return later in the day.",
|
"Today starts dry before showers return later in the day.",
|
||||||
"## Alert Digest",
|
"## Alert Digest",
|
||||||
"- **Wind Advisory**: Wind Advisory in effect from June 15 at 1:00 PM to June 15 at 8:00 PM. Secure outdoor objects.",
|
"- **Wind Advisory**: Wind Advisory in effect from June 15 at 1:00 PM to June 15 at 8:00 PM.",
|
||||||
"- **SPC Convective Outlook**: Slight risk for severe thunderstorms in effect from June 15 at 7:00 AM to June 16 at 7:00 AM.",
|
|
||||||
"Currently, it is 58°F and clear. It feels like 57°F, with a relative humidity of 61% and winds from the northwest at 9 mph.",
|
"Currently, it is 58°F and clear. It feels like 57°F, with a relative humidity of 61% and winds from the northwest at 9 mph.",
|
||||||
"- **Morning:** Sunny, with temperatures rising from the upper 50s to the upper 60s.",
|
"- **Morning:** Sunny, with temperatures rising from the upper 50s to the upper 60s.",
|
||||||
"- **Afternoon:** Showers, with temperatures in the upper 70s. Chance of precipitation is 70%.",
|
"- **Afternoon:** Showers, with temperatures in the upper 70s. Chance of precipitation is 70%.",
|
||||||
@@ -606,7 +613,7 @@ func TestRenderToday(t *testing.T) {
|
|||||||
"## Precipitation Timing",
|
"## Precipitation Timing",
|
||||||
"## Forecast Discussion",
|
"## Forecast Discussion",
|
||||||
})
|
})
|
||||||
for _, unwanted := range []string{"## Planning Notes", "Morning weather looks routine.", "Watch late-day shower timing.", "- **Evening:**", "Forecast details are limited"} {
|
for _, unwanted := range []string{"## Planning Notes", "Morning weather looks routine.", "Watch late-day shower timing.", "- **Evening:**", "Forecast details are limited", "Secure outdoor objects.", "Slight risk for severe thunderstorms"} {
|
||||||
if strings.Contains(text, unwanted) {
|
if strings.Contains(text, unwanted) {
|
||||||
t.Fatalf("rendered template included %q:\n%s", unwanted, text)
|
t.Fatalf("rendered template included %q:\n%s", unwanted, text)
|
||||||
}
|
}
|
||||||
@@ -736,6 +743,73 @@ func TestRenderTomorrowOmitsPrecipitationTimingWithoutWindows(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAlertDigestOmitsBelowThresholdSPCRiskOnlySection(t *testing.T) {
|
||||||
|
rendered, err := Render("hourly", testRenderContext{
|
||||||
|
Report: testReportContext{Title: "Hourly Report"},
|
||||||
|
GeneratedText: testGeneratedText{
|
||||||
|
Summary: "Storm chances remain low.",
|
||||||
|
ForecastDiscussion: "Only isolated severe storms are possible.",
|
||||||
|
},
|
||||||
|
Modules: testModules{
|
||||||
|
CurrentConditions: &testCurrentConditions{},
|
||||||
|
HourlyForecast: &testHourlyForecast{},
|
||||||
|
SPCConvectiveOutlooks: &testSPCOutlooks{
|
||||||
|
RiskDigest: []testSPCRiskDigest{{LabelText: "Slight Risk", RiskLabel: "Slight risk", PeriodBegins: "June 15 at 7:00 AM", PeriodEnds: "June 16 at 7:00 AM"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Render() error = %v", err)
|
||||||
|
}
|
||||||
|
text := string(rendered)
|
||||||
|
for _, unwanted := range []string{"## Alert Digest", "SPC Convective Outlook", "Slight risk for severe thunderstorms"} {
|
||||||
|
if strings.Contains(text, unwanted) {
|
||||||
|
t.Fatalf("rendered template included %q for below-threshold SPC-only digest:\n%s", unwanted, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertDigestRendersEnhancedOrHigherSPCRiskWithoutAlerts(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
label string
|
||||||
|
risk string
|
||||||
|
}{
|
||||||
|
{label: "Enhanced Risk", risk: "Enhanced risk"},
|
||||||
|
{label: "Moderate Risk", risk: "Moderate risk"},
|
||||||
|
{label: "High Risk", risk: "High risk"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.label, func(t *testing.T) {
|
||||||
|
rendered, err := Render("hourly", testRenderContext{
|
||||||
|
Report: testReportContext{Title: "Hourly Report"},
|
||||||
|
GeneratedText: testGeneratedText{
|
||||||
|
Summary: "Severe storms are possible.",
|
||||||
|
ForecastDiscussion: "SPC outlooks highlight the risk.",
|
||||||
|
},
|
||||||
|
Modules: testModules{
|
||||||
|
CurrentConditions: &testCurrentConditions{},
|
||||||
|
HourlyForecast: &testHourlyForecast{},
|
||||||
|
SPCConvectiveOutlooks: &testSPCOutlooks{
|
||||||
|
RiskDigest: []testSPCRiskDigest{{LabelText: tt.label, RiskLabel: tt.risk, PeriodBegins: "June 15 at 7:00 AM", PeriodEnds: "June 16 at 7:00 AM"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Render() error = %v", err)
|
||||||
|
}
|
||||||
|
text := string(rendered)
|
||||||
|
for _, want := range []string{
|
||||||
|
"## Alert Digest",
|
||||||
|
"- **SPC Convective Outlook**: " + tt.risk + " for severe thunderstorms in effect from June 15 at 7:00 AM to June 16 at 7:00 AM.",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(text, want) {
|
||||||
|
t.Fatalf("rendered template missing %q:\n%s", want, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRenderHourlyOmitsConditionalSectionsForClearWeather(t *testing.T) {
|
func TestRenderHourlyOmitsConditionalSectionsForClearWeather(t *testing.T) {
|
||||||
rendered, err := Render("hourly", testRenderContext{
|
rendered, err := Render("hourly", testRenderContext{
|
||||||
Report: testReportContext{
|
Report: testReportContext{
|
||||||
|
|||||||
76
internal/reporttemplate/template_funcs.go
Normal file
76
internal/reporttemplate/template_funcs.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package reporttemplate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
func templateFuncs() template.FuncMap {
|
||||||
|
return template.FuncMap{
|
||||||
|
"hasRelevantAlerts": hasRelevantAlerts,
|
||||||
|
"hasEnhancedOrHigherSPCRisk": hasEnhancedOrHigherSPCRisk,
|
||||||
|
"isEnhancedOrHigherSPCRisk": isEnhancedOrHigherSPCRisk,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasRelevantAlerts(alertDigest any) bool {
|
||||||
|
value := dereferenceValue(reflect.ValueOf(alertDigest))
|
||||||
|
if !value.IsValid() || value.Kind() != reflect.Struct {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
relevant := value.FieldByName("Relevant")
|
||||||
|
return relevant.IsValid() && relevant.Kind() == reflect.Slice && relevant.Len() > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasEnhancedOrHigherSPCRisk(outlooks any) bool {
|
||||||
|
value := dereferenceValue(reflect.ValueOf(outlooks))
|
||||||
|
if !value.IsValid() || value.Kind() != reflect.Struct {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
riskDigest := value.FieldByName("RiskDigest")
|
||||||
|
if !riskDigest.IsValid() || riskDigest.Kind() != reflect.Slice {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < riskDigest.Len(); i++ {
|
||||||
|
if isEnhancedOrHigherSPCRisk(riskDigest.Index(i).Interface()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEnhancedOrHigherSPCRisk(risk any) bool {
|
||||||
|
value := dereferenceValue(reflect.ValueOf(risk))
|
||||||
|
if !value.IsValid() || value.Kind() != reflect.Struct {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
label := stringField(value, "LabelText")
|
||||||
|
if label == "" {
|
||||||
|
label = stringField(value, "RiskLabel")
|
||||||
|
}
|
||||||
|
switch strings.ToLower(strings.TrimSpace(label)) {
|
||||||
|
case "enhanced risk", "moderate risk", "high risk":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dereferenceValue(value reflect.Value) reflect.Value {
|
||||||
|
for value.IsValid() && (value.Kind() == reflect.Interface || value.Kind() == reflect.Pointer) {
|
||||||
|
if value.IsNil() {
|
||||||
|
return reflect.Value{}
|
||||||
|
}
|
||||||
|
value = value.Elem()
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringField(value reflect.Value, name string) string {
|
||||||
|
field := value.FieldByName(name)
|
||||||
|
if !field.IsValid() || field.Kind() != reflect.String {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return field.String()
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{{ define "alert_digest" }}{{ if or (and .Modules.AlertDigest .Modules.AlertDigest.Relevant) (and .Modules.SPCConvectiveOutlooks .Modules.SPCConvectiveOutlooks.RiskDigest) }}## Alert Digest
|
{{ define "alert_digest" }}{{ $hasAlerts := hasRelevantAlerts .Modules.AlertDigest }}{{ $hasSPC := hasEnhancedOrHigherSPCRisk .Modules.SPCConvectiveOutlooks }}{{ if or $hasAlerts $hasSPC }}## Alert Digest
|
||||||
{{ with .Modules.AlertDigest }}{{ range .Relevant -}}
|
{{ with .Modules.AlertDigest }}{{ range .Relevant -}}
|
||||||
- **{{ if .Event }}{{ .Event }}{{ else }}{{ .Headline }}{{ end }}**: {{ if .Event }}{{ .Event }}{{ else }}{{ .Headline }}{{ end }} in effect{{ with .PeriodBegins }} from {{ . }}{{ end }}{{ with .PeriodEnds }} to {{ . }}{{ end }}.{{ with .Instruction }} {{ . }}{{ else }}{{ with .Description }} {{ . }}{{ end }}{{ end }}
|
- **{{ if .Event }}{{ .Event }}{{ else }}{{ .Headline }}{{ end }}**: {{ if .Event }}{{ .Event }}{{ else }}{{ .Headline }}{{ end }} in effect{{ with .PeriodBegins }} from {{ . }}{{ end }}{{ with .PeriodEnds }} to {{ . }}{{ end }}.
|
||||||
{{ end }}{{ end }}{{ with .Modules.SPCConvectiveOutlooks }}{{ range .RiskDigest -}}
|
{{ end }}{{ end }}{{ with .Modules.SPCConvectiveOutlooks }}{{ range .RiskDigest }}{{ if isEnhancedOrHigherSPCRisk . -}}
|
||||||
- **SPC Convective Outlook**: {{ with .RiskLabel }}{{ . }}{{ else }}Convective risk{{ end }} for severe thunderstorms in effect{{ with .PeriodBegins }} from {{ . }}{{ end }}{{ with .PeriodEnds }} to {{ . }}{{ end }}.
|
- **SPC Convective Outlook**: {{ with .RiskLabel }}{{ . }}{{ else }}Convective risk{{ end }} for severe thunderstorms in effect{{ with .PeriodBegins }} from {{ . }}{{ end }}{{ with .PeriodEnds }} to {{ . }}{{ end }}.
|
||||||
{{ end }}{{ end }}
|
{{ end }}{{ end }}{{ end }}
|
||||||
|
|
||||||
{{ end }}{{ end }}
|
{{ end }}{{ end }}
|
||||||
|
|||||||
Reference in New Issue
Block a user