Skip to content

Commit bbf74fc

Browse files
committed
feat(calibration): per-provider reviewer track records from the consensus corpus (#8228)
1 parent 0c8d3d6 commit bbf74fc

4 files changed

Lines changed: 345 additions & 0 deletions

File tree

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// Per-provider reviewer track records (#8228, epic #8211 track F). Dual-reviewer consensus events exist
2+
// (reviewer-consensus-calibration.ts) and reversal labels now say which calls were RIGHT; this module joins
3+
// the two: measured precision per reviewer identity, per repo and overall, over decided cases. Providers are
4+
// opaque ids — no provider names hardcoded, no config coupling. Mirrors the consensus module's ingestion
5+
// discipline (typed inputs, explicit vote vocabulary) and the #8085 scorer's null-below-the-sample-floor
6+
// rule: a slice that decided nothing reports null, never 0.
7+
//
8+
// JOIN SEMANTICS (documented once, tested as invariants):
9+
// • A provider signal joins a labeled BacktestCase by exact `targetKey`. A signal whose target carries no
10+
// decided label is counted (`signals`) but contributes to no rate — undecided is not evidence.
11+
// • A provider "supported the firing" when it voted `fail` (the defect-flagging vote in the consensus
12+
// vocabulary). `precision` = P(label "confirmed" | this provider voted fail) — the same
13+
// correct-firing-as-numerator discipline as computeRulePrecision, at reviewer grain.
14+
// • `agreementRate` = share of this provider's decided votes that MATCHED the human label (fail↔confirmed,
15+
// pass/warn↔reversed) — a symmetric accuracy measure precision alone can't give a rarely-failing provider.
16+
// • `consensusRate` = share of this provider's signals on targets that another provider ALSO reviewed
17+
// where the two votes agreed (both-fail or both-non-fail); `splitRate` is its complement. Null when the
18+
// provider shares no targets — one-provider corpora have no consensus to measure.
19+
//
20+
// Same purity contract as the rest of this module family: no IO, no randomness, no wall-clock reads.
21+
22+
import type { BacktestCase } from "./backtest-corpus.js";
23+
import type { ReviewerConsensusVote } from "../reviewer-consensus-calibration.js";
24+
25+
export type ProviderReviewSignal = {
26+
/** Opaque reviewer identity — an id, never a hardcoded provider name. */
27+
provider: string;
28+
repoFullName: string;
29+
/** Joins to {@link BacktestCase.targetKey} (`owner/repo#N`). */
30+
targetKey: string;
31+
vote: ReviewerConsensusVote;
32+
};
33+
34+
export type ProviderTrackRecord = {
35+
provider: string;
36+
/** The repo this row aggregates, or null for the provider's overall rollup across every repo. */
37+
repoFullName: string | null;
38+
signals: number;
39+
decided: number;
40+
confirmed: number;
41+
reversed: number;
42+
precision: number | null;
43+
agreementRate: number | null;
44+
consensusRate: number | null;
45+
splitRate: number | null;
46+
};
47+
48+
type MutableStats = {
49+
signals: number;
50+
decided: number;
51+
confirmed: number;
52+
reversed: number;
53+
failDecided: number;
54+
failConfirmed: number;
55+
agreed: number;
56+
shared: number;
57+
consensus: number;
58+
};
59+
60+
function emptyStats(): MutableStats {
61+
return { signals: 0, decided: 0, confirmed: 0, reversed: 0, failDecided: 0, failConfirmed: 0, agreed: 0, shared: 0, consensus: 0 };
62+
}
63+
64+
function toRecord(provider: string, repoFullName: string | null, stats: MutableStats): ProviderTrackRecord {
65+
return {
66+
provider,
67+
repoFullName,
68+
signals: stats.signals,
69+
decided: stats.decided,
70+
confirmed: stats.confirmed,
71+
reversed: stats.reversed,
72+
precision: stats.failDecided > 0 ? stats.failConfirmed / stats.failDecided : null,
73+
agreementRate: stats.decided > 0 ? stats.agreed / stats.decided : null,
74+
consensusRate: stats.shared > 0 ? stats.consensus / stats.shared : null,
75+
splitRate: stats.shared > 0 ? (stats.shared - stats.consensus) / stats.shared : null,
76+
};
77+
}
78+
79+
/**
80+
* Compute per-(provider, repo) and per-provider-overall track records from reviewer signals joined against
81+
* a labeled corpus, per the join semantics documented in this module's header. Deterministic ordering:
82+
* providers ascending, and within each provider the overall rollup (repoFullName null) first, then repos
83+
* ascending. Aggregates only — provider ids, repo names, and numbers; never target keys or vote payloads.
84+
*/
85+
export function computeProviderTrackRecords(
86+
signals: readonly ProviderReviewSignal[],
87+
cases: readonly BacktestCase[],
88+
): ProviderTrackRecord[] {
89+
const labelByTarget = new Map<string, BacktestCase["label"]>();
90+
for (const backtestCase of cases) labelByTarget.set(backtestCase.targetKey, backtestCase.label);
91+
92+
// Which providers reviewed each target, with their fail/non-fail stance — the consensus/split join.
93+
const stancesByTarget = new Map<string, Map<string, boolean>>();
94+
for (const signal of signals) {
95+
let stances = stancesByTarget.get(signal.targetKey);
96+
if (stances === undefined) {
97+
stances = new Map();
98+
stancesByTarget.set(signal.targetKey, stances);
99+
}
100+
stances.set(signal.provider, signal.vote === "fail");
101+
}
102+
103+
const perRepo = new Map<string, Map<string, MutableStats>>(); // provider → repo → stats
104+
const overall = new Map<string, MutableStats>();
105+
for (const signal of signals) {
106+
let repos = perRepo.get(signal.provider);
107+
if (repos === undefined) {
108+
repos = new Map();
109+
perRepo.set(signal.provider, repos);
110+
}
111+
let repoStats = repos.get(signal.repoFullName);
112+
if (repoStats === undefined) {
113+
repoStats = emptyStats();
114+
repos.set(signal.repoFullName, repoStats);
115+
}
116+
let overallStats = overall.get(signal.provider);
117+
if (overallStats === undefined) {
118+
overallStats = emptyStats();
119+
overall.set(signal.provider, overallStats);
120+
}
121+
122+
const label = labelByTarget.get(signal.targetKey);
123+
const votedFail = signal.vote === "fail";
124+
const stances = stancesByTarget.get(signal.targetKey)!;
125+
for (const stats of [repoStats, overallStats]) {
126+
stats.signals += 1;
127+
if (label !== undefined) {
128+
stats.decided += 1;
129+
if (label === "confirmed") stats.confirmed += 1;
130+
else stats.reversed += 1;
131+
if (votedFail) {
132+
stats.failDecided += 1;
133+
if (label === "confirmed") stats.failConfirmed += 1;
134+
}
135+
// Matched the human: a fail vote on a confirmed firing, or a non-fail vote on a reversed one.
136+
if (votedFail === (label === "confirmed")) stats.agreed += 1;
137+
}
138+
if (stances.size > 1) {
139+
stats.shared += 1;
140+
let agreeingOthers = 0;
141+
let others = 0;
142+
for (const [otherProvider, otherFail] of stances) {
143+
if (otherProvider === signal.provider) continue;
144+
others += 1;
145+
if (otherFail === votedFail) agreeingOthers += 1;
146+
}
147+
if (agreeingOthers === others) stats.consensus += 1;
148+
}
149+
}
150+
}
151+
152+
const records: ProviderTrackRecord[] = [];
153+
for (const provider of [...overall.keys()].sort()) {
154+
records.push(toRecord(provider, null, overall.get(provider)!));
155+
const repos = perRepo.get(provider)!;
156+
for (const repoFullName of [...repos.keys()].sort()) {
157+
records.push(toRecord(provider, repoFullName, repos.get(repoFullName)!));
158+
}
159+
}
160+
return records;
161+
}

packages/loopover-engine/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ export * from "./calibration/backtest-track-record.js";
175175
// same way scripts/backtest-corpus-export.ts already imports BacktestCase.
176176
export * from "./calibration/backtest-split.js";
177177
export * from "./calibration/backtest-threshold.js";
178+
export * from "./calibration/provider-track-record.js";
178179
export {
179180
GOVERNOR_LEDGER_EVENT_TYPES,
180181
normalizeGovernorLedgerEvent,
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import assert from "node:assert/strict";
2+
import { test } from "node:test";
3+
4+
import { computeProviderTrackRecords, type BacktestCase, type ProviderReviewSignal } from "../dist/index.js";
5+
6+
function labeled(targetKey: string, label: BacktestCase["label"]): BacktestCase {
7+
return {
8+
ruleId: "ai_consensus_defect",
9+
targetKey,
10+
outcome: "close",
11+
label,
12+
firedAt: "2026-07-01T00:00:00.000Z",
13+
decidedAt: "2026-07-02T00:00:00.000Z",
14+
};
15+
}
16+
17+
function signal(provider: string, targetKey: string, vote: ProviderReviewSignal["vote"]): ProviderReviewSignal {
18+
return { provider, repoFullName: "acme/widgets", targetKey, vote };
19+
}
20+
21+
test("barrel: the public entrypoint re-exports the provider track-record computation (#8228)", () => {
22+
assert.equal(typeof computeProviderTrackRecords, "function");
23+
});
24+
25+
test("both-provider round-trip: precision + agreement + consensus rates land per provider", () => {
26+
const records = computeProviderTrackRecords(
27+
[
28+
signal("a", "acme/widgets#1", "fail"),
29+
signal("b", "acme/widgets#1", "fail"),
30+
signal("a", "acme/widgets#2", "fail"),
31+
signal("b", "acme/widgets#2", "pass"),
32+
],
33+
[labeled("acme/widgets#1", "confirmed"), labeled("acme/widgets#2", "reversed")],
34+
);
35+
const aOverall = records.find((r) => r.provider === "a" && r.repoFullName === null)!;
36+
assert.equal(aOverall.precision, 0.5);
37+
assert.equal(aOverall.consensusRate, 0.5);
38+
const bOverall = records.find((r) => r.provider === "b" && r.repoFullName === null)!;
39+
assert.equal(bOverall.precision, 1);
40+
assert.equal(bOverall.agreementRate, 1);
41+
});
42+
43+
test("null discipline: no fail votes -> null precision; no shared targets -> null consensus/split", () => {
44+
const records = computeProviderTrackRecords(
45+
[signal("solo", "acme/widgets#1", "pass")],
46+
[labeled("acme/widgets#1", "reversed")],
47+
);
48+
const overall = records.find((r) => r.repoFullName === null)!;
49+
assert.equal(overall.precision, null);
50+
assert.equal(overall.consensusRate, null);
51+
assert.equal(overall.splitRate, null);
52+
assert.equal(overall.agreementRate, 1);
53+
});
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
// Import the engine SOURCE directly (not the built dist) -- coverage.include lists
4+
// packages/loopover-engine/src/**, so only a source-path import exercises the .ts these branches live in
5+
// (the dist-importing twin in packages/loopover-engine/test/ covers the built barrel for the workspace
6+
// suite). Same pattern as backtest-corpus-engine.test.ts / repo-corpus-engine.test.ts.
7+
import {
8+
computeProviderTrackRecords,
9+
type ProviderReviewSignal,
10+
} from "../../packages/loopover-engine/src/calibration/provider-track-record";
11+
import type { BacktestCase } from "../../packages/loopover-engine/src/calibration/backtest-corpus";
12+
13+
function labeled(targetKey: string, label: BacktestCase["label"]): BacktestCase {
14+
return {
15+
ruleId: "ai_consensus_defect",
16+
targetKey,
17+
outcome: "close",
18+
label,
19+
firedAt: "2026-07-01T00:00:00.000Z",
20+
decidedAt: "2026-07-02T00:00:00.000Z",
21+
};
22+
}
23+
24+
function signal(provider: string, targetKey: string, vote: ProviderReviewSignal["vote"], repoFullName = "acme/widgets"): ProviderReviewSignal {
25+
return { provider, repoFullName, targetKey, vote };
26+
}
27+
28+
describe("computeProviderTrackRecords (#8228)", () => {
29+
it("computes precision, agreement, and consensus/split rates for a both-provider corpus, per repo and overall", () => {
30+
const cases = [
31+
labeled("acme/widgets#1", "confirmed"),
32+
labeled("acme/widgets#2", "reversed"),
33+
labeled("acme/widgets#3", "confirmed"),
34+
];
35+
const signals = [
36+
// #1: both fail on a confirmed firing — consensus, both correct.
37+
signal("provider-a", "acme/widgets#1", "fail"),
38+
signal("provider-b", "acme/widgets#1", "fail"),
39+
// #2: split — a fails (wrong: label reversed), b passes (right).
40+
signal("provider-a", "acme/widgets#2", "fail"),
41+
signal("provider-b", "acme/widgets#2", "pass"),
42+
// #3: only a reviews it, warns (non-fail on a confirmed firing — disagreed with the human).
43+
signal("provider-a", "acme/widgets#3", "warn"),
44+
];
45+
const records = computeProviderTrackRecords(signals, cases);
46+
47+
const aOverall = records.find((r) => r.provider === "provider-a" && r.repoFullName === null)!;
48+
expect(aOverall).toMatchObject({
49+
signals: 3,
50+
decided: 3,
51+
confirmed: 2,
52+
reversed: 1,
53+
precision: 0.5, // of a's 2 fail votes, 1 hit a confirmed firing
54+
agreementRate: 1 / 3, // matched the human only on #1
55+
consensusRate: 0.5, // shared #1 (agreed) and #2 (split)
56+
splitRate: 0.5,
57+
});
58+
const bOverall = records.find((r) => r.provider === "provider-b" && r.repoFullName === null)!;
59+
expect(bOverall).toMatchObject({ signals: 2, decided: 2, precision: 1, agreementRate: 1, consensusRate: 0.5, splitRate: 0.5 });
60+
61+
// Single-repo corpus: each provider's per-repo row equals its overall rollup.
62+
const aRepo = records.find((r) => r.provider === "provider-a" && r.repoFullName === "acme/widgets")!;
63+
expect(aRepo).toMatchObject({ signals: aOverall.signals, decided: aOverall.decided, precision: aOverall.precision });
64+
});
65+
66+
it("keeps a one-provider corpus's consensus/split rates null — no shared targets, no consensus to measure", () => {
67+
const records = computeProviderTrackRecords(
68+
[signal("solo", "acme/widgets#1", "fail"), signal("solo", "acme/widgets#2", "pass")],
69+
[labeled("acme/widgets#1", "confirmed"), labeled("acme/widgets#2", "reversed")],
70+
);
71+
const overall = records.find((r) => r.repoFullName === null)!;
72+
expect(overall.consensusRate).toBeNull();
73+
expect(overall.splitRate).toBeNull();
74+
expect(overall.precision).toBe(1);
75+
expect(overall.agreementRate).toBe(1);
76+
});
77+
78+
it("reports null (never 0) precision below the sample floor: undecided targets and providers that never voted fail", () => {
79+
const records = computeProviderTrackRecords(
80+
[
81+
signal("quiet", "acme/widgets#9", "pass"), // undecided target — no label exists
82+
signal("quiet", "acme/widgets#1", "warn"), // decided, but never a fail vote
83+
],
84+
[labeled("acme/widgets#1", "reversed")],
85+
);
86+
const overall = records.find((r) => r.repoFullName === null)!;
87+
expect(overall).toMatchObject({ signals: 2, decided: 1, precision: null, agreementRate: 1 });
88+
});
89+
90+
it("rolls per-repo rows up into the overall row exactly, with deterministic provider→overall→repo ordering", () => {
91+
const cases = [labeled("acme/widgets#1", "confirmed"), labeled("acme/gadgets#2", "confirmed")];
92+
const signals = [
93+
signal("zeta", "acme/widgets#1", "fail", "acme/widgets"),
94+
signal("zeta", "acme/gadgets#2", "fail", "acme/gadgets"),
95+
signal("alpha", "acme/widgets#1", "fail", "acme/widgets"),
96+
];
97+
const records = computeProviderTrackRecords(signals, cases);
98+
expect(records.map((r) => [r.provider, r.repoFullName])).toEqual([
99+
["alpha", null],
100+
["alpha", "acme/widgets"],
101+
["zeta", null],
102+
["zeta", "acme/gadgets"],
103+
["zeta", "acme/widgets"],
104+
]);
105+
const zetaOverall = records.find((r) => r.provider === "zeta" && r.repoFullName === null)!;
106+
const zetaRepos = records.filter((r) => r.provider === "zeta" && r.repoFullName !== null);
107+
expect(zetaRepos.reduce((sum, r) => sum + r.decided, 0)).toBe(zetaOverall.decided);
108+
expect(zetaRepos.reduce((sum, r) => sum + r.signals, 0)).toBe(zetaOverall.signals);
109+
// Determinism: identical inputs yield the identical result.
110+
expect(computeProviderTrackRecords(signals, cases)).toEqual(records);
111+
});
112+
113+
it("never leaks target keys into any returned shape — provider ids, repo names, and numbers only", () => {
114+
const records = computeProviderTrackRecords(
115+
[signal("provider-a", "acme/widgets#42", "fail")],
116+
[labeled("acme/widgets#42", "confirmed")],
117+
);
118+
expect(JSON.stringify(records)).not.toContain("#42");
119+
});
120+
121+
it("reports null agreement (never 0) for a provider whose every signal is undecided", () => {
122+
const records = computeProviderTrackRecords([signal("unjoined", "acme/widgets#404", "fail")], []);
123+
const overall = records.find((r) => r.repoFullName === null)!;
124+
expect(overall).toMatchObject({ signals: 1, decided: 0, precision: null, agreementRate: null });
125+
});
126+
127+
it("returns an empty list for empty inputs", () => {
128+
expect(computeProviderTrackRecords([], [])).toEqual([]);
129+
});
130+
});

0 commit comments

Comments
 (0)