diff --git a/docs/integrations/feedapi.md b/docs/integrations/feedapi.md new file mode 100644 index 0000000..9da06d4 --- /dev/null +++ b/docs/integrations/feedapi.md @@ -0,0 +1,138 @@ +# Feedapi Runtime Contract + +`weatherapi` uses feedapi as its generic HTTP runtime and configuration layer. +This document describes the feedapi behavior that `weatherapi` relies on. + +## Version + +`go.mod` depends on: + +- `gitea.maximumdirect.net/ejr/feedapi v0.1.0` + +Only feedapi behavior used by `weatherapi` is documented here. + +## Packages Used + +Runtime composition imports: + +- `feedapi/app` +- `feedapi/config` +- `feedapi/db` + +The HTTP adapter imports: + +- `feedapi/bind` +- `feedapi/endpoint` +- `feedapi/errors` +- `feedapi/render` +- `feedapi/response` + +Tests also use: + +- `feedapi/templates` +- `feedapi/transport/httpx` + +## Config Ownership + +Feedapi owns loading and validating the YAML config used by `weatherapi`. +`weatherapi` adds one local runtime check: `databases` must contain at least +one entry. + +The implemented config areas used by `weatherapi` are: + +- `server`: HTTP listen/default format/timeouts; +- `databases`: named database handles opened into a registry; +- `templates`: base directory for text templates. + +The canonical config reference is [`docs/config.md`](../config.md). + +## Database Registry + +`cmd/weatherapi` calls feedapi `db.OpenAll` with configured databases and passes +the resulting registry into `feedapi/app.New`. It also selects the first +configured database name from the registry as the primary weather store. + +Feedapi owns opening and closing database handles. `weatherapi` owns choosing +which opened handle is used by the Postgres repository. + +## Endpoint Registry + +`weatherapi` builds endpoint definitions with `httpapi.Definitions` and passes +them to feedapi through `feedapi/app.WithEndpoints`. + +Feedapi owns: + +- route adaptation; +- HTTP method/path matching; +- invoking endpoint binders; +- invoking endpoint handlers; +- rendering handler results. + +Endpoint definitions remain owned by `internal/adapters/inbound/httpapi`. + +## Renderers and Templates + +Each implemented endpoint declares JSON, XML, and text output through feedapi +render formats. Text endpoints also name a template file. + +Feedapi owns: + +- renderer registration; +- format negotiation; +- template loading from `templates.base_dir`; +- applying templates to response envelopes. + +`weatherapi` owns the template files under `templates/` and presenter output +shapes consumed by those templates. + +## Content Negotiation + +`weatherapi` relies on feedapi's negotiation order: + +1. `format` query parameter; +2. `Accept` header; +3. configured default format. + +Unsupported formats are exposed as structured API errors. See +[`docs/api.md`](../api.md) for the public HTTP contract. + +## Success and Error Envelopes + +Endpoint handlers return `response.Envelope{Data: ...}` for successful +responses. Nil data is rendered as `data: null`. + +Feedapi error handling exposes structured error envelopes with: + +- `error.code`; +- `error.message`. + +`weatherapi` relies on feedapi invalid-parameter and unsupported-format errors +for request validation and negotiation failures. + +## Middleware and Shutdown + +Feedapi owns generic HTTP middleware and server lifecycle. The architecture +policy records that recovery, request ID, and timing middleware are installed by +default. + +`weatherapi` supplies a signal-cancelable context to feedapi startup. Feedapi +owns graceful HTTP shutdown after that context is canceled. + +## Upgrade Checklist + +Before upgrading feedapi: + +- verify config field names and defaults still match [`docs/config.md`](../config.md); +- verify database registry behavior still supports first configured database + selection; +- verify endpoint definition APIs still support binders, handlers, formats, + and template names; +- verify negotiation order remains `format`, then `Accept`, then default; +- verify success and error envelopes still match [`docs/api.md`](../api.md); +- run `go test ./...` with private module access configured. + +## Related Docs + +- [`docs/internal/runtime.md`](../internal/runtime.md) +- [`docs/internal/http-adapter.md`](../internal/http-adapter.md) +- [`docs/api.md`](../api.md) diff --git a/docs/integrations/weatherfeeder-postgres.md b/docs/integrations/weatherfeeder-postgres.md new file mode 100644 index 0000000..0c5fcda --- /dev/null +++ b/docs/integrations/weatherfeeder-postgres.md @@ -0,0 +1,175 @@ +# Weatherfeeder Postgres Contract + +`weatherapi` reads weather data from Postgres tables owned and populated by +`weatherfeeder`. This document describes only the storage contract consumed by +`weatherapi`; it is not a full weatherfeeder schema reference. + +## Version + +`go.mod` depends on: + +- `gitea.maximumdirect.net/ejr/weatherfeeder v0.10.0` + +The repository code also depends on weatherfeeder canonical model types. Table +compatibility must match the SQL in `internal/adapters/outbound/postgres`. + +## Boundary + +`weatherapi` is read-only: + +- it does not create tables; +- it does not migrate tables; +- it does not ingest provider data; +- it does not write weatherfeeder events. + +Weatherfeeder owns provider polling, normalization, table shape, and writes. +Postgres owns persistence, backup, restore, and availability. + +## Table Families Read + +| Resource | Tables read | +| --- | --- | +| Latest observation | `observations`, `observation_present_weather` | +| Current conditions | `observations` | +| Active alerts | `alert_runs`, `alerts`, `alert_references` | +| Hourly forecast | `forecasts`, `forecast_periods` | +| Narrative forecast | `forecasts`, `forecast_periods` | +| Forecast discussion | `forecast_discussions`, `forecast_discussion_key_messages` | +| Weather story run | `weather_story_runs`, `weather_stories` | +| Latest weather story | `weather_stories` | + +## Latest Row Selection + +Latest parent resources use these ordering rules: + +- observations: `observed_at DESC, event_emitted_at DESC`; +- alert runs: `as_of DESC, event_emitted_at DESC`; +- hourly forecasts: `product = 'hourly'`, then `issued_at DESC, + event_emitted_at DESC`; +- narrative forecasts: `product = 'narrative'`, then `issued_at DESC, + event_emitted_at DESC`; +- forecast discussions: `issued_at DESC, event_emitted_at DESC`; +- weather story runs: `as_of DESC, event_emitted_at DESC`; +- latest individual weather story: `updated_at DESC, as_of DESC, + story_order ASC, story_index ASC`. + +Current conditions aggregate `observations` rows where `observed_at` is inside +the application-provided observation window. + +## Child Ordering + +Child rows are loaded separately and attached in stored order: + +- observation present weather: `weather_index ASC`; +- alerts: `alert_index ASC`; +- alert references: `alert_index ASC, reference_index ASC`; +- forecast periods: `period_index ASC`; +- forecast discussion key messages: `message_index ASC`; +- weather stories for a run: `story_index ASC`. + +## Columns Read + +The repository reads only these columns. + +### `observations` + +`event_id`, `station_id`, `station_name`, `observed_at`, `condition_code`, +`is_day`, `text_description`, `temperature_c`, `dewpoint_c`, +`wind_direction_degrees`, `wind_speed_kmh`, `wind_gust_kmh`, +`barometric_pressure_pa`, `visibility_meters`, `relative_humidity_percent`, +`apparent_temperature_c`, and `event_emitted_at`. + +Current conditions additionally aggregate recent `observations` values for +temperature, apparent temperature, dewpoint, humidity, wind speed, wind +direction, condition code, and latest `is_day`. + +### `observation_present_weather` + +`weather_index`, `raw_text`, and `event_id`. + +`raw_text` is decoded as JSON when present. Empty or null raw text maps to an +empty present-weather value; invalid JSON is returned as a repository error. + +### `alert_runs` + +`event_id`, `location_id`, `location_name`, `as_of`, `latitude`, `longitude`, +and `event_emitted_at`. + +### `alerts` + +`alert_index`, `alert_id`, `event`, `headline`, `severity`, `urgency`, +`certainty`, `status`, `message_type`, `category`, `response`, `description`, +`instruction`, `sent`, `effective`, `onset`, `expires`, `area_description`, +`sender_name`, and `run_event_id`. + +### `alert_references` + +`alert_index`, `reference_index`, `id`, `identifier`, `sender`, `sent`, and +`run_event_id`. + +### `forecasts` + +`event_id`, `location_id`, `location_name`, `issued_at`, `updated_at`, +`product`, `latitude`, `longitude`, `elevation_meters`, and +`event_emitted_at`. + +Only `product = 'hourly'` and `product = 'narrative'` are read by implemented +routes. + +### `forecast_periods` + +`period_index`, `start_time`, `end_time`, `name`, `is_day`, +`condition_code`, `text_description`, `temperature_c`, `temperature_c_min`, +`temperature_c_max`, `dewpoint_c`, `relative_humidity_percent`, +`wind_direction_degrees`, `wind_speed_kmh`, `wind_gust_kmh`, +`barometric_pressure_pa`, `visibility_meters`, `apparent_temperature_c`, +`cloud_cover_percent`, `probability_of_precipitation_percent`, +`precipitation_amount_mm`, `snowfall_depth_mm`, `uv_index`, and +`run_event_id`. + +### `forecast_discussions` + +`event_id`, `office_id`, `office_name`, `issued_at`, `updated_at`, `product`, +`short_term_qualifier`, `short_term_issued_at`, `short_term_text`, +`long_term_qualifier`, `long_term_issued_at`, `long_term_text`, and +`event_emitted_at`. + +### `forecast_discussion_key_messages` + +`message_index`, `message_text`, and `run_event_id`. + +### `weather_story_runs` + +`event_id`, `office_id`, `as_of`, and `event_emitted_at`. + +### `weather_stories` + +`story_index`, `office_id`, `start_time`, `end_time`, `updated_at`, `title`, +`description`, `alt_text`, `priority`, `story_order`, `download_url`, +`run_event_id`, and `as_of`. + +## Nullability and Time Assumptions + +The repository scans nullable columns with `sql.Null*` types and maps them to +nil pointers or omitted zero values depending on the canonical model field. + +Timestamps returned by the repository are normalized to UTC. Presentation +timezone conversion happens later in HTTP presenters. + +Missing latest parent rows return `nil, nil` from repository methods. HTTP +rendering exposes this as a successful response with `data: null`. + +## Compatibility Checklist + +Before changing weatherfeeder storage or upgrading the weatherfeeder module: + +- compare table names and columns with `internal/adapters/outbound/postgres`; +- preserve latest-row ordering columns used by `weatherapi`; +- preserve child index columns used for ordering; +- preserve nullable behavior expected by row mappers; +- run `go test ./internal/adapters/outbound/postgres` and affected HTTP tests. + +## Related Docs + +- [`docs/internal/postgres-repository.md`](../internal/postgres-repository.md) +- [`docs/operations.md`](../operations.md) diff --git a/docs/policy/development.md b/docs/policy/development.md new file mode 100644 index 0000000..b73aa77 --- /dev/null +++ b/docs/policy/development.md @@ -0,0 +1,159 @@ +# Development Policy + +This document gives developers and LLM coding agents the concrete workflow for +changing `weatherapi` safely. Architecture invariants are defined in +[`docs/policy/architecture.md`](architecture.md). + +## Repository Layout + +| Path | Purpose | +| --- | --- | +| `cmd/weatherapi` | executable composition root | +| `internal/app` | read use cases, repository port, current-conditions model | +| `internal/adapters/inbound/httpapi` | route definitions, query binding, handlers | +| `internal/adapters/inbound/httpapi/presenter` | response payload shaping | +| `internal/adapters/outbound/postgres` | SQL reads and row mapping | +| `templates` | text response templates | +| `docs` | current documentation, policy, internals, integrations, roadmap | +| `examples` | copyable config and request examples | + +## Build and Test Commands + +Build: + +```sh +go build ./cmd/weatherapi +``` + +Run all tests: + +```sh +go test ./... +``` + +Focused tests: + +```sh +go test ./internal/app +go test ./internal/adapters/inbound/httpapi +go test ./internal/adapters/inbound/httpapi/presenter +go test ./internal/adapters/outbound/postgres +``` + +Private module access is required for `feedapi` and `weatherfeeder` downloads. +If tests fail because Go cannot fetch a private module, fix module access before +treating package tests as behavior failures. + +## Coding Conventions + +- Keep `cmd/weatherapi` thin. +- Keep application use cases independent of HTTP, SQL, renderers, and templates. +- Keep query validation in the HTTP adapter. +- Keep unit conversion, rounding, timezone presentation, and payload copy + behavior in presenters. +- Keep SQL text, row structs, null handling, and UTC normalization in the + Postgres adapter. +- Prefer small focused changes over broad refactors. +- Preserve contextual error wrapping in repository and runtime code. + +## Dependency Policy + +Current direct dependencies are: + +- `feedapi` for config, DB registry, endpoint definitions, renderers, + middleware, templates, and HTTP runtime; +- `weatherfeeder` for canonical model and standards types; +- `github.com/lib/pq` for the Postgres driver. + +Do not add dependencies for small conveniences. New dependencies need a clear +adapter or domain purpose and should not leak through application boundaries +unless they are the explicit boundary contract. + +## Adding or Changing Endpoints + +1. Add or update the `Service` method in `internal/adapters/inbound/httpapi` if + the handler needs a new application read. +2. Add or update the application repository port in `internal/app`. +3. Implement the read in the Postgres adapter if needed. +4. Add the endpoint definition and binder in `internal/adapters/inbound/httpapi`. +5. Add presenter behavior in `presenter` instead of shaping payloads in handlers. +6. Add or update text templates when `format=text` should be supported. +7. Add endpoint tests for registration, query validation, formats, errors, + `data: null`, and representative payload behavior. +8. Update [`docs/api.md`](../api.md) and examples when the public HTTP contract changes. + +## Changing Query Parameters + +- Update binder functions and endpoint tests together. +- Preserve strict unknown-parameter rejection unless the route explicitly allows + a new parameter. +- Keep timezone parsing limited to route families that support it. +- Keep precision range validation aligned with presenter rounding support. +- Update [`docs/api.md`](../api.md) for public query behavior changes. + +## Adding Config Fields + +Config shape is loaded by feedapi. When `weatherapi` starts using a new config +field: + +1. update runtime composition or the relevant adapter; +2. update [`docs/config.md`](../config.md); +3. update examples under `examples/` if operators need to set it; +4. add config/runtime tests where practical; +5. avoid committing real credentials or private infrastructure details. + +## Adding CLI Flags + +The executable currently supports only `-config`. If adding a flag: + +1. keep parsing in `cmd/weatherapi`; +2. avoid putting business logic in `cmd`; +3. document precedence with environment variables if applicable; +4. update [`docs/cli.md`](../cli.md); +5. add tests when flag behavior is not trivial. + +## Changing Repository Reads + +- Keep SQL in `*_queries.go`. +- Keep row DTOs in `*_rows.go`. +- Keep mapping/null handling in `*_mapper.go`. +- Return `nil, nil` for missing latest parent rows. +- Normalize timestamps to UTC in mappers. +- Preserve child ordering from stored index columns. +- Update [`docs/internal/postgres-repository.md`](../internal/postgres-repository.md) + and [`docs/integrations/weatherfeeder-postgres.md`](../integrations/weatherfeeder-postgres.md) + when table or column assumptions change. + +## Changing Presenters or Templates + +- Copy input values before conversion, rounding, or timezone changes. +- Preserve nil pointer and `omitempty` behavior. +- Keep text-template helper fields out of JSON/XML when they are not public API + fields. +- Check `templates/*.txt.tmpl` for field references. +- Update presenter tests and endpoint text tests. +- Update [`docs/api.md`](../api.md) when response shape changes. + +## Updating Examples + +Examples must be copyable, valid, and free of secrets. + +- Config examples belong under `examples/config.*.yml`. +- HTTP examples belong in `examples/requests.http`. +- Do not include unimplemented routes. +- Re-run YAML syntax checks or config loading tests when config examples change. + +## Documentation Checklist + +When behavior changes, update the canonical doc in the same change: + +- README for project orientation and shortest useful command; +- `docs/api.md` for public HTTP behavior; +- `docs/config.md` for YAML config; +- `docs/cli.md` for executable flags and environment variables; +- `docs/operations.md` and `docs/troubleshooting.md` for operator behavior; +- `docs/internal/` for implementation boundaries; +- `docs/integrations/` for external storage/runtime contracts; +- `docs/roadmap/` only for unimplemented work. + +Do not describe unimplemented behavior outside `docs/roadmap/`.