CogniPrep publishes a page per employer telling a candidate what they are about to sit: the provider, the format, the traps. That is a promise about someone's job application, and for a while we were not keeping it.
Three files, three answers
The associations lived in three places that had drifted apart. Our HSBC guide implied Cubiks and SHL. The /employers/hsbc page itself was written about Arctic Shores. A third constant cited a prep site for Cubiks. Nothing reconciled them, so a guide could point a candidate at a provider it had no evidence for at all.
The worse part was in the old file's own comments. Several entries were described as "sector-appropriate selections". That is an honest label for a dishonest thing. Somebody had reasoned that a big bank probably uses SHL, written it down, and it had become a fact by being in a TypeScript file for six months.
An audit in August 2026 found that of 38 employers, only 15 had any sourced association. Four US guides claimed their employer appeared in Wonderlic's published customer logos, and Wonderlic's live customer page does not list them.
The rule, then the type
The rule is one line: every association carries a URL that somebody actually opened. No URL, no entry.
A rule in a doc is a suggestion. The way to make it stick is to put it in the shape of the data, so the only way to add an association is to type the evidence next to it:
export interface ProviderEvidence {
provider: AssessmentProvider;
/** A URL that was actually opened and checked, not a plausible-looking one. */
url: string;
type: EvidenceType;
/** Publication or retrieval date, ISO-ish. 'undated' when the source has none. */
date: string;
confidence: Confidence;
/** What the source actually says. Shown to editors, not rendered. */
note: string;
}
url is not optional. There is no inferred: true escape hatch, and adding one would be the whole bug coming back.
Five kinds of evidence, ranked
EvidenceType is not decoration. It is the ladder that decides how loudly a page is allowed to speak:
export type EvidenceType =
/** The provider's own case study, customer page or press release. */
| 'first-party-provider'
/** The employer's own careers page, job posting or transparency record. */
| 'employer-official'
/** Multiple independent, recent candidate reports agreeing. */
| 'candidate-reports'
/** Reporting by a publication with an editorial standard, naming both. */
| 'press'
/** An assessment-preparation site's claim. Weakest; never sufficient alone. */
| 'prep-site';
press was the interesting one to place. It sits above candidate reports because a named masthead is accountable for what it prints, and below first-party sourcing because it is still second-hand and usually undated beyond the article. Most taxonomies like this collapse under a case that does not fit; writing down why each rung sits where it does is what stops the next person filing everything as "high".
confidence then carries the weight in code:
-
high: first-party or employer-official, dated 2024 or later. Safe to state plainly. -
medium: first-party but undated, or several agreeing recent candidate reports. State as reported, not as confirmed. -
low: a single prep-site or candidate claim. Background only, never the subject of a page.
The floor is enforced in the accessor, not in the templates
The temptation is to let each page decide how to render evidence. That gets you 108 pages with 108 opinions. Instead there is one accessor, and it refuses to hand out weak associations:
const CONFIDENCE_RANK: Record<Confidence, number> = { high: 3, medium: 2, low: 1 };
export function providersForEmployer(
slug: string,
minConfidence: Confidence = 'medium'
): AssessmentProvider[] {
const profile = BY_SLUG[slug];
if (!profile) return [];
const floor = CONFIDENCE_RANK[minConfidence];
const ranked = profile.associations
.filter((a) => CONFIDENCE_RANK[a.confidence] >= floor)
.sort((a, b) => CONFIDENCE_RANK[b.confidence] - CONFIDENCE_RANK[a.confidence])
.map((a) => a.provider);
return [...new Set(ranked)];
}
The default floor of medium is the product rule in code form: one prep-site claim is not enough to put a provider in front of a candidate. Everything downstream inherits it. The internal linking between guides, the "employers that use this assessment" block on each provider hub, and the provider list behind our onboarding questions all read from this one function, so none of them can assert something the file does not hold a citation for.
The new Set at the end is worth a sentence. An employer can carry several citations for the same provider, and that is the strongest evidence shape there is: three employers here have both a provider-first-party citation and an employer-official one. Callers want the set of providers, not the count of citations, and one of them keys React list items by provider, where a repeat is a duplicate-key error rather than a cosmetic problem.
An empty array is a valid answer
This is the part that made the model actually work, and the part that is uncomfortable to ship.
{
slug: 'arcadis',
name: 'Arcadis',
sector: 'Engineering & Consultancy',
hasGuide: true,
associations: [],
},
Eight employers currently have no evidenced provider at all. They still have pages. The page just does not claim to know something it does not know.
Where the real vendor is something we do not simulate, there is a separate field for saying so out loud rather than substituting a provider we happen to sell:
{
slug: 'allstate',
name: 'Allstate',
hasGuide: true,
unsupportedVendor:
"JEPS (Job Effectiveness Prediction System), Allstate's own battery. Allstate is NOT " +
'on wonderlic.com/customers, contrary to what this guide used to claim.',
associations: [],
},
That comment is aimed at a future maintainer who finds the old claim in git history and wonders whether removing it was a mistake. Recording the negative result is what stops a retracted claim from being re-added by somebody being helpful.
Two fields for two audiences
Each association has a note, and some profiles have a summary. They look similar and are strictly separated:
-
noteis editor-facing commentary on the evidence. It is never rendered. It can say "prep-site sourcing, so medium not high". -
summaryis page copy. A candidate reads it, so it is bound by the same rule as everything else: it must not assert anythingassociationsdoes not already support, and it hedges wherever the evidence hedges.
Mixing those two is how an internal caveat becomes a public claim.
Where it landed
The file now holds 108 employer profiles. The evidence behind them:
| Evidence type | Count |
|---|---|
| first-party-provider | 79 |
| prep-site | 12 |
| employer-official | 10 |
| candidate-reports | 8 |
| press | 3 |
By confidence: 63 high, 46 medium, 3 low. Eight profiles carry nothing at all.
Twelve prep-site citations survive because they are paired with something stronger or explicitly capped at medium. None of them is the sole basis for a page.
The maintenance cost is real
Employers change vendors. Provider logo walls and case-study pages rotate. The file carries a Last verified date at the top and the instruction that when a link rots you downgrade or remove the association rather than leaving a citation that no longer resolves.
That is a genuine ongoing cost. It is much smaller than the cost of a candidate revising for the wrong test.
See it
Open two pages side by side:
- cogniprep.app/employers/thales is backed by a first-party Arctic Shores case study, so the page names the provider plainly.
-
cogniprep.app/employers/amazon has an empty
associationsarray. Amazon runs its own instruments, so the page describes the Work Simulation and the Work Style Assessment, and is explicit that the Arctic Shores route only applies to some graduate and apprentice pipelines.
The full index is at cogniprep.app/employers. Pick any employer and see whether the page commits to a provider or declines to. The difference is not editorial mood, it is whether a URL exists in a TypeScript file.
Top comments (0)