diff --git a/src/review/predicted-gate-calibration-ledger.ts b/src/review/predicted-gate-calibration-ledger.ts index c0bdbbc9bd..0394492021 100644 --- a/src/review/predicted-gate-calibration-ledger.ts +++ b/src/review/predicted-gate-calibration-ledger.ts @@ -141,13 +141,14 @@ export async function recordPredictedGateCalibration( export async function computeContributorCalibration(env: PredictedGateCalibrationEnv, login: string | null | undefined): Promise { const trimmed = login?.trim(); if (!trimmed) return null; + const normalizedLogin = trimmed.toLowerCase(); try { const row = await env.DB.prepare( `SELECT COUNT(*) AS sampleSize, COALESCE(AVG(agreed), 0) AS agreementRate FROM predicted_gate_calibration_ledger - WHERE login = ?`, + WHERE lower(login) = ?`, ) - .bind(trimmed) + .bind(normalizedLogin) .first<{ sampleSize: number; agreementRate: number }>(); // COUNT(*)/AVG(...) with no GROUP BY always returns exactly one row, even over zero matches (COUNT: 0, // AVG: NULL -> COALESCE: 0) -- .first()'s nullable return type is a TypeScript-level formality here, not diff --git a/test/unit/predicted-gate-calibration-ledger.test.ts b/test/unit/predicted-gate-calibration-ledger.test.ts index ea4d9f06ec..f3814aed20 100644 --- a/test/unit/predicted-gate-calibration-ledger.test.ts +++ b/test/unit/predicted-gate-calibration-ledger.test.ts @@ -220,6 +220,20 @@ describe("computeContributorCalibration — per-login calibration read (#2349)", expect(await computeContributorCalibration(env, "someone-else")).toEqual({ sampleSize: 1, agreementRate: 1 }); }); + it("canonicalizes GitHub login casing so case variants cannot bypass or infer calibration", async () => { + const env = createTestEnv(); + await seedLedgerRow(env, { login: "OctoCat", pullNumber: 1, agreed: false }); + await seedLedgerRow(env, { login: "octocat", pullNumber: 2, agreed: false }); + await seedLedgerRow(env, { login: "OCTOCAT", pullNumber: 3, agreed: true }); + await seedLedgerRow(env, { login: "someone-else", pullNumber: 1, agreed: true }); + + const expected = { sampleSize: 3, agreementRate: 1 / 3 }; + expect(await computeContributorCalibration(env, "OctoCat")).toEqual(expected); + expect(await computeContributorCalibration(env, " octocat ")).toEqual(expected); + expect(await computeContributorCalibration(env, "OCTOCAT")).toEqual(expected); + expect(await computeContributorCalibration(env, "someone-else")).toEqual({ sampleSize: 1, agreementRate: 1 }); + }); + it("aggregates across ALL of a login's history regardless of which repo each pairing came from", async () => { const env = createTestEnv(); await seedLedgerRow(env, { login: "octocat", project: "owner/repo-a", pullNumber: 1, agreed: true });