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

@@ -7,16 +7,18 @@ import {
SettingCategory,
CompetitionCategory,
OceanIssue,
StageType,
TrackKind,
RoutingMode,
DecisionMode,
StageStatus,
ProjectStageStateValue,
ProjectStatus,
SubmissionSource,
// Competition architecture enums
CompetitionStatus,
RoundType,
RoundStatus,
CapMode,
JuryGroupMemberRole,
AdvancementRuleType,
} from '@prisma/client'
import bcrypt from 'bcryptjs'
import { defaultRoundConfig } from '../src/types/competition-configs'
import { readFileSync } from 'fs'
import { parse } from 'csv-parse/sync'
import { join, dirname } from 'path'
@@ -424,368 +426,10 @@ async function main() {
})
console.log(` ✓ Program: ${program.name} ${program.year}`)
// ==========================================================================
// 7. Pipeline
// ==========================================================================
console.log('\n🔗 Creating pipeline...')
const pipeline = await prisma.pipeline.upsert({
where: { slug: 'mopc-2026' },
update: {
name: 'MOPC 2026 Main Pipeline',
status: 'ACTIVE',
},
create: {
programId: program.id,
name: 'MOPC 2026 Main Pipeline',
slug: 'mopc-2026',
status: 'ACTIVE',
settingsJson: {
description: 'Main pipeline for MOPC 2026 competition',
allowParallelTracks: true,
autoAdvanceOnClose: false,
},
},
})
console.log(` ✓ Pipeline: ${pipeline.name}`)
// Legacy Pipeline/Track/Stage system removed - Competition/Round architecture now in use
// ==========================================================================
// 8. Tracks (4)
// ==========================================================================
console.log('\n🛤 Creating tracks...')
const mainTrack = await prisma.track.upsert({
where: { pipelineId_slug: { pipelineId: pipeline.id, slug: 'main' } },
update: { name: 'Main Competition' },
create: {
pipelineId: pipeline.id,
name: 'Main Competition',
slug: 'main',
kind: TrackKind.MAIN,
sortOrder: 0,
settingsJson: { description: 'Primary competition track for all applicants' },
},
})
const innovationTrack = await prisma.track.upsert({
where: { pipelineId_slug: { pipelineId: pipeline.id, slug: 'innovation-award' } },
update: { name: 'Ocean Innovation Award' },
create: {
pipelineId: pipeline.id,
name: 'Ocean Innovation Award',
slug: 'innovation-award',
kind: TrackKind.AWARD,
routingMode: RoutingMode.SHARED,
decisionMode: DecisionMode.JURY_VOTE,
sortOrder: 1,
settingsJson: { description: 'Award for most innovative ocean technology' },
},
})
const impactTrack = await prisma.track.upsert({
where: { pipelineId_slug: { pipelineId: pipeline.id, slug: 'impact-award' } },
update: { name: 'Ocean Impact Award' },
create: {
pipelineId: pipeline.id,
name: 'Ocean Impact Award',
slug: 'impact-award',
kind: TrackKind.AWARD,
routingMode: RoutingMode.EXCLUSIVE,
decisionMode: DecisionMode.AWARD_MASTER_DECISION,
sortOrder: 2,
settingsJson: { description: 'Award for highest community impact on ocean health' },
},
})
const peoplesTrack = await prisma.track.upsert({
where: { pipelineId_slug: { pipelineId: pipeline.id, slug: 'peoples-choice' } },
update: { name: "People's Choice" },
create: {
pipelineId: pipeline.id,
name: "People's Choice",
slug: 'peoples-choice',
kind: TrackKind.SHOWCASE,
routingMode: RoutingMode.SHARED,
sortOrder: 3,
settingsJson: { description: 'Public audience voting for fan favorite' },
},
})
console.log(` ✓ Main Competition (MAIN)`)
console.log(` ✓ Ocean Innovation Award (AWARD, SHARED)`)
console.log(` ✓ Ocean Impact Award (AWARD, EXCLUSIVE)`)
console.log(` ✓ People's Choice (SHOWCASE, SHARED)`)
// ==========================================================================
// 9. Stages
// ==========================================================================
console.log('\n📊 Creating stages...')
// --- Main track stages ---
const mainStages = await Promise.all([
prisma.stage.upsert({
where: { trackId_slug: { trackId: mainTrack.id, slug: 'intake' } },
update: {},
create: {
trackId: mainTrack.id,
stageType: StageType.INTAKE,
name: 'Application Intake',
slug: 'intake',
status: StageStatus.STAGE_CLOSED,
sortOrder: 0,
configJson: {
fileRequirements: [
{ name: 'Executive Summary', type: 'PDF', maxSizeMB: 50, required: true },
{ name: 'Video Pitch', type: 'VIDEO', maxSizeMB: 500, required: false },
],
deadline: '2026-01-31T23:59:00Z',
maxSubmissions: 1,
},
},
}),
prisma.stage.upsert({
where: { trackId_slug: { trackId: mainTrack.id, slug: 'screening' } },
update: {},
create: {
trackId: mainTrack.id,
stageType: StageType.FILTER,
name: 'AI Screening',
slug: 'screening',
status: StageStatus.STAGE_ACTIVE,
sortOrder: 1,
configJson: {
deterministic: {
rules: [
{ field: 'competitionCategory', operator: 'is_not_null', label: 'Has category' },
{ field: 'description', operator: 'min_length', value: 50, label: 'Description >= 50 chars' },
],
},
ai: { rubricVersion: '2026-v1', model: 'gpt-4o' },
confidenceBands: {
high: { threshold: 0.8, action: 'auto_pass' },
medium: { threshold: 0.5, action: 'manual_review' },
low: { threshold: 0, action: 'auto_reject' },
},
},
},
}),
prisma.stage.upsert({
where: { trackId_slug: { trackId: mainTrack.id, slug: 'evaluation' } },
update: {},
create: {
trackId: mainTrack.id,
stageType: StageType.EVALUATION,
name: 'Expert Evaluation',
slug: 'evaluation',
status: StageStatus.STAGE_DRAFT,
sortOrder: 2,
configJson: {
criteriaVersion: '2026-v1',
assignmentStrategy: 'smart',
requiredReviews: 3,
minAssignmentsPerJuror: 5,
maxAssignmentsPerJuror: 20,
},
},
}),
prisma.stage.upsert({
where: { trackId_slug: { trackId: mainTrack.id, slug: 'selection' } },
update: {},
create: {
trackId: mainTrack.id,
stageType: StageType.SELECTION,
name: 'Semi-Final Selection',
slug: 'selection',
status: StageStatus.STAGE_DRAFT,
sortOrder: 3,
configJson: {
rankingSource: 'evaluation_scores',
finalistTarget: 6,
selectionMethod: 'top_n_with_admin_override',
},
},
}),
prisma.stage.upsert({
where: { trackId_slug: { trackId: mainTrack.id, slug: 'grand-final' } },
update: {},
create: {
trackId: mainTrack.id,
stageType: StageType.LIVE_FINAL,
name: 'Grand Final',
slug: 'grand-final',
status: StageStatus.STAGE_DRAFT,
sortOrder: 4,
configJson: {
sessionMode: 'cohort',
votingEnabled: true,
audienceVoting: true,
audienceVoteWeight: 0.2,
presentationDurationMinutes: 10,
qaDurationMinutes: 5,
},
},
}),
prisma.stage.upsert({
where: { trackId_slug: { trackId: mainTrack.id, slug: 'results' } },
update: {},
create: {
trackId: mainTrack.id,
stageType: StageType.RESULTS,
name: 'Results & Awards',
slug: 'results',
status: StageStatus.STAGE_DRAFT,
sortOrder: 5,
configJson: {
rankingWeights: { juryScore: 0.8, audienceScore: 0.2 },
publicationPolicy: 'after_ceremony',
announcementDate: '2026-06-15',
},
},
}),
])
// --- Innovation Award track stages ---
const innovationStages = await Promise.all([
prisma.stage.upsert({
where: { trackId_slug: { trackId: innovationTrack.id, slug: 'innovation-review' } },
update: {},
create: {
trackId: innovationTrack.id,
stageType: StageType.EVALUATION,
name: 'Innovation Jury Review',
slug: 'innovation-review',
status: StageStatus.STAGE_DRAFT,
sortOrder: 0,
configJson: {
criteriaVersion: 'innovation-2026-v1',
assignmentStrategy: 'manual',
requiredReviews: 2,
},
},
}),
prisma.stage.upsert({
where: { trackId_slug: { trackId: innovationTrack.id, slug: 'innovation-results' } },
update: {},
create: {
trackId: innovationTrack.id,
stageType: StageType.RESULTS,
name: 'Innovation Results',
slug: 'innovation-results',
status: StageStatus.STAGE_DRAFT,
sortOrder: 1,
configJson: { publicationPolicy: 'after_ceremony' },
},
}),
])
// --- Impact Award track stages ---
const impactStages = await Promise.all([
prisma.stage.upsert({
where: { trackId_slug: { trackId: impactTrack.id, slug: 'impact-review' } },
update: {},
create: {
trackId: impactTrack.id,
stageType: StageType.EVALUATION,
name: 'Impact Assessment',
slug: 'impact-review',
status: StageStatus.STAGE_DRAFT,
sortOrder: 0,
configJson: {
criteriaVersion: 'impact-2026-v1',
assignmentStrategy: 'award_master',
requiredReviews: 1,
},
},
}),
prisma.stage.upsert({
where: { trackId_slug: { trackId: impactTrack.id, slug: 'impact-results' } },
update: {},
create: {
trackId: impactTrack.id,
stageType: StageType.RESULTS,
name: 'Impact Results',
slug: 'impact-results',
status: StageStatus.STAGE_DRAFT,
sortOrder: 1,
configJson: { publicationPolicy: 'after_ceremony' },
},
}),
])
// --- People's Choice track stages ---
const peoplesStages = await Promise.all([
prisma.stage.upsert({
where: { trackId_slug: { trackId: peoplesTrack.id, slug: 'public-vote' } },
update: {},
create: {
trackId: peoplesTrack.id,
stageType: StageType.LIVE_FINAL,
name: 'Public Voting',
slug: 'public-vote',
status: StageStatus.STAGE_DRAFT,
sortOrder: 0,
configJson: {
votingMode: 'favorites',
maxFavorites: 3,
requireIdentification: false,
votingDurationMinutes: 30,
},
},
}),
prisma.stage.upsert({
where: { trackId_slug: { trackId: peoplesTrack.id, slug: 'peoples-results' } },
update: {},
create: {
trackId: peoplesTrack.id,
stageType: StageType.RESULTS,
name: "People's Choice Results",
slug: 'peoples-results',
status: StageStatus.STAGE_DRAFT,
sortOrder: 1,
configJson: { publicationPolicy: 'after_ceremony' },
},
}),
])
const allStages = [...mainStages, ...innovationStages, ...impactStages, ...peoplesStages]
console.log(` ✓ Created ${allStages.length} stages across 4 tracks`)
// ==========================================================================
// 10. Stage Transitions (linear within each track)
// ==========================================================================
console.log('\n🔀 Creating stage transitions...')
const trackStageGroups = [
{ name: 'Main', stages: mainStages },
{ name: 'Innovation', stages: innovationStages },
{ name: 'Impact', stages: impactStages },
{ name: "People's", stages: peoplesStages },
]
let transitionCount = 0
for (const group of trackStageGroups) {
for (let i = 0; i < group.stages.length - 1; i++) {
await prisma.stageTransition.upsert({
where: {
fromStageId_toStageId: {
fromStageId: group.stages[i].id,
toStageId: group.stages[i + 1].id,
},
},
update: {},
create: {
fromStageId: group.stages[i].id,
toStageId: group.stages[i + 1].id,
isDefault: true,
},
})
transitionCount++
}
}
console.log(` ✓ Created ${transitionCount} transitions`)
// ==========================================================================
// 11. Parse CSV & Create Applicants + Projects
// 7. Parse CSV & Create Applicants + Projects
// ==========================================================================
console.log('\n📄 Checking for existing projects...')
@@ -820,9 +464,6 @@ async function main() {
// Create applicant users and projects
console.log('\n🚀 Creating applicant users and projects...')
const intakeStage = mainStages[0] // INTAKE - CLOSED
const filterStage = mainStages[1] // FILTER - ACTIVE
let skippedNoEmail = 0
for (let rowIdx = 0; rowIdx < validRecords.length; rowIdx++) {
const row = validRecords[rowIdx]
@@ -871,7 +512,7 @@ async function main() {
})
// Create project
const project = await prisma.project.create({
await prisma.project.create({
data: {
programId: program.id,
title: projectName || `Project by ${name}`,
@@ -896,108 +537,325 @@ async function main() {
},
})
// Create ProjectStageState: INTAKE stage = PASSED (intake closed)
await prisma.projectStageState.create({
data: {
projectId: project.id,
trackId: mainTrack.id,
stageId: intakeStage.id,
state: ProjectStageStateValue.PASSED,
enteredAt: new Date('2026-01-15'),
exitedAt: new Date('2026-01-31'),
},
})
// Create ProjectStageState: FILTER stage = PENDING (current active stage)
await prisma.projectStageState.create({
data: {
projectId: project.id,
trackId: mainTrack.id,
stageId: filterStage.id,
state: ProjectStageStateValue.PENDING,
enteredAt: new Date('2026-02-01'),
},
})
projectCount++
if (projectCount % 50 === 0) {
console.log(` ... ${projectCount} projects created`)
}
}
console.log(` ✓ Created ${projectCount} projects with stage states`)
console.log(` ✓ Created ${projectCount} projects`)
if (skippedNoEmail > 0) {
console.log(` ⚠ Skipped ${skippedNoEmail} rows with no valid email`)
}
}
// ==========================================================================
// 12. Evaluation Form (for Expert Evaluation stage)
// ==========================================================================
console.log('\n📝 Creating evaluation form...')
// Legacy evaluation forms and special awards removed - Competition/Round architecture now in use
const evaluationStage = mainStages[2] // EVALUATION stage
await prisma.evaluationForm.upsert({
where: { stageId_version: { stageId: evaluationStage.id, version: 1 } },
// ==========================================================================
// 8. Competition Architecture
// ==========================================================================
console.log('\n🏗 Creating competition architecture...')
const competition = await prisma.competition.upsert({
where: { slug: 'mopc-2026' },
update: {},
create: {
stageId: evaluationStage.id,
version: 1,
isActive: true,
criteriaJson: [
{ id: 'need_clarity', label: 'Need Clarity', description: 'How clearly is the problem/need articulated?', scale: '1-5', weight: 20, type: 'numeric', required: true },
{ id: 'solution_relevance', label: 'Solution Relevance', description: 'How relevant and innovative is the proposed solution?', scale: '1-5', weight: 25, type: 'numeric', required: true },
{ id: 'ocean_impact', label: 'Ocean Impact', description: 'What is the potential positive impact on ocean conservation?', scale: '1-5', weight: 25, type: 'numeric', required: true },
{ id: 'feasibility', label: 'Feasibility & Scalability', description: 'How feasible and scalable is the project?', scale: '1-5', weight: 20, type: 'numeric', required: true },
{ id: 'team_strength', label: 'Team Strength', description: 'How strong and capable is the team?', scale: '1-5', weight: 10, type: 'numeric', required: true },
],
scalesJson: {
'1-5': { min: 1, max: 5, labels: { 1: 'Poor', 2: 'Below Average', 3: 'Average', 4: 'Good', 5: 'Excellent' } },
programId: program.id,
name: 'MOPC 2026',
slug: 'mopc-2026',
status: CompetitionStatus.ACTIVE,
categoryMode: 'SHARED',
startupFinalistCount: 3,
conceptFinalistCount: 3,
notifyOnRoundAdvance: true,
notifyOnDeadlineApproach: true,
deadlineReminderDays: [7, 3, 1],
},
})
console.log(` ✓ Competition: ${competition.name}`)
// --- Jury Groups ---
const juryGroup1 = await prisma.juryGroup.upsert({
where: { competitionId_slug: { competitionId: competition.id, slug: 'screening-jury' } },
update: {},
create: {
competitionId: competition.id,
name: 'Screening Jury',
slug: 'screening-jury',
sortOrder: 0,
defaultMaxAssignments: 30,
defaultCapMode: CapMode.SOFT,
softCapBuffer: 5,
categoryQuotasEnabled: false,
},
})
const juryGroup2 = await prisma.juryGroup.upsert({
where: { competitionId_slug: { competitionId: competition.id, slug: 'expert-jury' } },
update: {},
create: {
competitionId: competition.id,
name: 'Expert Jury',
slug: 'expert-jury',
sortOrder: 1,
defaultMaxAssignments: 20,
defaultCapMode: CapMode.SOFT,
softCapBuffer: 2,
categoryQuotasEnabled: true,
defaultCategoryQuotas: {
STARTUP: { min: 5, max: 15 },
BUSINESS_CONCEPT: { min: 3, max: 10 },
},
},
})
console.log(' ✓ Evaluation form created (5 criteria)')
// ==========================================================================
// 13. Special Awards
// ==========================================================================
console.log('\n🏆 Creating special awards...')
await prisma.specialAward.upsert({
where: { trackId: innovationTrack.id },
const juryGroup3 = await prisma.juryGroup.upsert({
where: { competitionId_slug: { competitionId: competition.id, slug: 'finals-jury' } },
update: {},
create: {
programId: program.id,
name: 'Ocean Innovation Award',
description: 'Recognizes the most innovative technology solution for ocean protection',
status: 'DRAFT',
trackId: innovationTrack.id,
scoringMode: 'PICK_WINNER',
useAiEligibility: true,
criteriaText: 'Projects demonstrating breakthrough technological innovation for ocean conservation',
competitionId: competition.id,
name: 'Finals Jury',
slug: 'finals-jury',
sortOrder: 2,
defaultMaxAssignments: 10,
defaultCapMode: CapMode.HARD,
softCapBuffer: 0,
categoryQuotasEnabled: false,
},
})
await prisma.specialAward.upsert({
where: { trackId: impactTrack.id },
console.log(' ✓ Jury Groups: Screening, Expert, Finals')
// --- Add jury members to groups ---
// Split 8 jurors: 4 in screening, 6 in expert (some overlap), all 8 in finals
const juryGroupAssignments = [
{ groupId: juryGroup1.id, userIds: juryUserIds.slice(0, 4), role: JuryGroupMemberRole.MEMBER },
{ groupId: juryGroup2.id, userIds: juryUserIds.slice(0, 6), role: JuryGroupMemberRole.MEMBER },
{ groupId: juryGroup3.id, userIds: juryUserIds, role: JuryGroupMemberRole.MEMBER },
]
let memberCount = 0
for (const assignment of juryGroupAssignments) {
for (let i = 0; i < assignment.userIds.length; i++) {
const userId = assignment.userIds[i]
await prisma.juryGroupMember.upsert({
where: {
juryGroupId_userId: { juryGroupId: assignment.groupId, userId },
},
update: {},
create: {
juryGroupId: assignment.groupId,
userId,
role: i === 0 ? JuryGroupMemberRole.CHAIR : assignment.role,
},
})
memberCount++
}
}
console.log(`${memberCount} jury group memberships created`)
// --- Demo self-service preferences ---
// Enable self-service on the Expert Panel and set preferences for first 2 members
await prisma.juryGroup.update({
where: { id: juryGroup2.id },
data: { allowJurorCapAdjustment: true, allowJurorRatioAdjustment: true },
})
// Juror 0 sets a lower cap and prefers startups
const selfServiceMember1 = await prisma.juryGroupMember.findUnique({
where: { juryGroupId_userId: { juryGroupId: juryGroup2.id, userId: juryUserIds[0] } },
})
if (selfServiceMember1) {
await prisma.juryGroupMember.update({
where: { id: selfServiceMember1.id },
data: { selfServiceCap: 12, selfServiceRatio: 0.7 },
})
}
// Juror 1 sets a moderate ratio preference
const selfServiceMember2 = await prisma.juryGroupMember.findUnique({
where: { juryGroupId_userId: { juryGroupId: juryGroup2.id, userId: juryUserIds[1] } },
})
if (selfServiceMember2) {
await prisma.juryGroupMember.update({
where: { id: selfServiceMember2.id },
data: { selfServiceRatio: 0.4 },
})
}
console.log(' ✓ Self-service preferences: 2 jurors in Expert Panel')
// --- Submission Windows ---
const submissionWindow1 = await prisma.submissionWindow.upsert({
where: { competitionId_slug: { competitionId: competition.id, slug: 'r1-application-docs' } },
update: {},
create: {
programId: program.id,
name: 'Ocean Impact Award',
description: 'Recognizes the project with highest community and environmental impact',
status: 'DRAFT',
trackId: impactTrack.id,
scoringMode: 'PICK_WINNER',
useAiEligibility: false,
criteriaText: 'Projects with measurable, significant impact on ocean health and coastal communities',
competitionId: competition.id,
name: 'R1 Application Documents',
slug: 'r1-application-docs',
roundNumber: 1,
sortOrder: 0,
windowOpenAt: new Date('2026-01-01'),
windowCloseAt: new Date('2026-01-31'),
isLocked: true,
},
})
console.log(' ✓ Ocean Innovation Award → innovation-award track')
console.log(' ✓ Ocean Impact Award → impact-award track')
const submissionWindow2 = await prisma.submissionWindow.upsert({
where: { competitionId_slug: { competitionId: competition.id, slug: 'r4-semifinal-docs' } },
update: {},
create: {
competitionId: competition.id,
name: 'R4 Semi-Finalist Documents',
slug: 'r4-semifinal-docs',
roundNumber: 4,
sortOrder: 1,
windowOpenAt: new Date('2026-04-01'),
windowCloseAt: new Date('2026-04-30'),
isLocked: false,
},
})
console.log(' ✓ Submission Windows: R1 Application, R4 Semi-finalist')
// --- File Requirements ---
await prisma.submissionFileRequirement.upsert({
where: { submissionWindowId_slug: { submissionWindowId: submissionWindow1.id, slug: 'executive-summary' } },
update: {},
create: {
submissionWindowId: submissionWindow1.id,
label: 'Executive Summary',
slug: 'executive-summary',
description: 'PDF document summarizing the project',
mimeTypes: ['application/pdf'],
maxSizeMb: 50,
required: true,
sortOrder: 0,
},
})
await prisma.submissionFileRequirement.upsert({
where: { submissionWindowId_slug: { submissionWindowId: submissionWindow1.id, slug: 'video-pitch' } },
update: {},
create: {
submissionWindowId: submissionWindow1.id,
label: 'Video Pitch',
slug: 'video-pitch',
description: 'Short video pitching the project (max 5 minutes)',
mimeTypes: ['video/mp4', 'video/quicktime'],
maxSizeMb: 500,
required: false,
sortOrder: 1,
},
})
await prisma.submissionFileRequirement.upsert({
where: { submissionWindowId_slug: { submissionWindowId: submissionWindow2.id, slug: 'updated-business-plan' } },
update: {},
create: {
submissionWindowId: submissionWindow2.id,
label: 'Updated Business Plan',
slug: 'updated-business-plan',
description: 'Updated business plan with financials',
mimeTypes: ['application/pdf'],
maxSizeMb: 50,
required: true,
sortOrder: 0,
},
})
console.log(' ✓ File Requirements: Exec Summary, Video Pitch, Business Plan')
// --- Rounds (8-round Monaco flow) ---
const roundDefs = [
{ name: 'R1 - Application Intake', slug: 'r1-intake', roundType: RoundType.INTAKE, sortOrder: 0, status: RoundStatus.ROUND_CLOSED, juryGroupId: null, submissionWindowId: submissionWindow1.id },
{ name: 'R2 - AI Screening', slug: 'r2-screening', roundType: RoundType.FILTERING, sortOrder: 1, status: RoundStatus.ROUND_ACTIVE, juryGroupId: juryGroup1.id, submissionWindowId: null },
{ name: 'R3 - Expert Evaluation', slug: 'r3-evaluation', roundType: RoundType.EVALUATION, sortOrder: 2, status: RoundStatus.ROUND_DRAFT, juryGroupId: juryGroup2.id, submissionWindowId: null },
{ name: 'R4 - Document Submission', slug: 'r4-submission', roundType: RoundType.SUBMISSION, sortOrder: 3, status: RoundStatus.ROUND_DRAFT, juryGroupId: null, submissionWindowId: submissionWindow2.id },
{ name: 'R5 - Semi-Final Evaluation', slug: 'r5-semi-eval', roundType: RoundType.EVALUATION, sortOrder: 4, status: RoundStatus.ROUND_DRAFT, juryGroupId: juryGroup2.id, submissionWindowId: null },
{ name: 'R6 - Mentoring', slug: 'r6-mentoring', roundType: RoundType.MENTORING, sortOrder: 5, status: RoundStatus.ROUND_DRAFT, juryGroupId: null, submissionWindowId: null },
{ name: 'R7 - Grand Final', slug: 'r7-grand-final', roundType: RoundType.LIVE_FINAL, sortOrder: 6, status: RoundStatus.ROUND_DRAFT, juryGroupId: juryGroup3.id, submissionWindowId: null },
{ name: 'R8 - Deliberation', slug: 'r8-deliberation', roundType: RoundType.DELIBERATION, sortOrder: 7, status: RoundStatus.ROUND_DRAFT, juryGroupId: juryGroup3.id, submissionWindowId: null },
]
const rounds = []
for (const def of roundDefs) {
const config = defaultRoundConfig(def.roundType)
const round = await prisma.round.upsert({
where: { competitionId_slug: { competitionId: competition.id, slug: def.slug } },
update: {},
create: {
competitionId: competition.id,
name: def.name,
slug: def.slug,
roundType: def.roundType,
status: def.status,
sortOrder: def.sortOrder,
configJson: config as object,
juryGroupId: def.juryGroupId,
submissionWindowId: def.submissionWindowId,
},
})
rounds.push(round)
}
console.log(`${rounds.length} rounds created (R1-R8)`)
// --- Advancement Rules (auto-advance between rounds) ---
for (let i = 0; i < rounds.length - 1; i++) {
await prisma.advancementRule.upsert({
where: {
roundId_sortOrder: { roundId: rounds[i].id, sortOrder: 0 },
},
update: {},
create: {
roundId: rounds[i].id,
ruleType: AdvancementRuleType.AUTO_ADVANCE,
sortOrder: 0,
targetRoundId: rounds[i + 1].id,
configJson: {},
},
})
}
console.log(`${rounds.length - 1} advancement rules created`)
// --- Round-Submission Visibility (which rounds can see which submission windows) ---
// R2 and R3 can see R1 docs, R5 can see R4 docs
const visibilityLinks = [
{ roundId: rounds[1].id, submissionWindowId: submissionWindow1.id }, // R2 sees R1 docs
{ roundId: rounds[2].id, submissionWindowId: submissionWindow1.id }, // R3 sees R1 docs
{ roundId: rounds[4].id, submissionWindowId: submissionWindow1.id }, // R5 sees R1 docs
{ roundId: rounds[4].id, submissionWindowId: submissionWindow2.id }, // R5 sees R4 docs
]
for (const link of visibilityLinks) {
await prisma.roundSubmissionVisibility.upsert({
where: {
roundId_submissionWindowId: {
roundId: link.roundId,
submissionWindowId: link.submissionWindowId,
},
},
update: {},
create: link,
})
}
console.log(`${visibilityLinks.length} submission visibility links created`)
// --- Feature flag: enable competition model ---
await prisma.systemSettings.upsert({
where: { key: 'feature.useCompetitionModel' },
update: { value: 'true' },
create: {
key: 'feature.useCompetitionModel',
value: 'true',
type: SettingType.BOOLEAN,
category: SettingCategory.FEATURE_FLAGS,
description: 'Use Competition/Round model (legacy Pipeline system removed)',
},
})
console.log(' ✓ Feature flag: feature.useCompetitionModel = true')
// ==========================================================================
// 14. Notification Email Settings
// 9. Notification Email Settings
// ==========================================================================
console.log('\n🔔 Creating notification email settings...')
@@ -1052,27 +910,12 @@ async function main() {
console.log(` ✓ Created ${notificationSettings.length} notification email settings`)
// ==========================================================================
// 16. Summary
// 10. Summary
// ==========================================================================
console.log('\n' + '='.repeat(60))
console.log('✅ SEEDING COMPLETE')
console.log('='.repeat(60))
console.log(`
Program: ${program.name} ${program.year}
Pipeline: ${pipeline.name} (${pipeline.slug})
Tracks: 4 (Main, Innovation Award, Impact Award, People's Choice)
Stages: ${allStages.length} total
Transitions: ${transitionCount}
Projects: ${projectCount} (from CSV)
Users: ${3 + juryMembers.length + mentors.length + observers.length + projectCount} total
- Admin/Staff: 3
- Jury: ${juryMembers.length}
- Mentors: ${mentors.length}
- Observers: ${observers.length}
- Applicants: ${projectCount}
Login: matt@monaco-opc.com / 195260Mp!
`)
console.log(`\n Program: ${program.name} ${program.year}\n\n Competition: ${competition.name} (${competition.slug})\n Rounds: ${rounds.length} (R1-R8)\n Jury Groups: 3 (Screening, Expert, Finals)\n Sub. Windows: 2 (R1 Application, R4 Semi-finalist)\n\n Projects: ${projectCount} (from CSV)\n Users: ${3 + juryMembers.length + mentors.length + observers.length + projectCount} total\n - Admin/Staff: 3\n - Jury: ${juryMembers.length}\n - Mentors: ${mentors.length}\n - Observers: ${observers.length}\n - Applicants: ${projectCount}\n\n Login: matt@monaco-opc.com / 195260Mp!\n `)
}
main()