Skip to content

PM-3813 ai reviewer configs#1737

Merged
kkartunov merged 38 commits intodevelopfrom
PM-3813_ai-reviewer-configs
Mar 12, 2026
Merged

PM-3813 ai reviewer configs#1737
kkartunov merged 38 commits intodevelopfrom
PM-3813_ai-reviewer-configs

Conversation

@vas3a
Copy link
Collaborator

@vas3a vas3a commented Mar 11, 2026

This was never merged into dev. we tested & QAd it via feature branch.


Open with Devin

vas3a and others added 30 commits February 17, 2026 13:38
…figuration

PM-3851 ai review configuration
@vas3a vas3a requested review from jmgasper and kkartunov March 11, 2026 11:47
context: org-global
filters: &filters-dev
branches:
<<<<<<< HEAD

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
There is a merge conflict marker in the code. This needs to be resolved before merging. Ensure that the correct branches are specified in the only filter.

}

return scorecards.filter(scorecard => (
scorecard &&

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 style]
The check scorecard && is redundant here because scorecards is already defaulted to an empty array, and filter will skip undefined elements.

incrementalCoefficient: defaultReviewer.incrementalCoefficient
})

if (updatedReviewers[index] && (updatedReviewers[index].isMemberReview !== false)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The condition updatedReviewers[index] && (updatedReviewers[index].isMemberReview !== false) is potentially problematic if updatedReviewers[index] is undefined. Consider ensuring updatedReviewers[index] is always defined before this point or handle the case where it might be undefined.

))
const fallbackScorecardId = hasDefaultScorecard
? defaultScorecardId
: normalizeIdValue(scorecardsForPhase[0] && scorecardsForPhase[0].id)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The fallback logic for fallbackScorecardId assumes that scorecardsForPhase always has at least one element. Consider adding a check to ensure scorecardsForPhase is not empty before accessing scorecardsForPhase[0].id.

This comment was marked as resolved.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 6 potential issues.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment on lines +471 to +476
const assignedPhaseIds = new Set(
(challenge.reviewers || [])
.filter((r, i) => i !== index && (r.isMemberReview !== false))
.map(r => r.phaseId)
.filter(id => id !== undefined && id !== null)
)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 HumanReviewTab phase filter excludes wrong reviewer due to index mismatch with full reviewers array

In renderReviewerForm, the assignedPhaseIds filter at line 472-473 iterates over challenge.reviewers (the full array including AI reviewers) and excludes items where i !== index. But index is the human-reviewer-filtered index, while i is the index in the full challenge.reviewers array. When AI reviewers are present, this excludes the wrong reviewer from the phase-assignment check, potentially hiding valid phase options or showing phases that should be excluded.

Prompt for agents
In src/components/ChallengeEditor/ChallengeReviewer-Field/HumanReviewTab.js, in renderReviewerForm (around line 471-476), the assignedPhaseIds computation filters challenge.reviewers using i !== index, but index is the human-filtered index while i iterates over the full reviewers array (including AI reviewers). You need to compute the actual index of the current reviewer in the full challenge.reviewers array first (similar to how updateReviewer does it with indexOf), then use that actualIndex for the exclusion filter. Change the filter from (r, i) => i !== index to exclude the actual reviewer object, for example by comparing against the reviewer object reference or using the actual index in the full array.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Contributor

@kkartunov kkartunov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vas3a few things to check. Other than that great work!

Comment on lines +471 to +476
const assignedPhaseIds = new Set(
(challenge.reviewers || [])
.filter((r, i) => i !== index && (r.isMemberReview !== false))
.map(r => r.phaseId)
.filter(id => id !== undefined && id !== null)
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const updatedReviewers = allChallengeReviewers.filter((_, i) => i !== actualIndex)
onUpdateReviewers({ field: 'reviewers', value: updatedReviewers })
}
}, [challenge.reviewers, onUpdateReviewers, aiReviewers])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ performance]
The addition of aiReviewers to the dependency array of the useCallback hook is correct to ensure that the callback updates when aiReviewers changes. However, ensure that aiReviewers is stable and doesn't change unnecessarily, as it could cause unnecessary re-renders or function re-creations.

<tbody>
{workflows.map((workflow, index) => {
const isAssigned = (challenge.reviewers || []).some(r => r.aiWorkflowId === workflow.workflowId)
const workflowDetails = (availableWorkflows || []).find(w => w.id === workflow.workflowId) || {}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 maintainability]
The use of (availableWorkflows || []) is a defensive programming practice to handle cases where availableWorkflows might be null or undefined. However, since availableWorkflows has a default value of an empty array in defaultProps, this check might be redundant. Consider removing it to simplify the code.

const newAssigned = currentAssigned.slice(0, newCount)

for (const member of removedMembers) {
if (member && member.roleId && member.handle) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The deleteResource function now requires three parameters: challenge.id, member.roleId, and member.handle. Ensure that the deleteResource function is updated accordingly to handle these parameters correctly.

incrementalCoefficient: defaultReviewer.incrementalCoefficient
})

if (updatedReviewers[actualIndex] && (updatedReviewers[actualIndex].isMemberReview !== false)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The index variable index was replaced with actualIndex. Ensure that actualIndex is correctly calculated and corresponds to the intended reviewer in the updatedReviewers array.

const firstPlacePrize = this.getFirstPlacePrizeValue(challenge)
const estimatedSubmissionsCount = 2

const reviewersCost = calculateReviewCost(reviewers, challenge)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The calculation of reviewersCost has been refactored to use calculateReviewCost. Ensure that this function is thoroughly tested and handles all edge cases, as it is critical for calculating costs accurately.

const role = getResourceRoleByName(metadata.resourceRoles || [], roleName)
const resourceRoleId = role && role.id

if (!resourceRoleId) return ''

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Returning an empty string instead of an empty array may lead to unexpected behavior if the caller expects an array. Consider returning an empty array to maintain consistency with the expected return type.


const totalHumanReviewerCount = humanReviewers.reduce((sum, r) => sum + (parseInt(r.memberReviewerCount) || 1), 0)

const reviewCost = calculateReviewCost(humanReviewers, challenge)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
Ensure that the calculateReviewCost function handles all edge cases, such as when humanReviewers or challenge is undefined or null. This will prevent potential runtime errors.

export function calculateReviewCost(humanReviewers = [], challenge = {}) {
const total = humanReviewers
.reduce((sum, r) => {
const memberCount = parseInt(r.memberReviewerCount) || 1

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Using parseInt without a radix can lead to unexpected results if the input string starts with '0'. Consider specifying a radix, e.g., parseInt(r.memberReviewerCount, 10).

const total = humanReviewers
.reduce((sum, r) => {
const memberCount = parseInt(r.memberReviewerCount) || 1
const baseAmount = parseFloat(r.fixedAmount) || 0

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Using parseFloat on potentially undefined values like r.fixedAmount could lead to NaN if r.fixedAmount is not a valid number. Consider validating the input before parsing.

const memberCount = parseInt(r.memberReviewerCount) || 1
const baseAmount = parseFloat(r.fixedAmount) || 0
const prizeSet = challenge.prizeSets && challenge.prizeSets[0]
const prizeValue = prizeSet && prizeSet.prizes && prizeSet.prizes[0] && prizeSet.prizes[0].value

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The nested access of prizeSet.prizes[0].value assumes the structure is always present. Consider adding checks to ensure prizes is an array and has at least one element to avoid runtime errors.

: 0

const estimatedSubmissions = 2
const baseCoefficient = parseFloat(r.baseCoefficient) || 0.13

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Using parseFloat on potentially undefined values like r.baseCoefficient could lead to NaN if r.baseCoefficient is not a valid number. Consider validating the input before parsing.


const estimatedSubmissions = 2
const baseCoefficient = parseFloat(r.baseCoefficient) || 0.13
const incrementalCoefficient = parseFloat(r.incrementalCoefficient) || 0.05

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Using parseFloat on potentially undefined values like r.incrementalCoefficient could lead to NaN if r.incrementalCoefficient is not a valid number. Consider validating the input before parsing.

return sum + calculatedCost
}, 0)

return total.toFixed(2)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 design]
Returning a string from toFixed might not be suitable if the caller expects a number. Consider returning a number by using parseFloat(total.toFixed(2)).

export function calculateReviewCost (humanReviewers = [], challenge = {}) {
const total = humanReviewers
.reduce((sum, r) => {
const memberCount = parseInt(r.memberReviewerCount) || 1

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
Using parseInt without a radix can lead to unexpected results if the input string starts with '0'. Consider specifying a radix, e.g., parseInt(r.memberReviewerCount, 10).

const total = humanReviewers
.reduce((sum, r) => {
const memberCount = parseInt(r.memberReviewerCount) || 1
const baseAmount = parseFloat(r.fixedAmount) || 0

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Using parseFloat without validation can lead to NaN if the input is not a valid number. Consider adding validation to ensure r.fixedAmount is a valid number before parsing.

: 0

const estimatedSubmissions = 2
const baseCoefficient = parseFloat(r.baseCoefficient) || 0.13

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Using parseFloat without validation can lead to NaN if the input is not a valid number. Consider adding validation to ensure r.baseCoefficient is a valid number before parsing.


const estimatedSubmissions = 2
const baseCoefficient = parseFloat(r.baseCoefficient) || 0.13
const incrementalCoefficient = parseFloat(r.incrementalCoefficient) || 0.05

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Using parseFloat without validation can lead to NaN if the input is not a valid number. Consider adding validation to ensure r.incrementalCoefficient is a valid number before parsing.

@vas3a
Copy link
Collaborator Author

vas3a commented Mar 12, 2026

@kkartunov I addressed the concerns 👍

@vas3a vas3a requested a review from kkartunov March 12, 2026 14:28
Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 12 additional findings in Devin Review.

Open in Devin Review

Comment on lines +85 to +87
const resetConfiguration = useCallback((newConfig = initialConfig) => {
setConfiguration(newConfig)
}, [setConfiguration])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 resetConfiguration captures stale initialConfig closure

In useConfigurationState.js at line 85, resetConfiguration uses initialConfig as a default parameter value, but initialConfig is captured in the closure from the hook's first call. Since initialConfig is a default parameter with an object literal, this is stable. However, the useCallback dependency array only lists [setConfiguration] and omits initialConfig. If the caller ever passes a different initialConfig on re-render, resetConfiguration() would use the stale initial value. In practice this is unlikely since the default is always used, making this non-severe.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +33 to +34
challenge.track.name,
challenge.type.name

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 useTemplateManager crashes with challengeTrack.toUpperCase() when challengeTrack is undefined

In useTemplateManager.js at line 50, challengeTrack.toUpperCase() is called without null-checking. The loadTemplates callback is invoked from the effect at line 63-67 only when challengeTrack && challengeType are truthy, but loadTemplates itself is a standalone callback that could be called from outside. More critically, TemplateConfigurationView.js:33 passes challenge.track.name and challenge.type.name — if challenge.track or challenge.type is a string rather than an object (which is possible given the codebase handles both cases at src/components/ChallengeEditor/ChallengeReviewer-Field/index.js:53-58), accessing .name on a string returns undefined, and .toUpperCase() on undefined throws.

Suggested change
challenge.track.name,
challenge.type.name
typeof challenge.track === 'string' ? challenge.track : challenge.track.name,
typeof challenge.type === 'string' ? challenge.type : challenge.type.name
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@kkartunov kkartunov merged commit 077ba98 into develop Mar 12, 2026
6 of 7 checks passed
@vas3a vas3a deleted the PM-3813_ai-reviewer-configs branch March 12, 2026 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants