Challenge delivery system for HackProduct — listing, filtering, loading FLOW steps, managing attempts, and the workspace state machine. Use when building challenge list API, detail API, step loading, attempt management, workspace page, or FlowStepper. Triggers on: challenge list, detail, load step, start attempt, resume, workspace, FLOW stepper, taxonomy filters, option shuffling, state machine.
GET /api/v2/challengesSELECT c.*, COUNT(a.id) as attempt_count, MAX(a.total_score) as best_score,
BOOL_OR(a.status = 'completed') as is_completed
FROM challenges c
LEFT JOIN challenge_attempts_v2 a ON a.challenge_id = c.id AND a.user_id = $user_id
WHERE c.is_published = true
AND ($paradigm IS NULL OR c.paradigm = $paradigm)
AND ($role IS NULL OR $role = ANY(c.relevant_roles))
GROUP BY c.id
Query params: paradigm, industry, role, difficulty, framework, company, page, limit.
POST /api/v2/challenges/[id]/startchallenge_attempts_v2 with , status: 'in_progress'current_step: 'frame'{ attempt_id, role_id, first_step: 'frame' }GET /api/v2/challenges/[id]/step/[step]CRITICAL: Strip quality, points, explanation, competencies from options.
Shuffle deterministically:
function seededShuffle<T>(arr: T[], seed: number): T[] {
const result = [...arr]
let s = seed
for (let i = result.length - 1; i > 0; i--) {
s = (s * 1103515245 + 12345) & 0x7fffffff
const j = s % (i + 1)
;[result[i], result[j]] = [result[j], result[i]]
}
return result
}
// Seed: hashtext(user_id + challenge_id + step) or use JS equivalent
Resolve nudge via nudge-resolver.ts. Check step_attempts for already-answered questions.
Response: { step, step_nudge, questions[{ id, text, nudge, options[{id, label, text}], already_answered }], progress }
type WorkspacePhase =
| 'loading'
| 'question' // StepQuestion component
| 'question_reveal' // StepReveal after submit
| 'step_summary' // Step score before advancing
| 'complete' // ChallengeComplete
// Transitions:
// submit → question → question_reveal
// continue → more questions? → next question
// continue → step done? → step_summary
// continue from summary → next step's first question
// last step done → complete
Resume on refresh: read attempt.current_step + current_question_sequence.
const isV2 = !isUUID(params.id) // v2 = TEXT like "HP-AG-FIN-GM-042"
if (isV2) return <FlowWorkspace challengeId={params.id} />
return <LegacyWorkspace challengeId={params.id} />
src/app/api/v2/challenges/route.ts
src/app/api/v2/challenges/[id]/route.ts
src/app/api/v2/challenges/[id]/start/route.ts
src/app/api/v2/challenges/[id]/step/[step]/route.ts
src/app/api/v2/roles/route.ts
src/hooks/useChallengeV2.ts
src/hooks/useFlowStep.ts
src/components/v2/FlowWorkspace.tsx
src/components/v2/FlowStepper.tsx