Skip to content

Commit 1819298

Browse files
authored
feat(benchmark): reversal-aware realized-history ground truth (#9261) (#9598)
The labels every candidate proposal is scored against: for each work unit in a snapshot, what the maintainer ACTUALLY did within the prediction horizon, emitted in #9260's exact action vocabulary so proposal and outcome compare with no translation layer that could drift. Three rules the surrounding system already believes, now enforced here: - REVERSAL-AWARE SETTLEMENT. A merge later reverted is not a clean merge. The label is the settled state at horizon end with the reversal recorded alongside -- the treatment that makes public-rule-precision.ts's precision honest. The vocabulary is the established reversal_reopened/reverted/superseded set, never a benchmark-local re-invention, so benchmark and internal backtest ground truth cannot disagree about the same event. A reversal counts only inside the horizon AND strictly after the action it overturns; the earliest one wins. - UNRESOLVED IS EXPLICIT AND EXCLUDED. No action inside the horizon is a first-class `unresolved`, never silently a correct abstention (which would reward declining exactly what nobody decided) nor an incorrect prediction (which would punish the horizon being short). It leaves the denominator, and the unresolved RATE is published so a climbing rate reads as "the horizon is wrong" rather than as a scoring artifact. - STABILITY. Every decision is a pure function of events dated inside the window, so a later event -- including a later reversal -- cannot move a settled label. Asserted directly: the same input plus a year of post-horizon history yields byte-identical output. Closes #9261
1 parent b543e1e commit 1819298

3 files changed

Lines changed: 414 additions & 0 deletions

File tree

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
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+
}

packages/loopover-engine/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ export * from "./calibration/reliability-curve.js";
184184
export * from "./calibration/attestation-envelope.js";
185185
export * from "./calibration/attester.js";
186186
export * from "./calibration/benchmark-proposal.js";
187+
export * from "./calibration/benchmark-ground-truth.js";
187188
export {
188189
GOVERNOR_LEDGER_EVENT_TYPES,
189190
normalizeGovernorLedgerEvent,

0 commit comments

Comments
 (0)