Complete protocol-resolution and History launch safeguards - #5
Merged
Conversation
…ecord and the export The resolver seam disclosed an assumed movement on the capture screen and nothing carried it any further. A bare /camera or /upload arrival - which the nav bar and both landing CTAs produce - analyzed as a squat, and from that navigation onward the report, the stored session, the history list and the exported evidence artifact all named "Bodyweight squat" with nothing anywhere recording that no one had said so. SessionResult now carries an optional ProtocolResolution, reconciled against the protocol the analysis actually ran under so a record cannot claim authority the analysis did not have. Absence reads as unknown, never as a selection: legacy records predate the field and must not be upgraded into consent for free. Surfaced where the claim is made rather than where it originated - beside the report title, on the history row, in the report JSON, and as a banner plus a "Movement decided by" row in the exported HTML. Report schema goes to v2: the addition is additive, but a v1 reader cannot tell "chosen" from "this build could not record either way", which is the whole distinction. Also fixes a related overclaim: historyObservation compared two sessions by protocolId, so an assumed squat and a chosen squat were presented as the same movement over time. It now discloses instead of dropping the comparison - silencing history for everyone who never opened the picker would cost more. Verified: tsc clean, 811 unit tests, 19/20 chromium e2e (1 skipped), and the full flow driven in-browser through capture, reload, history and both exports.
The disclosure shipped in the previous commit told an athlete their recording
may not have been the movement it was measured against, and then offered no
way to act on it. The only remedy was to record the set again - but the
recording is the expensive part and it is not the part that was wrong.
The stored pose tape is the substrate, so a finished session can be re-read
under any protocol that has a runtime. The resulting resolution is
'user-corrected', a source kept distinct from 'user-selected' on purpose: both
carry the athlete's authority, but a correction is applied to footage framed
by the setup guidance of a different movement, and the report says so.
The correction runs the pipeline the app WOULD have run had the movement been
chosen at capture time. A naive cold replay does not: it feeds the calibration
preroll to a state machine that never saw it and starts that machine from
standing when the live one was seeded mid-descent. Found in the browser - a
19-rep set round-tripped through the lunge and came back reporting 0 reps.
Re-analysis now reproduces the recorded filtering, skips the preroll, and
seeds the entry state, and the same round trip returns 19 reps exactly.
Whole-session runtimes still get raw frames, as the upload path feeds them.
Corrections rewrite the record under its own id and original timestamp: a new
id would orphan the video and pose tape, which are keyed by session id, and
leave two reports of one recording in History.
History fixes found while verifying this, all of the same family - the screen
was written when squat was the only protocol:
- it printed the raw protocol id, which read acceptably as "squat" and
became "ForwardLungeStrideReturn" the moment a second protocol shipped
- it counted a transition movement in "reps"; the repo already has
attemptVocabulary for exactly this, and this screen was never migrated
- the longitudinal copy leaked the raw id too
- rows now mark a corrected movement, as they already marked an assumed one
Both lookups tolerate an unregistered protocol, because History is where a
record gets deleted and stranding one there is the worst place to fail.
Verified: tsc clean, 837 unit tests, 20/21 chromium e2e (1 skipped), and the
full correction round trip driven in-browser including History and the report.
The report footer has always told the athlete "delete it any time from
History", and History could only delete everything. Someone who wanted one
recording gone had to choose between keeping it and losing every other session
they had - a promise the shipped copy made and the screen could not keep.
Reports and capture media live in separate databases, so the per-row delete
removes from both. Deleting only the report would leave the actual footage on
the device: the more sensitive half, and the opposite of what the control
promises. Proven in-browser against both IndexedDB stores - the deleted row's
pose tape is gone and the other session's survives.
The control is a sibling of the row link rather than nested inside it: an
interactive element inside an anchor is invalid, and a keyboard user would
otherwise land on a destructive button while trying to open a report. Its
accessible name says which session, because a screen-reader user moving
through the list hears the button out of context.
Two style fixes found while verifying:
- the row carried text-transform: capitalize to dress up the raw protocol id
it used to print, and that rewrote the authored label "Bodyweight squat"
into "Bodyweight Squat"
- --color-danger is now a real token instead of a literal hiding behind an
undefined var() fallback, and the history rows point at --color-text-muted
rather than the undefined --text-muted they were silently falling back
from
Also fixes two races in the e2e specs added earlier in this branch: they read
or reloaded the /results hand-off URL before it had been replaced by the
record's own address, which passed in isolation and failed in a full run.
Verified: tsc clean, 837 unit tests, 21/22 chromium e2e (1 skipped) twice in a
row, and the delete flow driven in-browser at desktop and 375px with no
overlap or horizontal overflow.
…never recorded /results/<id> for a session this device does not have rendered the same "No session yet - record a set on the camera page" card as the bare hand-off address. Those are different situations and only one of them is "you haven't recorded anything yet". A specific report that isn't here was deleted, or was recorded in another browser - sessions are local-only and never sync, so a link that works on one device legitimately fails on another. The per-row delete shipped in the previous commit makes this a realistic path rather than a corner case: a bookmark, the back button, or a History link followed after the record is gone all land here. The hook now distinguishes `missing` (nothing was asked for) from `not-found` (an id was asked for and this device cannot produce it), including the read that fails outright - that is still a specific session the athlete asked for and did not get. Verified: tsc clean, 837 unit tests, 22/23 chromium e2e (1 skipped), both states rendered in-browser, and an e2e that records, deletes, and returns to the dead link.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
What changed
Why
Several post-capture surfaces could present an assumed movement as chosen, lose replay parity during correction, strand stored media, or publish recommendations from a capture explicitly classified as questionable. History also leaked squat-specific vocabulary into transition protocols.
Impact
Reports now retain an honest account of how the movement was selected, corrections remain reproducible, privacy controls remove all session artifacts, questionable captures stay observation-only, and mixed-protocol History reads consistently.
Validation
npm test: 120 test files, 838 tests passed.npm run build: TypeScript and Vite production build passed.Known CI evidence debt remains unchanged; no evidence was rebound to make CI green.