From bd3d14f837ca4242776bc700054afb35d35e3cd2 Mon Sep 17 00:00:00 2001 From: e35ventura Date: Sun, 12 Jul 2026 18:30:12 -0500 Subject: [PATCH] fix(prs): keep time decay chart in sync with subnet multiplier Resolve per-repo decay config case-insensitively: /repos/{name} only matches canonical casing while PR records may carry a lowercased repository name, so the chart silently fell back to default curve params (e.g. jsonbored/metagraphed showed 0.91x while the subnet reported 0.11x). On a direct miss the config now falls back to a case-insensitive match against the repositories list. As a guard for any remaining param drift, the Now marker and score prefer the subnet-reported multiplier whenever it disagrees with the local curve beyond rounding tolerance, with a subline note. Co-Authored-By: Claude Fable 5 --- src/api/DashboardApi.ts | 10 ++++++++-- src/api/ReposApi.ts | 17 +++++++++++++++++ src/components/prs/PRTimeDecayChart.tsx | 12 ++++++------ src/components/prs/prTimeDecayModel.ts | 25 +++++++++++++++++++++++-- 4 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/api/DashboardApi.ts b/src/api/DashboardApi.ts index 5822d3c9..555edde1 100644 --- a/src/api/DashboardApi.ts +++ b/src/api/DashboardApi.ts @@ -30,8 +30,14 @@ export const useStats = () => useDashboardQuery('useStats', '/stats'); export const getReposQueryKey = () => ['useReposAndWeights', '/dash/repos', undefined] as const; -export const useReposAndWeights = () => - useDashboardQuery('useReposAndWeights', '/repos'); +export const useReposAndWeights = (enabled?: boolean) => + useDashboardQuery( + 'useReposAndWeights', + '/repos', + undefined, + undefined, + enabled, + ); export const useLanguagesAndWeights = () => useDashboardQuery('useLanguagesAndWeights', '/languages'); diff --git a/src/api/ReposApi.ts b/src/api/ReposApi.ts index 64946147..2806d074 100644 --- a/src/api/ReposApi.ts +++ b/src/api/ReposApi.ts @@ -1,5 +1,6 @@ // Repository API hooks - uses /repos endpoints import { useApiQuery } from './ApiUtils'; +import { useReposAndWeights } from './DashboardApi'; import { type RepositoryMaintainer, type RepositoryIssue } from './models'; import { type Repository, type RepositoryMiner } from './models/Dashboard'; @@ -29,6 +30,22 @@ export const useRepositoryConfig = (repo: string) => `/${encodeURIComponent(repo)}`, ); +/** + * Repository config resolved case-insensitively. `/repos/{name}` matches the + * canonical casing only, while PR records may carry a lowercased repository + * name — on a direct miss this falls back to matching the full repositories + * list by lowercased name. + */ +export const useResolvedRepositoryConfig = ( + repo: string, +): Repository | null => { + const direct = useRepositoryConfig(repo); + const fallback = useReposAndWeights(direct.isError); + if (direct.data) return direct.data; + const lower = repo.toLowerCase(); + return fallback.data?.find((r) => r.fullName.toLowerCase() === lower) ?? null; +}; + /** * Get maintainers (assignees) for a specific repository * @param repo - Full repository name (e.g., "opentensor/btcli") diff --git a/src/components/prs/PRTimeDecayChart.tsx b/src/components/prs/PRTimeDecayChart.tsx index 7728d953..7554a024 100644 --- a/src/components/prs/PRTimeDecayChart.tsx +++ b/src/components/prs/PRTimeDecayChart.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from 'react'; import { Box, Typography, alpha, useTheme } from '@mui/material'; import { type Theme } from '@mui/material/styles'; import ReactECharts from 'echarts-for-react'; -import { useGeneralConfig, useRepositoryConfig } from '../../api'; +import { useGeneralConfig, useResolvedRepositoryConfig } from '../../api'; import { STATUS_COLORS, TEXT_OPACITY } from '../../theme'; import { echartsAxisTooltipChrome, @@ -65,10 +65,10 @@ function buildGradient(opacities?: number[]) { function resolveNowMarker(projection: DecayProjection): NowMarker | null { if (!projection.isMerged || !projection.inWindow) return null; if (projection.daysSinceMerge == null) return null; - if (projection.chartNowMultiplier == null) return null; + if (projection.nowMultiplier == null) return null; return { day: +projection.daysSinceMerge.toFixed(2), - multiplier: projection.chartNowMultiplier, + multiplier: projection.nowMultiplier, }; } @@ -272,7 +272,7 @@ function PRTimeDecayChart({ }: PRTimeDecayChartProps) { const theme = useTheme(); const { data: generalConfig } = useGeneralConfig(); - const { data: repoData } = useRepositoryConfig(repository); + const repoData = useResolvedRepositoryConfig(repository); const params = useMemo( () => resolveDecayParams(generalConfig, repoData?.config), [generalConfig, repoData], @@ -347,7 +347,7 @@ function PRTimeDecayChart({ )} - {showNow && projection.chartNowMultiplier != null && ( + {showNow && projection.nowMultiplier != null && ( - {projection.chartNowMultiplier.toFixed(2)}× + {projection.nowMultiplier.toFixed(2)}× multiplier diff --git a/src/components/prs/prTimeDecayModel.ts b/src/components/prs/prTimeDecayModel.ts index 82db56c4..74c4538f 100644 --- a/src/components/prs/prTimeDecayModel.ts +++ b/src/components/prs/prTimeDecayModel.ts @@ -34,10 +34,21 @@ export interface DecayProjection { chartNowMultiplier: number | null; /** Subnet-reported multiplier (used to back-derive pre-decay score). */ currentMultiplier: number | null; + /** Multiplier to display — the subnet value wins when the curve disagrees. */ + nowMultiplier: number | null; + /** True when the subnet-reported multiplier contradicts the local curve. */ + curveMismatch: boolean; preDecayScore: number | null; chartNowScore: number | null; } +/** + * Subnet multipliers arrive rounded to 2dp, so small drift from the local + * curve is expected; beyond this the curve params must be wrong (e.g. the + * repo config failed to load) and the subnet value is authoritative. + */ +const NOW_MULTIPLIER_TOLERANCE = 0.05; + const DEFAULT_PARAMS: DecayParams = { graceHours: 12, midpoint: 10, @@ -176,6 +187,11 @@ export function buildDecayProjection( earned, currentMultiplier ?? chartNowMultiplier, ); + const curveMismatch = + currentMultiplier != null && + chartNowMultiplier != null && + Math.abs(currentMultiplier - chartNowMultiplier) > NOW_MULTIPLIER_TOLERANCE; + const nowMultiplier = curveMismatch ? currentMultiplier : chartNowMultiplier; return { isMerged, inWindow: isWithinLookbackWindow(daysSinceMerge, params.lookbackDays), @@ -183,8 +199,10 @@ export function buildDecayProjection( daysSinceMerge, chartNowMultiplier, currentMultiplier, + nowMultiplier, + curveMismatch, preDecayScore, - chartNowScore: applyMultiplier(preDecayScore, chartNowMultiplier), + chartNowScore: applyMultiplier(preDecayScore, nowMultiplier), }; } @@ -194,5 +212,8 @@ export function buildDecaySubline(projection: DecayProjection): string { if (projection.daysSinceMerge > projection.lookbackDays) { return `Outside ${projection.lookbackDays}-day scoring window.`; } - return `${projection.daysSinceMerge.toFixed(1)} day(s) since merge`; + const base = `${projection.daysSinceMerge.toFixed(1)} day(s) since merge`; + return projection.curveMismatch + ? `${base} · showing subnet-reported multiplier (curve is approximate)` + : base; }