From 84b789db9b4a7eb37557c9385369f53b26aa8e0d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Jun 2026 23:16:36 +0000 Subject: [PATCH] Address code-review findings: odds band + inert UI knobs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - input-policy: revert the odds->Elo change that passed the policy's eloMin/eloMax into convertFuturesToElo. That widened the default odds-derived spread (1100/1900 vs the calibrated 1250/1750) — sharpening favorites for default-policy odds sims (UCL, World Cup, NCAA-FB, MLB) — and clamped the blend of a high directly-entered base Elo down to the ceiling. Odds now use the calibrated band; the policy's eloMin/eloMax still clamp the result (clampDerived), so a season's floor/ceiling narrows the spread without widening it or capping high base Elos. - admin simulator UI: restrict the structured Engine fields to a HONORED_ENGINE_KNOBS allowlist of keys actually read from config (by a simulator or the input-policy resolver). Bespoke per-sim constants present in a profile but not read from config (homeFieldElo, eloDivisor, srsEloScale, raceNoise, plBeta, fieldSize, bracketSize, ...) no longer render as editable controls that silently do nothing; the raw-JSON escape hatch still edits them. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01PAFMogMkFJf52YpHyCDvuf --- .../admin.sports-seasons.$id.simulator.tsx | 21 ++++++++++++++++++- app/services/simulations/input-policy.ts | 13 +++++------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/app/routes/admin.sports-seasons.$id.simulator.tsx b/app/routes/admin.sports-seasons.$id.simulator.tsx index f623c01..e6d2f87 100644 --- a/app/routes/admin.sports-seasons.$id.simulator.tsx +++ b/app/routes/admin.sports-seasons.$id.simulator.tsx @@ -69,6 +69,25 @@ interface ActionData { message: string; } +/** + * Engine knobs that a simulator (or the shared input-policy resolver) actually + * reads from config. The structured Engine fields are limited to these so the UI + * never shows a control that silently does nothing — bespoke per-sim constants + * (e.g. homeFieldElo, eloDivisor, srsEloScale, raceNoise) that live in a profile + * but are not read from config stay editable only via the raw-JSON escape hatch. + */ +const HONORED_ENGINE_KNOBS = new Set([ + "iterations", + "parityFactor", + "seasonGames", + "overtimeRate", + "matchParityFactor", + "averageOpponentElo", + "baseDrawRate", + "drawDecay", + "ratingScaleFactor", +]); + function parseOptionalNumber(value: string | undefined): number | null { if (value === undefined || value.trim() === "") return null; const parsed = Number(value); @@ -335,7 +354,7 @@ export default function AdminSportsSeasonSimulator({ loaderData }: Route.Compone // nested object edited in its own section). Driving the fields from the merged // config means each simulator shows exactly the knobs it actually reads. const engineEntries = Object.entries(config.config) - .filter(([key, value]) => key !== "inputPolicy" && typeof value === "number") + .filter(([key, value]) => HONORED_ENGINE_KNOBS.has(key) && typeof value === "number") .map(([key, value]) => [key, value as unknown as number] as [string, number]); return ( diff --git a/app/services/simulations/input-policy.ts b/app/services/simulations/input-policy.ts index d230b07..b2b8acf 100644 --- a/app/services/simulations/input-policy.ts +++ b/app/services/simulations/input-policy.ts @@ -254,14 +254,11 @@ export function resolveSourceElos( ); } if (oddsInputs.length >= 2) { - // Map onto the configured Elo band so the policy's floor/ceiling set the - // odds-derived *spread* (the real flattening dial), not just a post-hoc clamp. - const converted = convertFuturesToElo(oddsInputs, "american", { - exponent: 0.33, - eloMin: policy.eloMin, - eloMax: policy.eloMax, - }); - for (const [participantId, elo] of converted) { + // Odds map onto the calibrated band (probability-engine DEFAULT_CALIBRATION); + // the policy's eloMin/eloMax then clamp the result via clampDerived below, so + // a season's Elo floor/ceiling still narrows the odds-derived spread without + // widening it (and without clamping a high directly-entered base Elo's blend). + for (const [participantId, elo] of convertFuturesToElo(oddsInputs)) { oddsElo.set(participantId, elo); } }