diff --git a/src/components/prs/PRTimeDecayChart.tsx b/src/components/prs/PRTimeDecayChart.tsx index 7554a024..4a6d25f1 100644 --- a/src/components/prs/PRTimeDecayChart.tsx +++ b/src/components/prs/PRTimeDecayChart.tsx @@ -16,6 +16,7 @@ import { buildDecaySubline, type DecayParams, type DecayProjection, + fitCurveThroughPoint, resolveDecayParams, } from './prTimeDecayModel'; @@ -252,14 +253,18 @@ function buildChartOption( const fontFamily = echartsFontFamily(theme); const mutedHex = alpha(theme.palette.common.white, TEXT_OPACITY.tertiary); const marker = resolveNowMarker(projection); - const seriesData = injectNowPoint(buildDecayCurve(params), marker); + const curveParams = + projection.curveMismatch && marker + ? fitCurveThroughPoint(params, marker.day, marker.multiplier) + : params; + const seriesData = injectNowPoint(buildDecayCurve(curveParams), marker); return { ...echartsTransparentBackground(), grid: { left: 44, right: 16, top: 28, bottom: 36 }, tooltip: buildTooltip(theme, projection.preDecayScore, mutedHex), - xAxis: buildXAxis(axis, fontFamily, params.lookbackDays), + xAxis: buildXAxis(axis, fontFamily, curveParams.lookbackDays), yAxis: buildYAxis(axis), - series: [buildSeries(theme, axis, params, seriesData, marker)], + series: [buildSeries(theme, axis, curveParams, seriesData, marker)], }; } diff --git a/src/components/prs/prTimeDecayModel.ts b/src/components/prs/prTimeDecayModel.ts index 74c4538f..1df679df 100644 --- a/src/components/prs/prTimeDecayModel.ts +++ b/src/components/prs/prTimeDecayModel.ts @@ -104,6 +104,31 @@ export function resolveDecayParams( }; } +/** + * Re-fit the sigmoid midpoint so the curve passes through the subnet-reported + * (day, multiplier) point. Repos can override decay hyperparameters without + * the API publishing them (`/repos/{name}` only exposes emission shares), so + * when the reported multiplier contradicts the resolved params the fitted + * curve is the closest available approximation of the repo's real schedule. + */ +export function fitCurveThroughPoint( + params: DecayParams, + day: number, + multiplier: number, +): DecayParams { + if (!(multiplier > params.floor) || !(multiplier < 1) || day <= 0) { + return params; + } + const midpoint = day - Math.log(1 / multiplier - 1) / params.steepness; + return { + ...params, + midpoint, + // A reported multiplier below 1 means decay already started, so the + // grace period cannot extend past the merge age. + graceHours: Math.min(params.graceHours, day * 24), + }; +} + function decayAt(days: number, params: DecayParams): number { if (days <= params.graceHours / 24) return 1; const sigmoid = @@ -214,6 +239,6 @@ export function buildDecaySubline(projection: DecayProjection): string { } const base = `${projection.daysSinceMerge.toFixed(1)} day(s) since merge`; return projection.curveMismatch - ? `${base} · showing subnet-reported multiplier (curve is approximate)` + ? `${base} · curve fitted to subnet-reported multiplier (repo overrides decay params)` : base; }