Perfectionism is a prediction error. The perfectionist runs an implicit model: if this isn't flawless, catastrophe follows. A typo loses the client. A 90% effort is worthless. Shipping anything short of perfect ends in ruin.
The model is almost never tested — and when it is, the result is quietly ignored. I built a detector that does what the perfectionist won't: it counts every prediction, checks whether the predicted catastrophe actually happened, and computes the empirical disconfirmation rate. In 90 lines of vanilla JavaScript.
No AI. No ML. No statistics library. No Brier score, no log loss, no calibration curve, no expected calibration error. Just a count, a ratio, and three thresholds — because measuring how wrong a binary threat model is doesn't require a statistics subfield.
The clinical problem
Perfectionism maintains itself through untested predictions. The perfectionist predicts catastrophe, avoids the test (by polishing, checking, or never starting), and the avoidance "confirms" the prediction: I didn't submit it, so I don't know what would have happened — better safe than sorry.
The CBT intervention is the behavioral experiment: deliberately submit imperfect work and observe the actual outcome. But one experiment doesn't update a belief held for 20 years. You need accumulated evidence — a running tally of predictions versus outcomes.
That's what the detector computes. Each logged episode has:
- The perfectionistic thought (the prediction: "if I submit this with a typo, they'll fire me")
- The outcome (what actually happened: "Completed and it was fine")
The detector's job is to answer one question: across all your predictions, how often was the catastrophe wrong?
This is prediction calibration — the same concept ML uses Brier scores and reliability diagrams for. But the prediction is binary (catastrophe / no catastrophe) and the outcome is observable, so the calibration metric collapses to a ratio.
Why not ML?
ML calibration is a real field — Platt scaling, isotonic regression, temperature scaling, ECE. It exists because ML models output probabilities that need to be calibrated against outcomes.
Here, there are no probabilities. The perfectionist's model outputs a binary verdict: catastrophe will happen. The outcome is binary: it did / it didn't. Calibration of a binary-against-binary is a count:
disconfirmationRate = (times catastrophe did NOT happen) / (total predictions) × 100
ML would:
- Require a model to predict the outcome — but the user already recorded the actual outcome; there's nothing to predict
- Add a learned probability layer over a binary observable — pure overhead
- Be nondeterministic (the calibration score would vary across runs for the same data)
- Send sensitive mental-health data to a server to train on
- Obscure a one-line ratio behind a model card
Rule-based counting:
- Requires zero training (the outcome is a recorded fact, not an inference)
- Is deterministic (same log → same rate, every time)
- Costs nothing, runs client-side
- Is the exact computation a statistician would do by hand for binary-vs-binary
For a binary prediction against an observed binary outcome, a count isn't an approximation of the "real" calibration — it is the calibration. ML calibration tooling solves a different problem (continuous probability outputs); importing it here is cargo cult statistics.
The algorithm
Step 1: The episode shape
Each entry in the perfectionism log is a recorded prediction-outcome pair:
// One logged episode = one prediction + its actual outcome
var entry = {
when: '2026-08-18T09:00',
thought: 'if there is a typo they will think I am incompetent',
outcome: 'Completed and it was fine', // the catastrophe did NOT happen
timeCost: 'Substantial' // hours lost to polishing
};
The outcome field is the empirically observed result. The detector's entire job is to compare the implicit prediction (catastrophe) against this observed outcome.
Step 2: Map the time cost to a numeric severity
Perfectionism has a real cost: hours lost to polishing, checking, and avoidance. The user logs it as a label; we map it to a number for averaging:
function timeCostValue(tc) {
return { 'Minimal': 1, 'Moderate': 2, 'Substantial': 3, 'Severe': 4 }[tc] || 2;
}
Step 3: The core — compute the calibration
This is the whole detector. It counts predictions, counts disconfirmations, and computes three signals:
function analyzeCalibration(entries) {
if (!entries || entries.length === 0) return null;
var total = entries.length;
// Signal 1: the disconfirmation rate.
// How often did the predicted catastrophe NOT happen?
var fineCount = entries.filter(function(e) {
return e.outcome === 'Completed and it was fine';
}).length;
var disconfirmationRate = Math.round(fineCount / total * 100);
// Signal 2: the completion rate (perfectionism blocks finishing).
var completed = entries.filter(function(e) {
return e.outcome === 'Completed and it was fine' ||
e.outcome === 'Completed but exhausted' ||
e.outcome === 'Completed late';
}).length;
var completionRate = Math.round(completed / total * 100);
// Signal 3: the average time cost (hours lost to the rigid standard).
var avgTimeCost = entries.reduce(function(s, e) {
return s + timeCostValue(e.timeCost);
}, 0) / total;
return {
total: total,
disconfirmationRate: disconfirmationRate,
completionRate: completionRate,
avgTimeCost: avgTimeCost,
timeCostLabel: costLabel(avgTimeCost)
};
}
function costLabel(avg) {
if (avg >= 3.2) return 'severe';
if (avg >= 2.4) return 'substantial';
if (avg >= 1.6) return 'moderate';
return 'minimal';
}
Three signals, no model. The disconfirmationRate is the star — it's the empirical answer to "is your threat model calibrated?"
Step 4: The adaptive guidance engine
A number without an action is a dashboard, not an intervention. The guidance engine branches on the disconfirmation rate — three regimes, each with a specific next step:
function guidance(analysis) {
var r = analysis.disconfirmationRate;
if (r >= 60) {
return 'In ' + r + '% of episodes the predicted catastrophe did NOT happen. ' +
'Your threat model is empirically miscalibrated. Run bigger behavioral ' +
'experiments on higher-stakes tasks — each survived imperfection weakens ' +
'the all-or-nothing rule. You have the evidence; trust it.';
}
if (r >= 30) {
return 'In ' + r + '% of episodes the catastrophe did not happen. The prediction ' +
'is wrong at least some of the time. Log more episodes — especially ones ' +
'where you submit good-enough work — to build the corrective dataset. ' +
'The belief updates on accumulated evidence, not one instance.';
}
return 'You are mostly not yet submitting good-enough work to test the prediction. ' +
'The perfectionism is winning the avoidance battle. Start with one low-stakes ' +
'behavioral experiment this week — deliberately submit something slightly ' +
'imperfect and record the actual outcome. The first survived exposure is the ' +
'hardest and the most important.';
}
The thresholds (60 / 30) aren't arbitrary — they map to the CBT logic of belief updating. Below 30%, there isn't enough disconfirming evidence to move a strongly-held belief, so the guidance is generate more data (run one experiment). Above 60%, the evidence is overwhelming, so the guidance is trust it and escalate (bigger experiments). Between 30-60%, the belief is wobbling, so the guidance is accumulate (keep logging). This is the same three-regime structure a clinician uses, encoded as two if statements.
How it works in practice
var entries = [
{ when: '2026-08-15T09:00', thought: 'must be flawless', outcome: 'Completed late', timeCost: 'Substantial' },
{ when: '2026-08-16T14:00', thought: 'typo will ruin it', outcome: 'Completed and it was fine', timeCost: 'Moderate' },
{ when: '2026-08-17T08:00', thought: 'not ready to submit', outcome: 'Not completed', timeCost: 'Severe' },
{ when: '2026-08-18T12:00', thought: 'one more review', outcome: 'Completed and it was fine', timeCost: 'Substantial' },
{ when: '2026-08-19T08:00', thought: 'good enough to ship', outcome: 'Completed and it was fine', timeCost: 'Minimal' },
{ when: '2026-08-20T15:00', thought: 'they will judge me', outcome: 'Completed and it was fine', timeCost: 'Moderate' }
];
var analysis = analyzeCalibration(entries);
console.log(analysis.disconfirmationRate); // 67
console.log(analysis.completionRate); // 83
console.log(analysis.timeCostLabel); // "moderate"
console.log(guidance(analysis));
// "In 67% of episodes the predicted catastrophe did NOT happen.
// Your threat model is empirically miscalibrated. Run bigger
// behavioral experiments on higher-stakes tasks — each survived
// imperfection weakens the all-or-nothing rule. You have the
// evidence; trust it."
The detector finds that in 4 of 6 episodes the predicted catastrophe did not happen — a 67% disconfirmation rate. The perfectionist's model ("imperfect → catastrophe") is wrong two-thirds of the time, and the detector says so in plain language, with a specific next action. No model predicted this; it's a count of recorded facts.
Why this design
Counting, not predicting. The temptation is to build a model that predicts whether a catastrophe will happen. But the user already recorded the outcome — the prediction is a solved problem. The detector's job is to aggregate the recorded outcomes into a calibration signal. Predicting an outcome you already observed is the wrong abstraction.
Binary-vs-binary collapses calibration to a ratio. ML calibration (Brier score, ECE, reliability diagrams) exists for continuous probability outputs — a model says "72% likely" and you check whether 72%-confidence predictions are right 72% of the time. The perfectionist's model outputs a binary verdict ("catastrophe will happen"), and the outcome is binary ("it did" / "it didn't"). For binary-vs-binary, the calibration is the disconfirmation rate. Importing continuous-calibration tooling would be solving a problem that doesn't exist here.
Three regimes, not a score. A single calibration number (67%) is informative but not actionable. The guidance engine splits the rate into three regimes (≥60 / ≥30 / else), each mapping to a specific CBT next step. This is the difference between a dashboard and an intervention: the number tells you where you are; the regime tells you what to do next.
The completion rate as the cost signal. Disconfirmation rate measures the prediction error; completion rate measures the behavioral cost — perfectionism's main harm is that it blocks finishing. A high disconfirmation rate + low completion rate means the user knows the model is wrong but is still acting on it (the hardest case — the belief hasn't transferred to behavior). The detector surfaces both so the guidance can address the right problem.
The decision framework: counting vs. ML calibration
Use counting (this approach) when:
- The prediction is binary (catastrophe / no catastrophe), not a continuous probability
- The outcome is observed and recorded, not something to be inferred
- You need determinism (same log → same rate, every time)
- Privacy is required (mental-health data stays client-side)
- The goal is an actionable regime, not a calibration score for model selection
Use ML calibration when:
- The model outputs continuous probabilities that need to be aligned to observed frequencies
- You're selecting between models and need a comparable calibration metric
- The outcome is not directly observable and must itself be predicted
- You're operating at scale where manual threshold-setting doesn't generalize
Perfectionism prediction calibration is firmly in the counting camp. The prediction is binary, the outcome is a recorded fact, determinism matters (the user needs to trust the number), and the goal is a next action — not a model-selection score.
Try it
The full calibration detector is embedded in the CBT for Perfectionism guide — free, runs entirely in your browser, saves to localStorage. Log a few perfectionism episodes with their actual outcomes and watch the disconfirmation rate compute live. No signup, no server, no data leaves your device.
The complete toolkit has 23 free interactive CBT tools, all built with the same philosophy: vanilla JavaScript, zero dependencies, zero backend, zero data collection. CBT Toolkit Hub.
The pattern is this: when the prediction is binary and the outcome is observed, calibration is a count — not a model. The perfectionist's threat model says "catastrophe is certain." The detector counts how often that certainty was wrong and turns the count into an action. That's 90 lines of code that do what 20 years of avoidance prevented: look at the evidence. In mental health, the intervention that works is often the one that makes the hidden record visible — and a ratio anyone can read is harder to argue with than a feeling.
Top comments (0)