@@ -68,6 +68,52 @@ function normalizeChallengeDimension (value) {
6868 . replace ( / [ \s - ] + / g, '_' )
6969}
7070
71+ /**
72+ * Normalize one or more rating-context values into an array.
73+ * @param {* } value scalar or array option value
74+ * @returns {Array<*> } option values as an array
75+ */
76+ function toRatingContextArray ( value ) {
77+ if ( Array . isArray ( value ) ) {
78+ return value
79+ }
80+
81+ return value ? [ value ] : [ ]
82+ }
83+
84+ /**
85+ * Build source challenge filters and target unified dimensions for this rating run.
86+ * Defaults preserve the historical DEVELOPMENT / Challenge stream.
87+ * @param {Object } [options] rerate options
88+ * @param {string } [options.targetTrackName] unified stats track to update
89+ * @param {string } [options.targetTypeName] unified stats type to update
90+ * @param {Array<string>|string } [options.challengeTrackNames] source challenge tracks to replay
91+ * @param {Array<string>|string } [options.challengeTypeNames] source challenge types to replay
92+ * @returns {Object } normalized rating context
93+ */
94+ function buildRatingContext ( options = { } ) {
95+ const challengeTrackNames = toRatingContextArray ( options . challengeTrackNames || options . challengeTrackName )
96+ const challengeTypeNames = toRatingContextArray ( options . challengeTypeNames || options . challengeTypeName )
97+
98+ return {
99+ targetTrackName : String ( options . targetTrackName || TRACK_NAME ) . trim ( ) || TRACK_NAME ,
100+ targetTypeName : String ( options . targetTypeName || TYPE_NAME ) . trim ( ) || TYPE_NAME ,
101+ challengeTrackNames : ( challengeTrackNames . length > 0 ? challengeTrackNames : [ CHALLENGE_TRACK_NAME ] )
102+ . map ( normalizeChallengeDimension ) ,
103+ challengeTypeNames : ( challengeTypeNames . length > 0 ? challengeTypeNames : CHALLENGE_TYPE_NAMES )
104+ . map ( normalizeChallengeDimension )
105+ }
106+ }
107+
108+ /**
109+ * Build the human-readable label for rerate errors.
110+ * @param {Object } ratingContext normalized rating context
111+ * @returns {string } target track/type label
112+ */
113+ function getRatingContextLabel ( ratingContext ) {
114+ return `${ ratingContext . targetTrackName } /${ ratingContext . targetTypeName } `
115+ }
116+
71117/**
72118 * Add one non-empty challenge id candidate to the supplied set.
73119 * @param {Set<string> } candidates mutable challenge id candidate set
@@ -150,22 +196,23 @@ function historyEntryMatchesChallengeId (historyEntry, challengeId) {
150196}
151197
152198/**
153- * Resolve whether challenge metadata belongs to the Development Challenge rating stream.
154- * Development CODE challenge rows are rated into the same DEVELOP / Challenge
155- * stream as standard Development Challenge rows.
199+ * Resolve whether challenge metadata belongs to the configured Challenge rating stream.
200+ * By default, Development CODE challenge rows are rated into the same
201+ * DEVELOP / Challenge stream as standard Development Challenge rows.
156202 * @param {Object } challenge challenge metadata record
203+ * @param {Object } [ratingContext] normalized rating context
157204 * @returns {boolean } true when the challenge should be replayed by this engine
158205 */
159- function isDevelopmentRatingChallenge ( challenge ) {
206+ function isDevelopmentRatingChallenge ( challenge , ratingContext = buildRatingContext ( ) ) {
160207 if ( ! challenge || ! challenge . track || ! challenge . type ) {
161208 return false
162209 }
163210
164211 const normalizedTrackName = normalizeChallengeDimension ( challenge . track . name )
165212 const normalizedTypeName = normalizeChallengeDimension ( challenge . type . name )
166- const supportedTypeNames = CHALLENGE_TYPE_NAMES . map ( normalizeChallengeDimension )
167213
168- return normalizedTrackName === CHALLENGE_TRACK_NAME && supportedTypeNames . includes ( normalizedTypeName )
214+ return ratingContext . challengeTrackNames . includes ( normalizedTrackName ) &&
215+ ratingContext . challengeTypeNames . includes ( normalizedTypeName )
169216}
170217
171218function isCompletedChallenge ( challenge ) {
@@ -246,24 +293,25 @@ function buildUserStateKey (userId) {
246293}
247294
248295/**
249- * Resolve the unified track/type UUIDs used for DEVELOPMENT / Challenge rows .
296+ * Resolve the unified track/type UUIDs used for this challenge-result rating stream .
250297 * @param {Object } challengeClient prisma challenge client
298+ * @param {Object } [ratingContext] normalized rating context
251299 * @returns {Promise<{trackId: string, typeId: string, trackName: string, typeName: string, dimensionLookup: Object}> } resolved unified ids
252300 */
253- async function resolveUnifiedDimensionIds ( challengeClient ) {
301+ async function resolveUnifiedDimensionIds ( challengeClient , ratingContext = buildRatingContext ( ) ) {
254302 const dimensionLookup = await loadChallengeDimensionLookup ( challengeClient )
255- const trackId = resolveTrackIdFromLookup ( dimensionLookup , TRACK_NAME )
256- const typeId = resolveTypeIdFromLookup ( dimensionLookup , TYPE_NAME )
303+ const trackId = resolveTrackIdFromLookup ( dimensionLookup , ratingContext . targetTrackName )
304+ const typeId = resolveTypeIdFromLookup ( dimensionLookup , ratingContext . targetTypeName )
257305
258306 if ( ! trackId || ! typeId ) {
259- throw new Error ( `Unable to resolve unified dimension ids for ${ TRACK_NAME } / ${ TYPE_NAME } ` )
307+ throw new Error ( `Unable to resolve unified dimension ids for ${ getRatingContextLabel ( ratingContext ) } ` )
260308 }
261309
262310 return {
263311 trackId,
264312 typeId,
265- trackName : TRACK_NAME ,
266- typeName : TYPE_NAME ,
313+ trackName : ratingContext . targetTrackName ,
314+ typeName : ratingContext . targetTypeName ,
267315 dimensionLookup
268316 }
269317}
@@ -548,10 +596,12 @@ function isCanonicalReviewChallengeRow (row, challenge) {
548596 * @param {Object } [options] history filtering options
549597 * @param {boolean } [options.skipLegacyReviewIds=false] ignore legacy numeric review ids that are already represented by legacy subtrack history
550598 * @param {boolean } [options.useLegacySourceRatings=false] preserve challengeResult oldRating/newRating for legacy-backed rows
599+ * @param {Object } [options.ratingContext] source and target dimension config
551600 * @returns {Array<Object> } ordered challenge history entries for rerating
552601 */
553602function buildTargetHistory ( reviewRows , challengeMetadataById , options = { } ) {
554603 const historyByChallengeId = new Map ( )
604+ const ratingContext = options . ratingContext || buildRatingContext ( )
555605
556606 reviewRows . forEach ( ( row ) => {
557607 if ( ! isParticipantEligibleForRating ( row ) ) {
@@ -575,7 +625,7 @@ function buildTargetHistory (reviewRows, challengeMetadataById, options = {}) {
575625 return
576626 }
577627
578- if ( ! isDevelopmentRatingChallenge ( challenge ) ) {
628+ if ( ! isDevelopmentRatingChallenge ( challenge , ratingContext ) ) {
579629 return
580630 }
581631
@@ -942,6 +992,10 @@ async function refreshMostRecentHistoryFlag (tx, userId, dimensionIds) {
942992 * @param {boolean } [options.recalculateRanks=true] recompute Develop Challenge ranks after this member rerate
943993 * @param {boolean } [options.skipLegacyReviewIds=false] skip legacy numeric challengeResult aliases during full migration rerates
944994 * @param {boolean } [options.useLegacySourceRatings=false] preserve challengeResult oldRating/newRating for legacy-backed rows
995+ * @param {string } [options.targetTrackName=DEVELOP] unified stats track to update
996+ * @param {string } [options.targetTypeName=Challenge] unified stats type to update
997+ * @param {Array<string>|string } [options.challengeTrackNames=DEVELOPMENT] source challenge tracks to replay
998+ * @param {Array<string>|string } [options.challengeTypeNames=[Challenge,CODE]] source challenge types to replay
945999 * @returns {Promise<{challengesProcessed: number, ratingsUpdated: number}> } rerate counters
9461000 * @throws {Error } when required review DB or dimension data is unavailable
9471001 */
@@ -951,6 +1005,7 @@ async function rerateDevTrack (membersClient, challengeClient, reviewDbClient, u
9511005 }
9521006
9531007 const normalizedUserId = toBigIntUserId ( userId )
1008+ const ratingContext = buildRatingContext ( options )
9541009 const reviewRows = await fetchReviewResultsForUser ( reviewDbClient , normalizedUserId )
9551010 if ( reviewRows . length === 0 ) {
9561011 return {
@@ -966,7 +1021,8 @@ async function rerateDevTrack (membersClient, challengeClient, reviewDbClient, u
9661021
9671022 const targetHistory = buildTargetHistory ( reviewRows , challengeMetadataById , {
9681023 skipLegacyReviewIds : options . skipLegacyReviewIds === true ,
969- useLegacySourceRatings : options . useLegacySourceRatings === true
1024+ useLegacySourceRatings : options . useLegacySourceRatings === true ,
1025+ ratingContext
9701026 } )
9711027 if ( targetHistory . length === 0 ) {
9721028 return {
@@ -975,13 +1031,14 @@ async function rerateDevTrack (membersClient, challengeClient, reviewDbClient, u
9751031 }
9761032 }
9771033
978- const dimensionIds = await resolveUnifiedDimensionIds ( challengeClient )
1034+ const dimensionIds = await resolveUnifiedDimensionIds ( challengeClient , ratingContext )
1035+ const ratingLabel = getRatingContextLabel ( ratingContext )
9791036
9801037 let startIndex = 0
9811038 if ( fromChallengeId ) {
9821039 startIndex = targetHistory . findIndex ( ( entry ) => historyEntryMatchesChallengeId ( entry , fromChallengeId ) )
9831040 if ( startIndex < 0 ) {
984- throw new errors . BadRequestError ( `Challenge ${ fromChallengeId } is not a rated ${ TRACK_NAME } / ${ TYPE_NAME } event for this member` )
1041+ throw new errors . BadRequestError ( `Challenge ${ fromChallengeId } is not a rated ${ ratingLabel } event for this member` )
9851042 }
9861043 }
9871044
0 commit comments