/** * Extraction-confidence calibration from human review outcomes. * * Survey owns the review chain, so it owns the one signal no eval vendor can * publish: for every reviewed candidate, the stated extraction/candidate * confidence (the PREDICTION) against the human review decision (the LABEL). * Grouping those labeled samples by extractor (and field) and binning by * confidence yields an empirical calibration curve — "confidence in [0.8,0.9) * from extractor X was affirmed 17/20 times" — plus a calibration gap and an * empirically-grounded auto-accept threshold suggestion. * * EXPERIMENTAL. The curve is a descriptive summary of past review outcomes, not * a validated probability model. `suggestedThreshold` is withheld unless every * contributing bin clears a sample floor AND a one-sided 95% Wilson lower bound * on its accuracy clears the target (#279); even then it is not an auto-accept * gate — do not wire it into `autoAcceptMinConfidence` without your own * evaluation. * * ADVISORY ONLY (ADR 0003 §4, proposals-only). Calibration never decides a claim * or mutates a status. Projected claims carry status "proposed", exactly like * every other producer proposal. * * Machine auto-accepts 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 (circular). Only human review outcomes are * labeled samples. Set `includeAutoAccepted` to override. * * All computations are deterministic; `now` + `windowDays` window by `reviewedAt`. */ import type { Claim, Evidence, TrustBundle, VerificationEvent } from "@kontourai/surface"; import type { CandidateSet, Extraction, ReviewOutcome } from "./types.js"; /** * The three record arrays a calibration derivation reads — a subset of * {@link SurveyInput}. Kept narrow so a caller can pass an existing batch's * fields directly without constructing a whole SurveyInput. */ export interface CalibrationInput { readonly reviewOutcomes: readonly ReviewOutcome[]; readonly candidateSets: readonly CandidateSet[]; readonly extractions: readonly Extraction[]; } export interface DeriveCalibrationOptions { /** * Current time — required only when `windowDays` is set (windowing is by * `reviewedAt`). Injected for determinism; never read from the wall clock. */ readonly now?: Date; /** * Optional rolling window in days. Review outcomes whose `reviewedAt` is older * than `now - windowDays` are excluded. Requires `now` — setting `windowDays` * without `now` throws (rather than silently disabling windowing). When * omitted, all supplied outcomes are considered. */ readonly windowDays?: number; /** Number of equal-width confidence bins over [0,1]. Default 10 (deciles). */ readonly binCount?: number; /** * The accuracy the suggested threshold must clear. Default 0.95. A * `suggestedThreshold` is the lowest bin lower-bound at/above which every bin * has ≥ `minBinSamples` samples and a one-sided 95% Wilson lower confidence * bound on its accuracy (`CalibrationBin.accuracyLowerBound`) that meets this * target — the point estimate alone is not enough. */ readonly targetAccuracy?: number; /** * A bin needs at least this many samples to count toward `suggestedThreshold` * (an under-sampled bin ends the qualifying run). Default * {@link DEFAULT_MIN_BIN_SAMPLES} (30). The Wilson bound applies on top of this * floor, so an all-affirmed bin needs more samples than the floor when the * target is high (≈52 all-affirmed samples for the default 0.95 target). */ readonly minBinSamples?: number; /** * Include machine auto-accepted outcomes (actor === AUTO_ACCEPT_ACTOR) as * labeled samples. Default false — see the module note on circularity. */ readonly includeAutoAccepted?: boolean; /** * Count only audit labels: outcomes whose metadata records a score-blind * decision (`presentation.scoreBlind === true`) on a random audit sample * (`sampling.kind === "random-audit"`), as the review workbench writes them * for a session opened with `openRandomAuditSession`. Default false (every * human outcome counts, as before). Other outcomes are skipped and counted * in `skippedCount`, as windowed-out outcomes are. * * Calibration reads these fields as recorded on the outcome. The review * session validates them (a `random-audit` rate in (0, 1] and a stable seed) * and replay refuses events recorded under other conditions than the * snapshot's, but an outcome assembled outside Survey's apply path is taken * at its word: only feed outcomes from a store you trust. */ readonly auditSamplesOnly?: boolean; } /** One labeled calibration sample: a prediction paired with the human label. */ export interface CalibrationSample { /** The extractor that produced the proposed value (or "unknown"). */ readonly extractor: string; /** The extraction target / field the value belongs to. */ readonly field: string; /** The proposed candidate's stated confidence, clamped to [0,1]. */ readonly predictedConfidence: number; /** True iff the human review affirmed the proposed value. */ readonly correct: boolean; /** The review outcome this sample came from. */ readonly reviewOutcomeId: string; /** The candidate set the outcome reviewed. */ readonly candidateSetId: string; /** The outcome's `reviewedAt`, when present. */ readonly reviewedAt: string | undefined; } /** An equal-width confidence bin with its empirical accuracy. */ export interface CalibrationBin { /** Inclusive lower bound of the bin. */ readonly lowerBound: number; /** Exclusive upper bound (inclusive at 1.0 for the top bin). */ readonly upperBound: number; readonly sampleCount: number; readonly correctCount: number; /** correctCount / sampleCount; undefined when the bin has no samples. */ readonly empiricalAccuracy: number | undefined; /** Mean predicted confidence of samples in the bin; undefined when empty. */ readonly meanPredictedConfidence: number | undefined; /** * One-sided 95% Wilson lower confidence bound on the bin's accuracy; undefined * when the bin is empty. `suggestedThreshold` requires this bound, not the * point estimate, to meet `targetAccuracy`. Optional in the type so metrics * built by hand before this field existed still type-check. */ readonly accuracyLowerBound?: number; } /** A calibration rollup for one extractor (and optionally one field). */ export interface CalibrationGroup { readonly extractor: string; /** The field, or undefined for an extractor-level rollup across all fields. */ readonly field: string | undefined; readonly sampleCount: number; readonly correctCount: number; /** correctCount / sampleCount; undefined when the group has no samples. */ readonly empiricalAccuracy: number | undefined; /** Mean predicted confidence across the group; undefined when no samples. */ readonly meanPredictedConfidence: number | undefined; /** * meanPredictedConfidence − empiricalAccuracy. Positive → overconfident * (states more confidence than the humans bear out); negative → * underconfident. undefined when the group has no samples. */ readonly calibrationGap: number | undefined; /** Per-bin empirical accuracy, ascending by lowerBound. */ readonly bins: readonly CalibrationBin[]; /** * EXPERIMENTAL. Lowest bin lowerBound of the top-contiguous run of bins that * each have ≥ minBinSamples samples and an `accuracyLowerBound` ≥ * `targetAccuracy`. undefined when no bin qualifies — the data does not * support an empirical threshold at that target. Each contributing bin reports * its `sampleCount` and `accuracyLowerBound`. This is a descriptive summary, * not a validated auto-accept gate; calibration never sets any policy. */ readonly suggestedThreshold: number | undefined; } export interface CalibrationMetrics { /** One rollup per extractor (field === undefined), sorted by extractor. */ readonly byExtractor: readonly CalibrationGroup[]; /** One rollup per (extractor, field) pair, sorted by extractor then field. */ readonly byExtractorField: readonly CalibrationGroup[]; /** A single rollup across every labeled sample (extractor "*"). */ readonly overall: CalibrationGroup; /** Number of labeled samples used. */ readonly sampleCount: number; /** Outcomes that could not be turned into a labeled sample (see reasons). */ readonly skippedCount: number; /** ISO 8601 timestamp of the earliest labeled sample's `reviewedAt`. */ readonly windowStart: string | undefined; /** ISO 8601 timestamp of the latest labeled sample's `reviewedAt`. */ readonly windowEnd: string | undefined; /** The `windowDays` option echoed back (undefined when not windowed). */ readonly windowDays: number | undefined; } /** * Derives extractor/field confidence calibration from review outcomes. * * EXPERIMENTAL — see the module note. * * Each reviewed candidate set contributes one labeled sample: the confidence of * the SYSTEM-proposed candidate as the prediction, and whether the human review * affirmed that proposed value as the label. * * The proposed candidate is identified by its producer role, never by * `CandidateSet.selectedCandidateId` (which records the reviewer's pick on the * builder and canonical review paths, #279): the single candidate whose * `metadata.candidateRole` or `metadata.role` is `"proposed"`, or the only * candidate of a one-candidate set (whatever its role). A sample is skipped (and counted * in `skippedCount`) when it carries no human label or no prediction: * * - status "proposed" (not yet reviewed); * - resolution "could_not_confirm" (no human correctness label); * - a machine auto-accept, unless `includeAutoAccepted` is set; * - the proposed candidate cannot be determined (several candidates without * exactly one `"proposed"` role), or its confidence is missing or non-finite. * * A sample is "correct" when the outcome status is verified/assumed AND the * reviewer's pick (`ReviewOutcome.candidateId`, else `selectedCandidateId`) is * the proposed candidate; "incorrect" when the status is rejected or the * reviewer picked a different candidate (for example, kept the current value). */ export declare function deriveCalibration(input: CalibrationInput, options?: DeriveCalibrationOptions): CalibrationMetrics; /** A single projected calibration claim triple. */ export interface CalibrationClaim { readonly claim: Claim; readonly evidence: Evidence; readonly event: VerificationEvent; } export interface CalibrationClaimsSubject { /** Surface subject type (e.g. "extractor"). */ readonly subjectType: string; /** Surface subject id prefix (e.g. a producer or run id). */ readonly subjectId: string; /** Surface facet (e.g. "review.calibration"). */ readonly facet: string; /** Actor id to record on events. */ readonly actor: string; /** ISO 8601 timestamp for claim created/updated times. */ readonly observedAt: string; /** Producer identifier for the evidence `collectedBy` field. */ readonly collectedBy: string; } /** * Projects per-extractor calibration as Surface-ready claims (claimType * "calibration"). One claim per measurable metric per extractor: empirical * accuracy, calibration gap, and — when the data supports it — the suggested * auto-accept threshold. Every claim is status "proposed": calibration proposes, * it never decides (ADR 0003 §4). Groups with no labeled samples are skipped. */ export declare function calibrationToClaims(metrics: CalibrationMetrics, subject: CalibrationClaimsSubject): CalibrationClaim[]; /** * Merges calibration claims into an existing TrustBundle. Convenience wrapper * mirroring `mergeTrustBundleWithOversightMetrics`. */ export declare function mergeTrustBundleWithCalibration(bundle: TrustBundle, claims: readonly CalibrationClaim[]): TrustBundle;