The last section of cogniprep.app/privacy is a process commitment, and it is admirably specific:
We may update this privacy policy from time to time. When we do, we:
- Update the version number and the "Last updated" date at the top of this page
- Show a notice in your dashboard the next time you sign in, linking to the updated policy
We do not send emails about policy changes. The in-app notice is the only notification, so please read it when it appears.
Two steps. One is automated, the other is me typing. Load the page today and the header reads:
Last updated: 3 October 2026 · Version: 1.7.0
And then there is the commit log:
$ git log --format="%h %ad %s" --date=short 530d437d..HEAD -- app/privacy/page.tsx
b1e46e14 2026-10-18 Add school domain discounts and remove school discount codes
0d366d03 2026-10-03 Refactor publisher to CogniPrep Ltd and templateize outreach emails
2f8dd42a 2026-10-01 Shared fixes from the visual audits and a site-wide JSX space sweep
08b3ef41 2026-09-20 Charge in the visitor's currency, VAT inclusive, with purchase analytics
530d437d is the last commit that touched the version constants, on 12 September. 0d366d03 is where "3 October" was typed, and it bumped no version either (it renamed the publishing company). And the 18 October commit changed what the policy says we do with your data:
- <strong>School and promotional discount codes:</strong> a record of each
- redemption against your account, so per-user and per-year limits can be enforced.
+ <strong>School discounts:</strong> we compare the domain of your email address
+ with the schools that have a discount with us, to decide whether yours applies,
+ and keep a record of each purchase it discounts.
That is not a typo fix. It is a new comparison performed on the user's email address, written into the document that tells them what we do with it, and the page above it still says 3 October and 1.7.0. The policy's own two-item list went nought for two.
Why the manual step keeps losing
The version is not decoration. It is the key:
export const CURRENT_TOS_VERSION = '1.12.0';
export const CURRENT_PRIVACY_VERSION = '1.7.0';
const needsPolicyAcceptance =
!dbUser ||
dbUser.tos_version !== CURRENT_TOS_VERSION ||
dbUser.privacy_policy_version !== CURRENT_PRIVACY_VERSION;
Change one character in either string and the next visit by every existing account renders a banner asking them to re-accept. The second bullet in the policy is implemented, correctly, and it is the expensive one. So the bar for bumping is "is this worth interrupting everybody", and a sentence about email domains sits exactly where that question is genuinely hard.
The scoreboard that comes out of it is lopsided. 40 commits have touched app/privacy/page.tsx and 42 have touched app/terms/page.tsx. Ten commits have touched the two constants. The privacy version has moved six times since it was introduced at 1.1.0, for a document whose source file is 915 lines of JSX and renders 4,204 words.
That ratio is not wrong on its own. Most of those 40 commits are a heading class, a JSX spacing sweep, a character budget on the page title. The problem is that nothing distinguishes them. Whether an edit was material is decided in the moment, by me, with no record, and the two numbers on the page are the only artefact. A reader looking at "Version: 1.7.0 · 3 October" cannot tell that the text in front of them is not the text somebody accepted under 1.7.0 in September.
Note also the ordering it creates. The automated step cannot fire unless the manual step happens first, so the one piece of this that code guarantees is downstream of the one piece a person has to remember. That is the wrong way round, and it is the shape most "we will notify you" promises end up having.
The archive problem underneath it
When an account accepts, we store a timestamp and a version string. We do not store the text. The only archive of what version 1.5.0 actually said is the git history of a .tsx file, which is a perfectly good archive right up until someone has to produce it, at which point "run git log -S against our marketing repository" is the answer to a question nobody wants to be asked.
The terms page is the same shape, one section shorter and one promise wordier:
We reserve the right to modify these Terms at any time. When we do, we bump the version number and "Last updated" date at the top of this page and show a notice in your dashboard the next time you sign in.
868 lines of JSX, 4,299 words, Version: 1.12.0, and the same Last updated: 3 October 2026 as the privacy policy, because both dates were typed in the same commit.
What I am going to do about it
Not a CMS. These pages want to live in git, where a diff is readable and a review is a pull request.
What they want is a failing test. Hash the rendered text of each policy body, store the hash beside the version constant, and assert that the two move together:
it('policy text matches the version it is pinned to', () => {
expect(hashPolicyText('privacy')).toBe(PINNED.privacy[CURRENT_PRIVACY_VERSION]);
});
Edit a word and the suite goes red with one of two cures: bump the version and pin the new hash, which re-prompts everybody on purpose, or record in the pin file that this revision is cosmetic. Either way the decision gets made deliberately and leaves a trace, instead of being made by whoever forgets.
The honest caveat is where that test runs. This repository has no CI: the one GitHub Actions workflow is a nightly database backup, and the suite runs on my machine and in the pre-deploy build. So the guard is a local one, which is strictly better than a promise in a bullet list and strictly worse than a required check.
See it
Both headers are one request away:
curl -s https://cogniprep.app/privacy | grep -o 'Last updated:[^<]*'
curl -s https://cogniprep.app/terms | grep -o 'Version: [0-9.]*'
You get Last updated: 3 October 2026 · Version: 1.7.0 and Version: 1.12.0. Then read the "Changes to This Policy" section at the bottom of cogniprep.app/privacy and the school discount bullet in the data section above it: the bullet is the 18 October text, and the date above it is the 3 October date. The terms carry the same sentence in paragraph form.
The product these two documents govern is at cogniprep.app, where practice for 56 assessment providers is the easy part and keeping two numbers honest is apparently the hard one.
Top comments (0)