diff --git a/docs/internal/changes.md b/docs/internal/changes.md index 9c1dc76..086e504 100644 --- a/docs/internal/changes.md +++ b/docs/internal/changes.md @@ -1,75 +1,61 @@ # 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 -compact change records for prompt input data packages. +Each comparator receives a prior snapshot, a current snapshot, and +`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 -- current module snapshot -- comparison thresholds from configuration +| Comparator | Required snapshot data | Compared values | +| --- | --- | --- | +| `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 - value where useful +The application selects a comparator only after state lookup establishes a +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. -- It does not read filesystem state, find prior snapshots, render Markdown, - invoke Scriptorium, or compare generated report text. +Required stanzas that are absent or cannot be decoded return an error with the +snapshot and stanza context. Optional stanzas may be absent. A snapshot with no +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` -- `recent_change.precip_probability_points` -- `recent_change.wind_gust_miles_per_hour` -- `recent_change.precip_timing_shift_minutes` +Focused tests cover the daily, three-day, and weekend strategies, threshold +boundaries, indicator and alert changes, and missing required stanzas: -## External Adapters Used +```sh +go test ./internal/changes +``` -None. - -## 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. +Recent Changes always compare structured snapshot values, never report prose. diff --git a/docs/internal/facts.md b/docs/internal/facts.md index f946bdb..2779181 100644 --- a/docs/internal/facts.md +++ b/docs/internal/facts.md @@ -1,94 +1,67 @@ # 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 -from conservative report-scoped facts derived from them. The package gives app -orchestration one place to build reusable facts before module execution. +`BuildCollected` projects a `weatherdata.Bundle` into `CollectedFacts`. It +retains the fetched timestamp and every normalized product: observations, +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 -- resolved report definition and valid period -- report timezone -- configured daypart definitions +`BuildDerived` requires a valid resolved period and a valid report timezone. It +uses half-open period overlap to select hourly, narrative, daily, and alert +data; it also derives precipitation timing. Convective outlooks are retained +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 - provenance and warnings. SPC convective outlook source data is carried - through when present in the bundle, including upstream geometry and source - provenance. -- `facts.DerivedFacts` with valid-period forecast slices, alert overlaps, - report-period SPC convective outlooks and discussions, daily summaries, - daypart summaries, and Storm Report window summary +| Report family | Derived summary | +| --- | --- | +| Hourly | Rolling-period selections and precipitation timing; no daily or daypart summary | +| Daily, Today, Tomorrow | One local civil-day summary and its dayparts | +| Three-day, Weekend | One clipped daily summary for each overlapping local day | +| Storm | One summary for the explicit report window | -Hourly Report uses the generic valid-period hourly and narrative selection -for its rolling six-hour window. Its derived facts include precipitation timing -from the selected hourly periods, alert overlaps for the six-hour period, and -SPC outlooks/discussions overlapping that period. It does not build daily -summaries, daypart summaries, or a storm-window summary. +`DaypartSummaries` is collected from the resulting daily or storm summaries. +The detailed grouping, daypart-window, and alert rules are owned by +[forecast derivation](forecast-derivation.md). -## Boundaries +## Missing data and failures -- This package owns fact assembly and reusable deterministic derivation for a - report run. -- SPC convective outlook derivation selects already-collected outlooks whose - half-open valid intervals overlap the resolved report period and retains - 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. +Optional normalized products remain nil or yield empty selections; the package +does not create substitute values. A present convective-outlook run with no +matching outlooks produces non-nil empty outlook and discussion slices, while +a missing run produces nil slices. -## 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` -- `dayparts[].start` -- `dayparts[].end` -- `weather_api.timezone` +## Verification and invariants -## 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 - -None. Source provenance and warnings remain data fields for downstream metadata -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. +Facts are derived once for a resolved report from already collected data. +They remain reusable structured values: prompt wording, state persistence, +prior-report comparison, and template presentation are owned elsewhere. diff --git a/docs/internal/forecast-derivation.md b/docs/internal/forecast-derivation.md index 129be7f..a73de33 100644 --- a/docs/internal/forecast-derivation.md +++ b/docs/internal/forecast-derivation.md @@ -1,76 +1,63 @@ # Forecast Derivation Internals -This document describes deterministic forecast summarization in -`internal/forecast`. +`internal/forecast` deterministically selects and summarizes normalized +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 -summaries used by fact builders and module builders. +Selections use `timeutil.Period` half-open overlap: a value is selected only +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` -- local date or resolved report period -- timezone -- configured daypart definitions +`BuildDailySummary` requires an hourly run with at least one period. It adds +the selected narrative periods, discussion, alert overlaps, source provenance, +source warnings, and one `DaypartSummary` per resolved window. A daypart keeps +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 -- one clipped daily summary per local day or partial day from - `BuildPeriodDailySummaries` -- daypart summaries with selected hourly periods, ranges, timed maximums, - conditions, indicators, and alert overlaps +Alert overlap parsing supports the normalized alert payload's available timing +fields. Unparseable alerts and invalid intervals are ignored; valid overlaps +are clipped to the requested period and ordered by alert start time. -## Boundaries +## Missing data and failures -- This package groups, selects, and summarizes already-normalized forecast - data. -- It does not perform HTTP calls, parse CLI flags, resolve report definitions, - compare prior snapshots, build prompt input packages, or invoke Scriptorium. +Empty selections yield empty summary fields rather than generated prose. +Direct daily or period-summary calls fail when their required bundle, valid +period, hourly data, or daypart definitions are invalid. A nil location uses +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` -- `dayparts[].start` -- `dayparts[].end` +## Verification and invariants -Threshold constants for basic indicators live in forecast code rather than -configuration. +Focused tests cover local civil days, clipped periods, daypart resolution, +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`. - -## 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. +The package preserves normalized inputs as inspectable structured values and +never decides report identity, delivery, or presentation wording. diff --git a/docs/internal/state.md b/docs/internal/state.md index ce20481..44bea39 100644 --- a/docs/internal/state.md +++ b/docs/internal/state.md @@ -26,10 +26,10 @@ the paths safe to use below the configured workspace. | Generated-text source and result | `snapshots///generated_text_raw..json` and `generated_text_result..json` | | Generated-text render context | `snapshots///render_context..json` | -Notification records use their own configured date directory, because they -are not necessarily tied to a report valid period. Report producers create -parent directories as needed and write the report body; state is responsible -for the surrounding paths and saved run artifacts. +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. Batch Distributor notifications are derived separately as `notifications/batches///distributor..json`.