fix: ranking shows all reviewed projects, fix override badge sync issue
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m17s

- AI ranking now includes ALL projects (never filters/excludes any)
- Updated system prompt: filter criteria inform priority, not exclusion
- Dynamic maxTokens scaling for large project pools (80 tokens/project)
- Fallback: projects AI omits are appended sorted by composite score
- Override badge uses snapshotOrder state (synced with localOrder in same
  useEffect) instead of rankingMap.originalIndex to prevent stale-render
  mismatch where all items incorrectly showed as overridden

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 14:10:48 +01:00
parent d2e0dbdc94
commit 80a7bedddc
2 changed files with 47 additions and 9 deletions

View File

@@ -90,6 +90,7 @@ type SortableProjectRowProps = {
jurorScores: JurorScore[] | undefined jurorScores: JurorScore[] | undefined
onSelect: () => void onSelect: () => void
isSelected: boolean isSelected: boolean
originalRank: number | undefined // from snapshotOrder — always in sync with localOrder
} }
// ─── Sub-component: SortableProjectRow ──────────────────────────────────────── // ─── Sub-component: SortableProjectRow ────────────────────────────────────────
@@ -102,6 +103,7 @@ function SortableProjectRow({
jurorScores, jurorScores,
onSelect, onSelect,
isSelected, isSelected,
originalRank,
}: SortableProjectRowProps) { }: SortableProjectRowProps) {
const { const {
attributes, attributes,
@@ -117,8 +119,9 @@ function SortableProjectRow({
transition, transition,
} }
// isOverridden: admin drag-reordered this project from its original snapshot position // isOverridden: admin drag-reordered this project from its original snapshot position.
const isOverridden = entry !== undefined && currentRank !== entry.originalIndex // Uses snapshotOrder (set in same effect as localOrder) so they are always in sync.
const isOverridden = originalRank !== undefined && currentRank !== originalRank
// Compute yes count from juror scores // Compute yes count from juror scores
const yesCount = jurorScores?.filter((j) => j.decision === true).length ?? 0 const yesCount = jurorScores?.filter((j) => j.decision === true).length ?? 0
@@ -236,6 +239,9 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
STARTUP: [], STARTUP: [],
BUSINESS_CONCEPT: [], BUSINESS_CONCEPT: [],
}) })
// Track the original snapshot order (projectId → 1-based rank) for override detection.
// Updated in the same effect as localOrder so they are always in sync.
const [snapshotOrder, setSnapshotOrder] = useState<Record<string, number>>({})
const initialized = useRef(false) const initialized = useRef(false)
const pendingReorderCount = useRef(0) const pendingReorderCount = useRef(0)
@@ -404,6 +410,11 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
STARTUP: startup.map((r) => r.projectId), STARTUP: startup.map((r) => r.projectId),
BUSINESS_CONCEPT: concept.map((r) => r.projectId), BUSINESS_CONCEPT: concept.map((r) => r.projectId),
}) })
// Track original order for override detection (same effect = always in sync)
const order: Record<string, number> = {}
startup.forEach((r, i) => { order[r.projectId] = i + 1 })
concept.forEach((r, i) => { order[r.projectId] = i + 1 })
setSnapshotOrder(order)
initialized.current = true initialized.current = true
} }
}, [snapshot]) }, [snapshot])
@@ -853,6 +864,7 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
jurorScores={evalScores?.[projectId]} jurorScores={evalScores?.[projectId]}
onSelect={() => setSelectedProjectId(projectId)} onSelect={() => setSelectedProjectId(projectId)}
isSelected={selectedProjectId === projectId} isSelected={selectedProjectId === projectId}
originalRank={snapshotOrder[projectId]}
/> />
</motion.div> </motion.div>
{isCutoffRow && ( {isCutoffRow && (

View File

@@ -153,12 +153,13 @@ Return JSON only:
] ]
} }
Rules: CRITICAL Rules:
- Apply filter rules first (remove projects that fail the filter) - You MUST include EVERY project in the ranked output — never exclude or filter out any project
- Apply sort rules next (order remaining projects) - Apply sort rules to determine the ranking order
- Apply limit rules last (keep only top N) - If filter criteria exist, use them to inform ranking priority (projects meeting all criteria rank higher, those failing criteria rank lower) but still include ALL projects
- Projects not in the ranked output are considered excluded (not ranked last) - Ignore any limit rules — always return all projects
- Use the project_id values exactly as given — do not change them` - Use the project_id values exactly as given — do not change them
- Ranks must be contiguous (1, 2, 3, …) with no gaps`
// ─── Helpers ────────────────────────────────────────────────────────────────── // ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -471,7 +472,8 @@ export async function executeAIRanking(
], ],
jsonMode: true, jsonMode: true,
temperature: 0, temperature: 0,
maxTokens: 2000, // ~50 tokens per project entry; scale for large pools with generous buffer
maxTokens: Math.max(2000, projects.length * 80),
}) })
let response: Awaited<ReturnType<typeof openai.chat.completions.create>> let response: Awaited<ReturnType<typeof openai.chat.completions.create>>
@@ -531,6 +533,30 @@ export async function executeAIRanking(
}) })
.sort((a, b) => a.rank - b.rank) .sort((a, b) => a.rank - b.rank)
// ─── Ensure ALL projects are included (AI may omit some due to token limits) ──
const rankedIds = new Set(rankedProjects.map((r) => r.projectId))
const unrankedProjects = projects
.filter((p) => !rankedIds.has(p.id))
.map((p) => ({
projectId: p.id,
rank: 0,
compositeScore: computeCompositeScore(p, maxEvaluatorCount, criteriaWeights, criterionDefs),
avgGlobalScore: p.avgGlobalScore,
normalizedAvgScore: p.normalizedAvgScore,
passRate: p.passRate,
evaluatorCount: p.evaluatorCount,
}))
.sort((a, b) => b.compositeScore - a.compositeScore)
let nextRank = rankedProjects.length + 1
for (const proj of unrankedProjects) {
proj.rank = nextRank++
rankedProjects.push(proj)
}
// Re-normalize ranks to be contiguous (1, 2, 3, …)
rankedProjects.forEach((p, i) => { p.rank = i + 1 })
return { return {
category, category,
rankedProjects, rankedProjects,