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
|
|
|
/**
|
|
|
|
|
* Deliberation Service
|
|
|
|
|
*
|
|
|
|
|
* Full deliberation lifecycle: session management, voting, aggregation,
|
|
|
|
|
* tie-breaking, and finalization.
|
|
|
|
|
*
|
|
|
|
|
* Session transitions: DELIB_OPEN → VOTING → TALLYING → DELIB_LOCKED
|
|
|
|
|
* → RUNOFF → TALLYING (max 3 runoff rounds)
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import type {
|
|
|
|
|
PrismaClient,
|
|
|
|
|
DeliberationMode,
|
|
|
|
|
DeliberationStatus,
|
|
|
|
|
TieBreakMethod,
|
|
|
|
|
CompetitionCategory,
|
|
|
|
|
DeliberationParticipantStatus,
|
|
|
|
|
Prisma,
|
|
|
|
|
} from '@prisma/client'
|
|
|
|
|
import { logAudit } from '@/server/utils/audit'
|
|
|
|
|
|
|
|
|
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export type SessionTransitionResult = {
|
|
|
|
|
success: boolean
|
|
|
|
|
session?: { id: string; status: DeliberationStatus }
|
|
|
|
|
errors?: string[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type AggregationResult = {
|
|
|
|
|
rankings: Array<{
|
|
|
|
|
projectId: string
|
|
|
|
|
rank: number
|
|
|
|
|
voteCount: number
|
|
|
|
|
score: number
|
|
|
|
|
}>
|
|
|
|
|
hasTies: boolean
|
|
|
|
|
tiedProjectIds: string[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const MAX_RUNOFF_ROUNDS = 3
|
|
|
|
|
|
|
|
|
|
// ─── Valid Transitions ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
const VALID_SESSION_TRANSITIONS: Record<string, string[]> = {
|
|
|
|
|
DELIB_OPEN: ['VOTING'],
|
|
|
|
|
VOTING: ['TALLYING'],
|
|
|
|
|
TALLYING: ['DELIB_LOCKED', 'RUNOFF'],
|
|
|
|
|
RUNOFF: ['TALLYING'],
|
|
|
|
|
DELIB_LOCKED: [],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Session Lifecycle ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a new deliberation session with participants.
|
|
|
|
|
*/
|
|
|
|
|
export async function createSession(
|
|
|
|
|
params: {
|
|
|
|
|
competitionId: string
|
|
|
|
|
roundId: string
|
|
|
|
|
category: CompetitionCategory
|
|
|
|
|
mode: DeliberationMode
|
|
|
|
|
tieBreakMethod: TieBreakMethod
|
|
|
|
|
showCollectiveRankings?: boolean
|
|
|
|
|
showPriorJuryData?: boolean
|
|
|
|
|
participantUserIds: string[] // JuryGroupMember IDs
|
|
|
|
|
},
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
prisma: PrismaClient,
|
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
|
|
|
) {
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
return prisma.$transaction(async (tx: Prisma.TransactionClient) => {
|
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 session = await tx.deliberationSession.create({
|
|
|
|
|
data: {
|
|
|
|
|
competitionId: params.competitionId,
|
|
|
|
|
roundId: params.roundId,
|
|
|
|
|
category: params.category,
|
|
|
|
|
mode: params.mode,
|
|
|
|
|
tieBreakMethod: params.tieBreakMethod,
|
|
|
|
|
showCollectiveRankings: params.showCollectiveRankings ?? false,
|
|
|
|
|
showPriorJuryData: params.showPriorJuryData ?? false,
|
|
|
|
|
status: 'DELIB_OPEN',
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Create participant records
|
|
|
|
|
for (const userId of params.participantUserIds) {
|
|
|
|
|
await tx.deliberationParticipant.create({
|
|
|
|
|
data: {
|
|
|
|
|
sessionId: session.id,
|
|
|
|
|
userId,
|
|
|
|
|
status: 'REQUIRED',
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await tx.decisionAuditLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
eventType: 'deliberation.created',
|
|
|
|
|
entityType: 'DeliberationSession',
|
|
|
|
|
entityId: session.id,
|
|
|
|
|
actorId: null,
|
|
|
|
|
detailsJson: {
|
|
|
|
|
competitionId: params.competitionId,
|
|
|
|
|
roundId: params.roundId,
|
|
|
|
|
category: params.category,
|
|
|
|
|
mode: params.mode,
|
|
|
|
|
participantCount: params.participantUserIds.length,
|
|
|
|
|
} as Prisma.InputJsonValue,
|
|
|
|
|
snapshotJson: { timestamp: new Date().toISOString(), emittedBy: 'deliberation' },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return session
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Open voting: DELIB_OPEN → VOTING
|
|
|
|
|
*/
|
|
|
|
|
export async function openVoting(
|
|
|
|
|
sessionId: string,
|
|
|
|
|
actorId: string,
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
prisma: PrismaClient,
|
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
|
|
|
): Promise<SessionTransitionResult> {
|
|
|
|
|
return transitionSession(sessionId, 'DELIB_OPEN', 'VOTING', actorId, prisma)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Close voting: VOTING → TALLYING
|
|
|
|
|
* Triggers vote aggregation.
|
|
|
|
|
*/
|
|
|
|
|
export async function closeVoting(
|
|
|
|
|
sessionId: string,
|
|
|
|
|
actorId: string,
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
prisma: PrismaClient,
|
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
|
|
|
): Promise<SessionTransitionResult> {
|
|
|
|
|
return transitionSession(sessionId, 'VOTING', 'TALLYING', actorId, prisma)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Vote Submission ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Submit a vote in a deliberation session.
|
|
|
|
|
* Validates: session is VOTING (or RUNOFF), juryMember is active participant.
|
|
|
|
|
*/
|
|
|
|
|
export async function submitVote(
|
|
|
|
|
params: {
|
|
|
|
|
sessionId: string
|
|
|
|
|
juryMemberId: string // JuryGroupMember ID
|
|
|
|
|
projectId: string
|
|
|
|
|
rank?: number
|
|
|
|
|
isWinnerPick?: boolean
|
|
|
|
|
runoffRound?: number
|
|
|
|
|
},
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
prisma: PrismaClient,
|
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 session = await prisma.deliberationSession.findUnique({
|
|
|
|
|
where: { id: params.sessionId },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!session) {
|
|
|
|
|
throw new Error('Deliberation session not found')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (session.status !== 'VOTING' && session.status !== 'RUNOFF') {
|
|
|
|
|
throw new Error(`Cannot vote: session status is ${session.status}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify participant is active
|
|
|
|
|
const participant = await prisma.deliberationParticipant.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
sessionId_userId: {
|
|
|
|
|
sessionId: params.sessionId,
|
|
|
|
|
userId: params.juryMemberId,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!participant) {
|
|
|
|
|
throw new Error('Juror is not a participant in this deliberation')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (participant.status !== 'REQUIRED' && participant.status !== 'REPLACEMENT_ACTIVE') {
|
|
|
|
|
throw new Error(`Participant status ${participant.status} does not allow voting`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const runoffRound = params.runoffRound ?? 0
|
|
|
|
|
|
|
|
|
|
return prisma.deliberationVote.upsert({
|
|
|
|
|
where: {
|
|
|
|
|
sessionId_juryMemberId_projectId_runoffRound: {
|
|
|
|
|
sessionId: params.sessionId,
|
|
|
|
|
juryMemberId: params.juryMemberId,
|
|
|
|
|
projectId: params.projectId,
|
|
|
|
|
runoffRound,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
create: {
|
|
|
|
|
sessionId: params.sessionId,
|
|
|
|
|
juryMemberId: params.juryMemberId,
|
|
|
|
|
projectId: params.projectId,
|
|
|
|
|
rank: params.rank,
|
|
|
|
|
isWinnerPick: params.isWinnerPick ?? false,
|
|
|
|
|
runoffRound,
|
|
|
|
|
},
|
|
|
|
|
update: {
|
|
|
|
|
rank: params.rank,
|
|
|
|
|
isWinnerPick: params.isWinnerPick ?? false,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Aggregation ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Aggregate votes for a session.
|
|
|
|
|
* - SINGLE_WINNER_VOTE: count isWinnerPick=true per project
|
|
|
|
|
* - FULL_RANKING: Borda count (N points for rank 1, N-1 for rank 2, etc.)
|
|
|
|
|
*/
|
|
|
|
|
export async function aggregateVotes(
|
|
|
|
|
sessionId: string,
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
prisma: PrismaClient,
|
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
|
|
|
): Promise<AggregationResult> {
|
|
|
|
|
const session = await prisma.deliberationSession.findUnique({
|
|
|
|
|
where: { id: sessionId },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!session) {
|
|
|
|
|
throw new Error('Deliberation session not found')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the latest runoff round
|
|
|
|
|
const latestVote = await prisma.deliberationVote.findFirst({
|
|
|
|
|
where: { sessionId },
|
|
|
|
|
orderBy: { runoffRound: 'desc' },
|
|
|
|
|
select: { runoffRound: true },
|
|
|
|
|
})
|
|
|
|
|
const currentRound = latestVote?.runoffRound ?? 0
|
|
|
|
|
|
|
|
|
|
const votes = await prisma.deliberationVote.findMany({
|
|
|
|
|
where: { sessionId, runoffRound: currentRound },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const projectScores = new Map<string, number>()
|
|
|
|
|
const projectVoteCounts = new Map<string, number>()
|
|
|
|
|
|
|
|
|
|
if (session.mode === 'SINGLE_WINNER_VOTE') {
|
|
|
|
|
// Count isWinnerPick=true per project
|
|
|
|
|
for (const vote of votes) {
|
|
|
|
|
if (vote.isWinnerPick) {
|
|
|
|
|
projectScores.set(vote.projectId, (projectScores.get(vote.projectId) ?? 0) + 1)
|
|
|
|
|
projectVoteCounts.set(vote.projectId, (projectVoteCounts.get(vote.projectId) ?? 0) + 1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// FULL_RANKING: Borda count
|
|
|
|
|
// First, find N = total unique projects being ranked
|
|
|
|
|
const uniqueProjects = new Set(votes.map((v: any) => v.projectId))
|
|
|
|
|
const n = uniqueProjects.size
|
|
|
|
|
|
|
|
|
|
for (const vote of votes) {
|
|
|
|
|
if (vote.rank != null) {
|
|
|
|
|
// Borda: rank 1 gets N points, rank 2 gets N-1, etc.
|
|
|
|
|
const score = Math.max(0, n + 1 - vote.rank)
|
|
|
|
|
projectScores.set(vote.projectId, (projectScores.get(vote.projectId) ?? 0) + score)
|
|
|
|
|
projectVoteCounts.set(vote.projectId, (projectVoteCounts.get(vote.projectId) ?? 0) + 1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort by score descending
|
|
|
|
|
const sorted = [...projectScores.entries()]
|
|
|
|
|
.sort(([, a], [, b]) => b - a)
|
|
|
|
|
.map(([projectId, score], index) => ({
|
|
|
|
|
projectId,
|
|
|
|
|
rank: index + 1,
|
|
|
|
|
voteCount: projectVoteCounts.get(projectId) ?? 0,
|
|
|
|
|
score,
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
// Detect ties: projects with same score get same rank
|
|
|
|
|
const rankings: typeof sorted = []
|
|
|
|
|
let currentRank = 1
|
|
|
|
|
for (let i = 0; i < sorted.length; i++) {
|
|
|
|
|
if (i > 0 && sorted[i].score === sorted[i - 1].score) {
|
|
|
|
|
rankings.push({ ...sorted[i], rank: rankings[i - 1].rank })
|
|
|
|
|
} else {
|
|
|
|
|
rankings.push({ ...sorted[i], rank: currentRank })
|
|
|
|
|
}
|
|
|
|
|
currentRank = rankings[i].rank + 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find tied projects (projects sharing rank 1, or if no clear winner)
|
|
|
|
|
const topScore = rankings.length > 0 ? rankings[0].score : 0
|
|
|
|
|
const tiedProjectIds = rankings.filter((r) => r.score === topScore && topScore > 0).length > 1
|
|
|
|
|
? rankings.filter((r) => r.score === topScore).map((r) => r.projectId)
|
|
|
|
|
: []
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
rankings,
|
|
|
|
|
hasTies: tiedProjectIds.length > 1,
|
|
|
|
|
tiedProjectIds,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Tie-Breaking ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initiate a runoff vote for tied projects.
|
|
|
|
|
* TALLYING → RUNOFF
|
|
|
|
|
*/
|
|
|
|
|
export async function initRunoff(
|
|
|
|
|
sessionId: string,
|
|
|
|
|
tiedProjectIds: string[],
|
|
|
|
|
actorId: string,
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
prisma: PrismaClient,
|
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
|
|
|
): Promise<SessionTransitionResult> {
|
|
|
|
|
const session = await prisma.deliberationSession.findUnique({
|
|
|
|
|
where: { id: sessionId },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!session) {
|
|
|
|
|
return { success: false, errors: ['Session not found'] }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (session.status !== 'TALLYING') {
|
|
|
|
|
return { success: false, errors: [`Cannot init runoff: status is ${session.status}`] }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check max runoff rounds
|
|
|
|
|
const latestVote = await prisma.deliberationVote.findFirst({
|
|
|
|
|
where: { sessionId },
|
|
|
|
|
orderBy: { runoffRound: 'desc' },
|
|
|
|
|
select: { runoffRound: true },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const nextRound = (latestVote?.runoffRound ?? 0) + 1
|
|
|
|
|
if (nextRound > MAX_RUNOFF_ROUNDS) {
|
|
|
|
|
return { success: false, errors: [`Maximum runoff rounds (${MAX_RUNOFF_ROUNDS}) exceeded`] }
|
|
|
|
|
}
|
|
|
|
|
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
return prisma.$transaction(async (tx: Prisma.TransactionClient) => {
|
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 updated = await tx.deliberationSession.update({
|
|
|
|
|
where: { id: sessionId },
|
|
|
|
|
data: { status: 'RUNOFF' },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await tx.decisionAuditLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
eventType: 'deliberation.runoff_initiated',
|
|
|
|
|
entityType: 'DeliberationSession',
|
|
|
|
|
entityId: sessionId,
|
|
|
|
|
actorId,
|
|
|
|
|
detailsJson: {
|
|
|
|
|
runoffRound: nextRound,
|
|
|
|
|
tiedProjectIds,
|
|
|
|
|
} as Prisma.InputJsonValue,
|
|
|
|
|
snapshotJson: { timestamp: new Date().toISOString(), emittedBy: 'deliberation' },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
session: { id: updated.id, status: updated.status },
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Admin override: directly set final rankings.
|
|
|
|
|
*/
|
|
|
|
|
export async function adminDecide(
|
|
|
|
|
sessionId: string,
|
|
|
|
|
rankings: Array<{ projectId: string; rank: number }>,
|
|
|
|
|
reason: string,
|
|
|
|
|
actorId: string,
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
prisma: PrismaClient,
|
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
|
|
|
): Promise<SessionTransitionResult> {
|
|
|
|
|
const session = await prisma.deliberationSession.findUnique({
|
|
|
|
|
where: { id: sessionId },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!session) {
|
|
|
|
|
return { success: false, errors: ['Session not found'] }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (session.status !== 'TALLYING') {
|
|
|
|
|
return { success: false, errors: [`Cannot admin-decide: status is ${session.status}`] }
|
|
|
|
|
}
|
|
|
|
|
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
return prisma.$transaction(async (tx: Prisma.TransactionClient) => {
|
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 updated = await tx.deliberationSession.update({
|
|
|
|
|
where: { id: sessionId },
|
|
|
|
|
data: {
|
|
|
|
|
adminOverrideResult: {
|
|
|
|
|
rankings,
|
|
|
|
|
reason,
|
|
|
|
|
decidedBy: actorId,
|
|
|
|
|
decidedAt: new Date().toISOString(),
|
|
|
|
|
} as Prisma.InputJsonValue,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await tx.decisionAuditLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
eventType: 'deliberation.admin_override',
|
|
|
|
|
entityType: 'DeliberationSession',
|
|
|
|
|
entityId: sessionId,
|
|
|
|
|
actorId,
|
|
|
|
|
detailsJson: {
|
|
|
|
|
rankings,
|
|
|
|
|
reason,
|
|
|
|
|
} as Prisma.InputJsonValue,
|
|
|
|
|
snapshotJson: { timestamp: new Date().toISOString(), emittedBy: 'deliberation' },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
session: { id: updated.id, status: updated.status },
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Finalization ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Finalize deliberation results: TALLYING → DELIB_LOCKED
|
|
|
|
|
* Creates DeliberationResult records.
|
|
|
|
|
*/
|
|
|
|
|
export async function finalizeResults(
|
|
|
|
|
sessionId: string,
|
|
|
|
|
actorId: string,
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
prisma: PrismaClient,
|
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
|
|
|
): Promise<SessionTransitionResult> {
|
|
|
|
|
const session = await prisma.deliberationSession.findUnique({
|
|
|
|
|
where: { id: sessionId },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!session) {
|
|
|
|
|
return { success: false, errors: ['Session not found'] }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (session.status !== 'TALLYING') {
|
|
|
|
|
return { success: false, errors: [`Cannot finalize: status is ${session.status}`] }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If admin override exists, use those rankings
|
|
|
|
|
const override = session.adminOverrideResult as {
|
|
|
|
|
rankings: Array<{ projectId: string; rank: number }>
|
|
|
|
|
} | null
|
|
|
|
|
|
|
|
|
|
let finalRankings: Array<{ projectId: string; rank: number; voteCount: number; isAdminOverridden: boolean }>
|
|
|
|
|
|
|
|
|
|
if (override?.rankings) {
|
|
|
|
|
finalRankings = override.rankings.map((r) => ({
|
|
|
|
|
projectId: r.projectId,
|
|
|
|
|
rank: r.rank,
|
|
|
|
|
voteCount: 0,
|
|
|
|
|
isAdminOverridden: true,
|
|
|
|
|
}))
|
|
|
|
|
} else {
|
|
|
|
|
// Use aggregated votes
|
|
|
|
|
const agg = await aggregateVotes(sessionId, prisma)
|
|
|
|
|
finalRankings = agg.rankings.map((r) => ({
|
|
|
|
|
projectId: r.projectId,
|
|
|
|
|
rank: r.rank,
|
|
|
|
|
voteCount: r.voteCount,
|
|
|
|
|
isAdminOverridden: false,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
return prisma.$transaction(async (tx: Prisma.TransactionClient) => {
|
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
|
|
|
// Create result records
|
|
|
|
|
for (const ranking of finalRankings) {
|
|
|
|
|
await tx.deliberationResult.upsert({
|
|
|
|
|
where: {
|
|
|
|
|
sessionId_projectId: {
|
|
|
|
|
sessionId,
|
|
|
|
|
projectId: ranking.projectId,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
create: {
|
|
|
|
|
sessionId,
|
|
|
|
|
projectId: ranking.projectId,
|
|
|
|
|
finalRank: ranking.rank,
|
|
|
|
|
voteCount: ranking.voteCount,
|
|
|
|
|
isAdminOverridden: ranking.isAdminOverridden,
|
|
|
|
|
overrideReason: ranking.isAdminOverridden
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
? (session.adminOverrideResult as Record<string, unknown> | null)?.reason as string ?? null
|
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
|
|
|
: null,
|
|
|
|
|
},
|
|
|
|
|
update: {
|
|
|
|
|
finalRank: ranking.rank,
|
|
|
|
|
voteCount: ranking.voteCount,
|
|
|
|
|
isAdminOverridden: ranking.isAdminOverridden,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Transition to DELIB_LOCKED
|
|
|
|
|
const updated = await tx.deliberationSession.update({
|
|
|
|
|
where: { id: sessionId },
|
|
|
|
|
data: { status: 'DELIB_LOCKED' },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await tx.decisionAuditLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
eventType: 'deliberation.finalized',
|
|
|
|
|
entityType: 'DeliberationSession',
|
|
|
|
|
entityId: sessionId,
|
|
|
|
|
actorId,
|
|
|
|
|
detailsJson: {
|
|
|
|
|
resultCount: finalRankings.length,
|
|
|
|
|
isAdminOverride: finalRankings.some((r) => r.isAdminOverridden),
|
|
|
|
|
} as Prisma.InputJsonValue,
|
|
|
|
|
snapshotJson: {
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
emittedBy: 'deliberation',
|
|
|
|
|
rankings: finalRankings,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: tx,
|
|
|
|
|
userId: actorId,
|
|
|
|
|
action: 'DELIBERATION_FINALIZE',
|
|
|
|
|
entityType: 'DeliberationSession',
|
|
|
|
|
entityId: sessionId,
|
|
|
|
|
detailsJson: { resultCount: finalRankings.length },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
session: { id: updated.id, status: updated.status },
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Participant Management ─────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update a participant's status (e.g. mark absent, replace).
|
|
|
|
|
*/
|
|
|
|
|
export async function updateParticipantStatus(
|
|
|
|
|
sessionId: string,
|
|
|
|
|
userId: string,
|
|
|
|
|
status: DeliberationParticipantStatus,
|
|
|
|
|
replacedById?: string,
|
|
|
|
|
actorId?: string,
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
prisma?: PrismaClient,
|
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 db = prisma ?? (await import('@/lib/prisma')).prisma
|
|
|
|
|
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
return db.$transaction(async (tx: Prisma.TransactionClient) => {
|
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 updated = await tx.deliberationParticipant.update({
|
|
|
|
|
where: { sessionId_userId: { sessionId, userId } },
|
|
|
|
|
data: {
|
|
|
|
|
status,
|
|
|
|
|
replacedById: replacedById ?? null,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// If replacing, create participant record for replacement
|
|
|
|
|
if (status === 'REPLACED' && replacedById) {
|
|
|
|
|
await tx.deliberationParticipant.upsert({
|
|
|
|
|
where: { sessionId_userId: { sessionId, userId: replacedById } },
|
|
|
|
|
create: {
|
|
|
|
|
sessionId,
|
|
|
|
|
userId: replacedById,
|
|
|
|
|
status: 'REPLACEMENT_ACTIVE',
|
|
|
|
|
},
|
|
|
|
|
update: {
|
|
|
|
|
status: 'REPLACEMENT_ACTIVE',
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (actorId) {
|
|
|
|
|
await tx.decisionAuditLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
eventType: 'deliberation.participant_updated',
|
|
|
|
|
entityType: 'DeliberationParticipant',
|
|
|
|
|
entityId: updated.id,
|
|
|
|
|
actorId,
|
|
|
|
|
detailsJson: { userId, newStatus: status, replacedById } as Prisma.InputJsonValue,
|
|
|
|
|
snapshotJson: { timestamp: new Date().toISOString(), emittedBy: 'deliberation' },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return updated
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Queries ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a deliberation session with votes, results, and participants.
|
|
|
|
|
*/
|
|
|
|
|
export async function getSessionWithVotes(
|
|
|
|
|
sessionId: string,
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
prisma: PrismaClient,
|
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
|
|
|
) {
|
|
|
|
|
return prisma.deliberationSession.findUnique({
|
|
|
|
|
where: { id: sessionId },
|
|
|
|
|
include: {
|
|
|
|
|
votes: {
|
|
|
|
|
include: {
|
|
|
|
|
project: { select: { id: true, title: true, teamName: true } },
|
|
|
|
|
juryMember: {
|
|
|
|
|
include: {
|
|
|
|
|
user: { select: { id: true, name: true, email: true } },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
orderBy: [{ runoffRound: 'desc' }, { rank: 'asc' }],
|
|
|
|
|
},
|
|
|
|
|
results: {
|
|
|
|
|
include: {
|
|
|
|
|
project: { select: { id: true, title: true, teamName: true } },
|
|
|
|
|
},
|
|
|
|
|
orderBy: { finalRank: 'asc' },
|
|
|
|
|
},
|
|
|
|
|
participants: {
|
|
|
|
|
include: {
|
|
|
|
|
user: {
|
|
|
|
|
include: {
|
|
|
|
|
user: { select: { id: true, name: true, email: true } },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
competition: { select: { id: true, name: true } },
|
|
|
|
|
round: { select: { id: true, name: true, roundType: true } },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Internal Helpers ───────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
async function transitionSession(
|
|
|
|
|
sessionId: string,
|
|
|
|
|
expectedStatus: DeliberationStatus,
|
|
|
|
|
newStatus: DeliberationStatus,
|
|
|
|
|
actorId: string,
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
prisma: PrismaClient,
|
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
|
|
|
): Promise<SessionTransitionResult> {
|
|
|
|
|
try {
|
|
|
|
|
const session = await prisma.deliberationSession.findUnique({
|
|
|
|
|
where: { id: sessionId },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!session) {
|
|
|
|
|
return { success: false, errors: ['Session not found'] }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (session.status !== expectedStatus) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
errors: [`Cannot transition: status is ${session.status}, expected ${expectedStatus}`],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const valid = VALID_SESSION_TRANSITIONS[expectedStatus] ?? []
|
|
|
|
|
if (!valid.includes(newStatus)) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
errors: [`Invalid transition: ${expectedStatus} → ${newStatus}`],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
const updated = await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
|
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 result = await tx.deliberationSession.update({
|
|
|
|
|
where: { id: sessionId },
|
|
|
|
|
data: { status: newStatus },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await tx.decisionAuditLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
eventType: `deliberation.${newStatus.toLowerCase()}`,
|
|
|
|
|
entityType: 'DeliberationSession',
|
|
|
|
|
entityId: sessionId,
|
|
|
|
|
actorId,
|
|
|
|
|
detailsJson: {
|
|
|
|
|
previousStatus: expectedStatus,
|
|
|
|
|
newStatus,
|
|
|
|
|
} as Prisma.InputJsonValue,
|
|
|
|
|
snapshotJson: { timestamp: new Date().toISOString(), emittedBy: 'deliberation' },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: tx,
|
|
|
|
|
userId: actorId,
|
|
|
|
|
action: `DELIBERATION_${newStatus}`,
|
|
|
|
|
entityType: 'DeliberationSession',
|
|
|
|
|
entityId: sessionId,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
session: { id: updated.id, status: updated.status },
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[Deliberation] Session transition failed:', error)
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
errors: [error instanceof Error ? error.message : 'Unknown error'],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|