We have around 150 scorers, one per practice game, each converting a finished game state into a score. They live under scoring/scorers/ and were written as self-contained units, which is the right shape: the scoring rules genuinely differ per game, and a shared base class would be a lie about how much they have in common.
What they did have in common was arithmetic. Before this cleanup the codebase contained:
- 38 local definitions of
clamp, in three mutually incompatible signatures - 10 local
meanfunctions - two
stdDevs - six verbatim copies of a min-max
normalise - four copies of Acklam's inverse normal CDF
The duplicated lines were not the problem. The problem was that a reader could not tell whether a difference between two scorers was deliberate.
clamp meant two different things
In fifteen files clamp(n) meant "clamp to 0 through 100". In twelve more it meant the same thing spelled differently. In eleven others clamp(value, min, max) meant something else entirely, because it took a range.
So a scorer calling clamp(x) and a scorer calling clamp(x, 0, 10) were calling functions with the same name and unrelated contracts, and anyone moving between files had to scroll up and re-read the local definition every single time. That is a small tax charged on every read forever.
The fix is not clever. One module, explicit names, no overloading:
export function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
export function clampPercent(value: number): number {
if (!Number.isFinite(value)) return 0;
return Math.min(100, Math.max(0, value));
}
Two functions, two names, because they are two behaviours. clampPercent guards against a non-finite input and floors it at 0; clamp deliberately does not, and lets NaN fall through both comparisons unchanged.
That guard is the entire reason the second function exists. A scorer dividing by a count that can be zero produces NaN, a NaN raw score propagates to a blank percentile in the UI, and the 27 scorers that shared this body had all independently decided to floor it rather than show a candidate nothing.
The rule that made this safe
Every function in the merged module feeds a percentile that a real person reads as their result. "Obviously equivalent" is not good enough when the output is a number someone is about to put on a job application.
So the rule written into the file is: treat these bodies as fixed unless you can show the new one agrees with the old across the reachable input domain, and record the argument.
Concretely, that meant things like keeping the nesting order:
Math.max(min, Math.min(max, value))rather than the other nesting order, because that is the exact expression the eleven pymetrics scorers used, and keeping it verbatim keeps NaN behaviour identical.
And it meant noticing where the copies disagreed on purpose. mean takes its empty-case fallback as a parameter:
export function mean(values: number[], fallback = 0): number {
if (values.length === 0) return fallback;
return values.reduce((sum, v) => sum + v, 0) / values.length;
}
because a pymetrics trait scorer averaging zero trials has no evidence and reports 0, while a TestGroup personality dimension with no answered items reports 50, the middle of its scale, so that an unanswered dimension does not read as a floor score. Two different right answers. A merge that picked one would have silently changed a published result.
The four probits
The interesting one was Acklam's rational approximation to the inverse normal CDF. We need it wherever a scale is defined in standard deviations from the mean (a sten, a T-score, a C-score) while our pipeline produces a percentile. Going from one to the other is a probit.
There were four copies. Three of them, in the TestGroup, Assessio and Thomas scorers, carried the coefficients in exponent form. The fourth, in a HireVue scorer, had the same coefficients written in plain decimal and rounded somewhere along the way to 15 significant digits.
So one of our four probits quietly disagreed with the other three in the twelfth decimal place.
Nobody was ever going to find that by reading. The way to merge it is to measure:
- The three exponent-form copies were sampled at 1,040,005 points across the clamped interval, including a dense sweep of both tail branches and of the branch boundaries at p = 0.02425 and p = 0.97575. Maximum absolute difference against the survivor: exactly 0.
- The rounded copy fed a d-prime, and its inputs are log-linear corrected rates of the form (k + 0.5) / (n + 1), which is a finite set. Enumerating it exhaustively for every n up to 400 gives a maximum difference of 2.2e-12 in z, 8.9e-11 in the 0 to 100 discrimination metric the scorer reports, and zero changes to that metric once rounded, over every (hits, false alarms, n) triple with n up to 60.
The surviving coefficients are the more precise ones, so where the two differ, the survivor is closer to the true inverse normal CDF. That is worth stating explicitly, because "we deleted the one that was wrong" is a much better changelog entry than "we deduplicated a function".
Boundaries are where merges go wrong
The copies also differed in clamping. Ours clamps the input into [1e-6, 1 - 1e-6], giving z of about -4.75 to +4.75, because a percentile of exactly 0 or 100 is reachable from a small population sample and has to read as the end of the scale rather than as infinity. Two of the old copies instead returned a sentinel of -6 or +6 outside the open interval.
That is a genuine behaviour difference, so it needed an argument rather than a shrug: neither call site can reach it. stenFromPercentile bounds its input to [0.1, 99.9] before dividing by 100, and a log-linear corrected rate would need more than half a million trials in one block to fall outside the clamp. Unreachable difference, safe merge, written down so the next person does not have to redo the analysis.
What I would keep
- If a helper's name means two things in one codebase, renaming is the fix, not documentation.
- When the output is user-visible, an equivalence claim needs evidence, and the evidence belongs next to the function rather than in a pull request nobody will find again.
- Duplicated constants do not drift dramatically. They drift in the twelfth decimal place, in one of four copies, in a file nobody has opened in a year.
The percentiles this feeds are the ones shown on the score pages behind https://cogniprep.app/tests/numerical-reasoning. If you play a numerical test twice on different days and the same raw score reports the same percentile, that is this module holding still on purpose.













