Confidence Calibration from Review Outcomes#
Context#
Every reviewed candidate in Survey is a labeled calibration sample: the system-proposed candidate carried a stated confidence (the prediction), and the human review either affirmed or overturned that value (the label). No eval vendor can publish a calibration curve, because none of them own the review outcomes — Survey does. Issue #114 framed the opening and the two design questions this record answers: where the calibration computation lives, and how a calibration curve may feed auto-accept thresholds without violating ADR 0003 §4 (proposals-only).
Decision#
Calibration is a pure Survey-side derivation over review outcomes
(src/calibration.ts: deriveCalibration), not a consumer-side computation.
Survey owns the source → extraction → candidate → review chain, so the labeled
samples live here and every downstream consumer reuses one derivation rather
than each re-deriving from its own store. The module mirrors
oversight-metrics.ts: a pure deriveX returning a metrics object plus an
xToClaims projection, deterministic with an injected now for windowing.
The derivation:
- Prediction = the confidence of the system-proposed candidate
(
CandidateSet.selectedCandidateId), falling back to its extraction's confidence. An outcome with no selected candidate or no finite confidence is skipped — there is no prediction to calibrate. - Label = whether the human affirmed the proposed value.
verified/assumedwith no override → correct;rejectedor an override to a different candidate → incorrect. - Grouping by extractor and by (extractor, field); confidence is binned into equal-width deciles with per-bin empirical accuracy, a group calibration gap (mean stated confidence − empirical accuracy; positive = overconfident), and a suggested threshold.
Two hard constraints#
Advisory only (ADR 0003 §4). Calibration informs policy; it never decides. The
suggestedThresholdis the lowest decile lower-bound whose top-contiguous run of populated bins clears a target accuracy — an operator MAY wire it intoautoAcceptMinConfidence(evaluateAutoAccept), but calibration itself sets nothing. Projected claims carry statusproposed, exactly like every other producer proposal.Human labels only. Machine auto-accepts (
actor === AUTO_ACCEPT_ACTOR) are excluded by default: an auto-accepted outcome is the threshold accepting its own guess, so counting it as a "correct" label would let the policy validate itself.includeAutoAcceptedoverrides this for offline analysis.
Producing conclusionConfidence.value (#137)#
The derivation's empirical accuracy is the natural calibrated conclusion
probability, so buildSurveyTrustBundle now produces it. With the
calibration option enabled, an affirmed claim's conclusionConfidence.value
is set to the affirmation rate of its extractor's proposals — the finer
(extractor, field) group when it clears a sample floor, else the extractor-level
group, else left unset (an ungrounded claim gets no number). method records
which granularity produced the value. This is the "produce" side of the confidence
loop: Survey previously only carried comfortZone and left value unset.
conclusionConfidence.value is "probability the conclusion is correct", so it is
produced only for affirmed conclusions (status verified/assumed).
Attaching an affirmation rate to a rejected (or not-yet-reviewed) conclusion
would assert the opposite of what the human decided, so those claims get no value.
Two honesty constraints hold here too:
- Sample floor (default 20) — below it,
valuestays unset rather than emitting a poorly-grounded number. - Prefer a longer history — callers SHOULD pass precomputed
metricsderived over more than the current batch; the batch-derived fallback is a convenience and carries a mild self-reference (a claim's own outcome is one sample in the group that sets its value). Either way this only enriches the emitted confidence; it never changes claimstatus(ADR 0003 §4).
Guards (#279)#
The first cut overstated what the numbers support, so calibration is now marked experimental and guarded rather than redesigned:
- Label source is the proposer role. On the builder and canonical review
paths
selectedCandidateIdrecords the reviewer's pick, so reading it as "the system proposal" scored keep-current decisions as correct. The prediction is now the single candidate whosemetadata.candidateRoleormetadata.roleis"proposed", or the only candidate of a one-candidate set whatever its role (one candidate leaves no pick to confuse with a proposal); a multi-candidate set without exactly one"proposed"marker is skipped and counted inskippedCount. This supersedes theselectedCandidateIdrule in the Decision section above. - Threshold needs evidence.
suggestedThresholdrequires each contributing decile to have at leastminBinSamplessamples (default raised from 1 to 30) and a one-sided 95% Wilson lower bound on accuracy that meetstargetAccuracy; each bin reports that bound asaccuracyLowerBound. It is no longer described as a number to wire intoautoAcceptMinConfidence. conclusionConfidence.valueis opt-in and named experimental. It is a group base rate, not a per-claim probability, sobuildSurveyTrustBundlesets it only withcalibration: { experimentalConclusionValue: true }; asking for calibration without the flag logs a once-per-processconsole.warn, the same channel as thedefineProductVocabularydeprecation notice.
Deferred work#
- Consumer wiring — a downstream consumer reading
suggestedThresholdinto itsautoAcceptMinConfidencepolicy (still an operator decision, never automatic).