Skip to content

Commit 43b02c9

Browse files
kai392RealDiligent
andauthored
feat(calibration): deterministic seeded held-out/visible split of the backtest corpus (#8087) (#8097)
Co-authored-by: RealDiligent <brave.challenge007@gmail.com>
1 parent 653a391 commit 43b02c9

3 files changed

Lines changed: 159 additions & 0 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Deterministic held-out/visible corpus split (#8087) -- the dual-target evaluation method: iterate a
2+
// candidate rule against the visible slice, score it against BOTH slices, so a fix can't be hand-tuned to
3+
// just the specific incidents already known about. Reuses the same content-hash approach as
4+
// stableProposalId in ../miner/deny-hook-synthesis.ts (sha256 over a composite key): a case's assignment
5+
// depends only on (seed, ruleId, targetKey) -- never on its position or on cases.length -- so a corpus
6+
// that grows over time never reshuffles which already-processed cases were previously held out.
7+
//
8+
// Same purity contract as the rest of this module family: no IO, no Math.random(), no wall-clock reads.
9+
10+
import { createHash } from "node:crypto";
11+
import type { BacktestCase } from "./backtest-corpus.js";
12+
13+
/**
14+
* Partition `cases` into a visible slice and a held-out slice of roughly `heldOutFraction` of the corpus.
15+
* Deterministic: sha256(`${seed}:${ruleId}:${targetKey}`), first 8 hex chars as a base-16 integer over
16+
* 0xffffffff, held out when strictly below `heldOutFraction` -- identical inputs always produce
17+
* byte-identical output. Each case keeps its original input-order position within its assigned bucket
18+
* (no sorting, no shuffling). Throws when `heldOutFraction` is outside the inclusive [0, 1] range.
19+
*/
20+
export function splitBacktestCorpus(
21+
cases: readonly BacktestCase[],
22+
heldOutFraction: number,
23+
seed: string,
24+
): { visible: BacktestCase[]; heldOut: BacktestCase[] } {
25+
// Negated compound form so a NaN fraction also fails closed instead of silently splitting nothing out.
26+
if (!(heldOutFraction >= 0 && heldOutFraction <= 1)) {
27+
throw new Error(`invalid_held_out_fraction: ${heldOutFraction}`);
28+
}
29+
const visible: BacktestCase[] = [];
30+
const heldOut: BacktestCase[] = [];
31+
for (const backtestCase of cases) {
32+
const digest = createHash("sha256")
33+
.update(`${seed}:${backtestCase.ruleId}:${backtestCase.targetKey}`)
34+
.digest("hex");
35+
const value = parseInt(digest.slice(0, 8), 16) / 0xffffffff;
36+
if (value < heldOutFraction) heldOut.push(backtestCase);
37+
else visible.push(backtestCase);
38+
}
39+
return { visible, heldOut };
40+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import assert from "node:assert/strict";
2+
import { test } from "node:test";
3+
4+
import type { BacktestCase } from "../dist/index.js";
5+
import { splitBacktestCorpus } from "../dist/calibration/backtest-split.js";
6+
7+
function corpusCase(targetKey: string, overrides: Partial<BacktestCase> = {}): BacktestCase {
8+
return {
9+
ruleId: "missing_linked_issue",
10+
targetKey,
11+
outcome: "block",
12+
label: "confirmed",
13+
firedAt: "2026-07-22T00:00:00.000Z",
14+
decidedAt: "2026-07-22T01:00:00.000Z",
15+
...overrides,
16+
};
17+
}
18+
19+
/** A dozen distinct targets -- enough that a 0.5 split reliably lands cases in BOTH buckets and that two
20+
* different seeds reliably disagree on at least one case, without depending on any specific hash value. */
21+
const corpus = Array.from({ length: 12 }, (_, index) => corpusCase(`acme/widgets#${index + 1}`));
22+
23+
test("splitBacktestCorpus: heldOutFraction 0 keeps every case visible", () => {
24+
const { visible, heldOut } = splitBacktestCorpus(corpus, 0, "seed-a");
25+
assert.deepEqual(visible, corpus);
26+
assert.deepEqual(heldOut, []);
27+
});
28+
29+
test("splitBacktestCorpus: heldOutFraction 1 holds every case out", () => {
30+
const { visible, heldOut } = splitBacktestCorpus(corpus, 1, "seed-a");
31+
assert.deepEqual(heldOut, corpus);
32+
assert.deepEqual(visible, []);
33+
});
34+
35+
test("splitBacktestCorpus: identical inputs produce byte-identical output, including per-bucket order", () => {
36+
const first = splitBacktestCorpus(corpus, 0.5, "seed-a");
37+
const second = splitBacktestCorpus(corpus, 0.5, "seed-a");
38+
assert.deepEqual(second, first);
39+
});
40+
41+
test("splitBacktestCorpus: preserves each case's original input order within its bucket, with both buckets populated", () => {
42+
const { visible, heldOut } = splitBacktestCorpus(corpus, 0.5, "seed-a");
43+
assert.ok(visible.length > 0 && heldOut.length > 0, "0.5 over 12 distinct targets must populate both buckets");
44+
const inputIndex = (backtestCase: BacktestCase) => corpus.indexOf(backtestCase);
45+
for (const bucket of [visible, heldOut]) {
46+
const order = bucket.map(inputIndex);
47+
assert.deepEqual(order, [...order].sort((a, b) => a - b));
48+
}
49+
});
50+
51+
test("splitBacktestCorpus: a different seed produces a different split for at least one case", () => {
52+
const first = splitBacktestCorpus(corpus, 0.5, "seed-a");
53+
const second = splitBacktestCorpus(corpus, 0.5, "seed-b");
54+
assert.notDeepEqual(
55+
{ visible: first.visible.map((c) => c.targetKey), heldOut: first.heldOut.map((c) => c.targetKey) },
56+
{ visible: second.visible.map((c) => c.targetKey), heldOut: second.heldOut.map((c) => c.targetKey) },
57+
);
58+
});
59+
60+
test("splitBacktestCorpus: throws on an out-of-range heldOutFraction in both directions, naming the value", () => {
61+
assert.throws(() => splitBacktestCorpus(corpus, -0.1, "seed-a"), /invalid_held_out_fraction: -0\.1/);
62+
assert.throws(() => splitBacktestCorpus(corpus, 1.5, "seed-a"), /invalid_held_out_fraction: 1\.5/);
63+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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 artifact for the workspace
6+
// suite). Same pattern as backtest-corpus-engine.test.ts / miner-deny-hook-synthesis.test.ts.
7+
import { splitBacktestCorpus } from "../../packages/loopover-engine/src/calibration/backtest-split";
8+
import type { BacktestCase } from "../../packages/loopover-engine/src/calibration/backtest-corpus";
9+
10+
function corpusCase(targetKey: string): BacktestCase {
11+
return {
12+
ruleId: "missing_linked_issue",
13+
targetKey,
14+
outcome: "block",
15+
label: "confirmed",
16+
firedAt: "2026-07-22T00:00:00.000Z",
17+
decidedAt: "2026-07-22T01:00:00.000Z",
18+
};
19+
}
20+
21+
// A dozen distinct targets -- enough that a 0.5 split reliably populates BOTH buckets and two different
22+
// seeds reliably disagree on at least one case, without depending on any specific hash value.
23+
const corpus = Array.from({ length: 12 }, (_, index) => corpusCase(`acme/widgets#${index + 1}`));
24+
25+
describe("splitBacktestCorpus (#8087)", () => {
26+
it("keeps every case visible at fraction 0 and holds every case out at fraction 1", () => {
27+
expect(splitBacktestCorpus(corpus, 0, "seed-a")).toEqual({ visible: corpus, heldOut: [] });
28+
expect(splitBacktestCorpus(corpus, 1, "seed-a")).toEqual({ visible: [], heldOut: corpus });
29+
});
30+
31+
it("is deterministic: identical inputs produce byte-identical output, including per-bucket order", () => {
32+
const first = splitBacktestCorpus(corpus, 0.5, "seed-a");
33+
expect(splitBacktestCorpus(corpus, 0.5, "seed-a")).toEqual(first);
34+
});
35+
36+
it("preserves original input order within each bucket, with both buckets populated at 0.5", () => {
37+
const { visible, heldOut } = splitBacktestCorpus(corpus, 0.5, "seed-a");
38+
expect(visible.length).toBeGreaterThan(0);
39+
expect(heldOut.length).toBeGreaterThan(0);
40+
for (const bucket of [visible, heldOut]) {
41+
const order = bucket.map((backtestCase) => corpus.indexOf(backtestCase));
42+
expect(order).toEqual([...order].sort((a, b) => a - b));
43+
}
44+
});
45+
46+
it("produces a different split for at least one case when only the seed changes", () => {
47+
const withSeedA = splitBacktestCorpus(corpus, 0.5, "seed-a");
48+
const withSeedB = splitBacktestCorpus(corpus, 0.5, "seed-b");
49+
expect(withSeedB.heldOut.map((c) => c.targetKey)).not.toEqual(withSeedA.heldOut.map((c) => c.targetKey));
50+
});
51+
52+
it("throws on an out-of-range heldOutFraction in both directions, naming the invalid value", () => {
53+
expect(() => splitBacktestCorpus(corpus, -0.1, "seed-a")).toThrow("invalid_held_out_fraction: -0.1");
54+
expect(() => splitBacktestCorpus(corpus, 1.5, "seed-a")).toThrow("invalid_held_out_fraction: 1.5");
55+
});
56+
});

0 commit comments

Comments
 (0)