Competition/Round architecture: full platform rewrite (Phases 1-9)
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s

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>
This commit is contained in:
2026-02-15 23:04:15 +01:00
parent 9ab4717f96
commit 6ca39c976b
349 changed files with 69938 additions and 28767 deletions

View File

@@ -68,7 +68,7 @@ interface ScoringPatterns {
export interface EvaluationSummaryResult {
id: string
projectId: string
stageId: string
roundId: string
summaryJson: AIResponsePayload & { scoringPatterns: ScoringPatterns }
generatedAt: Date
model: string
@@ -123,6 +123,15 @@ Analyze these evaluations and return a JSON object with this exact structure:
"recommendation": "A brief recommendation based on the evaluation consensus"
}
Example output:
{
"overallAssessment": "The project received strong scores (avg 7.8/10) with high consensus among evaluators. Key strengths in innovation were balanced by concerns about scalability.",
"strengths": ["Innovative approach to coral reef monitoring", "Strong team expertise in marine biology"],
"weaknesses": ["Limited scalability plan", "Budget projections need more detail"],
"themes": [{"theme": "Innovation", "sentiment": "positive", "frequency": 3}, {"theme": "Scalability", "sentiment": "negative", "frequency": 2}],
"recommendation": "Recommended for advancement with condition to address scalability concerns in next round."
}
Guidelines:
- Base your analysis only on the provided evaluation data
- Identify common themes across evaluator feedback
@@ -194,12 +203,12 @@ export function computeScoringPatterns(
*/
export async function generateSummary({
projectId,
stageId,
roundId,
userId,
prisma,
}: {
projectId: string
stageId: string
roundId: string
userId: string
prisma: PrismaClient
}): Promise<EvaluationSummaryResult> {
@@ -216,13 +225,13 @@ export async function generateSummary({
throw new TRPCError({ code: 'NOT_FOUND', message: 'Project not found' })
}
// Fetch submitted evaluations for this project in this stage
// Fetch submitted evaluations for this project in this round
const evaluations = await prisma.evaluation.findMany({
where: {
status: 'SUBMITTED',
assignment: {
projectId,
stageId,
roundId,
},
},
select: {
@@ -244,13 +253,13 @@ export async function generateSummary({
if (evaluations.length === 0) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'No submitted evaluations found for this project in this stage',
message: 'No submitted evaluations found for this project in this round',
})
}
// Get evaluation form criteria for this stage
// Get evaluation form criteria for this round
const form = await prisma.evaluationForm.findFirst({
where: { stageId, isActive: true },
where: { roundId, isActive: true },
select: { criteriaJson: true },
})
@@ -278,49 +287,83 @@ export async function generateSummary({
let aiResponse: AIResponsePayload
let tokensUsed = 0
const MAX_PARSE_RETRIES = 2
let parseAttempts = 0
let response: Awaited<ReturnType<typeof openai.chat.completions.create>>
try {
const params = buildCompletionParams(model, {
messages: [
{ role: 'user', content: prompt },
],
jsonMode: true,
temperature: 0.3,
temperature: 0.1,
maxTokens: 2000,
})
const response = await openai.chat.completions.create(params)
const usage = extractTokenUsage(response)
response = await openai.chat.completions.create(params)
let usage = extractTokenUsage(response)
tokensUsed = usage.totalTokens
const content = response.choices[0]?.message?.content
if (!content) {
throw new Error('Empty response from AI')
// Parse with retry logic
while (true) {
try {
const content = response.choices[0]?.message?.content
if (!content) {
throw new Error('Empty response from AI')
}
aiResponse = JSON.parse(content) as AIResponsePayload
break
} catch (parseError) {
if (parseError instanceof SyntaxError && parseAttempts < MAX_PARSE_RETRIES) {
parseAttempts++
console.warn(`[AI Evaluation Summary] JSON parse failed, retrying (${parseAttempts}/${MAX_PARSE_RETRIES})`)
// Retry the API call with hint
const retryParams = buildCompletionParams(model, {
messages: [
{ role: 'user', content: prompt + '\n\nIMPORTANT: Please ensure valid JSON output.' },
],
jsonMode: true,
temperature: 0.1,
maxTokens: 2000,
})
response = await openai.chat.completions.create(retryParams)
const retryUsage = extractTokenUsage(response)
tokensUsed += retryUsage.totalTokens
continue
}
// If retry limit reached or non-syntax error
if (parseError instanceof SyntaxError) {
const parseErrorObj = createParseError((parseError as Error).message)
logAIError('EvaluationSummary', 'generateSummary', parseErrorObj)
await logAIUsage({
userId,
action: 'EVALUATION_SUMMARY',
entityType: 'Project',
entityId: projectId,
model,
promptTokens: 0,
completionTokens: 0,
totalTokens: tokensUsed,
itemsProcessed: 0,
status: 'ERROR',
errorMessage: parseErrorObj.message,
})
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to parse AI response. Please try again.',
})
}
throw parseError
}
}
aiResponse = JSON.parse(content) as AIResponsePayload
} catch (error) {
if (error instanceof SyntaxError) {
const parseError = createParseError(error.message)
logAIError('EvaluationSummary', 'generateSummary', parseError)
await logAIUsage({
userId,
action: 'EVALUATION_SUMMARY',
entityType: 'Project',
entityId: projectId,
model,
promptTokens: 0,
completionTokens: 0,
totalTokens: tokensUsed,
itemsProcessed: 0,
status: 'ERROR',
errorMessage: parseError.message,
})
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to parse AI response. Please try again.',
})
if (error instanceof TRPCError) {
throw error
}
const classified = classifyAIError(error)
@@ -359,11 +402,11 @@ export async function generateSummary({
const summary = await prisma.evaluationSummary.upsert({
where: {
projectId_stageId: { projectId, stageId },
projectId_roundId: { projectId, roundId },
},
create: {
projectId,
stageId,
roundId,
summaryJson: summaryJsonValue,
generatedById: userId,
model,
@@ -395,7 +438,7 @@ export async function generateSummary({
return {
id: summary.id,
projectId: summary.projectId,
stageId: summary.stageId,
roundId: summary.roundId,
summaryJson: summaryJson as AIResponsePayload & { scoringPatterns: ScoringPatterns },
generatedAt: summary.generatedAt,
model: summary.model,