|
| 1 | +#!/usr/bin/env node |
| 2 | +// Replay-derived provider track records (#8278, seeding #8229 stage 1). Reads the counterfactual replay |
| 3 | +// harness's cached raw outputs (the #8221 artifacts dir) for one or more variants over the SAME seeded |
| 4 | +// fixture sample, maps each parsed verdict onto the consensus vote vocabulary, and aggregates with |
| 5 | +// `computeProviderTrackRecords` (#8228) — the identical function live votes will feed, zero new math. |
| 6 | +// REPLAY-DERIVED, NEVER LIVE: signals exist only in this offline report; nothing is written to any store |
| 7 | +// and nothing masquerades as a live reviewer_vote event (the #8278 segregation requirement). |
| 8 | +// |
| 9 | +// tsx scripts/provider-replay-track-record.ts --fixtures corpus.json --artifacts dir \ |
| 10 | +// --variant <promptVersion@modelSpec> [--variant …] [--seed-suffix s] [--max-fixtures N] |
| 11 | +// |
| 12 | +// Verdict → vote mapping: would_flag ⇒ "fail" (the defect-flagging vote), would_not_flag ⇒ "pass", |
| 13 | +// abstained ⇒ NO signal (an abstention is not a vote — the same never-coerced discipline as scoring). |
| 14 | +import { existsSync, readFileSync } from "node:fs"; |
| 15 | +import { join } from "node:path"; |
| 16 | +import { |
| 17 | + computeProviderTrackRecords, |
| 18 | + COUNTERFACTUAL_SAMPLE_SEED_PREFIX, |
| 19 | + type BacktestCase, |
| 20 | + type CounterfactualVariant, |
| 21 | + type ProviderReviewSignal, |
| 22 | + type ProviderTrackRecord, |
| 23 | +} from "@loopover/engine"; |
| 24 | +import { artifactKey, parseVariantVerdict, planReplay, type CounterfactualReplayPlan } from "./counterfactual-replay-core.js"; |
| 25 | + |
| 26 | +/** PURE: map one variant's cached raw outputs onto provider signals over the shared plan. The provider id |
| 27 | + * is the variant's modelSpec (the reviewer identity live votes carry); abstentions and uncached fixtures |
| 28 | + * yield no signal, counted separately so the report can say how much of the sample actually voted. */ |
| 29 | +export function artifactsToProviderSignals( |
| 30 | + plan: CounterfactualReplayPlan, |
| 31 | + variant: CounterfactualVariant, |
| 32 | + readArtifact: (key: string) => string | null, |
| 33 | +): { signals: ProviderReviewSignal[]; abstained: number; uncached: number } { |
| 34 | + const signals: ProviderReviewSignal[] = []; |
| 35 | + let abstained = 0; |
| 36 | + let uncached = 0; |
| 37 | + for (const fixture of plan.fixtures) { |
| 38 | + const raw = readArtifact(artifactKey(variant, fixture.fixtureId)); |
| 39 | + if (raw === null) { |
| 40 | + uncached += 1; |
| 41 | + continue; |
| 42 | + } |
| 43 | + const verdict = parseVariantVerdict(raw); |
| 44 | + if (verdict === "abstained") { |
| 45 | + abstained += 1; |
| 46 | + continue; |
| 47 | + } |
| 48 | + signals.push({ |
| 49 | + provider: variant.modelSpec, |
| 50 | + repoFullName: fixture.fixtureId.split("#")[0] ?? fixture.fixtureId, |
| 51 | + targetKey: fixture.fixtureId, |
| 52 | + vote: verdict === "would_flag" ? "fail" : "pass", |
| 53 | + }); |
| 54 | + } |
| 55 | + return { signals, abstained, uncached }; |
| 56 | +} |
| 57 | + |
| 58 | +/** Render the overall-rollup rows (repoFullName null) as the markdown table #8229's stage-1 comment quotes. */ |
| 59 | +export function renderProviderTable(records: readonly ProviderTrackRecord[]): string { |
| 60 | + const overall = records.filter((record) => record.repoFullName === null); |
| 61 | + const lines = [ |
| 62 | + "| Provider | Signals | Decided | Precision (fail⇒confirmed) | Agreement | Consensus |", |
| 63 | + "| --- | --- | --- | --- | --- | --- |", |
| 64 | + ]; |
| 65 | + const fmt = (value: number | null): string => (value === null ? "n/a" : value.toFixed(3)); |
| 66 | + for (const record of overall) { |
| 67 | + lines.push( |
| 68 | + `| ${record.provider} | ${record.signals} | ${record.decided} | ${fmt(record.precision)} | ${fmt(record.agreementRate)} | ${fmt(record.consensusRate)} |`, |
| 69 | + ); |
| 70 | + } |
| 71 | + return lines.join("\n"); |
| 72 | +} |
| 73 | + |
| 74 | +function parseArgs(argv: string[]): { fixtures?: string; artifacts: string; variants: string[]; seedSuffix: string; maxFixtures: number } { |
| 75 | + const args = { fixtures: undefined as string | undefined, artifacts: ".counterfactual-artifacts", variants: [] as string[], seedSuffix: "default", maxFixtures: 500 }; |
| 76 | + for (let i = 0; i < argv.length; i += 1) { |
| 77 | + const flag = argv[i]; |
| 78 | + if (flag === "--fixtures") args.fixtures = argv[++i]; |
| 79 | + else if (flag === "--artifacts") args.artifacts = argv[++i] ?? args.artifacts; |
| 80 | + else if (flag === "--variant") args.variants.push(argv[++i] ?? ""); |
| 81 | + else if (flag === "--seed-suffix") args.seedSuffix = argv[++i] ?? "default"; |
| 82 | + else if (flag === "--max-fixtures") args.maxFixtures = Number(argv[++i]); |
| 83 | + } |
| 84 | + return args; |
| 85 | +} |
| 86 | + |
| 87 | +async function main(): Promise<number> { |
| 88 | + const args = parseArgs(process.argv.slice(2)); |
| 89 | + if (!args.fixtures || args.variants.length === 0 || args.variants.some((variant) => !variant.includes("@"))) { |
| 90 | + console.error("Usage: tsx scripts/provider-replay-track-record.ts --fixtures <corpus.json> --artifacts <dir> --variant <promptVersion@modelSpec> [--variant …]"); |
| 91 | + return 1; |
| 92 | + } |
| 93 | + const manifest = JSON.parse(readFileSync(args.fixtures, "utf8")) as { cases?: BacktestCase[] }; |
| 94 | + if (!Array.isArray(manifest.cases)) { |
| 95 | + console.error("--fixtures file has no cases[] — expected a backtest-corpus-export manifest."); |
| 96 | + return 1; |
| 97 | + } |
| 98 | + const plan = planReplay(manifest.cases, { seed: `${COUNTERFACTUAL_SAMPLE_SEED_PREFIX}:${args.seedSuffix}`, maxFixtures: args.maxFixtures }); |
| 99 | + |
| 100 | + const allSignals: ProviderReviewSignal[] = []; |
| 101 | + for (const raw of args.variants) { |
| 102 | + const [promptVersion, ...modelParts] = raw.split("@"); |
| 103 | + const variant: CounterfactualVariant = { promptVersion: promptVersion!, modelSpec: modelParts.join("@") }; |
| 104 | + const { signals, abstained, uncached } = artifactsToProviderSignals(plan, variant, (key) => { |
| 105 | + const path = join(args.artifacts, `${key}.txt`); |
| 106 | + return existsSync(path) ? readFileSync(path, "utf8") : null; |
| 107 | + }); |
| 108 | + console.log(`${raw}: ${signals.length} signal(s), ${abstained} abstained, ${uncached} uncached (of ${plan.fixtures.length} planned)`); |
| 109 | + allSignals.push(...signals); |
| 110 | + } |
| 111 | + |
| 112 | + console.log(""); |
| 113 | + console.log(renderProviderTable(computeProviderTrackRecords(allSignals, manifest.cases))); |
| 114 | + console.log("\nREPLAY-DERIVED (offline #8221 artifacts) — not live reviewer votes; nothing was persisted."); |
| 115 | + return 0; |
| 116 | +} |
| 117 | + |
| 118 | +main().then( |
| 119 | + (code) => process.exit(code), |
| 120 | + (error) => { |
| 121 | + console.error(error instanceof Error ? error.message : String(error)); |
| 122 | + process.exit(1); |
| 123 | + }, |
| 124 | +); |
0 commit comments