Skip to content

Commit dc2a69d

Browse files
authored
Merge pull request #130 from topcoder-platform/PM-5223-1
PM-5223: route QA challenges into Data Science ratings
2 parents 546eddd + cc63dc3 commit dc2a69d

3 files changed

Lines changed: 216 additions & 10 deletions

File tree

src/services/StatisticsService.js

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ const RATING_SOURCE_DEVELOPMENT = 'DEVELOPMENT_CHALLENGE'
6969
const RATING_SOURCE_DATA_SCIENCE_CHALLENGE = 'DATA_SCIENCE_CHALLENGE'
7070
const RATING_SOURCE_MARATHON_MATCH = 'MARATHON_MATCH'
7171
const RERATE_MARATHON_ACTOR = 'rerate-mm-stats'
72+
const CHALLENGE_TRACK_QUALITY_ASSURANCE = 'QUALITY_ASSURANCE'
7273
const CHALLENGE_WINNER_PLACEMENT_TYPE = 'PLACEMENT'
7374
const CHALLENGE_WINNER_PASSED_REVIEW_TYPE = 'PASSED_REVIEW'
7475
const CHALLENGE_WINNER_HISTORY_TYPES = [CHALLENGE_WINNER_PLACEMENT_TYPE, CHALLENGE_WINNER_PASSED_REVIEW_TYPE]
@@ -364,6 +365,38 @@ function isChallengeRatingEnabled (challenge) {
364365
return isChallengeRated(challenge)
365366
}
366367

368+
/**
369+
* Normalize challenge track labels for source-routing checks.
370+
* @param {*} value raw challenge track label, enum value, or abbreviation
371+
* @returns {string} uppercase source track key
372+
*/
373+
function normalizeChallengeSourceTrack (value) {
374+
return String(value || '')
375+
.trim()
376+
.toUpperCase()
377+
.replace(/[\s-]+/g, '_')
378+
}
379+
380+
/**
381+
* Check whether a challenge source track is Quality Assurance.
382+
* QA Challenge results are rated in the public Data Science Challenge bucket.
383+
* @param {*} value raw challenge track label, enum value, or abbreviation
384+
* @returns {boolean} true when the source track is QA
385+
*/
386+
function isQualityAssuranceChallengeSourceTrack (value) {
387+
const normalizedTrack = normalizeChallengeSourceTrack(value)
388+
return normalizedTrack === CHALLENGE_TRACK_QUALITY_ASSURANCE || normalizedTrack === 'QA'
389+
}
390+
391+
/**
392+
* Build the source tracks replayed for Data Science Challenge ratings.
393+
* QA Challenges share the public Data Science Challenge rating bucket.
394+
* @returns {Array<string>} challenge source track labels
395+
*/
396+
function getDataScienceChallengeSourceTrackNames () {
397+
return [TRACK_NAMES.DATA_SCIENCE, CHALLENGE_TRACK_QUALITY_ASSURANCE]
398+
}
399+
367400
/**
368401
* Load challenge metadata needed to decide which ratings apply.
369402
* @param {Object} challengeClient prisma challenge client
@@ -405,7 +438,8 @@ async function fetchChallengeForRatingUpdate (challengeClient, challengeId) {
405438
typeId: true,
406439
track: {
407440
select: {
408-
name: true
441+
name: true,
442+
track: true
409443
}
410444
},
411445
type: {
@@ -432,14 +466,16 @@ async function fetchChallengeForRatingUpdate (challengeClient, challengeId) {
432466
* @returns {string|null} source identifier or null when unsupported
433467
*/
434468
function resolveChallengeRatingSource (challenge) {
435-
const trackName = getCanonicalTrackName(_.get(challenge, 'track.name') || _.get(challenge, 'trackId'))
469+
const rawTrackName = _.get(challenge, 'track.track') || _.get(challenge, 'track.name') || _.get(challenge, 'trackId')
470+
const trackName = getCanonicalTrackName(rawTrackName)
436471
const typeName = getCanonicalTypeName(_.get(challenge, 'type.name') || _.get(challenge, 'typeId'))
437472

438473
if (trackName === TRACK_NAMES.DEVELOP && typeName === TYPE_NAMES.CHALLENGE) {
439474
return RATING_SOURCE_DEVELOPMENT
440475
}
441476

442-
if (trackName === TRACK_NAMES.DATA_SCIENCE && typeName === TYPE_NAMES.CHALLENGE) {
477+
if ((trackName === TRACK_NAMES.DATA_SCIENCE || isQualityAssuranceChallengeSourceTrack(rawTrackName)) &&
478+
typeName === TYPE_NAMES.CHALLENGE) {
443479
return RATING_SOURCE_DATA_SCIENCE_CHALLENGE
444480
}
445481

@@ -554,7 +590,7 @@ async function fetchChallengeResultParticipantIds (reviewDbClient, challengeId)
554590

555591
/**
556592
* Fetch placement winner participants from challenge-api for completed
557-
* Development rating rerates. This covers QA-created challenges where winners
593+
* Development/Data Science rating rerates. This covers challenges where winners
558594
* can be assigned without a review-api challengeResult row for the same member.
559595
* @param {Object} challengeClient challenge Prisma client
560596
* @param {string|number} challengeId challenge identifier
@@ -695,7 +731,7 @@ async function rerateChallengeRatingJobForMember (challengeClient, reviewDbClien
695731
? {
696732
targetTrackName: TRACK_NAMES.DATA_SCIENCE,
697733
targetTypeName: TYPE_NAMES.CHALLENGE,
698-
challengeTrackNames: [TRACK_NAMES.DATA_SCIENCE],
734+
challengeTrackNames: getDataScienceChallengeSourceTrackNames(),
699735
challengeTypeNames: [TYPE_NAMES.CHALLENGE]
700736
}
701737
: undefined
@@ -4270,7 +4306,8 @@ refreshMemberStats.schema = {
42704306
* Re-rate every existing submitter on a completed challenge for all applicable
42714307
* rating dimensions. This includes the native challenge track/type rating when
42724308
* supported and any configured named rating paths whose tags/skills match the
4273-
* challenge, such as the default AI path.
4309+
* challenge, such as the default AI path. Quality Assurance Challenge rows are
4310+
* replayed into the public DATA_SCIENCE / Challenge rating bucket.
42744311
* @param {Object} currentUser the user who performs operation
42754312
* @param {Object} data rerate payload containing the completed challenge id
42764313
* @returns {Object} summary of participants, rating jobs, updates, and per-member failures
@@ -4433,6 +4470,8 @@ rerateChallengeSubmitterRatings.schema = {
44334470
* Trigger a DEVELOPMENT / Challenge, DATA_SCIENCE / Challenge,
44344471
* DATA_SCIENCE / MARATHON_MATCH, or configured tag- or skill-based rating path
44354472
* re-rating pass beginning with the supplied challenge.
4473+
* DATA_SCIENCE / Challenge rerates also replay Quality Assurance Challenge
4474+
* source rows because QA history is surfaced in that public rating bucket.
44364475
* The relevant review-api results are reprocessed in chronological order and
44374476
* persisted into the existing unified rating tables for the member.
44384477
* @param {Object} currentUser the user who performs operation
@@ -4485,7 +4524,7 @@ async function rerateMemberStats (currentUser, handle, data) {
44854524
{
44864525
targetTrackName: TRACK_NAMES.DATA_SCIENCE,
44874526
targetTypeName: TYPE_NAMES.CHALLENGE,
4488-
challengeTrackNames: [TRACK_NAMES.DATA_SCIENCE],
4527+
challengeTrackNames: getDataScienceChallengeSourceTrackNames(),
44894528
challengeTypeNames: [TYPE_NAMES.CHALLENGE]
44904529
}
44914530
)

test/unit/DevelopRatingEngine.test.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,92 @@ describe('develop rating engine unit tests', () => {
598598
members.state.rankRecalculationCalls[0].typeId.should.equal(CHALLENGE_TYPE_ID)
599599
})
600600

601+
it('rerateDevTrack should include QA ChallengeWinner placements in Data Science Challenge rerates', async () => {
602+
const targetUserId = toBigInt(89770374)
603+
const opponentUserId = toBigInt(100000039)
604+
const challengeId = 'qa-rating-jun-2'
605+
const eventDate = new Date('2026-06-02T06:36:07.735Z')
606+
const members = createMembersClient({
607+
historyRows: [],
608+
statsRows: [],
609+
maxRatingRows: []
610+
})
611+
const reviewDbClient = createReviewDbClient([])
612+
const challengeClient = createChallengeClient({
613+
[challengeId]: {
614+
id: challengeId,
615+
status: 'COMPLETED',
616+
endDate: eventDate,
617+
track: { name: 'Quality Assurance' },
618+
type: { name: 'Challenge' },
619+
metadata: []
620+
}
621+
}, [
622+
{
623+
challengeId,
624+
userId: Number(targetUserId),
625+
type: 'PLACEMENT',
626+
placement: 1,
627+
createdAt: new Date('2026-06-02T06:37:00.000Z')
628+
},
629+
{
630+
challengeId,
631+
userId: Number(opponentUserId),
632+
type: 'PLACEMENT',
633+
placement: 2,
634+
createdAt: new Date('2026-06-02T06:37:00.000Z')
635+
}
636+
])
637+
const expectedParticipants = [
638+
createParticipant(targetUserId, 0, 0, 0, -1),
639+
createParticipant(opponentUserId, 0, 0, 0, -2)
640+
]
641+
runQubitsRating(expectedParticipants)
642+
const expectedTarget = expectedParticipants.find((participant) => participant.coderId === String(targetUserId))
643+
644+
const result = await rerateDevTrack(
645+
members.client,
646+
challengeClient,
647+
reviewDbClient,
648+
targetUserId,
649+
challengeId,
650+
{
651+
targetTrackName: 'DATA_SCIENCE',
652+
targetTypeName: 'Challenge',
653+
challengeTrackNames: ['DATA_SCIENCE', 'QUALITY_ASSURANCE'],
654+
challengeTypeNames: ['Challenge']
655+
}
656+
)
657+
658+
result.challengesProcessed.should.equal(1)
659+
result.ratingsUpdated.should.equal(1)
660+
661+
const statsRow = members.state.statsRows.find((row) =>
662+
String(row.userId) === String(targetUserId) &&
663+
row.trackId === DATA_SCIENCE_TRACK_ID &&
664+
row.typeId === CHALLENGE_TYPE_ID
665+
)
666+
should.exist(statsRow)
667+
statsRow.rating.should.equal(expectedTarget.rating)
668+
statsRow.volatility.should.equal(expectedTarget.volatility)
669+
statsRow.challenges.should.equal(1)
670+
statsRow.mostRecentEventDate.should.deep.equal(eventDate)
671+
672+
const historyRow = findHistoryRow(members.state.historyRows, targetUserId, challengeId)
673+
should.exist(historyRow)
674+
historyRow.trackId.should.equal(DATA_SCIENCE_TRACK_ID)
675+
historyRow.typeId.should.equal(CHALLENGE_TYPE_ID)
676+
historyRow.newRating.should.equal(expectedTarget.rating)
677+
historyRow.mostRecent.should.equal(true)
678+
679+
const maxRatingRow = members.state.maxRatingRows.find((row) => String(row.userId) === String(targetUserId))
680+
should.exist(maxRatingRow)
681+
maxRatingRow.rating.should.equal(expectedTarget.rating)
682+
maxRatingRow.track.should.equal('DATA_SCIENCE')
683+
maxRatingRow.subTrack.should.equal('Challenge')
684+
maxRatingRow.ratingColor.should.equal(getRatingColor(expectedTarget.rating))
685+
})
686+
601687
it('rerateDevTrack should seed rerates from prior history instead of current snapshots', async () => {
602688
const targetUserId = toBigInt(1001)
603689
const opponentUserId = toBigInt(2002)

test/unit/StatisticsService.test.js

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -923,7 +923,7 @@ describe('statistics service unit tests', () => {
923923
options: {
924924
targetTrackName: 'DATA_SCIENCE',
925925
targetTypeName: 'Challenge',
926-
challengeTrackNames: ['DATA_SCIENCE'],
926+
challengeTrackNames: ['DATA_SCIENCE', 'QUALITY_ASSURANCE'],
927927
challengeTypeNames: ['Challenge']
928928
}
929929
},
@@ -933,7 +933,7 @@ describe('statistics service unit tests', () => {
933933
options: {
934934
targetTrackName: 'DATA_SCIENCE',
935935
targetTypeName: 'Challenge',
936-
challengeTrackNames: ['DATA_SCIENCE'],
936+
challengeTrackNames: ['DATA_SCIENCE', 'QUALITY_ASSURANCE'],
937937
challengeTypeNames: ['Challenge']
938938
}
939939
}
@@ -1003,7 +1003,88 @@ describe('statistics service unit tests', () => {
10031003
options: {
10041004
targetTrackName: 'DATA_SCIENCE',
10051005
targetTypeName: 'Challenge',
1006-
challengeTrackNames: ['DATA_SCIENCE'],
1006+
challengeTrackNames: ['DATA_SCIENCE', 'QUALITY_ASSURANCE'],
1007+
challengeTypeNames: ['Challenge']
1008+
}
1009+
}
1010+
])
1011+
} finally {
1012+
restore()
1013+
}
1014+
})
1015+
1016+
it('rerateChallengeSubmitterRatings should rate QA Challenge winners in the Data Science Challenge bucket', async () => {
1017+
const qaRerateCalls = []
1018+
const { service, restore } = loadStatisticsService({
1019+
challengeRow: {
1020+
id: 'qa-winner-only-challenge',
1021+
status: 'COMPLETED',
1022+
endDate: new Date('2026-06-10T05:41:34.931Z'),
1023+
trackId: 'track-qa-id',
1024+
typeId: 'type-challenge-id',
1025+
track: { name: 'Quality Assurance', track: 'QUALITY_ASSURANCE' },
1026+
type: { name: 'Challenge' },
1027+
tags: [],
1028+
skills: [],
1029+
metadata: []
1030+
},
1031+
reviewRows: [],
1032+
challengeWinnerRows: [
1033+
{ challengeId: 'qa-winner-only-challenge', userId: 89770374, type: 'PLACEMENT', placement: 1 },
1034+
{ challengeId: 'qa-winner-only-challenge', userId: 100000039, type: 'PLACEMENT', placement: 2 }
1035+
],
1036+
prismaStub: {
1037+
member: {
1038+
findMany: async ({ where }) => where.userId.in.map((userId) => ({ userId }))
1039+
}
1040+
},
1041+
rerateDevTrack: async (membersClient, challengeClient, reviewDbClient, userId, challengeId, options) => {
1042+
qaRerateCalls.push({
1043+
userId: String(userId),
1044+
challengeId,
1045+
options
1046+
})
1047+
return {
1048+
challengesProcessed: 1,
1049+
ratingsUpdated: 1
1050+
}
1051+
}
1052+
})
1053+
1054+
try {
1055+
const result = await service.rerateChallengeSubmitterRatings({ isMachine: true }, {
1056+
challengeId: 'qa-winner-only-challenge'
1057+
})
1058+
1059+
result.rerated.should.equal(true)
1060+
result.membersProcessed.should.equal(2)
1061+
result.ratingsAttempted.should.equal(2)
1062+
result.ratingsUpdated.should.equal(2)
1063+
result.participantIds.should.deep.equal(['89770374', '100000039'])
1064+
result.ratings.should.deep.equal([
1065+
{
1066+
trackId: 'DATA_SCIENCE',
1067+
typeId: 'Challenge'
1068+
}
1069+
])
1070+
qaRerateCalls.should.deep.equal([
1071+
{
1072+
userId: '89770374',
1073+
challengeId: 'qa-winner-only-challenge',
1074+
options: {
1075+
targetTrackName: 'DATA_SCIENCE',
1076+
targetTypeName: 'Challenge',
1077+
challengeTrackNames: ['DATA_SCIENCE', 'QUALITY_ASSURANCE'],
1078+
challengeTypeNames: ['Challenge']
1079+
}
1080+
},
1081+
{
1082+
userId: '100000039',
1083+
challengeId: 'qa-winner-only-challenge',
1084+
options: {
1085+
targetTrackName: 'DATA_SCIENCE',
1086+
targetTypeName: 'Challenge',
1087+
challengeTrackNames: ['DATA_SCIENCE', 'QUALITY_ASSURANCE'],
10071088
challengeTypeNames: ['Challenge']
10081089
}
10091090
}

0 commit comments

Comments
 (0)