@@ -603,24 +603,40 @@ function resolveRatingPathSource (challenge) {
603603 return null
604604}
605605
606+ /**
607+ * Determine whether a challenge has complete placement data for score ordering.
608+ * Partial placement backfills are common in older MM imports; if only some rows
609+ * have placements, aggregate scores are the only comparable value across all
610+ * participants.
611+ * @param {Array<Object> } participantRows challenge participant rows
612+ * @returns {boolean } true when every participant has a positive placement
613+ */
614+ function shouldUsePlacementScores ( participantRows ) {
615+ return ( participantRows || [ ] ) . length > 0 &&
616+ ( participantRows || [ ] ) . every ( row => ! ! toOptionalPlacement ( row && row . placement ) )
617+ }
618+
606619/**
607620 * Normalize one Marathon Match score for Qubits ordering.
608- * Source placements are authoritative final standings when present . Otherwise,
621+ * Complete source placements are authoritative final standings. Otherwise,
609622 * relative-scoring aggregates are treated as higher-is-better, while
610623 * non-relative MINIMIZE challenges need inversion to match standings order.
624+ * Placement is only a fallback when the row does not have an aggregate score.
611625 * @param {Object } row participant result row
612626 * @param {Object } scoringConfig relative scoring configuration for the challenge
627+ * @param {Object } [options] score normalization options
628+ * @param {boolean } [options.usePlacementScore=false] whether to rank by placement
613629 * @returns {number } normalized Qubits score
614630 */
615- function normalizeScore ( row , scoringConfig ) {
631+ function normalizeScore ( row , scoringConfig , options = { } ) {
616632 const placement = toOptionalPlacement ( row && row . placement )
617- if ( placement ) {
633+ if ( options . usePlacementScore && placement ) {
618634 return - placement
619635 }
620636
621637 const aggregateScore = Number ( row . aggregateScore )
622638 if ( ! Number . isFinite ( aggregateScore ) ) {
623- return 0
639+ return placement ? - placement : 0
624640 }
625641
626642 if (
@@ -834,24 +850,18 @@ function computePlacementByCoderId (participants) {
834850
835851/**
836852 * Build optional history metadata for the target participant.
837- * Source-provided placements are preferred; score ordering is the fallback for
838- * historical MM submissions that do not expose a stored placement.
853+ * Placement is computed from the same normalized scores used by the rating run,
854+ * with the source placement kept as a fallback when computed placement is absent .
839855 * @param {Array<Object> } participants rated participant states for one challenge
840856 * @param {string } targetUserKey normalized target user id
841857 * @param {Object } sourceRow source participant row for the target user
842858 * @returns {Object } persisted memberStatsHistory metadata
843859 */
844860function buildHistoryResultFields ( participants , targetUserKey , sourceRow ) {
845- const sourcePlacement = toOptionalPlacement ( sourceRow && sourceRow . placement )
846- if ( sourcePlacement ) {
847- return {
848- placement : sourcePlacement
849- }
850- }
851-
852861 const computedPlacement = computePlacementByCoderId ( participants ) . get ( String ( targetUserKey ) )
862+ const sourcePlacement = toOptionalPlacement ( sourceRow && sourceRow . placement )
853863 return omitUndefinedFields ( {
854- placement : computedPlacement
864+ placement : computedPlacement || sourcePlacement
855865 } )
856866}
857867
@@ -1650,14 +1660,15 @@ function resolveRatingPathParticipantId (row, source) {
16501660 * @param {Object } row participant source row
16511661 * @param {string } source rating path source
16521662 * @param {Object } scoringConfig MM scoring config when source is Marathon Match
1663+ * @param {Object } [options] score normalization options
16531664 * @returns {number } normalized Qubits score
16541665 */
1655- function normalizeRatingPathScore ( row , source , scoringConfig ) {
1666+ function normalizeRatingPathScore ( row , source , scoringConfig , options = { } ) {
16561667 if ( source === RATING_PATH_SOURCE_DEVELOPMENT ) {
16571668 return normalizeDevelopmentScore ( row )
16581669 }
16591670
1660- return normalizeScore ( row , scoringConfig )
1671+ return normalizeScore ( row , scoringConfig , options )
16611672}
16621673
16631674/**
@@ -1729,6 +1740,8 @@ async function rerateMmRatingPath (membersClient, challengeClient, mmDbClient, r
17291740
17301741 ratingPathChallengesProcessed += 1
17311742
1743+ const usePlacementScore = historyEntry . source === RATING_PATH_SOURCE_MARATHON_MATCH &&
1744+ shouldUsePlacementScores ( participantRows )
17321745 const targetStateBeforeRun = cloneState ( stateByUserId . get ( targetUserKey ) )
17331746 const participantRowsByUserId = new Map ( )
17341747 const participants = participantRows . map ( ( row ) => {
@@ -1741,7 +1754,7 @@ async function rerateMmRatingPath (membersClient, challengeClient, mmDbClient, r
17411754 rating : participantState . rating ,
17421755 volatility : participantState . volatility ,
17431756 numRatings : participantState . numRatings ,
1744- score : normalizeRatingPathScore ( row , historyEntry . source , scoringConfig )
1757+ score : normalizeRatingPathScore ( row , historyEntry . source , scoringConfig , { usePlacementScore } )
17451758 }
17461759 } )
17471760
@@ -1952,6 +1965,7 @@ async function rerateMmTrack (membersClient, challengeClient, mmDbClient, review
19521965 }
19531966
19541967 const participantIds = participantRows . map ( ( row ) => toBigIntUserId ( row . memberId ) )
1968+ const usePlacementScore = shouldUsePlacementScores ( participantRows )
19551969 const missingParticipantIds = participantIds . filter ( ( participantId ) => ! stateByUserId . has ( buildUserStateKey ( participantId ) ) )
19561970 if ( missingParticipantIds . length > 0 ) {
19571971 await loadParticipantHistoryCache ( membersClient , missingParticipantIds , participantHistoryByUserId , dimensionIds )
@@ -1975,7 +1989,7 @@ async function rerateMmTrack (membersClient, challengeClient, mmDbClient, review
19751989 rating : participantState . rating ,
19761990 volatility : participantState . volatility ,
19771991 numRatings : participantState . numRatings ,
1978- score : normalizeScore ( row , scoringConfig )
1992+ score : normalizeScore ( row , scoringConfig , { usePlacementScore } )
19791993 }
19801994 } )
19811995
0 commit comments