From f0179dc9b48c2dc18e7a5a9397c6d7c992d4fb4c Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Fri, 17 Jul 2026 01:18:42 +0800 Subject: [PATCH] fix(engine): treat non-finite calibration inputs as cold-start / safe clamp Closes #6627 Co-authored-by: Cursor --- .../src/signals/contributor-calibration.ts | 9 ++++++++- test/unit/predicted-gate.test.ts | 12 ++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/loopover-engine/src/signals/contributor-calibration.ts b/packages/loopover-engine/src/signals/contributor-calibration.ts index 17fd77b340..37bb6d12cd 100644 --- a/packages/loopover-engine/src/signals/contributor-calibration.ts +++ b/packages/loopover-engine/src/signals/contributor-calibration.ts @@ -16,6 +16,9 @@ // ever public" boundary is preserved because the raw numbers never reach the output at all. function clamp(value: number, min: number, max: number): number { + // Non-finite input fails toward `min` (safe end of the range) — mirrors governor clampFraction / + // finiteNonNegativeInt so a NaN agreementRate can never propagate a NaN readinessScore (#6627). + if (!Number.isFinite(value)) return min; return Math.min(max, Math.max(min, value)); } @@ -53,7 +56,11 @@ export function applyContributorCalibration( calibration: ContributorCalibrationSignal | null | undefined, ): number | null { if (baselineReadinessScore === null) return null; - if (!calibration || calibration.sampleSize < MIN_CALIBRATION_SAMPLES) return baselineReadinessScore; + // Non-finite sampleSize is "insufficient history" — NaN < N is always false in JS, so without this + // guard a malformed sample count would silently bypass cold-start and apply a full adjustment (#6627). + if (!calibration || !Number.isFinite(calibration.sampleSize) || calibration.sampleSize < MIN_CALIBRATION_SAMPLES) { + return baselineReadinessScore; + } const agreementRate = clamp(calibration.agreementRate, 0, 1); const rawAdjustment = (agreementRate - NEUTRAL_AGREEMENT_RATE) * 2 * MAX_READINESS_ADJUSTMENT; const adjustment = clamp(rawAdjustment, -MAX_READINESS_ADJUSTMENT, MAX_READINESS_ADJUSTMENT); diff --git a/test/unit/predicted-gate.test.ts b/test/unit/predicted-gate.test.ts index 9290e94ce0..71d348adde 100644 --- a/test/unit/predicted-gate.test.ts +++ b/test/unit/predicted-gate.test.ts @@ -623,6 +623,18 @@ describe("applyContributorCalibration (#2349)", () => { const perfect: ContributorCalibrationSignal = { sampleSize: 500, agreementRate: 1 }; expect(applyContributorCalibration(null, perfect)).toBeNull(); }); + + it("REGRESSION (#6627): a non-finite sampleSize is cold-start — baseline unchanged, never a silent adjustment", () => { + const nanSample: ContributorCalibrationSignal = { sampleSize: Number.NaN, agreementRate: 0.9 }; + expect(applyContributorCalibration(70, nanSample)).toBe(70); + }); + + it("REGRESSION (#6627): a non-finite agreementRate fails toward 0 (clamp min) — returns a finite [0, 100] score, never NaN", () => { + const nanRate: ContributorCalibrationSignal = { sampleSize: 20, agreementRate: Number.NaN }; + const result = applyContributorCalibration(70, nanRate); + expect(result).toBe(70 - MAX_READINESS_ADJUSTMENT); + expect(Number.isFinite(result)).toBe(true); + }); }); describe("buildPredictedGateVerdict — personalized calibration wiring (#2349)", () => {