Skip to content

Commit aae63ae

Browse files
authored
feat(calibration): slop-corpus replay backfill + replay-derived provider track records (#8281)
* feat(calibration): slop-corpus replay backfill — phase 3 of the backfill family (#8277) Replays the deterministic slop scorer over the archived raw-context diffs (the #8130/#8170 corpus manifest) and synthesizes provenance-tagged slop_gate_score fired/override pairs labeled by realized outcomes — the phase-1 'mapping a' counterfactual framing, applied to the #8224 knob. Signal-subset honesty is enforced by ALLOWLIST: only the three diff-derivable signals may contribute (the scorer fires empty_pr_description on an UNDEFINED description — right live, inflating here), risk/band recompute from exactly their weights, and every row records the computed signal codes beside the provenance tag. Zero GitHub traffic; idempotent upserts; wrangler/--pg dual path. Applied + verified on both stores (460 fired + 460 override rows each): distribution zero 165 / low 3 / elevated 292 / high 0 — all mass below the 0.60 ceiling, so the rows are NEUTRAL to ladder comparisons by construction (they feed the reliability curve and drift reads without being able to fabricate proposals; the discriminating source for the ladder band remains live full-signal capture). * feat(calibration): replay-derived provider track records adapter (#8278) Maps the #8221 harness's cached per-fixture verdicts onto ProviderReviewSignal (would_flag => fail, would_not_flag => pass, abstentions yield NO signal) and aggregates with computeProviderTrackRecords — the identical function live reviewer_vote rows feed. Offline report only: nothing persists, and the rendered table carries the replay-derived disclaimer (#8278's segregation rule). First same-seed two-provider table recorded on #8229. * fix(scripts): exactOptionalPropertyTypes on the provider-replay arg parser * fix(scripts): entry-guard the phase-3 CLI wrappers so test imports never process.exit Both wrappers export pure helpers the unit suites import; an import-time main().then(process.exit) fails the whole vitest run as an unhandled rejection even with every test green (CI shard 2's exact failure — all 6568 tests passed). The audit-quality-gate-min-score.ts direct-execution idiom fixes it; dry-run CLI behavior unchanged.
1 parent 9f03459 commit aae63ae

5 files changed

Lines changed: 611 additions & 0 deletions
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// Pure core for the slop-corpus replay backfill (#8277) — phase 3 of the calibration backfill family.
2+
// The slop scorer is deterministic and in-repo, so the #8224 report-only knob's evidence can be
3+
// manufactured honestly: replay `buildSlopAssessment` over each archived PR diff (the #8130/#8170
4+
// raw-context corpus) and synthesize provenance-tagged `slop_gate_score` fired/override pairs labeled by
5+
// what humans actually did — the same counterfactual "mapping a" framing the phase-1 close-confidence
6+
// backfill documented. This core is transform-only (no IO), mirroring backfill-calibration-corpus-core.ts;
7+
// the CLI wrapper owns the manifest read and the wrangler/pg writes.
8+
//
9+
// SIGNAL-SUBSET HONESTY: only the diff-derivable signals can replay (trivialWhitespaceChurn,
10+
// missingTestEvidence, nonSubstantivePadding — changedFiles parse out of the archived unified diff). The
11+
// unarchived inputs (description, commit messages, duplicate-cluster membership, linked-issue state) are
12+
// passed UNDEFINED so their signals SKIP rather than fire spuriously — a replayed score is therefore a
13+
// LOWER BOUND on what live scoring would have produced. Every synthesized row records the computed signal
14+
// codes plus the provenance tag so the eventual flip-to-live decision can weigh exactly that.
15+
16+
import { buildSlopAssessment, SLOP_WEIGHTS, slopBandFor, type SlopChangedFile } from "../packages/loopover-engine/src/signals/slop";
17+
import type { SynthesizedAuditRow } from "./backfill-calibration-corpus-core.js";
18+
19+
export const SLOP_BACKFILL_RULE_ID = "slop_gate_score";
20+
export const SLOP_BACKFILL_PROVENANCE = "slop_replay_backfill_v1";
21+
const FIRED_EVENT_TYPE = `signal.rule_fired:${SLOP_BACKFILL_RULE_ID}`;
22+
const OVERRIDE_EVENT_TYPE = `signal.human_override:${SLOP_BACKFILL_RULE_ID}`;
23+
24+
/** One replayable source case, projected from a backtest-corpus manifest (backtest-corpus-export.ts):
25+
* the archived bounded diff plus the human label the original rule's history already established. */
26+
/** The diff-derivable signal codes the replay may score, mapped to the scorer's own weights. Everything
27+
* else needs unarchived inputs and is EXCLUDED even if the scorer fires it on an undefined field. */
28+
const REPLAYABLE_SIGNAL_WEIGHTS: Record<string, number | undefined> = {
29+
trivial_whitespace_churn: SLOP_WEIGHTS.trivialWhitespaceChurn,
30+
missing_test_evidence: SLOP_WEIGHTS.missingTestEvidence,
31+
non_substantive_padding: SLOP_WEIGHTS.nonSubstantivePadding,
32+
};
33+
34+
export type SlopReplaySourceCase = {
35+
targetKey: string;
36+
label: "confirmed" | "reversed";
37+
firedAt: string;
38+
decidedAt: string;
39+
diff: string;
40+
};
41+
42+
/**
43+
* Parse a unified diff into the scorer's {@link SlopChangedFile} shape: one entry per `diff --git` block
44+
* (b-side path — the post-change name), with per-file added/deleted line counts (`+`/`-` bodies only,
45+
* never the `+++`/`---` headers). Tolerant of the 45KB truncation marker the phase-2 apply path appends:
46+
* a truncated tail simply yields fewer counted lines — the parse never throws on any string input.
47+
*/
48+
export function parseDiffChangedFiles(diff: string): SlopChangedFile[] {
49+
const files: SlopChangedFile[] = [];
50+
let current: { path: string; additions: number; deletions: number } | null = null;
51+
for (const line of diff.split("\n")) {
52+
const header = /^diff --git a\/.+ b\/(.+)$/.exec(line);
53+
if (header) {
54+
if (current) files.push(current);
55+
current = { path: header[1]!, additions: 0, deletions: 0 };
56+
continue;
57+
}
58+
if (!current) continue;
59+
if (line.startsWith("+++") || line.startsWith("---")) continue;
60+
if (line.startsWith("+")) current.additions += 1;
61+
else if (line.startsWith("-")) current.deletions += 1;
62+
}
63+
if (current) files.push(current);
64+
return files;
65+
}
66+
67+
export type SlopReplayReport = {
68+
replayed: number;
69+
skippedEmptyDiff: number;
70+
skippedNoFiles: number;
71+
skippedDuplicateTarget: number;
72+
/** Histogram of replayed risks by band edge — the evidence summary the issue asks for. */
73+
riskCounts: { zero: number; low: number; elevated: number; high: number };
74+
reversed: number;
75+
confirmed: number;
76+
rows: SynthesizedAuditRow[];
77+
};
78+
79+
/**
80+
* Replay the deterministic slop scorer over each source case and synthesize the provenance-tagged
81+
* fired/override pair (ids derive from the targetKey alone, so re-runs upsert idempotently — the phase-1
82+
* insert builder's ON CONFLICT discipline applies unchanged). The fired event's `occurredAt` reuses the
83+
* source case's own firedAt and the override sits at its decidedAt (floored to 1s after the firing when
84+
* the archive's timestamps collide), so buildBacktestCorpus's strictly-after pairing always matches.
85+
* Deterministic output for deterministic input.
86+
*/
87+
export function replaySlopCorpus(cases: readonly SlopReplaySourceCase[]): SlopReplayReport {
88+
const report: SlopReplayReport = {
89+
replayed: 0,
90+
skippedEmptyDiff: 0,
91+
skippedNoFiles: 0,
92+
skippedDuplicateTarget: 0,
93+
riskCounts: { zero: 0, low: 0, elevated: 0, high: 0 },
94+
reversed: 0,
95+
confirmed: 0,
96+
rows: [],
97+
};
98+
const seen = new Set<string>();
99+
for (const sourceCase of cases) {
100+
if (!sourceCase.diff || !sourceCase.diff.trim()) {
101+
report.skippedEmptyDiff += 1;
102+
continue;
103+
}
104+
if (seen.has(sourceCase.targetKey)) {
105+
report.skippedDuplicateTarget += 1;
106+
continue;
107+
}
108+
const changedFiles = parseDiffChangedFiles(sourceCase.diff);
109+
if (changedFiles.length === 0) {
110+
report.skippedNoFiles += 1;
111+
continue;
112+
}
113+
seen.add(sourceCase.targetKey);
114+
// Signal-subset honesty, enforced by ALLOWLIST rather than trusting undefined-skipping: the scorer
115+
// treats a missing description as an empty one (buildEmptyDescriptionFinding fires on undefined —
116+
// correct live, where absence IS emptiness, but inflating here, where the field simply wasn't
117+
// archived). Only the three diff-derivable signals may contribute; risk and band recompute from
118+
// exactly their weights via the scorer's own exported constants.
119+
const assessment = buildSlopAssessment({ changedFiles });
120+
const replayableFindings = assessment.findings.filter((finding) => REPLAYABLE_SIGNAL_WEIGHTS[finding.code] !== undefined);
121+
const slopRisk = Math.min(
122+
100,
123+
replayableFindings.reduce((sum, finding) => sum + REPLAYABLE_SIGNAL_WEIGHTS[finding.code]!, 0),
124+
);
125+
const band = slopBandFor(slopRisk);
126+
const computedSignals = replayableFindings.map((finding) => finding.code).sort();
127+
128+
report.replayed += 1;
129+
if (slopRisk >= 60) report.riskCounts.high += 1;
130+
else if (slopRisk >= 30) report.riskCounts.elevated += 1;
131+
else if (slopRisk > 0) report.riskCounts.low += 1;
132+
else report.riskCounts.zero += 1;
133+
if (sourceCase.label === "reversed") report.reversed += 1;
134+
else report.confirmed += 1;
135+
136+
const firedMs = Date.parse(sourceCase.firedAt);
137+
const decidedMs = Date.parse(sourceCase.decidedAt);
138+
const overrideIso = new Date(
139+
Number.isFinite(decidedMs) && Number.isFinite(firedMs) && decidedMs > firedMs ? decidedMs : (Number.isFinite(firedMs) ? firedMs : 0) + 1000,
140+
).toISOString();
141+
report.rows.push(
142+
{
143+
id: `backfill:${SLOP_BACKFILL_RULE_ID}:${sourceCase.targetKey}:fired`,
144+
eventType: FIRED_EVENT_TYPE,
145+
actor: "loopover",
146+
targetKey: sourceCase.targetKey,
147+
outcome: slopRisk >= 60 ? "above_threshold" : "below_threshold",
148+
detail: `rule ${SLOP_BACKFILL_RULE_ID} replayed against ${sourceCase.targetKey} [backfilled]`,
149+
metadataJson: JSON.stringify({
150+
confidence: slopRisk / 100,
151+
band,
152+
computedSignals,
153+
backfilled: true,
154+
provenance: SLOP_BACKFILL_PROVENANCE,
155+
}),
156+
createdAt: sourceCase.firedAt,
157+
},
158+
{
159+
id: `backfill:${SLOP_BACKFILL_RULE_ID}:${sourceCase.targetKey}:override`,
160+
eventType: OVERRIDE_EVENT_TYPE,
161+
actor: "human",
162+
targetKey: sourceCase.targetKey,
163+
outcome: "completed",
164+
detail: `human ${sourceCase.label} rule ${SLOP_BACKFILL_RULE_ID} against ${sourceCase.targetKey} [backfilled]`,
165+
metadataJson: JSON.stringify({ verdict: sourceCase.label, backfilled: true, provenance: SLOP_BACKFILL_PROVENANCE }),
166+
createdAt: overrideIso,
167+
},
168+
);
169+
}
170+
return report;
171+
}
172+
173+
/** Render the dry-run/apply summary — the shape the #8277 evidence comment quotes. */
174+
export function renderSlopReplayReport(report: SlopReplayReport, mode: "dry-run" | "apply"): string {
175+
return [
176+
`Slop corpus replay backfill (${mode}) — provenance ${SLOP_BACKFILL_PROVENANCE}`,
177+
` replayed: ${report.replayed} (confirmed ${report.confirmed}, reversed ${report.reversed})`,
178+
` risk bands: zero ${report.riskCounts.zero} | low ${report.riskCounts.low} | elevated ${report.riskCounts.elevated} | high ${report.riskCounts.high}`,
179+
` skipped: empty-diff ${report.skippedEmptyDiff}, no-files ${report.skippedNoFiles}, duplicate-target ${report.skippedDuplicateTarget}`,
180+
mode === "dry-run" ? "dry-run only — re-run with --apply to write. Rows upsert idempotently by id." : ` rows written: ${report.rows.length}`,
181+
].join("\n");
182+
}

scripts/backfill-slop-corpus.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env node
2+
// Slop-corpus replay backfill CLI (#8277) — phase 3 of the calibration backfill family. Reads a
3+
// backtest-corpus MANIFEST (backtest-corpus-export.ts's output for ai_consensus_defect — the archived
4+
// raw-context diffs plus human labels), replays the deterministic slop scorer over each diff via the pure
5+
// core (backfill-slop-corpus-core.ts), and — ONLY with --apply — writes the provenance-tagged
6+
// `slop_gate_score` fired/override pairs back. Zero GitHub traffic: every input is already on disk or in
7+
// the store. Mirrors backfill-calibration-corpus.ts's exact wrangler/--pg dual-path + dry-run-default.
8+
//
9+
// tsx scripts/backfill-slop-corpus.ts --corpus corpus.json [--apply] [--db loopover] [--remote]
10+
// tsx scripts/backfill-slop-corpus.ts --corpus corpus.json --apply --pg postgres://… (bare --pg uses DATABASE_URL)
11+
import { readFileSync } from "node:fs";
12+
import { spawnSync } from "node:child_process";
13+
import type { BacktestCase } from "@loopover/engine";
14+
import { openPgDatabase, resolvePgConnection, type PgCliSession } from "./pg-cli.js";
15+
import { buildBackfillInsertStatements } from "./backfill-calibration-corpus-core.js";
16+
import { renderSlopReplayReport, replaySlopCorpus, type SlopReplaySourceCase } from "./backfill-slop-corpus-core.js";
17+
18+
type Args = { corpus: string | undefined; db: string; remote: boolean; apply: boolean; pgPresent: boolean; pgValue: string | undefined };
19+
20+
function parseArgs(argv: string[]): Args {
21+
const args: Args = { corpus: undefined, db: "loopover", remote: false, apply: false, pgPresent: false, pgValue: undefined };
22+
for (let i = 0; i < argv.length; i += 1) {
23+
const flag = argv[i];
24+
if (flag === "--corpus") args.corpus = argv[++i];
25+
else if (flag === "--remote") args.remote = true;
26+
else if (flag === "--apply") args.apply = true;
27+
else if (flag === "--db") args.db = argv[++i]!;
28+
else if (flag === "--pg") {
29+
args.pgPresent = true;
30+
if (argv[i + 1] !== undefined && !argv[i + 1]!.startsWith("--")) args.pgValue = argv[++i];
31+
}
32+
}
33+
return args;
34+
}
35+
36+
function d1Execute(db: string, remote: boolean, sql: string): void {
37+
const result = spawnSync("npx", ["wrangler", "d1", "execute", db, remote ? "--remote" : "--local", "--json", "--command", sql], {
38+
encoding: "utf8",
39+
maxBuffer: 256 * 1024 * 1024,
40+
});
41+
if (result.status !== 0) {
42+
throw new Error(`wrangler d1 execute failed (${result.status}): ${(result.stderr || result.stdout || "").slice(0, 500)}`);
43+
}
44+
}
45+
46+
/** Project manifest cases into replay sources: only cases carrying a non-empty archived diff qualify
47+
* (the core re-checks and counts, so the numbers stay honest either way). */
48+
export function manifestToSourceCases(cases: readonly BacktestCase[]): SlopReplaySourceCase[] {
49+
const sources: SlopReplaySourceCase[] = [];
50+
for (const backtestCase of cases) {
51+
const diff = backtestCase.metadata?.diff;
52+
sources.push({
53+
targetKey: backtestCase.targetKey,
54+
label: backtestCase.label,
55+
firedAt: backtestCase.firedAt,
56+
decidedAt: backtestCase.decidedAt,
57+
diff: typeof diff === "string" ? diff : "",
58+
});
59+
}
60+
return sources;
61+
}
62+
63+
async function main(): Promise<number> {
64+
const args = parseArgs(process.argv.slice(2));
65+
if (!args.corpus) {
66+
console.error("Usage: tsx scripts/backfill-slop-corpus.ts --corpus <manifest.json> [--apply] [--db loopover|--remote|--pg …]");
67+
return 1;
68+
}
69+
const manifest = JSON.parse(readFileSync(args.corpus, "utf8")) as { cases?: BacktestCase[] };
70+
if (!Array.isArray(manifest.cases)) {
71+
console.error("--corpus file has no cases[] — expected a backtest-corpus-export manifest.");
72+
return 1;
73+
}
74+
75+
const report = replaySlopCorpus(manifestToSourceCases(manifest.cases));
76+
console.log(renderSlopReplayReport(report, args.apply ? "apply" : "dry-run"));
77+
if (!args.apply || report.rows.length === 0) return 0;
78+
79+
const pgConnection = resolvePgConnection(args.pgPresent, args.pgValue, process.env.DATABASE_URL);
80+
const pgSession: PgCliSession | null = pgConnection ? openPgDatabase(pgConnection) : null;
81+
try {
82+
for (const statement of buildBackfillInsertStatements(report.rows)) {
83+
if (pgSession) await pgSession.db.prepare(statement).run();
84+
else d1Execute(args.db, args.remote, statement);
85+
}
86+
console.log(`applied ${report.rows.length} row(s) to ${pgSession ? "postgres" : `d1:${args.db}${args.remote ? " (remote)" : ""}`}.`);
87+
return 0;
88+
} finally {
89+
await pgSession?.close();
90+
}
91+
}
92+
93+
// Entry guard (the audit-quality-gate-min-score.ts idiom): tests import this module's exported helpers,
94+
// so main() must run ONLY under direct execution — an import-time process.exit fails the whole vitest run
95+
// as an unhandled rejection even with every test green.
96+
if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) {
97+
main().then(
98+
(code) => process.exit(code),
99+
(error) => {
100+
console.error(error instanceof Error ? error.message : String(error));
101+
process.exit(1);
102+
},
103+
);
104+
}

0 commit comments

Comments
 (0)