FramingThe brief, and the trap in it
A company runs performance reviews across roughly two thousand people. Managers rate their reports on a five-point scale. The evidence about what each person actually did is scattered across five systems, the ticket tracker, the code host, the KPI warehouse, the weekly self-updates, and the HR roster, and nobody has the time to read it, so the rating is made mostly from impression and whatever is easiest to recall.
The request, verbatim in spirit, was: "we should use AI to make performance reviews more objective and consistent."
Taken literally, that sentence asks for a model that reads the evidence and returns a rating, or a ranking, or a recommendation. That is the one thing this system must never do. A tool that proposes a rating gets argued with about its output. A tool that assembles the evidence gets argued with about the evidence, and that is the conversation worth having.
So what I built is not a rating engine. It is a pre-conversation brief and a review queue: for each person it gathers the evidence, scores it against a published rubric, and surfaces where the manager's rating and the evidence disagree, with the specific gap named. It has no write path into anyone's rating and never proposes one. The most useful thing it produces on a quarter of the roster is the sentence "I cannot score this person fairly, and here is exactly why."
This started life as a take-home design-and-build exercise. I did not join the company and signed nothing, so it is published here with the client unnamed and with only the synthetic dataset that came with the exercise. Every person, email, rating and KPI below is fabricated test data; the employee IDs (E0047, E0073, and so on) are the dataset's own placeholders, not real people. The repository is private for the same reason; see how to read the code at the end.
Part 1What the rating actually measures
Before designing anything I ran read-only analyses over the raw data. The headline is uncomfortable and it is the reason the rest of the design looks the way it does.
The manager rating largely measures how much a person writes about themselves. Word count in the weekly update predicts the rating at rho +0.51 while tracking their actual business result at only +0.10. The measure that does track the result, delivered work at +0.54, the rating barely registers, at +0.24.
Rank correlation (Spearman rho), across the people whose KPI row could be attributed to them unambiguously:
| Signal | vs manager rating | vs business result |
|---|---|---|
| Words per weekly update | +0.51 | +0.10 |
| Filler sentences written | +0.55 | +0.11 |
| Delivery tasks closed | +0.25 | +0.55 |
| Delivery effort points | +0.24 | +0.54 |
| Weeks with delivered work | +0.20 | +0.45 |
| Lines of code added | +0.18 | +0.14 |
| Pull request merge rate | +0.17 | +0.06 |
| Tenure · team size | <0.05 | <0.09 |
| The manager rating itself | — | +0.26 |
Read the first two rows against the third and fourth. The two measures that best predict the rating a person receives are the two with almost nothing to do with their business result. The rating is not random, at +0.26 against the result it carries real signal, and any replacement has to beat that rather than dismiss it. But roughly twice as much of it is explained by writing volume as by outcomes.
It gets worse when you read the text. The weekly-update corpus is 444 updates built from 47 distinct sentences, and eight filler templates account for 76% of every sentence written:
"I proactively drove clarity on priorities and helped
everyone move in the same direction."
"I championed best practices and helped raise the bar
on quality across the board."
"I spent meaningful time unblocking dependencies…"Against a handful of concrete ones, "Onboarded 3 merchants; hit quota." · "Cut report runtime from 40s to 6s." Longer updates are longer because they repeat the filler, often verbatim within one update. So length is not merely uninformative, it is inversely related to substance while being the single strongest predictor of the rating. Any design that rewards writing volume, even indirectly, would automate this bias and hand it a number.
Part 2Four traps in the data
Each of these produces a wrong answer silently, an attributed number rather than an error, which is the dangerous kind. None was visible in the data dictionary; each came out of measurement.
- Email is not a unique key. The roster has 222 people and 211 email addresses. Eleven addresses belong to two different people each, distinct IDs, different departments, tenure and rating. Because the ticket handle derives from the email, it collides too, and one shared handle carries KPI attainment of both 45.0 and 95.0. Guessing which is which moves a person between the bottom and top decile of their cohort. Everything here keys on the immutable employee ID, never on email.
- 61% of the task tracker is not delivered work. Of the tracker's rows, only the
deliverytype is real output; the rest are self-filed status items and stretch goals. Every self-update row and every stretch-goal row was filed by the person it is assigned to. A naive "tasks closed" count therefore scores people mostly on how diligently they file their own paperwork, which is whydeliveryis the only task type this system scores. - Alias handles carry real work. Twelve alias handles in the tracker carry delivery work only. Ignoring them silently deletes real delivered work for twelve people. Each resolves to a person with a matching name stem via a single dominant task creator, so they are recovered rather than dropped.
- Confidential HR matters sit in the self-updates. Twelve weekly updates, all written by Employee Relations staff, describe live HR and legal matters, performance plans, harassment allegations, grievances, conduct investigations, and name 22 real people from the roster. Fed into a scoring pipeline unfiltered, that text would copy legally sensitive third-party information into a system a different set of managers can read. It is withheld in full before anything else reads it (Part 4).
Part 3Identity is the fairness core
The single most consequential decision in the system is not the rubric. It is what happens when the data cannot say, with confidence, whose work this is. The instinct is to pick the most likely match and move on. On a performance record, that instinct is the bug.
A handle resolves to a candidate set, not a person, narrowed only by role-consistent evidence, project codes and KPI names that are specific to a department and learned from the unambiguous people at runtime rather than hardcoded.
- Collisions that split across departments resolve cleanly, because an analytics project code cannot belong to the engineer who shares the email.
- Collisions that are two people in the same department with the same KPI have nothing to separate them. Those abstain.
- Derivable-but-missing handles are recovered by an explicit, testable rule; opaque handles with no relation to the name are declared unrecoverable rather than guessed.
55 of 222 people (25%) are not scored at all, principally the 23 whose handle cannot be derived and the 22 who share one with a colleague. Their mean manager rating is already 2.87 against 3.04 for everyone else. Scoring them on whatever evidence survived a broken join would hand a low number to the people already rated lowest, produced entirely by a plumbing failure. The output for those people is not a score, it is the specific broken join, addressed to whoever can fix it.
Abstention has its own failure mode, and the design states it rather than hiding it: 25% abstention means silence exactly where the evidence is thinnest, which may correlate with roles that are already under-observed. That is a reason to drive identity coverage up before rollout, not a reason to paper over it with a guess.
Part 4Redaction runs before anything reads
The privacy control is a gate on input, not a filter on output. It runs before any stage that reads text, so no later stage, model or rule, can see an unscreened string even by accident. This is the opposite of the common pattern, where text flows freely and names are stripped from the final report; by then the sensitive string has already been read, joined and possibly copied into someone else's evidence.
Sensitive updates are withheld in full, not name-masked, because the surrounding sentence usually identifies the case anyway, "the grievance filed against" a masked name still points at one person on a small team. Any roster colleague named in somebody else's ordinary update is replaced with a neutral placeholder, so no evidence is ever created about a person from text a different person wrote. The affected ER staff still get credit for the case work through their tracked case tasks; only the text is withheld.
One person named in those withheld updates is rated 2 of 5 while the evidence puts them at the 89th percentile of their department, one of the largest under-ratings on the roster; another named alongside them sits at rated 2 against a 90th-percentile evidence score. Whether those facts are connected is exactly the question a manager should answer and a scoring system should never guess at. The brief raises the gap and never mentions the case.
Part 5What is scored, and what is deliberately not
Four dimensions, each ranked within department because delivery volume differs by an order of magnitude across them, and weights redistributed when a dimension has no evidence behind it rather than scoring a zero it did not earn.
| Dimension | Weight | Built from | vs result |
|---|---|---|---|
| Outcome | 40 | KPI attainment, ranked within the same KPI | is the result |
| Delivery | 30 | delivered work only: effort points and closed count | +0.54 |
| Consistency | 15 | distinct weeks containing delivered work | +0.45 |
| Corroboration | 15 | share of self-reported claims found in the systems of record | a check, not a count |
KPI attainment must be ranked within its own KPI, never across the company: the KPIs have different spreads, so a raw cross-role comparison would rank the KPI, not the person. For departments whose KPI is itself an output count, the score carries a confidence penalty, not a score penalty, because there the outcome and the delivery evidence are the same signal wearing two hats, and the system should rest less weight on it and widen its flag threshold accordingly.
Writing volume, filler count, self-filed process items, lines of code, PR merge rate, tenure and team size are all excluded from the score, each with the measurement that justifies the exclusion. But they are still computed, carried on every evidence pack marked scored=False, and printed in each brief with that person's own numbers. So the exclusion is visible rather than implied, a manager sees the verbosity contrast on their own report, and the system can audit itself for having quietly let one back in.
Part 6Where AI earns its place, and where it is banned
Most of the pipeline is deterministic arithmetic and set membership, and a model has no business there. There is exactly one job where a model earns its place, and it earns it on a measurement rather than an assertion.
The self-updates are free text. To use them for corroboration, did what a person said they did show up in the systems of record, you first have to turn prose into checkable claims. The templated corpus here is easy, a regex handles it, and that proves nothing, because real weekly updates are not templated. So I hand-labelled eighteen realistic updates, written the way people actually write, with hedging, negation, multi-clause sentences and claims that span sentence boundaries, and measured two extractors against them:
| Extractor | Precision | Recall | F1 |
|---|---|---|---|
| Pattern (deterministic) | 0.88 | 0.44 | 0.58 |
| Cheapest model tier | 0.93 | 0.88 | 0.90 |
Recall is the whole story. The pattern extractor is precise about what it finds and misses more than half of it, it reads "I had hoped to ship X but it is still in review" as a delivery, and it cannot follow a claim across a sentence boundary. Doubling recall is the difference between a corroboration dimension that works on real prose and one that only works on templates. The model earns its place, and it does so on the cheapest tier, which is the other half of the point.
Read English, propose, cite
Turns prose into checkable claims, writes the brief's one opening paragraph, and acts as a recall backstop behind the keyword redactor, because a word list cannot catch a phrasing it has never seen.
Produce a number, or a rating
Banned from any number in a score, from seeing unredacted text, and from deciding a rating. Every citation it writes is verified against the evidence pack and stripped if invented; on any failure it degrades to the deterministic path.
LLM proposes, deterministic code disposes. The boundary is enforced, not asked for politely. The screening gate raises rather than degrading if handed unscreened text; invented citations are stripped by a checker a unit test keeps honest; and the default configuration runs with no model at all, fully deterministic, so the repository runs anywhere and nobody's evidence depends on a network call.
All three jobs are mechanical and high-volume, not hard: pull discrete claims out of a three-sentence update, classify one update as sensitive, write one paragraph. At 2,000 people writing weekly, extraction alone is ~26,000 calls a quarter, a recurring operating cost, not a one-off. Rough quarterly extraction spend: ~$120 on the cheapest tier against ~$500 and ~$1,500 on the larger ones. Pay for a bigger tier only when the eval shows the cheap one cannot do the job, which is exactly what the eval is for.
Part 7The evaluation, built without a ground truth
There is no ground truth for performance here, the manager rating is explicitly not one, so every check is built to be falsifiable without one. Checks that assert can fail the run; checks that measure print their number either way, because a harness that can only pass measures nothing.
| Check | Result | |
|---|---|---|
| E1 | Predictive validity, target held out | evidence rho +0.716 vs result against the rating's +0.256 — 2.8× |
| E2 | Gaming resistance | 8 filler updates + 40 self-filed tasks + 3× padded diffs moved the score 0.00 points |
| E3 | Bias audit | strongest tie to an excluded input 0.044 (the rating's is 0.596) |
| E4 | Stability | split-half +0.457, leave-one-source-out +0.989 |
| E5 | Abstention and identity | every ambiguous identity abstains; 12 alias handles recovered; no abstention scored |
| E6 | Claim extraction on held-out prose | model F1 0.90 vs the pattern extractor's 0.58 — recall 0.88 vs 0.44 |
| E7 | Privacy | 0 third-party names across 444 generated artefacts |
E1 is non-circular by construction. The Outcome dimension is 40% of the production score, so measuring that score against KPI attainment would be measuring the KPI against itself. E1 rebuilds the score with the outcome data excluded entirely, delivery, consistency and corroboration only, and then tests it against the result. It beats the manager rating by 2.8× on a target neither was allowed to see.
E4 is reported honestly. Split-half +0.457 is moderate, and it is a limit on the whole exercise rather than a property of the rubric: this dataset covers four weeks, which is not a performance period. Nothing here should reach a calibration meeting without being re-measured over a real quarter, and the eval says so in the number rather than in a footnote.
E7 initially failed on three people; the "leak" was each person's own manager's name, a structural field, so the check now permits exactly that and nothing else. Separately, the model extractor's fallback-to-pattern behaviour would have made E6 silently compare the pattern extractor against itself and print a dead heat as if the model had been measured, so E6 now counts fallbacks and refuses to report a comparison that did not actually happen. A harness that cannot embarrass its author is decoration.
Part 8Running it for two thousand people
The whole roster scores in about a second on one core with no dependencies; two thousand is roughly ten seconds. Compute is not the constraint; identity is. Drive the 25% abstention rate down before anything else, because that is where the system is silent.
What stays human
| Decision | Owner | Why not the system |
|---|---|---|
| The rating | Manager | The system sees only instrumented work. Mentoring, incident response, the quarter someone carried a failing project, all invisible to every source here. |
| Resolving a flag | Manager | A flag is a question. The most common correct answer is "the evidence is incomplete, here is what it missed." |
| Anything about an abstained person | Manager + People Ops | The system declines. Forcing a number is worse than having none. |
| Identity conflicts | IT / People Ops | People share handles with a colleague. No scoring rule may guess between them. |
| Live HR matters | Employee Relations only | Never enters this pipeline at all. |
| Rubric and weights | People Ops, versioned | A rubric that changes silently cannot be appealed. |
The feedback loop matters more than the rubric. Every decision is captured as (person, evidence score, rating, accepted or overridden, reason). Override reasons are clustered each cycle, and a recurring cluster is a rubric bug, not a manager problem, "runs the on-call rota" appearing twenty times means on-call is real work the instrumentation cannot see. Clusters drive changes in priority order: instrument the missing work first, widen abstention as the safe fallback, re-weight the rubric last and only with a published diff. The eval suite gates every rubric change, a weight change that improves predictive validity but degrades the bias audit does not ship.
Shadow in one department (briefs generated, nobody rated on them), then advisory in a few (briefs used in real conversations, ratings still manual), then calibration input company-wide (evidence packs as one input among several, fairness metrics published). That is the ceiling. There is no phase in which the score sets a rating.
Part 9What a governance reviewer would ask
| Question | Answer |
|---|---|
| Can this system change a rating? | No. It has no write path into any rating and never proposes one. |
| Can a model produce a number in a score? | No. The model reads English; every number in a score comes from deterministic code, and the system runs with no model at all. |
| Is a given score reproducible? | Yes. The deterministic path is the same input to the same output every time, and the score-gating eval never calls a model. |
| What is sent to a model provider? | Only already-screened text, after the redaction gate. No unredacted update, ever, and the screening assertion raises rather than degrading. |
| Can one person see evidence built from another's words? | No. Sensitive updates are withheld in full and named colleagues are replaced before any read. |
| Can a person read their own evidence pack? | Yes, by design. A system that assembles a file about someone they may not see should not exist. |
| What happens to a person who cannot be attributed? | They abstain. The output is the specific broken join, not a score built on a guess. |
The metric, and the guardrails
Three things are watched every cycle. Working: flag precision (below 60% the queue is noise), flag volume per manager, and predictive validity as a time series. Fair: the score's correlation with every excluded input, thresholded at 0.20 and currently 0.044, plus abstention rate by department, because a concentration in one team is an instrumentation gap being mistaken for a performance gap. Safe: the privacy assertion must be zero leaks every run, or the run does not publish.
"Top-decile on verbosity, bottom-quartile on delivered work" is a prompt to check where an impression came from. It is not evidence about a person and must never enter a rating record. The system produces it for the conversation and refuses to store it as a judgement.
Part 10Limits, and what I'd do with another quarter
The limits are stated up front because they bound how much any of the above should be trusted. The period is four weeks, not a performance period, which is why stability is only moderate. Nearly every manager in the sample has exactly one report, so per-manager leniency, the single most valuable calibration signal in a real deployment, cannot be measured here at all. And the data is synthetic: the method transfers, the specific numbers do not.
- Drive identity coverage up. 25% abstention is the one thing that most limits the system's reach, and it is a data-plumbing problem with named owners, not a modelling problem.
- Re-measure over a real quarter. Split-half reliability of +0.457 on four weeks is not enough to put a rating conversation on. Everything predictive should be re-run on a genuine performance period before it informs a calibration meeting.
- Add per-manager calibration. Once the roster has managers with several reports, manager leniency becomes the largest correctable bias and the first thing worth adding.
- Broaden the negative set for the claim reader. The realistic prose set bounds recall; it does not yet stress how often the model invents a claim across the full range of real updates.
- Exclude structurally invisible roles until they have instrumented outcomes. Some functions do work that will never appear in a tracker, and it is precisely the work that must not be described in text this system reads. They should be out of automated scoring, not scored on a proxy.
CodeReading the code
Everything described above is a working repository, not a diagram: the identity resolver and its claiming and disambiguation rules, the redaction gate that runs first, the four-dimension rubric with its abstention and confidence logic, the flags that compare rating against evidence, the optional model adapters that are off by default, the E1–E7 evaluation, 29 unit tests, an analysis write-up in which every number is recomputed by one command, and an operations blueprint for running it across two thousand people.
It carries the exercise brief and the synthetic dataset that came with it, and neither is mine to publish. The code is. If you would like read access, or a walkthrough of any decision on this page, write to admin@snezik.cz and I will share it.
The model is allowed to read English and is not allowed anywhere near a rating.
The useful output of a performance system is not a better number. It is the evidence a manager can argue with, the disagreement stated plainly, and the honesty to say "I cannot score this person fairly" instead of guessing.