Follow the walk: watchPosition, gated so the list cannot re-sort under a thumb - #97
Merged
Merged
Conversation
…r a thumb The app asked the phone where it was exactly once, at launch, and ranked every answer for the rest of the session from that point. The product is one number, usable = window - walk - packup, and the walk was measured from a place the student leaves the moment they set off toward the room. #87 option 1. watchPosition starts once the boot fix has landed and feeds the existing useOrigin/refresh() path. Two thresholds throttle it, both in js/state.js: FOLLOW_M 40, which is 0.51 minutes at the engine's WALK_MPM of 78 and sits under the 75 m the app already calls a coarse fix, and FOLLOW_MS 15000, which is a guess and says so. The repaint is gated by followAction(), because refresh() carries a rule of its own: a list that re-sorts under a thumb loses the row somebody was reaching for. A picked origin stops the watch, a finger on the sheet holds the order, a room open redraws its walk minutes in place and leaves the ranking alone, a selected row or a scrolled list holds, and only an idle untouched list re-ranks. The dot and the line move in every case, because the render loop already reads state.origin every frame. Also: the watch stops on visibilitychange to hidden and restarts on visible; maximumAge is 0 on the watch where the boot fix keeps 60 s; the off-campus circle moved into offCampus() so it runs on every accepted position rather than only the first; enableHighAccuracy stays false on both. scripts/test/follow.test.mjs is 25 tests, one per gate, and every gate was checked by deleting it and watching its test go red. No phone was involved: the battery measurement #87 asks for has not been taken and the done-when stays open, which docs/DECISIONS.md says out loud. privacy.html said the app "does not keep watching" and has been corrected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xiX95r6nnUUmhD3KwMnWM
…hat are not true Merges main, which had moved five lanes ahead, and fixes what review found. ## The suite was not evidence followFix and followAction are pure and were mutation-checked. The lifecycle around them was not: it lived in js/app.js, which touches the DOM at import and cannot be loaded under node, so follow.test.mjs asserted regexes over the source text. Reviewed with 26 mutations and five survived a green suite: clearWatch(0) instead of clearWatch(watchId) the watch never stops drop the watchId != null guard two live watches drop watchId = null in stopWatch never restarts drop either half of the throttle every fix re-ranks `grep -rn watchId scripts/test/` returned nothing. The handle is a state machine in js/state.js now, with the geolocation object and the visibility predicate injected, so all five are unit-testable. Re-run against the new tests, every one of them fails: clearWatch(0) 1 failing no double-start guard 1 failing handle not nulled 2 failing opens a watch on a hidden page 1 failing picked origin followed anyway 2 failing It is spelled `geolocation` rather than `geo` because install.test.mjs forbids the string `geo:` anywhere in js/ -- geo: URIs are inert on iOS -- and a property named `geo` trips that guard for nothing. ## Two false sentences on the privacy page "never starts at all if you picked a building by hand" was false for a pick made mid-session: pickBuilding wrote the origin and left the watch open until the next position arrived to be judged, which with maximumAge 0, no timeout and a student who has walked indoors can be minutes away or never. It calls stopWatch() now. "stops the moment you switch away" was false if the page was hidden before the boot fix landed: follow() called startWatch() with no visibility check, after visibilitychange had already fired, so the watch ran until the user foregrounded AND backgrounded again. createWatch refuses a hidden page. Two more in the same paragraph, both older than this branch and both plainly wrong: the position IS put in a URL -- mapsHref puts the gps lat,lon into saddr/origin and hands it to Apple or Google on the Directions tap -- and there are twelve JavaScript files, not four. The page describes the handoff now, says it happens only on a tap, and says the old claim was wrong. A test holds all three to the code. ## Two more from the review repaintRoom rewrote #room's innerHTML while orientationOff still closed over the detached compass nodes and its two window listeners stayed bound, so the needle vanished every FOLLOW_M of the walk it exists for and a second tap orphaned the first pair for good. It releases the compass first. Nothing cleared state.selected on the way back from a room, so followAction returned 'hold' forever, refresh() never ran, and answer() -- the only other thing that nulls it -- never ran either. Open the app, tap a room, press back, walk: the list held boot values for the rest of the session, which is the staleness #87 exists to remove. showList clears it. ## Still not done No battery measurement. No phone was involved in any of this and no number was invented. The done-when stays open and the DECISIONS entry says what has not been observed. 731 tests, 727 pass, 0 fail. check-dead-code --strict: 84 files, no dead code. Shell gzip re-measured through the documented pipe: 98,246 bytes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xiX95r6nnUUmhD3KwMnWM
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #87.
locate()asked the phone where it was exactly once, at boot, and every answer for the rest of the session was ranked from that point. The product is one number —usable = window - walk - packup— and the walk was measured from a place the student leaves the moment they set off toward the room.This is #87 option 1:
watchPosition, throttled, feeding the existinguseOrigin→refresh()path, with the repaint gated so the list cannot re-sort under a thumb.The suite was not evidence, and that was the reason to hold this
The first version of this PR reported "17 mutations, all caught". That was true of the seventeen chosen and it was the wrong conclusion.
followFixandfollowActionare pure and genuinely mutation-checked, but the lifecycle around them was not: it lived injs/app.js, which touches the DOM at import and cannot be loaded under node, sofollow.test.mjsasserted regexes over the source text. That kills a mutation which rewrites a quoted string and nothing else.Reviewed with 26 mutations. Five survived a green suite, and
grep -rn "watchId" scripts/test/returned nothing:clearWatch(0)instead ofclearWatch(watchId)watchId != nullguardwatchId = nullinstopWatchThe handle is a state machine in
js/state.jsnow (createWatch), with the geolocation object and the visibility predicate injected, so all five are unit-testable. Re-run against the new tests, every one fails:It is spelled
geolocationrather thangeobecauseinstall.test.mjsforbids the stringgeo:anywhere injs/—geo:URIs are inert on iOS — and a property namedgeotrips that guard for nothing.Two false sentences on the privacy page
Both were introduced by the first version of this branch, and both are now true of the code:
pickBuildingwrote the origin and left the watch open until the next position arrived to be judged, which withmaximumAge: 0, no timeout and a student who has just walked indoors can be minutes away or never. It callsstopWatch()now.follow()calledstartWatch()with no visibility check, aftervisibilitychangehad already fired, so the watch ran until the user foregrounded and backgrounded again.createWatchrefuses a hidden page.Two more in the same paragraph were older than this branch and plainly wrong. The position is put in a URL:
mapsHrefputs the gpslat,lonintosaddr/originand hands it to Apple or Google on the Directions tap. And there are twelve JavaScript files, not four. The page describes the handoff now, says it happens only on a tap and never on its own, and says the old claim was wrong. A test holds all three to the code.Two more from the review
repaintRoomkilled the compass and leaked its listeners. It rewrites#room'sinnerHTML, detaching the needle, its arrow and its label, whileorientationOffstill closed over the old nodes and its twowindowlisteners stayed bound. The needle silently vanished everyFOLLOW_Mof the walk it exists for, and a second tap orphaned the first pair permanently. It releases the compass first.state.selectedon the way back from a room.followActionreturned'hold'forever, under'hold'refresh()never ran, soanswer()— the only other thing that nulls it — never ran either. Open the app, tap a room, press back, walk: the list held boot values for the rest of the session, which is exactly the staleness Your location is read once at boot and never again, so the walk times are wrong the moment you start walking #87 exists to remove.showListclears it.The gates
refresh()carries the rule this change had to not break — a list that re-sorts under a thumb loses the row somebody was reaching for.followAction()is the whole gate, pure, and the order of its tests is the rule (screen === 'room'beforeselected, because opening a room always selects its row).FOLLOW_M = 40is 0.51 minutes at the engine'sWALK_MPMof 78, under theCOARSE_Mof 75.FOLLOW_MS = 15000is a guess and the comment says so — it is the number a battery run would move.Tests
731 tests, 727 pass, 0 fail, 4 skipped.
check-dead-code --strict: 84 files, no dead code. Shell gzip re-measured through the command the header documents: 98,246 bytes.mainhad moved five lanes ahead; this merges it.showRoom'sframes.wake()from #94 is kept alongside thewireRoom()extraction.What is still NOT done
There is no battery measurement. The done-when "measured on a real phone: battery cost over a 15 minute walk with the app open" is not satisfied and stays open. No phone was involved in any part of this and no number was invented. The
DECISIONS.mdentry lists what has not been observed:FOLLOW_MSwas guessed againstrepaintRoom's scroll and focus restoration is actually invisible to a readerThis remains the riskiest change of the batch: it alters runtime behaviour that cannot be validated without a device. The difference from the first version is that the suite now covers the parts that can be tested offline, rather than appearing to.
Also not done, on purpose: #88 is not fixed here (
clearPickedOriginis still unreachable; it gained one line so the watch returns if it is ever reached).sw.jsis not re-stamped — the stamp is a HEAD sha and belongs to whatever commit merges.🤖 Generated with Claude Code
https://claude.ai/code/session_017xiX95r6nnUUmhD3KwMnWM