Address code-review findings: odds band + inert UI knobs
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 2m55s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m18s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped

- 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAFMogMkFJf52YpHyCDvuf
This commit is contained in:
Claude 2026-06-30 23:16:36 +00:00
parent 4142b21c55
commit 84b789db9b
No known key found for this signature in database
2 changed files with 25 additions and 9 deletions

View file

@ -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 (

View file

@ -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);
}
}