2026-02-14 15:26:42 +01:00
import { z } from 'zod'
import { TRPCError } from '@trpc/server'
import { router , protectedProcedure , adminProcedure , juryProcedure } from '../trpc'
import { logAudit } from '@/server/utils/audit'
import { notifyAdmins , NotificationTypes } from '../services/in-app-notification'
2026-02-19 18:30:01 +01:00
import { reassignAfterCOI } from './assignment'
2026-02-19 12:15:51 +01:00
import { sendManualReminders } from '../services/evaluation-reminders'
2026-02-14 15:26:42 +01:00
import { generateSummary } from '@/server/services/ai-evaluation-summary'
export const evaluationRouter = router ( {
/ * *
* Get evaluation for an assignment
* /
get : protectedProcedure
. input ( z . object ( { assignmentId : z.string ( ) } ) )
. query ( async ( { ctx , input } ) = > {
// Verify ownership or admin
const assignment = await ctx . prisma . assignment . findUniqueOrThrow ( {
where : { id : input.assignmentId } ,
} )
if (
ctx . user . role === 'JURY_MEMBER' &&
assignment . userId !== ctx . user . id
) {
throw new TRPCError ( { code : 'FORBIDDEN' } )
}
return ctx . prisma . evaluation . findUnique ( {
where : { assignmentId : input.assignmentId } ,
include : {
form : true ,
} ,
} )
} ) ,
/ * *
* Start an evaluation ( creates draft )
* /
start : protectedProcedure
. input (
z . object ( {
assignmentId : z.string ( ) ,
} )
)
. mutation ( async ( { ctx , input } ) = > {
// Verify assignment ownership
const assignment = await ctx . prisma . assignment . findUniqueOrThrow ( {
where : { id : input.assignmentId } ,
} )
if ( assignment . userId !== ctx . user . id ) {
throw new TRPCError ( { code : 'FORBIDDEN' } )
}
// Get active form for this stage
const form = await ctx . prisma . evaluationForm . findFirst ( {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
where : { roundId : assignment.roundId , isActive : true } ,
2026-02-14 15:26:42 +01:00
} )
if ( ! form ) {
throw new TRPCError ( {
code : 'BAD_REQUEST' ,
message : 'No active evaluation form for this stage' ,
} )
}
// Check if evaluation exists
const existing = await ctx . prisma . evaluation . findUnique ( {
where : { assignmentId : input.assignmentId } ,
} )
if ( existing ) return existing
return ctx . prisma . evaluation . create ( {
data : {
assignmentId : input.assignmentId ,
formId : form.id ,
status : 'DRAFT' ,
} ,
} )
} ) ,
/ * *
* Autosave evaluation ( debounced on client )
* /
autosave : protectedProcedure
. input (
z . object ( {
id : z.string ( ) ,
criterionScoresJson : z.record ( z . union ( [ z . number ( ) , z . string ( ) , z . boolean ( ) ] ) ) . optional ( ) ,
globalScore : z.number ( ) . int ( ) . min ( 1 ) . max ( 10 ) . optional ( ) . nullable ( ) ,
binaryDecision : z.boolean ( ) . optional ( ) . nullable ( ) ,
feedbackText : z.string ( ) . optional ( ) . nullable ( ) ,
} )
)
. mutation ( async ( { ctx , input } ) = > {
const { id , . . . data } = input
// Verify ownership and status
const evaluation = await ctx . prisma . evaluation . findUniqueOrThrow ( {
where : { id } ,
include : { assignment : true } ,
} )
if ( evaluation . assignment . userId !== ctx . user . id ) {
throw new TRPCError ( { code : 'FORBIDDEN' } )
}
if (
evaluation . status === 'SUBMITTED' ||
evaluation . status === 'LOCKED'
) {
throw new TRPCError ( {
code : 'BAD_REQUEST' ,
message : 'Cannot edit submitted evaluation' ,
} )
}
return ctx . prisma . evaluation . update ( {
where : { id } ,
data : {
. . . data ,
status : 'DRAFT' ,
} ,
} )
} ) ,
/ * *
* Submit evaluation ( final )
* /
submit : protectedProcedure
. input (
z . object ( {
id : z.string ( ) ,
criterionScoresJson : z.record ( z . union ( [ z . number ( ) , z . string ( ) , z . boolean ( ) ] ) ) ,
2026-02-19 12:59:35 +01:00
globalScore : z.number ( ) . int ( ) . min ( 1 ) . max ( 10 ) . optional ( ) ,
binaryDecision : z.boolean ( ) . optional ( ) ,
feedbackText : z.string ( ) . optional ( ) ,
2026-02-14 15:26:42 +01:00
} )
)
. mutation ( async ( { ctx , input } ) = > {
const { id , . . . data } = input
// Verify ownership
const evaluation = await ctx . prisma . evaluation . findUniqueOrThrow ( {
where : { id } ,
include : {
assignment : true ,
} ,
} )
if ( evaluation . assignment . userId !== ctx . user . id ) {
throw new TRPCError ( { code : 'FORBIDDEN' } )
}
2026-02-19 12:59:35 +01:00
// Server-side COI check
const coi = await ctx . prisma . conflictOfInterest . findFirst ( {
where : { assignmentId : evaluation.assignmentId , hasConflict : true } ,
} )
if ( coi ) {
throw new TRPCError ( {
code : 'FORBIDDEN' ,
message : 'Cannot submit evaluation — conflict of interest declared' ,
} )
}
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
// Check voting window via round
const round = await ctx . prisma . round . findUniqueOrThrow ( {
where : { id : evaluation.assignment.roundId } ,
2026-02-14 15:26:42 +01:00
} )
const now = new Date ( )
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
if ( round . status !== 'ROUND_ACTIVE' ) {
2026-02-14 15:26:42 +01:00
throw new TRPCError ( {
code : 'BAD_REQUEST' ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
message : 'Round is not active' ,
2026-02-14 15:26:42 +01:00
} )
}
// Check for grace period
const gracePeriod = await ctx . prisma . gracePeriod . findFirst ( {
where : {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : round.id ,
2026-02-14 15:26:42 +01:00
userId : ctx.user.id ,
OR : [
{ projectId : null } ,
{ projectId : evaluation.assignment.projectId } ,
] ,
extendedUntil : { gte : now } ,
} ,
} )
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
const effectiveEndDate = gracePeriod ? . extendedUntil ? ? round . windowCloseAt
2026-02-14 15:26:42 +01:00
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
if ( round . windowOpenAt && now < round . windowOpenAt ) {
2026-02-14 15:26:42 +01:00
throw new TRPCError ( {
code : 'BAD_REQUEST' ,
message : 'Voting has not started yet' ,
} )
}
if ( effectiveEndDate && now > effectiveEndDate ) {
throw new TRPCError ( {
code : 'BAD_REQUEST' ,
message : 'Voting window has closed' ,
} )
}
2026-02-19 12:59:35 +01:00
// Load round config for validation
const config = ( round . configJson as Record < string , unknown > ) || { }
const scoringMode = ( config . scoringMode as string ) || 'criteria'
// Fix 3: Dynamic feedback validation based on config
const requireFeedback = config . requireFeedback !== false
if ( requireFeedback ) {
const feedbackMinLength = ( config . feedbackMinLength as number ) || 10
if ( ! data . feedbackText || data . feedbackText . length < feedbackMinLength ) {
throw new TRPCError ( {
code : 'BAD_REQUEST' ,
message : ` Feedback must be at least ${ feedbackMinLength } characters ` ,
} )
}
}
// Fix 4: Normalize binaryDecision and globalScore based on scoringMode
if ( scoringMode !== 'binary' ) {
data . binaryDecision = undefined
}
if ( scoringMode === 'binary' ) {
data . globalScore = undefined
}
// Fix 5: requireAllCriteriaScored validation
if ( config . requireAllCriteriaScored && scoringMode === 'criteria' ) {
const evalForm = await ctx . prisma . evaluationForm . findFirst ( {
where : { roundId : round.id , isActive : true } ,
select : { criteriaJson : true } ,
} )
if ( evalForm ? . criteriaJson ) {
const criteria = evalForm . criteriaJson as Array < { id : string ; type ? : string ; required? : boolean } >
const scorableCriteria = criteria . filter (
( c ) = > c . type !== 'section_header' && c . type !== 'text' && c . required !== false
)
const scores = data . criterionScoresJson as Record < string , unknown > | undefined
const missingCriteria = scorableCriteria . filter (
( c ) = > ! scores || typeof scores [ c . id ] !== 'number'
)
if ( missingCriteria . length > 0 ) {
throw new TRPCError ( {
code : 'BAD_REQUEST' ,
message : ` Missing scores for criteria: ${ missingCriteria . map ( ( c ) = > c . id ) . join ( ', ' ) } ` ,
} )
}
}
}
2026-02-14 15:26:42 +01:00
// Submit evaluation and mark assignment as completed atomically
2026-02-19 12:59:35 +01:00
const saveData = {
criterionScoresJson : data.criterionScoresJson ,
globalScore : data.globalScore ? ? null ,
binaryDecision : data.binaryDecision ? ? null ,
feedbackText : data.feedbackText ? ? null ,
}
2026-02-14 15:26:42 +01:00
const [ updated ] = await ctx . prisma . $transaction ( [
ctx . prisma . evaluation . update ( {
where : { id } ,
data : {
2026-02-19 12:59:35 +01:00
. . . saveData ,
2026-02-14 15:26:42 +01:00
status : 'SUBMITTED' ,
submittedAt : now ,
} ,
} ) ,
ctx . prisma . assignment . update ( {
where : { id : evaluation.assignmentId } ,
data : { isCompleted : true } ,
} ) ,
] )
// Audit log
await logAudit ( {
prisma : ctx.prisma ,
userId : ctx.user.id ,
action : 'EVALUATION_SUBMITTED' ,
entityType : 'Evaluation' ,
entityId : id ,
detailsJson : {
projectId : evaluation.assignment.projectId ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : evaluation.assignment.roundId ,
2026-02-14 15:26:42 +01:00
globalScore : data.globalScore ,
binaryDecision : data.binaryDecision ,
} ,
ipAddress : ctx.ip ,
userAgent : ctx.userAgent ,
} )
return updated
} ) ,
2026-02-18 14:03:38 +01:00
/ * *
* Reset ( erase ) an evaluation so a juror can start over ( admin only )
* Deletes the evaluation record and resets the assignment ' s isCompleted flag .
* /
resetEvaluation : adminProcedure
. input (
z . object ( {
assignmentId : z.string ( ) ,
} )
)
. mutation ( async ( { ctx , input } ) = > {
const assignment = await ctx . prisma . assignment . findUniqueOrThrow ( {
where : { id : input.assignmentId } ,
include : {
evaluation : true ,
user : { select : { id : true , name : true , email : true } } ,
project : { select : { id : true , title : true } } ,
} ,
} )
if ( ! assignment . evaluation ) {
throw new TRPCError ( {
code : 'NOT_FOUND' ,
message : 'No evaluation found for this assignment' ,
} )
}
// Delete the evaluation and reset assignment completion in a transaction
await ctx . prisma . $transaction ( [
ctx . prisma . evaluation . delete ( {
where : { id : assignment.evaluation.id } ,
} ) ,
ctx . prisma . assignment . update ( {
where : { id : input.assignmentId } ,
data : { isCompleted : false } ,
} ) ,
] )
// Audit log
await logAudit ( {
prisma : ctx.prisma ,
userId : ctx.user.id ,
action : 'EVALUATION_RESET' ,
entityType : 'Evaluation' ,
entityId : assignment.evaluation.id ,
detailsJson : {
assignmentId : input.assignmentId ,
jurorId : assignment.user.id ,
jurorName : assignment.user.name || assignment . user . email ,
projectId : assignment.project.id ,
projectTitle : assignment.project.title ,
previousStatus : assignment.evaluation.status ,
previousGlobalScore : assignment.evaluation.globalScore ,
} ,
ipAddress : ctx.ip ,
userAgent : ctx.userAgent ,
} )
return { success : true }
} ) ,
2026-02-14 15:26:42 +01:00
/ * *
* Get aggregated stats for a project ( admin only )
* /
getProjectStats : adminProcedure
. input ( z . object ( { projectId : z.string ( ) } ) )
. query ( async ( { ctx , input } ) = > {
const evaluations = await ctx . prisma . evaluation . findMany ( {
where : {
status : 'SUBMITTED' ,
assignment : { projectId : input.projectId } ,
} ,
} )
if ( evaluations . length === 0 ) {
return null
}
const globalScores = evaluations
. map ( ( e ) = > e . globalScore )
. filter ( ( s ) : s is number = > s !== null )
const yesVotes = evaluations . filter (
( e ) = > e . binaryDecision === true
) . length
return {
totalEvaluations : evaluations.length ,
averageGlobalScore :
globalScores . length > 0
? globalScores . reduce ( ( a , b ) = > a + b , 0 ) / globalScores . length
: null ,
minScore : globalScores.length > 0 ? Math . min ( . . . globalScores ) : null ,
maxScore : globalScores.length > 0 ? Math . max ( . . . globalScores ) : null ,
yesVotes ,
noVotes : evaluations.length - yesVotes ,
yesPercentage : ( yesVotes / evaluations . length ) * 100 ,
}
} ) ,
/ * *
* Get all evaluations for a stage ( admin only )
* /
listByStage : adminProcedure
. input (
z . object ( {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : z.string ( ) ,
2026-02-14 15:26:42 +01:00
status : z.enum ( [ 'NOT_STARTED' , 'DRAFT' , 'SUBMITTED' , 'LOCKED' ] ) . optional ( ) ,
} )
)
. query ( async ( { ctx , input } ) = > {
return ctx . prisma . evaluation . findMany ( {
where : {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
assignment : { roundId : input.roundId } ,
2026-02-14 15:26:42 +01:00
. . . ( input . status && { status : input.status } ) ,
} ,
include : {
assignment : {
include : {
user : { select : { id : true , name : true , email : true } } ,
project : { select : { id : true , title : true } } ,
} ,
} ,
} ,
orderBy : { updatedAt : 'desc' } ,
} )
} ) ,
/ * *
* Get my past evaluations ( read - only for jury )
* /
myPastEvaluations : protectedProcedure
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
. input ( z . object ( { roundId : z.string ( ) . optional ( ) } ) )
2026-02-14 15:26:42 +01:00
. query ( async ( { ctx , input } ) = > {
return ctx . prisma . evaluation . findMany ( {
where : {
assignment : {
userId : ctx.user.id ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
. . . ( input . roundId && { roundId : input.roundId } ) ,
2026-02-14 15:26:42 +01:00
} ,
status : 'SUBMITTED' ,
} ,
include : {
assignment : {
include : {
project : { select : { id : true , title : true } } ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
round : { select : { id : true , name : true } } ,
2026-02-14 15:26:42 +01:00
} ,
} ,
} ,
orderBy : { submittedAt : 'desc' } ,
} )
} ) ,
// =========================================================================
// Conflict of Interest (COI) Endpoints
// =========================================================================
/ * *
* Declare a conflict of interest for an assignment
* /
declareCOI : protectedProcedure
. input (
z . object ( {
assignmentId : z.string ( ) ,
hasConflict : z.boolean ( ) ,
conflictType : z.string ( ) . optional ( ) ,
description : z.string ( ) . optional ( ) ,
} )
)
. mutation ( async ( { ctx , input } ) = > {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
// Look up the assignment to get projectId, roundId, userId
2026-02-14 15:26:42 +01:00
const assignment = await ctx . prisma . assignment . findUniqueOrThrow ( {
where : { id : input.assignmentId } ,
include : {
project : { select : { title : true } } ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
round : { select : { id : true , name : true } } ,
2026-02-14 15:26:42 +01:00
} ,
} )
// Verify ownership
if ( assignment . userId !== ctx . user . id ) {
throw new TRPCError ( { code : 'FORBIDDEN' } )
}
// Upsert COI record
const coi = await ctx . prisma . conflictOfInterest . upsert ( {
where : { assignmentId : input.assignmentId } ,
create : {
assignmentId : input.assignmentId ,
userId : ctx.user.id ,
projectId : assignment.projectId ,
hasConflict : input.hasConflict ,
conflictType : input.hasConflict ? input.conflictType : null ,
description : input.hasConflict ? input.description : null ,
} ,
update : {
hasConflict : input.hasConflict ,
conflictType : input.hasConflict ? input.conflictType : null ,
description : input.hasConflict ? input.description : null ,
declaredAt : new Date ( ) ,
} ,
} )
// Notify admins if conflict declared
if ( input . hasConflict ) {
await notifyAdmins ( {
type : NotificationTypes . JURY_INACTIVE ,
title : 'Conflict of Interest Declared' ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
message : ` ${ ctx . user . name || ctx . user . email } declared a conflict of interest ( ${ input . conflictType || 'unspecified' } ) for project " ${ assignment . project . title } " in ${ assignment . round . name } . ` ,
linkUrl : ` /admin/stages/ ${ assignment . roundId } /coi ` ,
2026-02-14 15:26:42 +01:00
linkLabel : 'Review COI' ,
priority : 'high' ,
metadata : {
assignmentId : input.assignmentId ,
userId : ctx.user.id ,
projectId : assignment.projectId ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : assignment.roundId ,
2026-02-14 15:26:42 +01:00
conflictType : input.conflictType ,
} ,
} )
}
// Audit log
await logAudit ( {
prisma : ctx.prisma ,
userId : ctx.user.id ,
action : 'COI_DECLARED' ,
entityType : 'ConflictOfInterest' ,
entityId : coi.id ,
detailsJson : {
assignmentId : input.assignmentId ,
projectId : assignment.projectId ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : assignment.roundId ,
2026-02-14 15:26:42 +01:00
hasConflict : input.hasConflict ,
conflictType : input.conflictType ,
} ,
ipAddress : ctx.ip ,
userAgent : ctx.userAgent ,
} )
2026-02-19 18:30:01 +01:00
// Auto-reassign the project to another eligible juror
let reassignment : { newJurorId : string ; newJurorName : string } | null = null
if ( input . hasConflict ) {
try {
reassignment = await reassignAfterCOI ( {
assignmentId : input.assignmentId ,
auditUserId : ctx.user.id ,
auditIp : ctx.ip ,
auditUserAgent : ctx.userAgent ,
} )
} catch ( err ) {
// Don't fail the COI declaration if reassignment fails
console . error ( '[COI] Auto-reassignment failed:' , err )
}
}
return { . . . coi , reassignment }
2026-02-14 15:26:42 +01:00
} ) ,
/ * *
* Get COI status for an assignment
* /
getCOIStatus : protectedProcedure
. input ( z . object ( { assignmentId : z.string ( ) } ) )
. query ( async ( { ctx , input } ) = > {
return ctx . prisma . conflictOfInterest . findUnique ( {
where : { assignmentId : input.assignmentId } ,
} )
} ) ,
/ * *
* List COI declarations for a stage ( admin only )
* /
listCOIByStage : adminProcedure
. input (
z . object ( {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : z.string ( ) ,
2026-02-14 15:26:42 +01:00
hasConflictOnly : z.boolean ( ) . optional ( ) ,
} )
)
. query ( async ( { ctx , input } ) = > {
return ctx . prisma . conflictOfInterest . findMany ( {
where : {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
assignment : { roundId : input.roundId } ,
2026-02-14 15:26:42 +01:00
. . . ( input . hasConflictOnly && { hasConflict : true } ) ,
} ,
include : {
user : { select : { id : true , name : true , email : true } } ,
assignment : {
include : {
project : { select : { id : true , title : true } } ,
} ,
} ,
reviewedBy : { select : { id : true , name : true , email : true } } ,
} ,
orderBy : { declaredAt : 'desc' } ,
} )
} ) ,
/ * *
* Review a COI declaration ( admin only )
* /
reviewCOI : adminProcedure
. input (
z . object ( {
id : z.string ( ) ,
reviewAction : z.enum ( [ 'cleared' , 'reassigned' , 'noted' ] ) ,
} )
)
. mutation ( async ( { ctx , input } ) = > {
const coi = await ctx . prisma . conflictOfInterest . update ( {
where : { id : input.id } ,
data : {
reviewedById : ctx.user.id ,
reviewedAt : new Date ( ) ,
reviewAction : input.reviewAction ,
} ,
} )
2026-02-19 18:30:01 +01:00
// If admin chose "reassigned", trigger actual reassignment
let reassignment : { newJurorId : string ; newJurorName : string } | null = null
if ( input . reviewAction === 'reassigned' ) {
reassignment = await reassignAfterCOI ( {
assignmentId : coi.assignmentId ,
auditUserId : ctx.user.id ,
auditIp : ctx.ip ,
auditUserAgent : ctx.userAgent ,
} )
}
2026-02-14 15:26:42 +01:00
// Audit log
await logAudit ( {
prisma : ctx.prisma ,
userId : ctx.user.id ,
action : 'COI_REVIEWED' ,
entityType : 'ConflictOfInterest' ,
entityId : input.id ,
detailsJson : {
reviewAction : input.reviewAction ,
assignmentId : coi.assignmentId ,
userId : coi.userId ,
projectId : coi.projectId ,
2026-02-19 18:30:01 +01:00
reassignedTo : reassignment?.newJurorId ,
2026-02-14 15:26:42 +01:00
} ,
ipAddress : ctx.ip ,
userAgent : ctx.userAgent ,
} )
2026-02-19 18:30:01 +01:00
return { . . . coi , reassignment }
2026-02-14 15:26:42 +01:00
} ) ,
// =========================================================================
// Reminder Triggers
// =========================================================================
/ * *
* Manually trigger reminder check for a specific stage ( admin only )
* /
triggerReminders : adminProcedure
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
. input ( z . object ( { roundId : z.string ( ) } ) )
2026-02-14 15:26:42 +01:00
. mutation ( async ( { ctx , input } ) = > {
2026-02-19 12:15:51 +01:00
const result = await sendManualReminders ( input . roundId )
2026-02-14 15:26:42 +01:00
await logAudit ( {
prisma : ctx.prisma ,
userId : ctx.user.id ,
action : 'REMINDERS_TRIGGERED' ,
entityType : 'Stage' ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
entityId : input.roundId ,
2026-02-14 15:26:42 +01:00
detailsJson : {
sent : result.sent ,
errors : result.errors ,
} ,
ipAddress : ctx.ip ,
userAgent : ctx.userAgent ,
} )
return result
} ) ,
// =========================================================================
// AI Evaluation Summary Endpoints
// =========================================================================
/ * *
* Generate an AI - powered evaluation summary for a project ( admin only )
* /
generateSummary : adminProcedure
. input (
z . object ( {
projectId : z.string ( ) ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : z.string ( ) ,
2026-02-14 15:26:42 +01:00
} )
)
. mutation ( async ( { ctx , input } ) = > {
return generateSummary ( {
projectId : input.projectId ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : input.roundId ,
2026-02-14 15:26:42 +01:00
userId : ctx.user.id ,
prisma : ctx.prisma ,
} )
} ) ,
/ * *
* Get an existing evaluation summary for a project ( admin only )
* /
getSummary : adminProcedure
. input (
z . object ( {
projectId : z.string ( ) ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : z.string ( ) ,
2026-02-14 15:26:42 +01:00
} )
)
. query ( async ( { ctx , input } ) = > {
return ctx . prisma . evaluationSummary . findUnique ( {
where : {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
projectId_roundId : {
2026-02-14 15:26:42 +01:00
projectId : input.projectId ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : input.roundId ,
2026-02-14 15:26:42 +01:00
} ,
} ,
} )
} ) ,
/ * *
* Generate summaries for all projects in a stage with submitted evaluations ( admin only )
* /
generateBulkSummaries : adminProcedure
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
. input ( z . object ( { roundId : z.string ( ) } ) )
2026-02-14 15:26:42 +01:00
. mutation ( async ( { ctx , input } ) = > {
// Find all projects with at least 1 submitted evaluation in this stage
const assignments = await ctx . prisma . assignment . findMany ( {
where : {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : input.roundId ,
2026-02-14 15:26:42 +01:00
evaluation : {
status : 'SUBMITTED' ,
} ,
} ,
select : { projectId : true } ,
distinct : [ 'projectId' ] ,
} )
const projectIds = assignments . map ( ( a ) = > a . projectId )
let generated = 0
const errors : Array < { projectId : string ; error : string } > = [ ]
// Generate summaries sequentially to avoid rate limits
for ( const projectId of projectIds ) {
try {
await generateSummary ( {
projectId ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : input.roundId ,
2026-02-14 15:26:42 +01:00
userId : ctx.user.id ,
prisma : ctx.prisma ,
} )
generated ++
} catch ( error ) {
errors . push ( {
projectId ,
error : error instanceof Error ? error . message : 'Unknown error' ,
} )
}
}
return {
total : projectIds.length ,
generated ,
errors ,
}
} ) ,
// =========================================================================
// Side-by-Side Comparison (F4)
// =========================================================================
/ * *
* Get multiple projects with evaluations for side - by - side comparison
* /
getMultipleForComparison : juryProcedure
. input (
z . object ( {
projectIds : z.array ( z . string ( ) ) . min ( 2 ) . max ( 3 ) ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : z.string ( ) ,
2026-02-14 15:26:42 +01:00
} )
)
. query ( async ( { ctx , input } ) = > {
// Verify all projects are assigned to current user in this stage
const assignments = await ctx . prisma . assignment . findMany ( {
where : {
userId : ctx.user.id ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : input.roundId ,
2026-02-14 15:26:42 +01:00
projectId : { in : input . projectIds } ,
} ,
include : {
project : {
select : {
id : true ,
title : true ,
teamName : true ,
description : true ,
country : true ,
tags : true ,
files : {
select : {
id : true ,
fileName : true ,
fileType : true ,
size : true ,
} ,
} ,
} ,
} ,
evaluation : true ,
} ,
} )
if ( assignments . length !== input . projectIds . length ) {
throw new TRPCError ( {
code : 'FORBIDDEN' ,
message : 'You are not assigned to all requested projects in this stage' ,
} )
}
// Fetch the active evaluation form for this stage to get criteria labels
const evaluationForm = await ctx . prisma . evaluationForm . findFirst ( {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
where : { roundId : input.roundId , isActive : true } ,
2026-02-14 15:26:42 +01:00
select : { criteriaJson : true , scalesJson : true } ,
} )
return {
items : assignments.map ( ( a ) = > ( {
project : a.project ,
evaluation : a.evaluation ,
assignmentId : a.id ,
} ) ) ,
criteria : evaluationForm?.criteriaJson as Array < {
id : string ; label : string ; description? : string ; scale? : string ; weight? : number ; type ? : string
} > | null ,
scales : evaluationForm?.scalesJson as Record < string , { min : number ; max : number } > | null ,
}
} ) ,
// =========================================================================
// Peer Review & Discussion (F13)
// =========================================================================
/ * *
* Get anonymized peer evaluation summary for a project
* /
getPeerSummary : juryProcedure
. input (
z . object ( {
projectId : z.string ( ) ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : z.string ( ) ,
2026-02-14 15:26:42 +01:00
} )
)
. query ( async ( { ctx , input } ) = > {
// Verify user has submitted their own evaluation first
const userAssignment = await ctx . prisma . assignment . findFirst ( {
where : {
userId : ctx.user.id ,
projectId : input.projectId ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : input.roundId ,
2026-02-14 15:26:42 +01:00
} ,
include : { evaluation : true } ,
} )
if ( ! userAssignment || userAssignment . evaluation ? . status !== 'SUBMITTED' ) {
throw new TRPCError ( {
code : 'PRECONDITION_FAILED' ,
message : 'You must submit your own evaluation before viewing peer summaries' ,
} )
}
// Check stage settings for peer review
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
const stage = await ctx . prisma . round . findUniqueOrThrow ( {
where : { id : input.roundId } ,
2026-02-14 15:26:42 +01:00
} )
const settings = ( stage . configJson as Record < string , unknown > ) || { }
2026-02-19 12:59:35 +01:00
if ( ! settings . peerReviewEnabled ) {
2026-02-14 15:26:42 +01:00
throw new TRPCError ( {
code : 'FORBIDDEN' ,
message : 'Peer review is not enabled for this stage' ,
} )
}
// Get all submitted evaluations for this project
const evaluations = await ctx . prisma . evaluation . findMany ( {
where : {
status : 'SUBMITTED' ,
assignment : {
projectId : input.projectId ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : input.roundId ,
2026-02-14 15:26:42 +01:00
} ,
} ,
include : {
assignment : {
include : {
user : { select : { id : true , name : true } } ,
} ,
} ,
} ,
} )
if ( evaluations . length === 0 ) {
return { aggregated : null , individualScores : [ ] , totalEvaluations : 0 }
}
// Calculate average and stddev per criterion
const criterionData : Record < string , number [ ] > = { }
evaluations . forEach ( ( e ) = > {
const scores = e . criterionScoresJson as Record < string , number > | null
if ( scores ) {
Object . entries ( scores ) . forEach ( ( [ key , val ] ) = > {
if ( typeof val === 'number' ) {
if ( ! criterionData [ key ] ) criterionData [ key ] = [ ]
criterionData [ key ] . push ( val )
}
} )
}
} )
const aggregated : Record < string , { average : number ; stddev : number ; count : number ; distribution : Record < number , number > } > = { }
Object . entries ( criterionData ) . forEach ( ( [ key , scores ] ) = > {
const avg = scores . reduce ( ( a , b ) = > a + b , 0 ) / scores . length
const variance = scores . reduce ( ( sum , s ) = > sum + Math . pow ( s - avg , 2 ) , 0 ) / scores . length
const stddev = Math . sqrt ( variance )
const distribution : Record < number , number > = { }
scores . forEach ( ( s ) = > {
const bucket = Math . round ( s )
distribution [ bucket ] = ( distribution [ bucket ] || 0 ) + 1
} )
aggregated [ key ] = { average : avg , stddev , count : scores.length , distribution }
} )
// Anonymize individual scores based on round settings
2026-02-19 12:59:35 +01:00
const anonymizationLevel = ( settings . anonymizationLevel as string ) || 'fully_anonymous'
2026-02-14 15:26:42 +01:00
const individualScores = evaluations . map ( ( e ) = > {
let jurorLabel : string
if ( anonymizationLevel === 'named' ) {
jurorLabel = e . assignment . user . name || 'Juror'
} else if ( anonymizationLevel === 'show_initials' ) {
const name = e . assignment . user . name || ''
jurorLabel = name
. split ( ' ' )
. map ( ( n ) = > n [ 0 ] )
. join ( '' )
. toUpperCase ( ) || 'J'
} else {
jurorLabel = ` Juror ${ evaluations . indexOf ( e ) + 1 } `
}
return {
jurorLabel ,
globalScore : e.globalScore ,
binaryDecision : e.binaryDecision ,
criterionScoresJson : e.criterionScoresJson ,
}
} )
return {
aggregated ,
individualScores ,
totalEvaluations : evaluations.length ,
}
} ) ,
/ * *
* Get or create a discussion for a project evaluation
* /
getDiscussion : juryProcedure
. input (
z . object ( {
projectId : z.string ( ) ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : z.string ( ) ,
2026-02-14 15:26:42 +01:00
} )
)
. query ( async ( { ctx , input } ) = > {
// Get or create discussion
let discussion = await ctx . prisma . evaluationDiscussion . findUnique ( {
where : {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
projectId_roundId : {
2026-02-14 15:26:42 +01:00
projectId : input.projectId ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : input.roundId ,
2026-02-14 15:26:42 +01:00
} ,
} ,
include : {
comments : {
include : {
user : { select : { id : true , name : true } } ,
} ,
orderBy : { createdAt : 'asc' } ,
} ,
} ,
} )
if ( ! discussion ) {
discussion = await ctx . prisma . evaluationDiscussion . create ( {
data : {
projectId : input.projectId ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : input.roundId ,
2026-02-14 15:26:42 +01:00
} ,
include : {
comments : {
include : {
user : { select : { id : true , name : true } } ,
} ,
orderBy : { createdAt : 'asc' } ,
} ,
} ,
} )
}
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
// Anonymize comments based on round settings
const round = await ctx . prisma . round . findUniqueOrThrow ( {
where : { id : input.roundId } ,
2026-02-14 15:26:42 +01:00
} )
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
const settings = ( round . configJson as Record < string , unknown > ) || { }
2026-02-19 12:59:35 +01:00
const anonymizationLevel = ( settings . anonymizationLevel as string ) || 'fully_anonymous'
2026-02-14 15:26:42 +01:00
const anonymizedComments = discussion . comments . map ( ( c : { id : string ; userId : string ; user : { name : string | null } ; content : string ; createdAt : Date } , idx : number ) = > {
let authorLabel : string
if ( anonymizationLevel === 'named' || c . userId === ctx . user . id ) {
authorLabel = c . user . name || 'Juror'
} else if ( anonymizationLevel === 'show_initials' ) {
const name = c . user . name || ''
authorLabel = name
. split ( ' ' )
. map ( ( n : string ) = > n [ 0 ] )
. join ( '' )
. toUpperCase ( ) || 'J'
} else {
authorLabel = ` Juror ${ idx + 1 } `
}
return {
id : c.id ,
authorLabel ,
isOwn : c.userId === ctx . user . id ,
content : c.content ,
createdAt : c.createdAt ,
}
} )
return {
id : discussion.id ,
status : discussion.status ,
createdAt : discussion.createdAt ,
closedAt : discussion.closedAt ,
comments : anonymizedComments ,
}
} ) ,
/ * *
* Add a comment to a project evaluation discussion
* /
addComment : juryProcedure
. input (
z . object ( {
projectId : z.string ( ) ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : z.string ( ) ,
2026-02-14 15:26:42 +01:00
content : z.string ( ) . min ( 1 ) . max ( 2000 ) ,
} )
)
. mutation ( async ( { ctx , input } ) = > {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
// Check max comment length from round settings
const round = await ctx . prisma . round . findUniqueOrThrow ( {
where : { id : input.roundId } ,
2026-02-14 15:26:42 +01:00
} )
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
const settings = ( round . configJson as Record < string , unknown > ) || { }
2026-02-19 12:59:35 +01:00
const maxLength = ( settings . maxCommentLength as number ) || 2000
2026-02-14 15:26:42 +01:00
if ( input . content . length > maxLength ) {
throw new TRPCError ( {
code : 'BAD_REQUEST' ,
message : ` Comment exceeds maximum length of ${ maxLength } characters ` ,
} )
}
// Get or create discussion
let discussion = await ctx . prisma . evaluationDiscussion . findUnique ( {
where : {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
projectId_roundId : {
2026-02-14 15:26:42 +01:00
projectId : input.projectId ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : input.roundId ,
2026-02-14 15:26:42 +01:00
} ,
} ,
} )
if ( ! discussion ) {
discussion = await ctx . prisma . evaluationDiscussion . create ( {
data : {
projectId : input.projectId ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : input.roundId ,
2026-02-14 15:26:42 +01:00
} ,
} )
}
if ( discussion . status === 'closed' ) {
throw new TRPCError ( {
code : 'BAD_REQUEST' ,
message : 'This discussion has been closed' ,
} )
}
const comment = await ctx . prisma . discussionComment . create ( {
data : {
discussionId : discussion.id ,
userId : ctx.user.id ,
content : input.content ,
} ,
} )
// Audit log
try {
await logAudit ( {
prisma : ctx.prisma ,
userId : ctx.user.id ,
action : 'DISCUSSION_COMMENT_ADDED' ,
entityType : 'DiscussionComment' ,
entityId : comment.id ,
detailsJson : {
discussionId : discussion.id ,
projectId : input.projectId ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : input.roundId ,
2026-02-14 15:26:42 +01:00
} ,
ipAddress : ctx.ip ,
userAgent : ctx.userAgent ,
} )
} catch {
// Never throw on audit failure
}
return comment
} ) ,
/ * *
* Close a discussion ( admin only )
* /
closeDiscussion : adminProcedure
. input ( z . object ( { discussionId : z.string ( ) } ) )
. mutation ( async ( { ctx , input } ) = > {
const discussion = await ctx . prisma . evaluationDiscussion . update ( {
where : { id : input.discussionId } ,
data : {
status : 'closed' ,
closedAt : new Date ( ) ,
closedById : ctx.user.id ,
} ,
} )
// Audit log
try {
await logAudit ( {
prisma : ctx.prisma ,
userId : ctx.user.id ,
action : 'DISCUSSION_CLOSED' ,
entityType : 'EvaluationDiscussion' ,
entityId : input.discussionId ,
detailsJson : {
projectId : discussion.projectId ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : discussion.roundId ,
2026-02-14 15:26:42 +01:00
} ,
ipAddress : ctx.ip ,
userAgent : ctx.userAgent ,
} )
} catch {
// Never throw on audit failure
}
return discussion
} ) ,
2026-02-16 09:20:02 +01:00
// =========================================================================
// Evaluation Form CRUD (Admin)
// =========================================================================
/ * *
* Get active evaluation form for a round ( admin view with full details )
* /
getForm : adminProcedure
. input ( z . object ( { roundId : z.string ( ) } ) )
. query ( async ( { ctx , input } ) = > {
const form = await ctx . prisma . evaluationForm . findFirst ( {
where : { roundId : input.roundId , isActive : true } ,
} )
if ( ! form ) return null
return {
id : form.id ,
roundId : form.roundId ,
version : form.version ,
isActive : form.isActive ,
criteriaJson : form.criteriaJson as Array < {
id : string
label : string
description? : string
weight? : number
minScore? : number
maxScore? : number
} > ,
scalesJson : form.scalesJson as Record < string , unknown > | null ,
createdAt : form.createdAt ,
updatedAt : form.updatedAt ,
}
} ) ,
/ * *
* Create or update the evaluation form for a round .
* Deactivates any existing active form and creates a new versioned one .
* /
upsertForm : adminProcedure
. input (
z . object ( {
roundId : z.string ( ) ,
criteria : z.array (
z . object ( {
id : z.string ( ) ,
label : z.string ( ) . min ( 1 ) . max ( 255 ) ,
description : z.string ( ) . max ( 2000 ) . optional ( ) ,
2026-02-18 12:43:28 +01:00
type : z . enum ( [ 'numeric' , 'text' , 'boolean' , 'section_header' ] ) . optional ( ) ,
// Numeric fields
2026-02-16 09:20:02 +01:00
weight : z.number ( ) . min ( 0 ) . max ( 100 ) . optional ( ) ,
minScore : z.number ( ) . int ( ) . min ( 0 ) . optional ( ) ,
maxScore : z.number ( ) . int ( ) . min ( 1 ) . optional ( ) ,
2026-02-18 12:43:28 +01:00
scale : z.number ( ) . int ( ) . min ( 1 ) . max ( 10 ) . optional ( ) ,
required : z.boolean ( ) . optional ( ) ,
// Text fields
maxLength : z.number ( ) . int ( ) . min ( 1 ) . max ( 10000 ) . optional ( ) ,
placeholder : z.string ( ) . max ( 500 ) . optional ( ) ,
// Boolean fields
trueLabel : z.string ( ) . max ( 100 ) . optional ( ) ,
falseLabel : z.string ( ) . max ( 100 ) . optional ( ) ,
// Conditional visibility
condition : z.object ( {
criterionId : z.string ( ) ,
operator : z.enum ( [ 'equals' , 'greaterThan' , 'lessThan' ] ) ,
value : z.union ( [ z . number ( ) , z . string ( ) , z . boolean ( ) ] ) ,
} ) . optional ( ) ,
// Section grouping
sectionId : z.string ( ) . optional ( ) ,
2026-02-16 09:20:02 +01:00
} )
) . min ( 1 ) ,
} )
)
. mutation ( async ( { ctx , input } ) = > {
const { roundId , criteria } = input
// Verify round exists
await ctx . prisma . round . findUniqueOrThrow ( { where : { id : roundId } } )
// Get current max version for this round
const latestForm = await ctx . prisma . evaluationForm . findFirst ( {
where : { roundId } ,
orderBy : { version : 'desc' } ,
select : { version : true } ,
} )
const nextVersion = ( latestForm ? . version ? ? 0 ) + 1
2026-02-18 12:43:28 +01:00
// Build criteriaJson preserving all fields
const criteriaJson = criteria . map ( ( c ) = > {
const type = c . type || 'numeric'
const base = {
id : c.id ,
label : c.label ,
description : c.description || '' ,
type ,
required : c.required ? ? ( type !== 'section_header' ) ,
}
if ( type === 'numeric' ) {
const scaleVal = c . scale ? ? 10
return {
. . . base ,
weight : c.weight ? ? 1 ,
scale : ` ${ c . minScore ? ? 1 } - ${ c . maxScore ? ? scaleVal } ` ,
}
}
if ( type === 'text' ) {
return {
. . . base ,
maxLength : c.maxLength ? ? 1000 ,
placeholder : c.placeholder || '' ,
}
}
if ( type === 'boolean' ) {
return {
. . . base ,
trueLabel : c.trueLabel || 'Yes' ,
falseLabel : c.falseLabel || 'No' ,
}
}
// section_header
return base
} )
// Auto-generate scalesJson from numeric criteria
const numericCriteria = criteriaJson . filter ( ( c ) = > c . type === 'numeric' )
const scaleSet = new Set ( numericCriteria . map ( ( c ) = > ( c as { scale : string } ) . scale ) )
2026-02-16 09:20:02 +01:00
const scalesJson : Record < string , { min : number ; max : number } > = { }
for ( const scale of scaleSet ) {
const [ min , max ] = scale . split ( '-' ) . map ( Number )
scalesJson [ scale ] = { min , max }
}
// Transaction: deactivate old → create new
const form = await ctx . prisma . $transaction ( async ( tx ) = > {
await tx . evaluationForm . updateMany ( {
where : { roundId , isActive : true } ,
data : { isActive : false } ,
} )
return tx . evaluationForm . create ( {
data : {
roundId ,
version : nextVersion ,
criteriaJson ,
scalesJson ,
isActive : true ,
} ,
} )
} )
await logAudit ( {
prisma : ctx.prisma ,
userId : ctx.user.id ,
action : 'UPSERT_EVALUATION_FORM' ,
entityType : 'EvaluationForm' ,
entityId : form.id ,
detailsJson : {
roundId ,
version : nextVersion ,
criteriaCount : criteria.length ,
} ,
ipAddress : ctx.ip ,
userAgent : ctx.userAgent ,
} )
return form
} ) ,
2026-02-14 15:26:42 +01:00
// =========================================================================
// Phase 4: Stage-scoped evaluation procedures
// =========================================================================
/ * *
* Start a stage - scoped evaluation ( create or return existing draft )
* /
startStage : protectedProcedure
. input (
z . object ( {
assignmentId : z.string ( ) ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : z.string ( ) ,
2026-02-14 15:26:42 +01:00
} )
)
. mutation ( async ( { ctx , input } ) = > {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
// Verify assignment ownership and roundId match
2026-02-14 15:26:42 +01:00
const assignment = await ctx . prisma . assignment . findUniqueOrThrow ( {
where : { id : input.assignmentId } ,
} )
if ( assignment . userId !== ctx . user . id ) {
throw new TRPCError ( { code : 'FORBIDDEN' } )
}
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
if ( assignment . roundId !== input . roundId ) {
2026-02-14 15:26:42 +01:00
throw new TRPCError ( {
code : 'BAD_REQUEST' ,
message : 'Assignment does not belong to this stage' ,
} )
}
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
// Check round window
const round = await ctx . prisma . round . findUniqueOrThrow ( {
where : { id : input.roundId } ,
2026-02-14 15:26:42 +01:00
} )
const now = new Date ( )
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
if ( round . status !== 'ROUND_ACTIVE' ) {
2026-02-14 15:26:42 +01:00
throw new TRPCError ( {
code : 'PRECONDITION_FAILED' ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
message : 'Round is not active' ,
2026-02-14 15:26:42 +01:00
} )
}
// Check grace period
const gracePeriod = await ctx . prisma . gracePeriod . findFirst ( {
where : {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : input.roundId ,
2026-02-14 15:26:42 +01:00
userId : ctx.user.id ,
OR : [
{ projectId : null } ,
{ projectId : assignment.projectId } ,
] ,
extendedUntil : { gte : now } ,
} ,
} )
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
const effectiveClose = gracePeriod ? . extendedUntil ? ? round . windowCloseAt
if ( round . windowOpenAt && now < round . windowOpenAt ) {
2026-02-14 15:26:42 +01:00
throw new TRPCError ( {
code : 'PRECONDITION_FAILED' ,
message : 'Evaluation window has not opened yet' ,
} )
}
if ( effectiveClose && now > effectiveClose ) {
throw new TRPCError ( {
code : 'PRECONDITION_FAILED' ,
message : 'Evaluation window has closed' ,
} )
}
// Check for existing evaluation
const existing = await ctx . prisma . evaluation . findUnique ( {
where : { assignmentId : input.assignmentId } ,
} )
if ( existing ) return existing
// Get active evaluation form for this stage
const form = await ctx . prisma . evaluationForm . findFirst ( {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
where : { roundId : input.roundId , isActive : true } ,
2026-02-14 15:26:42 +01:00
} )
if ( ! form ) {
throw new TRPCError ( {
code : 'BAD_REQUEST' ,
message : 'No active evaluation form for this stage' ,
} )
}
return ctx . prisma . evaluation . create ( {
data : {
assignmentId : input.assignmentId ,
formId : form.id ,
status : 'DRAFT' ,
} ,
} )
} ) ,
/ * *
* Get the active evaluation form for a stage
* /
getStageForm : protectedProcedure
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
. input ( z . object ( { roundId : z.string ( ) } ) )
2026-02-14 15:26:42 +01:00
. query ( async ( { ctx , input } ) = > {
const form = await ctx . prisma . evaluationForm . findFirst ( {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
where : { roundId : input.roundId , isActive : true } ,
2026-02-14 15:26:42 +01:00
} )
if ( ! form ) {
return null
}
return {
id : form.id ,
criteriaJson : form.criteriaJson as Array < {
id : string
label : string
description? : string
scale? : string
weight? : number
type ? : string
required? : boolean
} > ,
scalesJson : form.scalesJson as Record < string , { min : number ; max : number ; labels ? : Record < string , string > } > | null ,
version : form.version ,
}
} ) ,
/ * *
* Check the evaluation window status for a stage
* /
checkStageWindow : protectedProcedure
. input (
z . object ( {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : z.string ( ) ,
2026-02-14 15:26:42 +01:00
userId : z.string ( ) . optional ( ) ,
} )
)
. query ( async ( { ctx , input } ) = > {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
const stage = await ctx . prisma . round . findUniqueOrThrow ( {
where : { id : input.roundId } ,
2026-02-14 15:26:42 +01:00
select : {
id : true ,
status : true ,
windowOpenAt : true ,
windowCloseAt : true ,
} ,
} )
const userId = input . userId ? ? ctx . user . id
const now = new Date ( )
// Check for grace period
const gracePeriod = await ctx . prisma . gracePeriod . findFirst ( {
where : {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : input.roundId ,
2026-02-14 15:26:42 +01:00
userId ,
extendedUntil : { gte : now } ,
} ,
orderBy : { extendedUntil : 'desc' } ,
} )
const effectiveClose = gracePeriod ? . extendedUntil ? ? stage . windowCloseAt
const isOpen =
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
stage . status === 'ROUND_ACTIVE' &&
2026-02-14 15:26:42 +01:00
( ! stage . windowOpenAt || now >= stage . windowOpenAt ) &&
( ! effectiveClose || now <= effectiveClose )
let reason = ''
if ( ! isOpen ) {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
if ( stage . status !== 'ROUND_ACTIVE' ) {
2026-02-14 15:26:42 +01:00
reason = 'Stage is not active'
} else if ( stage . windowOpenAt && now < stage . windowOpenAt ) {
reason = 'Window has not opened yet'
} else {
reason = 'Window has closed'
}
}
return {
isOpen ,
opensAt : stage.windowOpenAt ,
closesAt : stage.windowCloseAt ,
hasGracePeriod : ! ! gracePeriod ,
graceExpiresAt : gracePeriod?.extendedUntil ? ? null ,
reason ,
}
} ) ,
/ * *
* List evaluations for the current user in a specific stage
* /
listStageEvaluations : protectedProcedure
. input (
z . object ( {
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : z.string ( ) ,
2026-02-14 15:26:42 +01:00
projectId : z.string ( ) . optional ( ) ,
} )
)
. query ( async ( { ctx , input } ) = > {
const where : Record < string , unknown > = {
assignment : {
userId : ctx.user.id ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
roundId : input.roundId ,
2026-02-14 15:26:42 +01:00
. . . ( input . projectId ? { projectId : input.projectId } : { } ) ,
} ,
}
return ctx . prisma . evaluation . findMany ( {
where ,
include : {
assignment : {
include : {
project : { select : { id : true , title : true , teamName : true } } ,
} ,
} ,
form : {
select : { criteriaJson : true , scalesJson : true } ,
} ,
} ,
orderBy : { updatedAt : 'desc' } ,
} )
} ) ,
} )