|
| 1 | +// Realized-history ground truth for the frozen-repo benchmark (#9261, harness #9216, epic #8534). |
| 2 | +// |
| 3 | +// For each work unit in a snapshot: what the maintainer ACTUALLY did within the prediction horizon. These |
| 4 | +// are the labels every candidate proposal (#9260) is scored against, so they are emitted in that module's |
| 5 | +// exact action vocabulary — proposal and outcome are directly comparable, with no translation layer that |
| 6 | +// could drift. |
| 7 | +// |
| 8 | +// Three rules this module exists to enforce, each of which the surrounding system already believes and which |
| 9 | +// a benchmark must not quietly contradict: |
| 10 | +// |
| 11 | +// REVERSAL-AWARE SETTLEMENT. A merge later reverted is not a clean merge. The realized outcome is the |
| 12 | +// SETTLED state at horizon end, with the reversal recorded alongside — the same treatment that makes |
| 13 | +// `public-rule-precision.ts`'s precision honest (it subtracts reversals from confirmations rather than |
| 14 | +// counting the original decision). The reversal vocabulary is the established one |
| 15 | +// (`reversal_reopened` / `reversal_reverted` / `reversal_superseded`), not a new benchmark-local set, so |
| 16 | +// benchmark ground truth and internal backtest ground truth can never disagree about the same event. |
| 17 | +// |
| 18 | +// UNRESOLVED IS EXPLICIT AND EXCLUDED. A work unit with no maintainer action inside the horizon is |
| 19 | +// `unresolved`. It is NOT silently counted as a correct abstention (which would reward an agent for |
| 20 | +// declining exactly the cases nobody decided) nor as an incorrect prediction (which would punish an agent |
| 21 | +// for the horizon being too short). It leaves the scoring denominator entirely, and the unresolved RATE is |
| 22 | +// published so a climbing rate is legible as "the horizon is wrong" rather than as a scoring artifact. |
| 23 | +// |
| 24 | +// STABILITY. Ground truth for a given (snapshot, horizon) must not depend on WHEN extraction runs, once |
| 25 | +// the horizon has elapsed. Every decision here is a pure function of events dated inside the window, so a |
| 26 | +// later event — including a later reversal — cannot retroactively change a settled label. That is what |
| 27 | +// makes a leaderboard reproducible instead of drifting under its own scores. |
| 28 | +// |
| 29 | +// Pure core (this module); the IO wrapper that reads events from D1 lives engine-side of the harness, the |
| 30 | +// same split every sibling calibration module uses. |
| 31 | + |
| 32 | +import type { BenchmarkActionKind, BenchmarkCloseReasonClass } from "./benchmark-proposal.js"; |
| 33 | + |
| 34 | +/** The established reversal vocabulary — deliberately imported as literals from the same set the audit-event |
| 35 | + * table and `public-rule-precision.ts` already use, never a benchmark-local re-invention. */ |
| 36 | +export type RealizedReversalKind = "reversal_reopened" | "reversal_reverted" | "reversal_superseded"; |
| 37 | + |
| 38 | +/** One realized maintainer event inside (or outside) a horizon, already normalized by the IO wrapper. */ |
| 39 | +export type RealizedMaintainerEvent = { |
| 40 | + workUnitId: string; |
| 41 | + /** Same closed vocabulary the agent proposes in — comparison needs no translation. */ |
| 42 | + action: BenchmarkActionKind; |
| 43 | + /** ISO-8601. Events at exactly the horizon end are INSIDE the window (inclusive), matching how the |
| 44 | + * corpus builder's own windowing treats a boundary event as belonging to the window it closes. */ |
| 45 | + occurredAt: string; |
| 46 | + /** Present only for a `close`; the class realized history settled on. */ |
| 47 | + reasonClass?: BenchmarkCloseReasonClass | undefined; |
| 48 | + /** Present only for a `label`. */ |
| 49 | + labels?: readonly string[] | undefined; |
| 50 | +}; |
| 51 | + |
| 52 | +/** A reversal of a previously-realized action, in the established vocabulary. */ |
| 53 | +export type RealizedReversalEvent = { |
| 54 | + workUnitId: string; |
| 55 | + kind: RealizedReversalKind; |
| 56 | + occurredAt: string; |
| 57 | +}; |
| 58 | + |
| 59 | +/** The settled label for ONE work unit. `unresolved` carries no action by construction — the type makes |
| 60 | + * "unresolved but somehow also a merge" unrepresentable rather than merely untested. */ |
| 61 | +export type BenchmarkGroundTruth = |
| 62 | + | { workUnitId: string; outcome: "unresolved" } |
| 63 | + | { |
| 64 | + workUnitId: string; |
| 65 | + outcome: "settled"; |
| 66 | + action: BenchmarkActionKind; |
| 67 | + reasonClass?: BenchmarkCloseReasonClass | undefined; |
| 68 | + labels?: readonly string[] | undefined; |
| 69 | + settledAt: string; |
| 70 | + /** The reversal that overturned this action inside the horizon, if any. A reversed action is still |
| 71 | + * the realized action — it is simply not a CONFIRMED one, exactly as `public-rule-precision.ts` |
| 72 | + * treats a reversed decision: recorded, and subtracted from the confirmed count. */ |
| 73 | + reversal: { kind: RealizedReversalKind; occurredAt: string } | null; |
| 74 | + }; |
| 75 | + |
| 76 | +export type BenchmarkGroundTruthSet = { |
| 77 | + schemaVersion: 1; |
| 78 | + snapshotRef: string; |
| 79 | + horizonDays: number; |
| 80 | + frozenAt: string; |
| 81 | + /** The horizon's inclusive end — `frozenAt + horizonDays`, computed once and published so a consumer |
| 82 | + * never has to re-derive (and possibly re-derive differently) the window a label was settled in. */ |
| 83 | + horizonEnd: string; |
| 84 | + truths: BenchmarkGroundTruth[]; |
| 85 | + /** Denominator discipline, published rather than left implicit: `scoreable` is what a scorer divides by; |
| 86 | + * `unresolved` is excluded from it, and a climbing `unresolvedRate` is the signal the horizon is wrong. */ |
| 87 | + coverage: { |
| 88 | + workUnits: number; |
| 89 | + scoreable: number; |
| 90 | + unresolved: number; |
| 91 | + /** `unresolved / workUnits`, 3dp. `null` for an empty work-unit set — never 0, which would read as |
| 92 | + * "a fully resolved benchmark" (the same null-not-zero discipline as PublicRulePrecisionRow). */ |
| 93 | + unresolvedRate: number | null; |
| 94 | + }; |
| 95 | +}; |
| 96 | + |
| 97 | +const MS_PER_DAY = 24 * 60 * 60 * 1000; |
| 98 | + |
| 99 | +/** The inclusive horizon end for a snapshot. Exported so the harness, the scorer, and any third-party |
| 100 | + * verifier compute the SAME instant rather than three slightly different ones. */ |
| 101 | +export function benchmarkHorizonEnd(frozenAt: string, horizonDays: number): string { |
| 102 | + return new Date(Date.parse(frozenAt) + horizonDays * MS_PER_DAY).toISOString(); |
| 103 | +} |
| 104 | + |
| 105 | +function withinHorizon(occurredAt: string, frozenAtMs: number, horizonEndMs: number): boolean { |
| 106 | + const at = Date.parse(occurredAt); |
| 107 | + return Number.isFinite(at) && at >= frozenAtMs && at <= horizonEndMs; |
| 108 | +} |
| 109 | + |
| 110 | +/** |
| 111 | + * Derive the settled, reversal-aware ground-truth set for one snapshot's work units. |
| 112 | + * |
| 113 | + * PURE: no clock, no IO, no randomness. Everything is decided from events dated inside the window, which is |
| 114 | + * what makes the result stable regardless of when extraction runs (asserted as an invariant in the tests). |
| 115 | + * |
| 116 | + * `workUnitIds` is the authoritative roster from the snapshot — a work unit with no qualifying event is |
| 117 | + * emitted as `unresolved` rather than omitted, so the unresolved rate has an honest denominator and a |
| 118 | + * consumer can tell "nobody acted" apart from "the extractor forgot about it". |
| 119 | + */ |
| 120 | +export function deriveBenchmarkGroundTruth(input: { |
| 121 | + snapshotRef: string; |
| 122 | + frozenAt: string; |
| 123 | + horizonDays: number; |
| 124 | + workUnitIds: readonly string[]; |
| 125 | + events: readonly RealizedMaintainerEvent[]; |
| 126 | + reversals?: readonly RealizedReversalEvent[] | undefined; |
| 127 | +}): BenchmarkGroundTruthSet { |
| 128 | + const frozenAtMs = Date.parse(input.frozenAt); |
| 129 | + const horizonEnd = benchmarkHorizonEnd(input.frozenAt, input.horizonDays); |
| 130 | + const horizonEndMs = Date.parse(horizonEnd); |
| 131 | + |
| 132 | + // The SETTLED state is the LAST qualifying action in the window, not the first: a maintainer who labels, |
| 133 | + // then requests changes, then merges has settled on the merge. Ties on identical timestamps keep the |
| 134 | + // earlier-listed event, so a caller's stable input order yields a stable label. |
| 135 | + const settled = new Map<string, RealizedMaintainerEvent>(); |
| 136 | + for (const event of input.events) { |
| 137 | + if (!withinHorizon(event.occurredAt, frozenAtMs, horizonEndMs)) continue; |
| 138 | + const current = settled.get(event.workUnitId); |
| 139 | + if (!current || Date.parse(event.occurredAt) > Date.parse(current.occurredAt)) settled.set(event.workUnitId, event); |
| 140 | + } |
| 141 | + |
| 142 | + // A reversal counts only when it lands inside the same horizon AND strictly after the action it overturns |
| 143 | + // — a reversal predating the settled action reversed something else, and counting it would mislabel an |
| 144 | + // action nobody has yet overturned. The EARLIEST such reversal is recorded: the first overturning is the |
| 145 | + // one that makes the action non-confirmed. |
| 146 | + const reversalFor = (workUnitId: string, settledAt: string): { kind: RealizedReversalKind; occurredAt: string } | null => { |
| 147 | + let best: RealizedReversalEvent | undefined; |
| 148 | + for (const reversal of input.reversals ?? []) { |
| 149 | + if (reversal.workUnitId !== workUnitId) continue; |
| 150 | + if (!withinHorizon(reversal.occurredAt, frozenAtMs, horizonEndMs)) continue; |
| 151 | + if (Date.parse(reversal.occurredAt) <= Date.parse(settledAt)) continue; |
| 152 | + if (!best || Date.parse(reversal.occurredAt) < Date.parse(best.occurredAt)) best = reversal; |
| 153 | + } |
| 154 | + return best ? { kind: best.kind, occurredAt: best.occurredAt } : null; |
| 155 | + }; |
| 156 | + |
| 157 | + const truths: BenchmarkGroundTruth[] = []; |
| 158 | + let unresolved = 0; |
| 159 | + for (const workUnitId of input.workUnitIds) { |
| 160 | + const event = settled.get(workUnitId); |
| 161 | + if (!event) { |
| 162 | + unresolved += 1; |
| 163 | + truths.push({ workUnitId, outcome: "unresolved" }); |
| 164 | + continue; |
| 165 | + } |
| 166 | + truths.push({ |
| 167 | + workUnitId, |
| 168 | + outcome: "settled", |
| 169 | + action: event.action, |
| 170 | + // Spreads, not literal `undefined` values: an absent parameter must be OMITTED (the same |
| 171 | + // exactOptionalPropertyTypes discipline every sibling module follows), so a `merge` truth cannot |
| 172 | + // carry a present-but-undefined `reasonClass` that a strict comparison would trip over. |
| 173 | + ...(event.action === "close" && event.reasonClass !== undefined ? { reasonClass: event.reasonClass } : {}), |
| 174 | + ...(event.action === "label" && event.labels !== undefined ? { labels: [...event.labels] } : {}), |
| 175 | + settledAt: event.occurredAt, |
| 176 | + reversal: reversalFor(workUnitId, event.occurredAt), |
| 177 | + }); |
| 178 | + } |
| 179 | + |
| 180 | + const workUnits = input.workUnitIds.length; |
| 181 | + return { |
| 182 | + schemaVersion: 1, |
| 183 | + snapshotRef: input.snapshotRef, |
| 184 | + horizonDays: input.horizonDays, |
| 185 | + frozenAt: input.frozenAt, |
| 186 | + horizonEnd, |
| 187 | + truths, |
| 188 | + coverage: { |
| 189 | + workUnits, |
| 190 | + scoreable: workUnits - unresolved, |
| 191 | + unresolved, |
| 192 | + unresolvedRate: workUnits === 0 ? null : Math.round((unresolved / workUnits) * 1000) / 1000, |
| 193 | + }, |
| 194 | + }; |
| 195 | +} |
| 196 | + |
| 197 | +/** The work units a scorer may divide by — `unresolved` never enters the denominator (#9261 requirement 4). |
| 198 | + * Exported as the single definition of that rule so no scorer can re-implement it slightly differently. */ |
| 199 | +export function scoreableGroundTruths( |
| 200 | + set: BenchmarkGroundTruthSet, |
| 201 | +): Array<Extract<BenchmarkGroundTruth, { outcome: "settled" }>> { |
| 202 | + return set.truths.filter((truth): truth is Extract<BenchmarkGroundTruth, { outcome: "settled" }> => truth.outcome === "settled"); |
| 203 | +} |
0 commit comments