M2-10 (2/2): the heat map screen #55

Merged
wk merged 1 commit from m2-10-heat-map-screen into m2-10-heat-map 2026-08-10 14:57:24 -04:00
Member

The second half of M2-10 (#39). Based on m2-10-heat-map (#54), not on main, so this diff is
the screen alone — merge #54 first and this retargets cleanly.

What this is

  • A map icon in the top-left of the memo list, opposite Settings, opening a sheet.
  • The map: an MKOverlayRenderer over an MKMapView, with the heat drawn under the map's
    labels
    , so street names stay readable through it. The ground with no colour on it is the answer
    the map exists to give, and it is only readable if the streets around it are.
  • A date filter — All, Last 7 Days, Last 30 Days, Last 12 Months — as a stock menu, with the
    current period on screen rather than behind it.
  • The states with no map in them: route recording off (the explainer plus an Enable button), on
    with nothing recorded yet, a period that selects nothing, and a library that could not be read.
  • The counts of what could not be drawn, under every state rather than only under a map.
  • The centring fix: one location, only while the screen is in front, never written anywhere — and
    the documented exception to M2-06's rule that it creates.

Two things running it found that reading it did not

The overlay drew nothing at all. The screen framed correctly, reported the right number of
cells, and rendered an empty map. boundingRect was handing MapKit a rectangle whose origin sat
past the eastern edge of the world, which intersects no tile — so the renderer was never called.
The cause is a floating-point tie: for a library entirely in one hemisphere the plain span and the
one measured the other way round the world are the same distance, and (a + world) - (b + world)
is not bit-for-bit a - b at these magnitudes. Losing that tie put every x a world east. Fixed at
the comparison, with the origin wrapped back inside the world as a second line of defence.

Dark mode swallowed the faint end of the ramp — which is a street walked once, the evidence the
screen exists to show. The alpha ramp is now chosen from the colour scheme.

The disclosure problem, and what it cost

Both location purpose strings and the Settings explainer said Stash uses location while a memo is
recording and at no other time. Opening this screen asks the phone where it is, so that stopped
being true. Not writing the fix to disk does not make it not a use.

The strings are corrected — but iOS prompts for location exactly once, so a phone that already
granted it for route recording would never be shown the new sentence, and would have its
neighbourhood sent to Apple Maps under consent obtained for something else. So the screen says it
itself, once, before it takes any fix: the same explainer with a Continue button, and
HeatMapNotice recording that it has been shown. What is stored is that a sentence was shown
not consent, and not a history of what has been turned on and off.

What was validated

  • make test — 692 tests, green. make lint — clean.
  • Exercised on the simulator against a synthetic library of sixteen routes: a loop walked twelve
    times reads clearly hotter than streets walked once or twice; street names stay readable through
    the heat in both light and dark; the empty state and the first-open notice both render; the map
    opens centred on the simulator's location.
  • Codex code review: two high, four medium, one low — all accepted and fixed.
  • Codex test review: two high, four medium, two low — all accepted. One is worth naming here: my
    regression test for the framing bug passed against the unfixed code. Chasing why is what
    found the floating-point tie, which is the real defect. The tie is a rounding difference rather
    than a shape, so what the tests pin now is the invariant across four library shapes.
  • Codex security review: the disclosure problem above, accepted and fixed.

What was NOT validated

  • Nothing on a device. The numbered device checks are in .agent/m2-10-heat-map/PLAN.md.
  • The renderer's drawing has no test. draw(_:zoomScale:in:) needs a live MKMapView; every
    rule it consults is tested — cell selection, tile alignment across the date line, the ramp.
  • The gesture rule has no test. regionWillChangeAnimated needs real gestures. If it fails,
    the camera re-centres after a pan — that is device check 9.
  • The read time on a real library in a release build. The number in #54 is a debug simulator
    build; device check 10.

Full reasoning and every disposition: .agent/m2-10-heat-map/PLAN.md and
.agent/m2-10-heat-map-screen/SUMMARY.md.

⚠️ No screenshots of this feature anywhere — this repository is public and a heat map of daily walks
is a home address with extra steps. Every fixture is synthetic coordinates.

The second half of M2-10 (#39). **Based on `m2-10-heat-map` (#54), not on `main`**, so this diff is the screen alone — merge #54 first and this retargets cleanly. ## What this is - A map icon in the top-left of the memo list, opposite Settings, opening a sheet. - The map: an `MKOverlayRenderer` over an `MKMapView`, with the heat drawn **under the map's labels**, so street names stay readable through it. The ground with no colour on it is the answer the map exists to give, and it is only readable if the streets around it are. - A date filter — All, Last 7 Days, Last 30 Days, Last 12 Months — as a stock menu, with the current period on screen rather than behind it. - The states with no map in them: route recording off (the explainer plus an Enable button), on with nothing recorded yet, a period that selects nothing, and a library that could not be read. - The counts of what could not be drawn, under every state rather than only under a map. - The centring fix: one location, only while the screen is in front, never written anywhere — and the documented exception to M2-06's rule that it creates. ## Two things running it found that reading it did not **The overlay drew nothing at all.** The screen framed correctly, reported the right number of cells, and rendered an empty map. `boundingRect` was handing MapKit a rectangle whose origin sat past the eastern edge of the world, which intersects no tile — so the renderer was never called. The cause is a floating-point tie: for a library entirely in one hemisphere the plain span and the one measured the other way round the world are the same distance, and `(a + world) - (b + world)` is not bit-for-bit `a - b` at these magnitudes. Losing that tie put every x a world east. Fixed at the comparison, with the origin wrapped back inside the world as a second line of defence. **Dark mode swallowed the faint end of the ramp** — which is a street walked once, the evidence the screen exists to show. The alpha ramp is now chosen from the colour scheme. ## The disclosure problem, and what it cost Both location purpose strings and the Settings explainer said Stash uses location while a memo is recording *and at no other time*. Opening this screen asks the phone where it is, so that stopped being true. Not writing the fix to disk does not make it not a use. The strings are corrected — but **iOS prompts for location exactly once**, so a phone that already granted it for route recording would never be shown the new sentence, and would have its neighbourhood sent to Apple Maps under consent obtained for something else. So the screen says it itself, once, before it takes any fix: the same explainer with a Continue button, and `HeatMapNotice` recording that it has been shown. What is stored is that a sentence was *shown* — not consent, and not a history of what has been turned on and off. ## What was validated - `make test` — 692 tests, green. `make lint` — clean. - Exercised on the simulator against a synthetic library of sixteen routes: a loop walked twelve times reads clearly hotter than streets walked once or twice; street names stay readable through the heat in both light and dark; the empty state and the first-open notice both render; the map opens centred on the simulator's location. - Codex code review: two high, four medium, one low — all accepted and fixed. - Codex test review: two high, four medium, two low — all accepted. One is worth naming here: my regression test for the framing bug **passed against the unfixed code**. Chasing why is what found the floating-point tie, which is the real defect. The tie is a rounding difference rather than a shape, so what the tests pin now is the invariant across four library shapes. - Codex security review: the disclosure problem above, accepted and fixed. ## What was NOT validated - **Nothing on a device.** The numbered device checks are in `.agent/m2-10-heat-map/PLAN.md`. - **The renderer's drawing has no test.** `draw(_:zoomScale:in:)` needs a live `MKMapView`; every rule it consults is tested — cell selection, tile alignment across the date line, the ramp. - **The gesture rule has no test.** `regionWillChangeAnimated` needs real gestures. If it fails, the camera re-centres after a pan — that is device check 9. - **The read time on a real library in a release build.** The number in #54 is a debug simulator build; device check 10. Full reasoning and every disposition: `.agent/m2-10-heat-map/PLAN.md` and `.agent/m2-10-heat-map-screen/SUMMARY.md`. ⚠️ No screenshots of this feature anywhere — this repository is public and a heat map of daily walks is a home address with extra steps. Every fixture is synthetic coordinates.
The second half: the map icon, the screen, the renderer, the date
filter, the empty states, and the centring fix with the exception to
M2-06's rule that it creates.

The heat draws under the map's labels, so the streets with no colour
on them stay readable — that being the question the map is asked. The
alpha ramp is chosen from the colour scheme, because the faint end is
a street walked once and it does not survive light-mode alpha over a
dark map.

Two things running it found that reading it did not: the overlay drew
nothing at all, because a floating-point tie in the framing put its
origin a world east of where MapKit looks for it; and dark mode
swallowed the faint end of the ramp.

Both location purpose strings and the Settings explainer said Stash
uses location only while recording, which this makes untrue. They are
corrected here, and the screen shows the disclosure itself once —
iOS prompts for location exactly once, so a phone that already granted
it would never be shown the new sentence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wk merged commit 6c3affbe52 into m2-10-heat-map 2026-08-10 14:57:24 -04:00
wk deleted branch m2-10-heat-map-screen 2026-08-10 14:57:24 -04:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Stash/stash-ios!55
No description provided.