Two of our practice games are expression-recognition tasks: a face appears, a countdown runs, you pick an emotion. We shortened the per-face countdown on both. The clock reads one number from one file. The commit touched six.
That ratio is the whole post.
What the clock reads
emotion: {
totalTrials: 50,
totalTimeLimit: 250,
questionTimeLimit: 5,
emotions: ['happy', 'sad', 'angry', 'fearful', 'surprised', 'disgusted', 'neutral'],
facesPerEmotion: 2,
},
Two numbers changed there, from a 10 second cap and a 500 second total. They are not independent: totalTimeLimit is totalTrials * questionTimeLimit, and it stays that way on both games after the change (20 trials at 5 seconds for one, 50 at 5 for the other). We assert that relationship in a test for a different provider, with a comment that says why:
// The advertised per-item pace has to be the derived one, or the
// instructions would promise a budget the clock does not give.
expect(config.questionTimeLimit).toBe(
Math.round(config.totalTimeLimit / config.totalQuestions)
);
So the config half of this change is well-behaved: two numbers, one invariant between them, a test genre we already use a lot.
The other four files are sentences
Our games open with two pages of written instructions rather than an in-game practice round, so the format, the controls and the clock are all described in prose before you start. Those pages stated the old number, in digits, twice:
'The countdown runs for 9 seconds per face. If it reaches zero the trial is
recorded with no answer, so a guess always beats letting it lapse. There is
no Back button, no skipping and no review.',
That sentence has to carry the digit. The number is operational: it tells you whether you have time to deliberate, and the consequence of letting it lapse is a real rule of the sitting. Changing 9 to 5 there is the correct edit and the easy one.
The hard one was two bullets later.
A judgement about a number is not stale, it is wrong
'Answer at a natural pace. Nine seconds is far longer than most faces need,
and staring at an ambiguous one rarely improves the answer.',
Update the digit alone and you get "Five seconds is far longer than most faces need", which is advice we do not believe and which contradicts the reason we shortened the timer. The sentence is not a statement of the configuration, it is a claim about the configuration, so it had to be rewritten rather than edited:
'Answer at a natural pace. Five seconds is enough for most faces, and
staring at an ambiguous one rarely improves the answer.',
Same for the other game, where the prose had been quietly wrong the whole time:
- 'Trust the first read. On an ambiguous face your first impression beats the
- case you build over the next eight seconds.'
+ 'Trust the first read. On an ambiguous face your first impression beats the
+ case you build over the next few seconds.'
"Eight seconds", against a config that said ten. Nobody noticed because eight is a plausible number and no reader has the config open. The fix was not to write "five". It was to stop asserting a number in a sentence where the number is decoration: prose carries a number only where the number is the point.
The sourcing claim moves with the number
One more file, and this one is specific to what we build. Our instructions say which numbers are the provider's and which are ours:
'Pymetrics does not publish how many faces it shows or how long you get, so
the 20 trials and 5 seconds per face here are ours, as is the accuracy
breakdown you get at the end.',
So a timing change is also an edit to a provenance statement. We do this throughout the product, including labelling the bands on six of our tests either "candidates report" or "our estimate", and it has a maintenance cost that is easy to under-budget: every sentence that says "this number is ours" contains that number.
The alternative is interpolating it, {config.questionTimeLimit} seconds per face, which removes the drift and reads worse in every other way. Instruction prose has rhythm. "Five seconds is enough for most faces" with the five interpolated is a sentence that no longer reads like a sentence, and the guard it buys is the weakest of the three edits on this page, because this bullet and the operational bullet above it are in the same array, eight lines apart.
The card range nothing derives
The last two files are the catalogue entries that produce the game cards:
- duration: '2-3 minutes',
+ duration: '1-2 minutes',
20 trials at 5 seconds is 100 seconds of worst-case clock, and most people answer well before the cap, so the honest range is "1-2 minutes" rather than a computed one. That is a hand-written estimate of a human's experience, which is why it is a string in a content file instead of a function of the config.
It is also the number most likely to be seen: the Faces card on the Pymetrics page and the Expressions card on the Arctic Shores page are both public, both say what we just changed them to, and both are the first thing someone deciding how to spend their evening reads. A stale duration there is a small lie with real consequences for somebody planning a practice session before an assessment deadline.
The checklist I now use for a constant
Changing a timing constant in this codebase means:
- the config value, and anything derived from it that is asserted in a test
- the instruction bullets that state it operationally, in digits
- any sentence that makes a claim about it, which may need rewriting rather than editing
- the provenance line that says the number is ours
- the duration range on the public card, which is an estimate rather than a calculation
And the search that finds all of it is not grep 9. It is a grep for the digit and the word, because "9 seconds" and "Nine seconds" lived in the same file and only one of them is findable by the number. Written-out numerals are the single most reliable place for a stale constant to hide.
If you want to see what the change actually produced, both games are live on the Pymetrics and Arctic Shores provider pages, and the games index lists all 56 providers we cover with the same kind of duration estimate on every card.
Top comments (0)