Build1 publisher3 min readPublished
An eight-attempt history has to climb 3.5 points before CogniPrep draws an arrow
CogniPrep gates its trend arrow on a slope threshold of half a point per session plus a confidence score weighted toward sample size. Both constants are product decisions, and the published formula finishes earlier than its own comment says.
The Engineer · Build desk

What happened
- CogniPrep's trend module labels a score history improving or declining only when the regression slope passes 0.5 points per session in either direction, and returns stable anywhere inside that band.
- Its confidence score adds two deliberately unequal terms, a logarithmic one driven by how many sessions exist and a smaller one driven by the quality of the fit, and returns zero below two data points.
- The improvement verdict skips the regression entirely and compares the last 30 percent of sessions, floored at one and capped at ten, against the earlier ones.
- The engine and the HTTP route each fetched the same 50 rows of session history, and because the route awaited the engine before fetching its own copy, every request ran the identical query twice in sequence.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- decision A regression slope is never exactly zero, so a product that renders its sign has already decided to tell every user a story on every load. The width of the dead band is the decision, and an engineer has to pick the number.
- constraint At the eight or twelve attempts a practice product actually has, the defensible output is usually that nothing can be told yet. So the arrow has to be allowed to sit neutral for weeks. Nobody should read that as a bug.
- contradiction The snippet's comment promises 0.7 confidence at 30 sessions and 0.8 at 50. The formula reaches its 0.8 ceiling at 23. Help text written from the comment describes a curve the code does not have.
- exposure A user with eight flat scores and no usable fit still scores about 0.56 from sample size alone. Any UI that renders that figure as a percentage bar shows more than half full on a history the model cannot call.
The 0.5 constant is the whole gate, and what it means depends on how long the history is. Eight attempts is seven intervals, so the fitted line has to rise more than 3.5 points from first score to last before the module returns "improving" [1]. The dev.to write-up calls half a point per session a product decision and not a statistical one, roughly whether the person would notice the difference after ten more attempts [4]. Ten attempts at half a point is five points [2].
The confidence number is where the published snippet argues with itself. The comment above it says the volume term "Reaches 0.7 at 30 points, 0.8 at 50 points" [6]. Underneath, the code divides the log of the count by the log of 51 and clamps the result at 0.8 [5]. log(24)/log(51) is 0.808, so the clamp binds from 23 data points onward, and at 30 points the unclamped value is 0.873 [4]. Going from 40 sessions to 50 barely moves confidence, the post observes [15], and in the published formula those two histories get an identical volume term [4].
On the trend side, two low-data branches are hardcoded: zero sessions gets confidence 0, one session gets 0.5 [13]. Below two points the confidence function itself returns 0, and at exactly two it returns log(3)/log(51), which is 0.279 [5][5]. Two points always fit a line perfectly, so R-squared is 1 and the fit term pays its full 0.2, for 0.479 [5]. The second attempt therefore reports less confidence than the first. Those numbers come from different branches, so nothing in the code compares them.
Welch's t-test is the right call for the reason the post gives: someone who genuinely improved usually got more consistent at the same time, so the two samples have different variances and Student's assumption fails [9]. The recent window stops growing once a user passes 33 sessions, since 30 percent of 34 rounds up to 11 and the cap is 10 [7]. Significance is not the only gate; the verdict also requires a positive percentage change [10]. "Checking significance alone is one of the easiest ways to ship a cheerful message about someone getting worse," the post says [11].
Transferring 0.5 to another product depends on one measured quantity: session-to-session noise in your own metric, in your own units. If scores bounce by two points for reasons unrelated to learning, that threshold will label noise as progress on most histories. If the scale is coarse enough that five points is invisible to the user, the same constant hides a real trend. You need a measured noise figure before 0.5 means anything outside this codebase.
The part worth copying regardless of the constants is the return contract. Every branch that cannot make a claim returns the same shape as a branch that can, with the comparison period set to "insufficient data", no nulls and nothing thrown [12]. The post's stated reason is that caller-written guards are how one page ends up saying "not enough data yet" while another says "0% improvement" about the same user [12].
What to watch
- Whether the confidence comment is corrected to match the log(51) denominator, or the denominator changed to match the comment's 30 and 50 sessions.