From cfe6748330f08adaa65b03978b765d2cfa52cc5e Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 28 Jun 2026 16:44:34 -0500 Subject: [PATCH] Add a feature roadmap and implementation plan for a consensus-based algorithm for the condition code in the current conditions endpoint --- docs/roadmap/current.md | 56 +++++++++++++++++ docs/roadmap/implementation.md | 110 +++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 docs/roadmap/current.md create mode 100644 docs/roadmap/implementation.md diff --git a/docs/roadmap/current.md b/docs/roadmap/current.md new file mode 100644 index 0000000..0178bc3 --- /dev/null +++ b/docs/roadmap/current.md @@ -0,0 +1,56 @@ +# Current Conditions Condition-Code Selection + +## Summary + +Improve `/conditions/current` so numeric conditions continue to aggregate from recent observations, but `conditionCode` is selected by source-balanced WMO family consensus instead of numeric maximum. The goal is to prevent a single bad high WMO code, such as an erroneous thunderstorm code, from dominating current conditions while still returning a useful code when providers report semantically similar conditions. + +## Target Behavior + +Current conditions continue to use the application observation window, currently `app.ObservationWindowMinutesDefault`. + +Numeric and directional fields remain aggregate values over recent `observations` rows: + +- temperature, apparent temperature, dewpoint, relative humidity, and wind speed use averages; +- wind direction uses circular averaging; +- `isDay` comes from the latest row in the window. + +`conditionCode` uses source-balanced consensus: + +1. Select the latest observation per `event_source` within the current window. +2. Each source contributes at most one WMO condition-code vote. +3. Map each voted WMO code to a semantic family. +4. Select the family with the highest source vote count. +5. If the family vote is tied, return `model.WMOUnknown`. +6. Within the winning family, select the most frequent exact WMO code. +7. If exact-code vote is tied within the winning family, select the first code by family-specific representative ranking. +8. If there are no recognized condition-code votes, return `model.WMOUnknown`. + +Family mapping and tie ranking: + +| Family | Codes / tie ranking | +| --- | --- | +| `clear_or_cloud` | `0`, `1`, `2`, `3` | +| `fog` | `45`, `48` | +| `drizzle` | `51`, `53`, `55`, `56`, `57` | +| `rain` | `61`, `63`, `65`, `80`, `81`, `82`, `66`, `67` | +| `snow` | `71`, `73`, `75`, `85`, `86`, `77` | +| `thunderstorm` | `95`, `96`, `99` | + +Examples: + +| Source votes | Result | +| --- | --- | +| `0`, `1`, `2` | `0` | +| `1`, `2`, `95` | `1` | +| `0`, `95` | `model.WMOUnknown` | +| `61`, `63`, `80` | `61` | +| `61`, `95`, `0` | `model.WMOUnknown` | +| only `95` | `95` | + +## Policy Decisions + +- No public response schema change is required. +- No weatherfeeder change or database migration is required because `observations.event_source`, `condition_code`, `observed_at`, and `event_emitted_at` already exist. +- Provider-specific blacklists, trust weights, and source priorities are intentionally out of scope for this change. +- `WMOUnknown` is preferable to falsely choosing between tied precipitation, thunderstorm, and clear/cloud families. +- Current conditions remain a latest-window read model, not a durable derived table. diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md new file mode 100644 index 0000000..8220964 --- /dev/null +++ b/docs/roadmap/implementation.md @@ -0,0 +1,110 @@ +# Implement Source-Balanced Current Conditions Condition Codes + +## Summary + +Implement the target behavior in `docs/roadmap/current.md`: keep `/conditions/current` response shape unchanged, continue aggregating numeric observations over the current window, and replace SQL `MAX(condition_code)` with Go-based source-balanced WMO family consensus. + +This is a behavior change only. Do not add public fields, change query parameters, alter weatherfeeder tables, or add provider-specific blacklists. + +## Stage 1: Add Condition-Code Consensus Helpers + +Add package-local helpers in `internal/adapters/outbound/postgres` for current-conditions condition-code selection. + +Required data shape: + +- define a small candidate row/type containing `event_source` and `condition_code`; +- the selector accepts latest-per-source candidates and returns `model.WMOCode`. + +Required selector behavior: + +- use the family mapping and tie ranking from `docs/roadmap/current.md` exactly; +- ignore unrecognized WMO codes for family voting; +- return `model.WMOUnknown` when no recognized candidates exist; +- count each source at most once; +- return `model.WMOUnknown` on tied family votes; +- inside the winning family, choose the most frequent exact code; +- when exact codes tie inside the winning family, choose by family ranking. + +Add focused unit tests for the selector before wiring SQL changes. + +## Stage 2: Split Current-Conditions Condition-Code Querying + +Update the Postgres current-conditions read path so condition-code selection is no longer computed with `MAX(condition_code)`. + +Required SQL behavior: + +- keep the existing aggregate query for sample count, numeric averages, circular wind direction, and latest `is_day`; +- remove `MAX(condition_code)` from the aggregate query result; +- add a separate query that returns one latest condition-code candidate per `event_source` inside the same observation window; +- latest per source is ordered by `observed_at DESC, event_emitted_at DESC`; +- candidate columns should include `event_source` and `condition_code`; timestamp columns may remain SQL-only if used only for ordering. + +Required repository flow: + +1. query aggregate current conditions; +2. return `nil, nil` when the aggregate sample count maps to no data, preserving current behavior; +3. query condition-code candidates using the same observation window; +4. run the Go selector; +5. map numeric aggregate fields plus selected condition code into `app.CurrentConditions`. + +Keep row DTOs and mappers local to the Postgres adapter. Do not move SQL or row types into `internal/app`. + +## Stage 3: Preserve API And Presentation Behavior + +Keep all existing `/conditions/current` API behavior except condition-code selection. + +Required invariants: + +- `conditionCode` remains present in JSON/XML/text responses through the existing presenter path; +- `conditionText` continues to derive from selected `conditionCode` and `isDay`; +- `format`, `units`, and `precision` behavior is unchanged; +- no `tz` support is added; +- no public response fields are added or removed. + +Update existing endpoint or presenter tests only if expected condition codes need to change because of the new selector. + +## Stage 4: Update Current-Behavior Documentation + +After implementation is complete, update current-behavior docs outside roadmap: + +- `docs/api.md`: current conditions aggregate numeric fields and choose `conditionCode` by source-balanced WMO family consensus; +- `docs/internal/postgres-repository.md`: current conditions use an aggregate row plus a latest-per-source condition-code candidate query; +- `docs/integrations/weatherfeeder-postgres.md`: current conditions use `observations.event_source` for source-balanced condition-code selection. + +Do not document any unimplemented options such as provider blacklists, trust weights, or source priorities. + +## Test Plan + +Selector tests: + +- `0`, `1`, `2` returns `0` by clear/cloud ranking; +- `1`, `2`, `95` returns `1` because clear/cloud family wins; +- `0`, `95` returns `model.WMOUnknown` because families tie; +- `61`, `63`, `80` returns `61` by rain ranking; +- `61`, `95`, `0` returns `model.WMOUnknown` because three families tie; +- only `95` returns `95`; +- unrecognized-only candidates return `model.WMOUnknown`; +- duplicate candidates from the same source do not produce multiple votes if the selector receives them. + +Repository tests: + +- current conditions no longer chooses the highest numeric condition code; +- latest condition-code candidate per source uses `observed_at DESC, event_emitted_at DESC`; +- aggregate no-sample behavior still returns nil; +- numeric aggregate fields, wind direction, and `isDay` mapping remain unchanged; +- SQL/query errors from the candidate query include operation context. + +Verification commands: + +```sh +go test ./internal/adapters/outbound/postgres ./internal/app +go test ./internal/adapters/inbound/httpapi ./internal/adapters/inbound/httpapi/presenter +go test ./... +``` + +## Assumptions + +- `observations.event_source` is non-null in the weatherfeeder-owned schema and is safe to use as the source identity. +- The latest-per-source query can be implemented with existing Postgres features and does not require a migration. +- Current conditions remain repository-owned because they are a Postgres aggregate read model; no new app service method or public interface is needed. +- The implementation should remain standard-library and SQL based; do not add dependencies.