THE LAB · DATA MODEL
Everything this product keeps about you.
Not a summary of it — all of it. There is no database, no account, and no server-side record of any person: the entire model is a handful of keys in your own browser. Every key, cap and version below is read from the code that owns it rather than described here, so this page cannot quietly fall behind what the product actually does.
IT STORES ANSWERS, NEVER RESULTS
Nothing here holds a score. What is written is the raw answers you gave, in exactly the form a share link carries, and every number is recomputed from them on read. That is why a stored session and a shared link can never disagree — they are the same bytes through the same function — and why editing this storage by hand can only change which answers you claim to have given, not what they are worth.
Kept in this browser
Survives closing the tab and closing the browser. Lost if you clear site data, and invisible to any other device — there is no account tying them together. Everything here shares the gym. namespace, which is what lets forgetting this browser be one sweep rather than a list of keys somebody has to remember to update.
A finished session
src/lib/result-store.tsgym.result.<instrument>
The raw answers from one completed sitting — the only thing this product records about a person. Threshold sessions are kept per ladder, so measuring pitch and then compression keeps both.
- payload
- the answers themselves, in exactly the form a share link carries
- poolVersion
- which version of the item pool those answers were given against
- savedAt
- when the sitting finished
- v
- the envelope format, currently 2
No result is stored — only answers. Every number is recomputed on read, so a stored session and a shared link cannot disagree, and editing the store by hand can only change which answers you claim to have given. Answers recorded against an older pool are dropped rather than scored against a pool that has since been reordered.
The history behind an arc
src/lib/result-store.tsgym.result.threshold.<ladder>
Sittings in time order, so a later session can be compared against the same person's earlier ones. This is what makes the retest arc possible.
- sessions[]
- up to 24 sittings, oldest evicted first
- (read order)
- an arc pools at most 2 sittings a side, weighted toward the newest
It cannot be ranked. There is no way to ask this store for a best, a maximum or a personal record — a history invites 'your best result', which is selection on the answer and the exact bias the Prestige Test exists to measure. Beyond the cap the oldest sittings are dropped, and nothing on screen says they existed.
The retest gate
src/lib/retest-cooldown.tsgym.result.threshold.<ladder>
Whether 7 days have passed since this family was last measured. A retest taken too soon measures memory rather than hearing, so the gate is about validity, not about withholding anything.
- (nothing of its own)
- the gate owns no key — it asks the session history above when this family was last measured, so finishing a session cannot write two records that disagree
- gym.lastCompleted.<family>
- retired. Read only when the session store has nothing, which means a browser that finished a session before the store existed. Nothing writes it, so it drains on its own
Device-local, and anyone who wants to can clear it. That is fine: it catches the person who forgot, not the person who insists, and the screen explains before any retest why an early number is worse.
Kept for one tab
Gone the moment the tab closes. None of it is a profile, and none of it is joined to a session record.
Which variant this tab is seeing
src/lib/experiment.tsvc_arm · vc_voice · vc_pb · vc_attr
A coin flip held still for the length of one visit, plus where the visit came from, so a reload cannot re-randomise a person and double-count them.
- vc_arm
- which onboarding variant this tab was assigned
- vc_voice
- which voice variant the reading is written in
- vc_pb
- what the visitor said they believed before measuring
- vc_attr
- the referral parameters the visit arrived with
One tab only, and gone when it closes. Nothing here is a profile and none of it is joined to a session record.
Your argument with a reading
src/content/reading/state.tsreading_choices
Which lines of a reading you rejected and which offered reading you chose, per illustrative listener, so the prompt stays as you left it if you follow the link to a hearing test and come back.
- <listener>.rejected
- the lines you rejected; they drop out of the prompt
- <listener>.chosen
- per line, the reading you picked: a, b, or neither
One tab only, and gone when it closes. It records choices about a fictional listener's plays, never your own listening, and nothing here leaves the browser.
What gets sent away
25 EVENTS · 6 SURFACES
Usage events — the one thing here that does leave your browser. Most carry only the fact that something happened. 2 of them carry your actual answers too, and those are marked ANSWERS below. That is deliberate rather than accidental: a set of real responses is the thing this project is trying to build, and it does not have one yet. Every event the code emits is listed here, because the page is built from the same registry the code is held to — an event that fires without appearing here breaks the build.
Site-wide
2Fires anywhere, including inside the gym.
- landing_view
- the homepage mounts
- client_error
- a route error boundary catches — site-wide, including the gym
The Prestige Test
11The flagship instrument, and the only path whose every step already emits — which is why the funnel specification is written against it.
- bias_frame_view
- the Hume frame is shown on /bias
- bias_start
- the blind pass begins
- bias_session_restored
- an interrupted session is resumed from its saved ratings
- bias_blind_complete
- the final blind rating is given and the pass ends
- bias_labeled_complete
- the final labelled rating is given; the verdict is computed
- bias_result
- ANSWERS the verdict is computed — the interim D6 record, carrying the raw ratings
- bias_debrief_view
- the mandatory debrief is shown, disclosing the swapped labels
- bias_share
- the share control is used on the reveal
- bias_result_view
- /bias/result renders — the taker's own return or someone else's link
- bias_locked_tier_tap
- a locked instrument is tapped from the gym floor
- gym_machine_tap
- a reveal's onward card to another instrument is taken, carrying from and to
The Delicacy Trials
6Same-moment A/B trials, each committed by a confidence tap.
- delicacy_frame_view
- the Delicacy frame is shown
- delicacy_start
- the practice trials are begun
- delicacy_trial_complete
- one scored trial is committed by the confidence tap
- delicacy_result
- ANSWERS all trials answered; the score is computed and banked
- delicacy_share
- the share control is used on the reveal
- delicacy_result_view
- /delicacy/result renders
The Threshold Test
3The adaptive staircase, which reports a sensitivity in physical units.
- threshold_start
- a staircase session begins, carrying its family and source
- threshold_complete
- the staircase converges and reports a threshold
- threshold_share
- the share control is used on the threshold result
The Ranking Test
2The critic-ranked listening set. These two events are the only record of a sitting that leaves the device; the answers themselves stay in the browser that gave them.
- spread_start
- the Ranking Test's frame is dismissed and listening begins
- spread_complete
- all six works are rated and the two spreads are computed
Share primitives
1Fired by the share buttons themselves, whichever surface they sit on.
- share_download
- a card image is downloaded
From a tap to a statistic
A list of events says what is recorded. It does not say how something you did becomes a number you read. Each row follows one action the whole way — and every step of it is checked against the module that owns it.
You rate a clip out of ten, having been told nothing about it.
- RECORDED AS
- bias_blind_complete
- ANSWER KEPT IN
- gym.result.bias → payload.blind
- SCORED BY
- src/engine/bias.ts
You rate the same clips again, this time with a name attached.
- RECORDED AS
- bias_labeled_complete
- ANSWER KEPT IN
- gym.result.bias → payload.labeled
- SCORED BY
- src/engine/bias.ts
You choose which of two clips is the untouched one.
- RECORDED AS
- delicacy_trial_complete
- ANSWER KEPT IN
- gym.result.delicacy → payload.picks
- SCORED BY
- src/engine/delicacy.ts
…and, in the same tap, how sure you were.
- RECORDED AS
- delicacy_trial_complete
- ANSWER KEPT IN
- gym.result.delicacy → payload.picks
- SCORED BY
- src/engine/calibration.ts
…and, across all of them, how many different scores you were willing to give.
- RECORDED AS
- bias_blind_complete
- ANSWER KEPT IN
- gym.result.bias → payload.blind
- SCORED BY
- src/engine/comparison.ts
You rate six works a critic ranked, having been told nothing about them.
- RECORDED AS
- spread_complete
- ANSWER KEPT IN
- gym.result.spread → payload.ratings, payload.recognised
- SCORED BY
- src/engine/spread.ts
You say whether you could hear the flaw, forty to eighty times.
- RECORDED AS
- threshold_complete
- ANSWER KEPT IN
- gym.result.threshold.<ladder> → payload.answers, payload.seed
- SCORED BY
- src/engine/threshold-fit.ts
- ENDS UP AS
- nothing in the dictionary
This one ends outside the dictionary, and deliberately. It produces a sensitivity in physical units — cents of detune, milliseconds of smear, kbps — which is a fact about one person on one evening, not a statistic describing the instrument. The dictionary holds the second kind. On most sittings it declines to print a single number at all and reports a band instead, because a point estimate from a noisy measurement is a claim the measurement cannot support.
Back to the Lab