Weather enrichment with a bounded sweeper #14

Closed
opened 2026-08-26 09:14:53 +00:00 by GKaszewski · 2 comments
Owner

What to build

Weather is the conditions at the place and instant a MoodEntry was logged, resolved server-side from its Location and expressed in one canonical vocabulary so any two entries' weather are comparable. It is always attributed to a Provider, never to the User — weather is observed, not stated.

Resolving server-side rather than accepting client-supplied weather means one implementation and one vocabulary. Three clients each implementing lookup would drift in vocabulary and units, and correlation across inconsistent labels is meaningless.

Timing: the lookup happens after the entry is written, never during it. Logging a mood is the app's most latency-sensitive action and must not wait on a third party. A failed or slow lookup leaves the entry saved with no weather.

The sweeper finds entries that have a Location but no Weather and fills them in — covering server restarts, upstream outages, and offline clients with one mechanism instead of three. It satisfies ADR 0004's rule: the work is rediscoverable from state, so losing a queued job costs only latency.

The realistic backlog is small. The Daylio importer carries no location data, so imported history never triggers lookups. Bound the sweep from configuration anyway — as a safety limit rather than a throughput design. Where the upstream accepts a date range for one location, group a backlog by rounded coordinates so it collapses into few requests rather than one per entry.

Outbound lookups must be disableable in config for self-hosters who do not want coordinates leaving the box.

Acceptance criteria

  • Weather resolves from Location after the entry is saved, never during the request
  • Saving an entry succeeds unchanged when the provider is slow, failing, or disabled
  • One canonical condition vocabulary is stored, not provider-specific strings
  • A sweeper finds entries with Location and no Weather and enriches them
  • The sweep is rate-bounded and batch-bounded from configuration
  • Backlogged lookups are grouped by location and date range rather than issued one per entry
  • A config flag disables outbound weather lookups entirely
  • Enrichment failures are visible with attempt count and last error, not silent

Blocked by

## What to build `Weather` is the conditions at the place and instant a `MoodEntry` was logged, resolved server-side from its `Location` and expressed in one canonical vocabulary so any two entries' weather are comparable. It is always attributed to a `Provider`, never to the User — weather is observed, not stated. Resolving server-side rather than accepting client-supplied weather means one implementation and one vocabulary. Three clients each implementing lookup would drift in vocabulary and units, and correlation across inconsistent labels is meaningless. **Timing:** the lookup happens after the entry is written, never during it. Logging a mood is the app's most latency-sensitive action and must not wait on a third party. A failed or slow lookup leaves the entry saved with no weather. **The sweeper** finds entries that have a `Location` but no `Weather` and fills them in — covering server restarts, upstream outages, and offline clients with one mechanism instead of three. It satisfies ADR 0004's rule: the work is rediscoverable from state, so losing a queued job costs only latency. The realistic backlog is small. The Daylio importer carries no location data, so imported history never triggers lookups. Bound the sweep from configuration anyway — as a safety limit rather than a throughput design. Where the upstream accepts a date range for one location, group a backlog by rounded coordinates so it collapses into few requests rather than one per entry. Outbound lookups must be disableable in config for self-hosters who do not want coordinates leaving the box. ## Acceptance criteria - [ ] Weather resolves from `Location` after the entry is saved, never during the request - [ ] Saving an entry succeeds unchanged when the provider is slow, failing, or disabled - [ ] One canonical condition vocabulary is stored, not provider-specific strings - [ ] A sweeper finds entries with `Location` and no `Weather` and enriches them - [ ] The sweep is rate-bounded and batch-bounded from configuration - [ ] Backlogged lookups are grouped by location and date range rather than issued one per entry - [ ] A config flag disables outbound weather lookups entirely - [ ] Enrichment failures are visible with attempt count and last error, not silent ## Blocked by - #4 - #9
GKaszewski added the ready-for-agent label 2026-08-26 09:14:53 +00:00
Author
Owner

Left open deliberately: seven of the eight criteria are met, one is not.

Met — weather resolves after the save via an ObserveWeather job, saving succeeds when the provider is slow/failing/disabled, one canonical condition vocabulary is stored, sweep_weather_backlog finds entries with a Location and no Weather, the sweep is bounded by worker.enqueued_per_sweep and worker.sweep_seconds, worker.look_up_weather = false disables outbound lookups by not constructing the lookup at all, and failures stay visible with attempt count and last error.

Not met — "Backlogged lookups are grouped by location and date range rather than issued one per entry". Each job still resolves one entry with one request. ADR 0012's "What is not built" section records the reasoning: a batch job would have to stand for many entries, which breaks the one-job-one-subject rule the queue and its sweep are built on, and the realistic backlog is small because imported history carries no coordinates.

So this is either a wontfix to close against ADR 0012, or a real remaining task — a judgement call rather than something to tick off.

Left open deliberately: seven of the eight criteria are met, one is not. Met — weather resolves after the save via an `ObserveWeather` job, saving succeeds when the provider is slow/failing/disabled, one canonical condition vocabulary is stored, `sweep_weather_backlog` finds entries with a `Location` and no `Weather`, the sweep is bounded by `worker.enqueued_per_sweep` and `worker.sweep_seconds`, `worker.look_up_weather = false` disables outbound lookups by not constructing the lookup at all, and failures stay visible with attempt count and last error. Not met — "Backlogged lookups are grouped by location and date range rather than issued one per entry". Each job still resolves one entry with one request. ADR 0012's "What is not built" section records the reasoning: a batch job would have to stand for many entries, which breaks the one-job-one-subject rule the queue and its sweep are built on, and the realistic backlog is small because imported history carries no coordinates. So this is either a wontfix to close against ADR 0012, or a real remaining task — a judgement call rather than something to tick off.
Author
Owner

Closing as wontfix.

Everything in this issue is built except the coordinate/date-range grouping of backlogged lookups, and that one is declined rather than deferred. ADR 0012 carries the reasoning: grouping needs a job that stands for many entries, which breaks the one-job-one-subject rule the queue and its sweep are built on — and the sweep's rediscoverability guarantee (ADR 0004) depends on that rule. Paying that structural cost buys request-count savings on a backlog that does not realistically occur: the Daylio importer carries no coordinates, so imported history never enqueues weather work at all.

worker.enqueued_per_sweep remains the bound if that assumption ever stops holding. If it does, this is a new issue against the queue's shape, not a leftover checkbox on this one.

Closing as wontfix. Everything in this issue is built except the coordinate/date-range grouping of backlogged lookups, and that one is declined rather than deferred. ADR 0012 carries the reasoning: grouping needs a job that stands for many entries, which breaks the one-job-one-subject rule the queue and its sweep are built on — and the sweep's rediscoverability guarantee (ADR 0004) depends on that rule. Paying that structural cost buys request-count savings on a backlog that does not realistically occur: the Daylio importer carries no coordinates, so imported history never enqueues weather work at all. `worker.enqueued_per_sweep` remains the bound if that assumption ever stops holding. If it does, this is a new issue against the queue's shape, not a leftover checkbox on this one.
GKaszewski added wontfix and removed ready-for-agent labels 2026-08-26 19:09:01 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: GKaszewski/k-mood#14