From 19fa1e2c96b89904185530f093c2d285031bde7f Mon Sep 17 00:00:00 2001 From: Navneet Date: Sat, 1 Aug 2026 14:56:11 +0530 Subject: [PATCH 1/3] feat(contests): add HireUp partial credit --- app/(protected)/admin/contests/actions.ts | 5 + app/(protected)/admin/problems/actions.ts | 7 +- app/contests/[slug]/actions.ts | 26 ++- app/contests/[slug]/page.tsx | 14 +- app/contests/[slug]/problems/[label]/page.tsx | 2 + app/problems/[slug]/actions.ts | 13 +- app/problems/[slug]/page.tsx | 2 + components/practice/problem-workspace.tsx | 11 + lib/contest.ts | 55 +++-- lib/judge/core.ts | 45 +++-- lib/judge/types.ts | 3 + prisma/hireup-problem-set.js | 190 ++++++++++-------- .../migration.sql | 20 ++ prisma/schema.prisma | 6 + prisma/seed.mjs | 16 +- .../reference-solutions/best-dispatch-hub.cpp | 59 ++++++ .../reference-solutions/best-dispatch-hub.py | 39 ++++ .../driver-rebalancing.cpp | 26 +++ .../reference-solutions/driver-rebalancing.py | 19 ++ scripts/simulate-contest-ratings.ts | 12 +- tests/unit/lib/contest.test.ts | 50 ++++- tests/unit/lib/judge.test.ts | 21 +- 22 files changed, 500 insertions(+), 141 deletions(-) create mode 100644 prisma/migrations/20260801090000_add_contest_partial_credit/migration.sql create mode 100644 scripts/reference-solutions/best-dispatch-hub.cpp create mode 100644 scripts/reference-solutions/best-dispatch-hub.py create mode 100644 scripts/reference-solutions/driver-rebalancing.cpp create mode 100644 scripts/reference-solutions/driver-rebalancing.py diff --git a/app/(protected)/admin/contests/actions.ts b/app/(protected)/admin/contests/actions.ts index f8e9e73..e168448 100644 --- a/app/(protected)/admin/contests/actions.ts +++ b/app/(protected)/admin/contests/actions.ts @@ -219,6 +219,10 @@ export async function finalizeContest(formData: FormData) { userId: true, contestProblemId: true, verdict: true, + passedCount: true, + totalCount: true, + earnedPoints: true, + possiblePoints: true, createdAt: true, }, }, @@ -274,6 +278,7 @@ export async function finalizeContest(formData: FormData) { userId, rank: standing?.rank ?? participantIds.size, solvedCount: standing?.solvedCount ?? 0, + score: standing?.score ?? 0, penalty: standing?.penalty ?? 0, ratingBefore, ratingAfter: change.newRating, diff --git a/app/(protected)/admin/problems/actions.ts b/app/(protected)/admin/problems/actions.ts index cf6dc39..b9572a6 100644 --- a/app/(protected)/admin/problems/actions.ts +++ b/app/(protected)/admin/problems/actions.ts @@ -10,6 +10,8 @@ type RunResult = verdict: string; passedCount: number; totalCount: number; + earnedPoints: number; + possiblePoints: number; runtimeMs: number | null; failureMessage?: string | null; } @@ -40,7 +42,10 @@ export async function runReferenceSolution(slug: string, language?: string): Pro select: { language: true, code: true }, }, timeLimitMs: true, - testCases: { orderBy: { order: "asc" }, select: { input: true, expectedOutput: true } }, + testCases: { + orderBy: { order: "asc" }, + select: { input: true, expectedOutput: true, points: true }, + }, }, }); diff --git a/app/contests/[slug]/actions.ts b/app/contests/[slug]/actions.ts index a33b744..655a333 100644 --- a/app/contests/[slug]/actions.ts +++ b/app/contests/[slug]/actions.ts @@ -13,6 +13,8 @@ type ContestPreviewResult = { verdict: string; passedCount: number; totalCount: number; + earnedPoints: number; + possiblePoints: number; runtimeMs: number | null; failureMessage?: string | null; }; @@ -156,7 +158,7 @@ export async function submitContestSolution(formData: FormData) { timeLimitMs: true, testCases: { orderBy: { order: "asc" }, - select: { input: true, expectedOutput: true, isSample: true }, + select: { input: true, expectedOutput: true, isSample: true, points: true }, }, }, }, @@ -184,6 +186,10 @@ export async function submitContestSolution(formData: FormData) { language: parsed.data.language, code: parsed.data.code, totalCount: contestProblem.problem.testCases.length, + possiblePoints: contestProblem.problem.testCases.reduce( + (total, testCase) => total + testCase.points, + 0, + ), }, select: { id: true }, }); @@ -207,6 +213,11 @@ export async function submitContestSolution(formData: FormData) { verdict: SubmissionVerdict.RUNTIME_ERROR, passedCount: 0, totalCount: contestProblem.problem.testCases.length, + earnedPoints: 0, + possiblePoints: contestProblem.problem.testCases.reduce( + (total, testCase) => total + testCase.points, + 0, + ), failureMessage: failureMessageFromError(error), }, }); @@ -275,7 +286,7 @@ export async function runContestSolution(formData: FormData): Promise total + testCase.points, + 0, + ), runtimeMs: null, failureMessage: failureMessageFromError(error), }; @@ -309,6 +325,8 @@ function previewError(message: string): ContestPreviewResult { verdict: SubmissionVerdict.RUNTIME_ERROR, passedCount: 0, totalCount: 0, + earnedPoints: 0, + possiblePoints: 0, runtimeMs: null, failureMessage: message, }; @@ -340,7 +358,7 @@ export async function runContestPreview(formData: FormData): Promise total + testCase.points, 0), runtimeMs: null, failureMessage: failureMessageFromError(error), }; diff --git a/app/contests/[slug]/page.tsx b/app/contests/[slug]/page.tsx index dd8e0e5..fc8004c 100644 --- a/app/contests/[slug]/page.tsx +++ b/app/contests/[slug]/page.tsx @@ -36,6 +36,10 @@ export default async function ContestDetailPage({ userId: true, contestProblemId: true, verdict: true, + passedCount: true, + totalCount: true, + earnedPoints: true, + possiblePoints: true, createdAt: true, }, }, @@ -192,8 +196,11 @@ export default async function ContestDetailPage({

Standings

- Solved · Time taken + Points · Solved · Time taken
+

+ Each problem is worth 100 points. Only your best submission per problem counts. +

{standings.length > 0 ? (
{standings.map((entry) => ( @@ -201,13 +208,14 @@ export default async function ContestDetailPage({ #{entry.rank} {entry.name} - {entry.solvedCount} · {entry.penalty} min + {entry.score}/{contest.problems.length * 100} · {entry.solvedCount} solved ·{" "} + {entry.penalty} min
))} ) : ( -
No accepted submissions yet.
+
No scored submissions yet.
)}
diff --git a/app/contests/[slug]/problems/[label]/page.tsx b/app/contests/[slug]/problems/[label]/page.tsx index 54f2457..f9ae76f 100644 --- a/app/contests/[slug]/problems/[label]/page.tsx +++ b/app/contests/[slug]/problems/[label]/page.tsx @@ -115,6 +115,8 @@ export default async function ContestProblemPage({ verdict: true, passedCount: true, totalCount: true, + earnedPoints: true, + possiblePoints: true, runtimeMs: true, failureMessage: true, }, diff --git a/app/problems/[slug]/actions.ts b/app/problems/[slug]/actions.ts index 0c37da6..3413168 100644 --- a/app/problems/[slug]/actions.ts +++ b/app/problems/[slug]/actions.ts @@ -19,6 +19,8 @@ type RunResult = { verdict: string; passedCount: number; totalCount: number; + earnedPoints: number; + possiblePoints: number; runtimeMs: number | null; failureMessage?: string | null; }; @@ -42,6 +44,8 @@ function runError(message: string): RunResult { verdict: SubmissionVerdict.RUNTIME_ERROR, passedCount: 0, totalCount: 0, + earnedPoints: 0, + possiblePoints: 0, runtimeMs: null, failureMessage: message, }; @@ -72,7 +76,7 @@ export async function submitSolution(formData: FormData) { timeLimitMs: true, testCases: { orderBy: { order: "asc" }, - select: { input: true, expectedOutput: true, isSample: true }, + select: { input: true, expectedOutput: true, isSample: true, points: true }, }, }, }); @@ -97,6 +101,7 @@ export async function submitSolution(formData: FormData) { language: parsed.data.language, code: parsed.data.code, totalCount: problem.testCases.length, + possiblePoints: problem.testCases.reduce((total, testCase) => total + testCase.points, 0), }, select: { id: true }, }); @@ -154,6 +159,8 @@ export async function submitSolution(formData: FormData) { verdict: SubmissionVerdict.RUNTIME_ERROR, passedCount: 0, totalCount: problem.testCases.length, + earnedPoints: 0, + possiblePoints: problem.testCases.reduce((total, testCase) => total + testCase.points, 0), failureMessage: failureMessageFromError(error), }, }); @@ -188,7 +195,7 @@ export async function runSolution(formData: FormData): Promise { testCases: { where: { isSample: true }, orderBy: { order: "asc" }, - select: { input: true, expectedOutput: true, isSample: true }, + select: { input: true, expectedOutput: true, isSample: true, points: true }, }, }, }); @@ -209,6 +216,8 @@ export async function runSolution(formData: FormData): Promise { verdict: SubmissionVerdict.RUNTIME_ERROR, passedCount: 0, totalCount: problem.testCases.length, + earnedPoints: 0, + possiblePoints: problem.testCases.reduce((total, testCase) => total + testCase.points, 0), runtimeMs: null, failureMessage: failureMessageFromError(error), }; diff --git a/app/problems/[slug]/page.tsx b/app/problems/[slug]/page.tsx index 08c6efe..34f8804 100644 --- a/app/problems/[slug]/page.tsx +++ b/app/problems/[slug]/page.tsx @@ -55,6 +55,8 @@ export default async function ProblemDetailPage({ verdict: true, passedCount: true, totalCount: true, + earnedPoints: true, + possiblePoints: true, runtimeMs: true, failureMessage: true, }, diff --git a/components/practice/problem-workspace.tsx b/components/practice/problem-workspace.tsx index 2371991..1db36c9 100644 --- a/components/practice/problem-workspace.tsx +++ b/components/practice/problem-workspace.tsx @@ -18,6 +18,8 @@ type Submission = { verdict: string; passedCount: number; totalCount: number; + earnedPoints: number; + possiblePoints: number; runtimeMs: number | null; failureMessage: string | null; note?: string; @@ -33,6 +35,8 @@ type EphemeralRunResult = { verdict: string; passedCount: number; totalCount: number; + earnedPoints: number; + possiblePoints: number; runtimeMs: number | null; failureMessage?: string | null; }; @@ -72,6 +76,11 @@ function SubmissionMeta({ submission }: Readonly<{ submission: Submission }>) { {submission.passedCount}/{submission.totalCount} tests + {submission.possiblePoints > 0 ? ( + + {submission.earnedPoints}/{submission.possiblePoints} points + + ) : null} {submission.runtimeMs ?? 0}ms {submission.note ? {submission.note} : null} @@ -265,6 +274,8 @@ export function ProblemWorkspace({ verdict: result.verdict, passedCount: result.passedCount, totalCount: result.totalCount, + earnedPoints: result.earnedPoints, + possiblePoints: result.possiblePoints, runtimeMs: result.runtimeMs, failureMessage: result.failureMessage ?? null, note, diff --git a/lib/contest.ts b/lib/contest.ts index 9a48e9c..d99a4e2 100644 --- a/lib/contest.ts +++ b/lib/contest.ts @@ -59,11 +59,16 @@ export type ContestSubmissionRow = { userId: string; contestProblemId: string; verdict: SubmissionVerdict; + passedCount: number; + totalCount: number; + earnedPoints: number; + possiblePoints: number; createdAt: Date; }; export type StandingRow = { userId: string; + score: number; solvedCount: number; penalty: number; lastAcAt: Date | null; @@ -192,6 +197,12 @@ function minutesFromStart(startsAt: Date, at: Date) { return Math.max(0, Math.floor((at.getTime() - startsAt.getTime()) / 60_000)); } +function submissionScore(submission: ContestSubmissionRow) { + const possible = submission.possiblePoints || submission.totalCount; + const earned = submission.possiblePoints ? submission.earnedPoints : submission.passedCount; + return possible > 0 ? Math.round((earned * 100) / possible) : 0; +} + export function computeStandings( submissions: ContestSubmissionRow[], startsAt: Date, @@ -206,35 +217,53 @@ export function computeStandings( byUserProblem.set(key, bucket); } - const stats = new Map(); + const stats = new Map< + string, + { score: number; solvedCount: number; penalty: number; lastAcAt: Date | null } + >(); for (const [key, attempts] of Array.from(byUserProblem.entries())) { const userId = key.split(":")[0]!; const ordered = [...attempts].sort( (left, right) => left.createdAt.getTime() - right.createdAt.getTime(), ); - const acceptedIndex = ordered.findIndex( - (attempt) => attempt.verdict === SubmissionVerdict.ACCEPTED, - ); + let bestIndex = -1; + let bestScore = 0; + for (let index = 0; index < ordered.length; index += 1) { + const attempt = ordered[index]!; + const score = submissionScore(attempt); + if (score > bestScore) { + bestScore = score; + bestIndex = index; + } + } - if (acceptedIndex < 0) { + if (bestIndex < 0) { continue; } - const accepted = ordered[acceptedIndex]!; + const bestAttempt = ordered[bestIndex]!; const wrongBefore = ordered - .slice(0, acceptedIndex) + .slice(0, bestIndex) .filter((attempt) => attempt.verdict !== SubmissionVerdict.PENDING).length; const problemPenalty = - minutesFromStart(startTimesByUser.get(userId) ?? startsAt, accepted.createdAt) + + minutesFromStart(startTimesByUser.get(userId) ?? startsAt, bestAttempt.createdAt) + wrongBefore * CONTEST_WRONG_PENALTY_MINUTES; - const current = stats.get(userId) ?? { solvedCount: 0, penalty: 0, lastAcAt: null }; - current.solvedCount += 1; + const current = stats.get(userId) ?? { + score: 0, + solvedCount: 0, + penalty: 0, + lastAcAt: null, + }; + current.score += bestScore; + if (bestAttempt.verdict === SubmissionVerdict.ACCEPTED) { + current.solvedCount += 1; + } current.penalty += problemPenalty; current.lastAcAt = - !current.lastAcAt || accepted.createdAt > current.lastAcAt - ? accepted.createdAt + !current.lastAcAt || bestAttempt.createdAt > current.lastAcAt + ? bestAttempt.createdAt : current.lastAcAt; stats.set(userId, current); } @@ -243,6 +272,7 @@ export function computeStandings( .map(([userId, row]) => ({ userId, ...row })) .sort( (left, right) => + right.score - left.score || right.solvedCount - left.solvedCount || left.penalty - right.penalty || (left.lastAcAt?.getTime() ?? 0) - (right.lastAcAt?.getTime() ?? 0), @@ -250,6 +280,7 @@ export function computeStandings( return ranked.map((row, index) => ({ userId: row.userId, + score: row.score, solvedCount: row.solvedCount, penalty: row.penalty, lastAcAt: row.lastAcAt, diff --git a/lib/judge/core.ts b/lib/judge/core.ts index cf949ff..ec827c8 100644 --- a/lib/judge/core.ts +++ b/lib/judge/core.ts @@ -55,7 +55,14 @@ export async function judgeSubmission({ timeLimitMs: number; }): Promise { let passedCount = 0; + let earnedPoints = 0; let runtimeMs = 0; + let verdict: SubmissionVerdict = SubmissionVerdict.ACCEPTED; + let failureMessage: string | null = null; + const possiblePoints = testCases.reduce( + (total, testCase) => total + Math.max(0, testCase.points ?? 1), + 0, + ); for (const testCase of testCases) { const result = await executor({ language, code, stdin: testCase.input, timeLimitMs }); @@ -69,46 +76,44 @@ export async function judgeSubmission({ verdict: SubmissionVerdict.COMPILE_ERROR, passedCount, totalCount: testCases.length, + earnedPoints: 0, + possiblePoints, runtimeMs, failureMessage: truncateFailureMessage(result.compileError), }; } if (result.timedOut || result.signal === "SIGKILL" || result.signal === "SIGTERM") { - return { - verdict: SubmissionVerdict.TLE, - passedCount, - totalCount: testCases.length, - runtimeMs, - }; + verdict = SubmissionVerdict.TLE; + continue; } if (result.exitCode !== 0) { - return { - verdict: SubmissionVerdict.RUNTIME_ERROR, - passedCount, - totalCount: testCases.length, - runtimeMs, - failureMessage: runtimeFailureMessage(result, testCase.isSample), - }; + if (verdict !== SubmissionVerdict.TLE) { + verdict = SubmissionVerdict.RUNTIME_ERROR; + } + failureMessage ??= runtimeFailureMessage(result, testCase.isSample); + continue; } if (normalizeOutput(result.stdout) !== normalizeOutput(testCase.expectedOutput)) { - return { - verdict: SubmissionVerdict.WRONG_ANSWER, - passedCount, - totalCount: testCases.length, - runtimeMs, - }; + if (verdict === SubmissionVerdict.ACCEPTED) { + verdict = SubmissionVerdict.WRONG_ANSWER; + } + continue; } passedCount += 1; + earnedPoints += Math.max(0, testCase.points ?? 1); } return { - verdict: SubmissionVerdict.ACCEPTED, + verdict, passedCount, totalCount: testCases.length, + earnedPoints, + possiblePoints, runtimeMs, + failureMessage, }; } diff --git a/lib/judge/types.ts b/lib/judge/types.ts index 693b7af..8f458a4 100644 --- a/lib/judge/types.ts +++ b/lib/judge/types.ts @@ -5,6 +5,7 @@ export type JudgeTestCase = { input: string; expectedOutput: string; isSample?: boolean; + points?: number; }; export type ExecutionResult = { @@ -28,6 +29,8 @@ export type JudgeResult = { verdict: SubmissionVerdict; passedCount: number; totalCount: number; + earnedPoints: number; + possiblePoints: number; runtimeMs: number | null; failureMessage?: string | null; }; diff --git a/prisma/hireup-problem-set.js b/prisma/hireup-problem-set.js index 859eee3..816ce09 100644 --- a/prisma/hireup-problem-set.js +++ b/prisma/hireup-problem-set.js @@ -1,12 +1,7 @@ // Problems for the "HireUp Online Assessment" — the first round of the HireUp -// mock-hiring event. Original problems written in the style of recent (last ~2 -// years) Uber / Amazon / Google online assessments, customized so they are not -// verbatim copies of any company's proprietary question. -// -// Both are HARD and target the tier top companies actually test: A is a 2D grid -// DP carrying an extra "skip budget" state; B is a shortest-path problem solved -// with Dijkstra over an augmented (node, upgrades-used) state. Seeded unpublished -// so they only appear inside the OA contest. +// mock-hiring event. The pair escalates from a prefix-imbalance observation to +// weighted tree rerooting, giving candidates an approachable entry point and a +// stronger second problem within the assessment window. // // Reference solutions live in scripts/reference-solutions/.{py,cpp} and are // attached by prisma/seed.mjs. Stress-test outputs below are closed-form and @@ -14,79 +9,87 @@ const HIREUP_OA_SLUG = "hireup-oa"; -function onesGrid(rows, cols, k) { - const row = `${new Array(cols).fill(1).join(" ")}\n`; - return `${rows} ${cols} ${k}\n${row.repeat(rows)}`; +function repeatValues(count, value) { + return `${new Array(count).fill(value).join(" ")}\n`; } -// Closed form for an all-ones R x C grid: a monotone path visits R + C - 1 cells; -// the start and end must be paid, so at most (R + C - 3) interior cells can be -// skipped. Cost = (cells) - min(k, interior). -function onesGridAnswer(rows, cols, k) { - const cells = rows + cols - 1; - const interior = Math.max(0, cells - 2); - return cells - Math.min(k, interior); +function sequenceLine(count, mapper) { + return `${Array.from({ length: count }, (_, index) => mapper(index)).join(" ")}\n`; } -function chainGraph(nodes, edges, k, weight) { - const lines = [`${nodes} ${edges} ${k}`]; - for (let i = 1; i <= edges; i += 1) { - lines.push(`${i} ${i + 1} ${weight}`); +function weightedChain(nodes, weight) { + const lines = [String(nodes)]; + for (let node = 1; node < nodes; node += 1) { + lines.push(`${node - 1} ${node} ${weight}`); } return `${lines.join("\n")}\n`; } -// --- Problem A: Grid Delivery With Skips -------------------------------------- -// Amazon/Google-flavored grid DP. The classic "minimum path sum" becomes a 3D DP -// once you can waive the cost of up to K cells: dp[i][j][k] = best cost to reach -// (i, j) having used k waivers, transitioning from the top/left and optionally -// skipping the current (non-corner) cell. -const gridDeliverySkips = { - slug: "grid-delivery-skips", - title: "Grid Delivery With Skips", +// --- Problem A: Driver Rebalancing Across City Corridors --------------------- +const driverRebalancing = { + slug: "driver-rebalancing", + title: "Driver Rebalancing Across City Corridors", statement: - "A delivery robot must cross an R x C grid of positive tolls. It starts at the top-left cell (0, 0), ends at the bottom-right cell (R-1, C-1), and may move only one cell right or one cell down at a time. Normally it pays the toll printed on every cell it visits.\n\nThe robot carries K skip passes. Using a pass on a cell waives that cell's toll entirely. It may use at most K passes over the whole trip, at most one per cell, but it can never skip the start cell (0, 0) or the end cell (R-1, C-1) — those tolls are always paid.\n\nPrint the minimum total toll the robot must pay to get from the start to the end.\n\nInput format:\n- First line: R C K\n- Next R lines: C integers each, the grid tolls row by row\n\nPrint a single integer. The answer can exceed a 32-bit integer.", - constraints: "1 <= R, C <= 200\n0 <= K <= 30\n1 <= toll[i][j] <= 10^9", - tags: ["Dynamic Programming", "Matrix", "Grid", "Amazon", "Google", "HireUp"], - difficulty: "HARD", - timeLimitMs: 4000, + "A city has N zones arranged in a straight line and numbered 1 through N. Zone i currently has current[i] available drivers, while target[i] is the desired number of drivers in that zone.\n\nIn one move, you may move one driver from a zone to either adjacent zone. Moving a driver across one zone boundary costs 1. A driver may cross several boundaries through several moves.\n\nThe total number of current drivers equals the total target, so it is always possible to reach the target distribution. Find the minimum total movement cost.\n\nInput format:\n- First line: N\n- Second line: N integers current[1], current[2], ..., current[N]\n- Third line: N integers target[1], target[2], ..., target[N]\n\nPrint a single integer: the minimum total movement cost. The answer can exceed a 32-bit integer.", + constraints: + "1 <= N <= 2 * 10^5\n0 <= current[i], target[i] <= 10^9\nsum(current) = sum(target) <= 10^13", + tags: ["Array", "Prefix Sum", "Greedy", "Uber", "HireUp"], + difficulty: "MEDIUM", + timeLimitMs: 2000, samples: [ - { input: "2 2 1\n1 2\n3 4\n", expectedOutput: "5\n" }, - { input: "2 2 0\n1 2\n3 4\n", expectedOutput: "7\n" }, + { input: "4\n0 3 0 2\n1 1 2 1\n", expectedOutput: "3\n", points: 0 }, + { + input: "5\n10 0 0 0 0\n0 0 0 0 10\n", + expectedOutput: "40\n", + points: 0, + }, ], hidden: [ - { input: "1 1 0\n5\n", expectedOutput: "5\n" }, - { input: "3 3 0\n1 1 1\n1 1 1\n1 1 1\n", expectedOutput: "5\n" }, - { input: "3 3 2\n1 1 1\n1 1 1\n1 1 1\n", expectedOutput: "3\n" }, - { input: "2 3 1\n1 5 1\n1 5 9\n", expectedOutput: "11\n" }, + { input: "1\n7\n7\n", expectedOutput: "0\n", points: 10 }, + { input: "5\n1 2 3 4 5\n1 2 3 4 5\n", expectedOutput: "0\n", points: 10 }, + { + input: "6\n5 0 4 0 0 3\n0 3 0 4 2 3\n", + expectedOutput: "15\n", + points: 20, + }, + { + input: "3\n0 0 1000000000\n1000000000 0 0\n", + expectedOutput: "2000000000\n", + points: 15, + }, ], }; -// --- Problem B: Network Upgrade Routing -------------------------------------- -// Google/Uber-flavored shortest path. From node 1 to node n in a directed -// weighted graph, you may zero out the weight of at most K edges. Dijkstra over -// (node, upgrades-used) states finds the cheapest route; report -1 if node n is -// unreachable. -const networkUpgradeRouting = { - slug: "network-upgrade-routing", - title: "Network Upgrade Routing", +// --- Problem B: Best Dispatch Hub --------------------------------------------- +const bestDispatchHub = { + slug: "best-dispatch-hub", + title: "Best Dispatch Hub", statement: - "A backbone network has n routers numbered 1..n and m one-way links. Link i sends traffic from router u to router v with latency w. You must route a stream from router 1 to router n along a sequence of links, paying the sum of the latencies you use.\n\nYou hold K upgrade credits. Spending a credit on a link makes its latency 0 for this route. You may spend at most K credits in total, at most one per link.\n\nPrint the minimum total latency of a route from router 1 to router n, or -1 if no route exists.\n\nInput format:\n- First line: n m K\n- Next m lines: u v w describing a directed link u -> v with latency w\n\nPrint a single integer. The answer can exceed a 32-bit integer.", - constraints: "1 <= n <= 10^5\n0 <= m <= 2*10^5\n0 <= K <= 10\n1 <= u, v <= n\n1 <= w <= 10^9", - tags: ["Graph", "Shortest Path", "Dijkstra", "Uber", "Google", "HireUp"], + "Uber operates in a city whose road network forms a tree. There are N intersections numbered 0 through N-1 and exactly N-1 bidirectional roads. Every intersection is reachable from every other intersection.\n\nEach road connects intersections u and v and has a positive travel time w. If a driver dispatch hub is placed at intersection r, its dispatch cost is the sum of the shortest travel times from r to all N intersections, including a travel time of 0 from r to itself.\n\nFor every possible hub location r, compute its dispatch cost.\n\nInput format:\n- First line: N\n- Next N-1 lines: three integers u, v, and w describing a bidirectional road\n\nPrint N space-separated integers. The value at index r must be the dispatch cost when the hub is placed at intersection r. Use 64-bit arithmetic.", + constraints: "1 <= N <= 2 * 10^5\n0 <= u, v < N\n1 <= w <= 10^6", + tags: ["Tree", "Dynamic Programming", "Rerooting", "DFS", "Uber", "HireUp"], difficulty: "HARD", - timeLimitMs: 4000, + timeLimitMs: 3000, samples: [ - { input: "4 4 1\n1 2 5\n2 4 5\n1 3 1\n3 4 1\n", expectedOutput: "1\n" }, - { input: "4 4 0\n1 2 5\n2 4 5\n1 3 1\n3 4 1\n", expectedOutput: "2\n" }, + { + input: "4\n0 1 1\n0 2 1\n2 3 1\n", + expectedOutput: "4 6 4 6\n", + points: 0, + }, + { input: "2\n0 1 7\n", expectedOutput: "7 7\n", points: 0 }, ], hidden: [ - { input: "3 1 2\n1 2 4\n", expectedOutput: "-1\n" }, - { input: "1 0 0\n", expectedOutput: "0\n" }, - { input: "3 2 2\n1 2 7\n2 3 9\n", expectedOutput: "0\n" }, + { input: "1\n", expectedOutput: "0\n", points: 10 }, + { input: "3\n0 1 2\n1 2 3\n", expectedOutput: "7 5 8\n", points: 15 }, + { + input: "5\n0 1 1\n0 2 2\n0 3 3\n0 4 4\n", + expectedOutput: "10 13 16 19 22\n", + points: 15, + }, { - input: "5 5 1\n1 2 10\n2 5 10\n1 3 3\n3 4 3\n4 5 3\n", - expectedOutput: "6\n", + input: "4\n0 1 1000000\n1 2 1000000\n2 3 1000000\n", + expectedOutput: "6000000 4000000 4000000 6000000\n", + points: 10, }, ], }; @@ -94,45 +97,58 @@ const networkUpgradeRouting = { // Efficiency / stress tests. Inputs are large; expected outputs are closed-form // and independently verified against the reference solutions. function buildHireupStressTests() { - // A: all-ones grids at the size limit exercise the O(R * C * K) DP. - const aFull = { - input: onesGrid(200, 200, 0), - expectedOutput: `${onesGridAnswer(200, 200, 0)}\n`, + const bigN = 200_000; + + // A: moving one large group from the first zone to the last makes every driver + // cross every boundary. + const aLongTransfer = { + input: `${bigN}\n100000000 ${repeatValues(bigN - 1, 0)}${repeatValues(bigN - 1, 0).trim()} 100000000\n`, + expectedOutput: `${(bigN - 1) * 100_000_000}\n`, + points: 20, }; - const aFullSkips = { - input: onesGrid(200, 200, 30), - expectedOutput: `${onesGridAnswer(200, 200, 30)}\n`, + + // A: each adjacent pair starts with its driver in the left zone and needs it + // in the right zone, so exactly half the boundaries carry one driver. + const aAlternating = { + input: `${bigN}\n${sequenceLine(bigN, (index) => (index % 2 === 0 ? 1 : 0))}${sequenceLine(bigN, (index) => (index % 2 === 0 ? 0 : 1))}`, + expectedOutput: `${bigN / 2}\n`, + points: 15, }; - const aMidSkips = { - input: onesGrid(150, 150, 30), - expectedOutput: `${onesGridAnswer(150, 150, 30)}\n`, + + const aAlreadyBalanced = { + input: `${bigN}\n${repeatValues(bigN, 1000000)}${repeatValues(bigN, 1000000)}`, + expectedOutput: "0\n", + points: 10, }; - // B: long chains 1 -> 2 -> ... force an efficient Dijkstra over the augmented - // state; a chain of e edges with weight w and k free edges costs (e - k) * w. - const bigNodes = 50_000; - const weight = 1_000_000_000; + // B: a long chain forces linear-time traversal and reroot propagation. For a + // node r, the sum is 1 + ... + r plus 1 + ... + (N-1-r). const bChain = { - input: chainGraph(bigNodes, bigNodes - 1, 0, weight), - expectedOutput: `${(bigNodes - 1) * weight}\n`, + input: weightedChain(bigN, 1), + expectedOutput: sequenceLine(bigN, (root) => { + const left = (root * (root + 1)) / 2; + const rightNodes = bigN - 1 - root; + const right = (rightNodes * (rightNodes + 1)) / 2; + return left + right; + }), + points: 30, }; - const bChainFree = { - input: chainGraph(bigNodes, bigNodes - 1, 10, weight), - expectedOutput: `${(bigNodes - 1 - 10) * weight}\n`, - }; - // Chain that stops one short of node n -> node n is unreachable -> -1. - const bUnreachable = { - input: chainGraph(bigNodes, bigNodes - 2, 10, weight), - expectedOutput: "-1\n", + + // B: in a unit-weight star the center costs N-1 and every leaf costs 2N-3. + const starEdges = Array.from({ length: bigN - 1 }, (_, index) => `0 ${index + 1} 1`).join("\n"); + const bStar = { + input: `${bigN}\n${starEdges}\n`, + expectedOutput: `${bigN - 1} ${repeatValues(bigN - 1, 2 * bigN - 3)}`, + points: 20, }; return { - "grid-delivery-skips": [aFull, aFullSkips, aMidSkips], - "network-upgrade-routing": [bChain, bChainFree, bUnreachable], + "driver-rebalancing": [aLongTransfer, aAlternating, aAlreadyBalanced], + "best-dispatch-hub": [bChain, bStar], }; } -const hireupProblems = [gridDeliverySkips, networkUpgradeRouting]; +const hireupProblems = [driverRebalancing, bestDispatchHub]; module.exports = { HIREUP_OA_SLUG, diff --git a/prisma/migrations/20260801090000_add_contest_partial_credit/migration.sql b/prisma/migrations/20260801090000_add_contest_partial_credit/migration.sql new file mode 100644 index 0000000..e6ab56a --- /dev/null +++ b/prisma/migrations/20260801090000_add_contest_partial_credit/migration.sql @@ -0,0 +1,20 @@ +-- Weighted test cases and partial-credit contest standings. +ALTER TABLE "TestCase" ADD COLUMN "points" INTEGER NOT NULL DEFAULT 1; + +ALTER TABLE "Submission" +ADD COLUMN "earnedPoints" INTEGER NOT NULL DEFAULT 0, +ADD COLUMN "possiblePoints" INTEGER NOT NULL DEFAULT 0; + +UPDATE "Submission" +SET "earnedPoints" = "passedCount", "possiblePoints" = "totalCount"; + +ALTER TABLE "ContestSubmission" +ADD COLUMN "earnedPoints" INTEGER NOT NULL DEFAULT 0, +ADD COLUMN "possiblePoints" INTEGER NOT NULL DEFAULT 0; + +UPDATE "ContestSubmission" +SET "earnedPoints" = "passedCount", "possiblePoints" = "totalCount"; + +ALTER TABLE "ContestParticipant" ADD COLUMN "score" INTEGER NOT NULL DEFAULT 0; + +UPDATE "ContestParticipant" SET "score" = "solvedCount" * 100; \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c920b32..50714f0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -325,6 +325,7 @@ model TestCase { input String expectedOutput String isSample Boolean @default(false) + points Int @default(1) order Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -344,6 +345,8 @@ model Submission { runtimeMs Int? passedCount Int @default(0) totalCount Int @default(0) + earnedPoints Int @default(0) + possiblePoints Int @default(0) failureMessage String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -547,6 +550,8 @@ model ContestSubmission { runtimeMs Int? passedCount Int @default(0) totalCount Int @default(0) + earnedPoints Int @default(0) + possiblePoints Int @default(0) failureMessage String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -565,6 +570,7 @@ model ContestParticipant { userId String rank Int solvedCount Int + score Int @default(0) penalty Int ratingBefore Int ratingAfter Int diff --git a/prisma/seed.mjs b/prisma/seed.mjs index b5feb02..42d88fa 100644 --- a/prisma/seed.mjs +++ b/prisma/seed.mjs @@ -1051,12 +1051,14 @@ async function main() { input: testCase.input, expectedOutput: testCase.expectedOutput, isSample: true, + points: testCase.points ?? 1, order: index, })), ...[...problem.hidden, ...(hireupStress[problem.slug] ?? [])].map((testCase, index) => ({ input: testCase.input, expectedOutput: testCase.expectedOutput, isSample: false, + points: testCase.points ?? 1, order: problem.samples.length + index, })), ]; @@ -1105,7 +1107,7 @@ async function main() { hireupProblemIds.push(savedProblem.id); } - // 1 August 8:00 PM IST (14:30 UTC), one-hour window, 60-minute personal timer. + // 1 August 8:00 PM IST (14:30 UTC), 90-minute window and personal timer. // Seeded PUBLISHED directly (never through the admin publish action) so it is // surfaced only inside the HireUp hub and is not mirrored onto the Events tab. // `status` is owned by the app after seeding, so it is only set on create. @@ -1114,19 +1116,19 @@ async function main() { update: { title: "HireUp Online Assessment", description: - "Online assessment for the HireUp mock hiring. Solve the problems within your 60-minute timer.", + "Online assessment for the HireUp mock hiring. Solve the problems within your 90-minute timer.", startsAt: new Date("2026-08-01T14:30:00.000Z"), - endsAt: new Date("2026-08-01T15:30:00.000Z"), - durationMinutes: 60, + endsAt: new Date("2026-08-01T16:00:00.000Z"), + durationMinutes: 90, }, create: { slug: HIREUP_OA_SLUG, title: "HireUp Online Assessment", description: - "Online assessment for the HireUp mock hiring. Solve the problems within your 60-minute timer.", + "Online assessment for the HireUp mock hiring. Solve the problems within your 90-minute timer.", startsAt: new Date("2026-08-01T14:30:00.000Z"), - endsAt: new Date("2026-08-01T15:30:00.000Z"), - durationMinutes: 60, + endsAt: new Date("2026-08-01T16:00:00.000Z"), + durationMinutes: 90, status: "PUBLISHED", }, select: { id: true }, diff --git a/scripts/reference-solutions/best-dispatch-hub.cpp b/scripts/reference-solutions/best-dispatch-hub.cpp new file mode 100644 index 0000000..2d9ca03 --- /dev/null +++ b/scripts/reference-solutions/best-dispatch-hub.cpp @@ -0,0 +1,59 @@ +#include +#include +#include +using namespace std; + +int main() { + ios::sync_with_stdio(false); + cin.tie(nullptr); + + int intersections; + cin >> intersections; + vector>> graph(intersections); + for (int edge = 0; edge + 1 < intersections; ++edge) { + int first, second; + long long weight; + cin >> first >> second >> weight; + graph[first].push_back({second, weight}); + graph[second].push_back({first, weight}); + } + + vector parent(intersections, -1); + vector subtreeSize(intersections, 1); + vector parentWeight(intersections, 0); + vector distance(intersections, 0); + vector order = {0}; + for (size_t index = 0; index < order.size(); ++index) { + int node = order[index]; + for (auto [neighbor, weight] : graph[node]) { + if (neighbor == parent[node]) continue; + parent[neighbor] = node; + parentWeight[neighbor] = weight; + distance[neighbor] = distance[node] + weight; + order.push_back(neighbor); + } + } + + long long rootCost = 0; + for (long long value : distance) rootCost += value; + for (int index = intersections - 1; index > 0; --index) { + int node = order[index]; + subtreeSize[parent[node]] += subtreeSize[node]; + } + + vector cost(intersections, 0); + cost[0] = rootCost; + for (size_t index = 1; index < order.size(); ++index) { + int node = order[index]; + long long outside = intersections - subtreeSize[node]; + cost[node] = + cost[parent[node]] + (outside - subtreeSize[node]) * parentWeight[node]; + } + + for (int node = 0; node < intersections; ++node) { + if (node > 0) cout << ' '; + cout << cost[node]; + } + cout << '\n'; + return 0; +} \ No newline at end of file diff --git a/scripts/reference-solutions/best-dispatch-hub.py b/scripts/reference-solutions/best-dispatch-hub.py new file mode 100644 index 0000000..f21e78e --- /dev/null +++ b/scripts/reference-solutions/best-dispatch-hub.py @@ -0,0 +1,39 @@ +import sys + + +def main(): + data = list(map(int, sys.stdin.buffer.read().split())) + intersections = data[0] + graph = [[] for _ in range(intersections)] + for index in range(1, len(data), 3): + first, second, weight = data[index : index + 3] + graph[first].append((second, weight)) + graph[second].append((first, weight)) + + parent = [-1] * intersections + parent_weight = [0] * intersections + distance = [0] * intersections + order = [0] + for node in order: + for neighbor, weight in graph[node]: + if neighbor == parent[node]: + continue + parent[neighbor] = node + parent_weight[neighbor] = weight + distance[neighbor] = distance[node] + weight + order.append(neighbor) + + subtree_size = [1] * intersections + for node in reversed(order[1:]): + subtree_size[parent[node]] += subtree_size[node] + + cost = [0] * intersections + cost[0] = sum(distance) + for node in order[1:]: + outside = intersections - subtree_size[node] + cost[node] = cost[parent[node]] + (outside - subtree_size[node]) * parent_weight[node] + + print(*cost) + + +main() \ No newline at end of file diff --git a/scripts/reference-solutions/driver-rebalancing.cpp b/scripts/reference-solutions/driver-rebalancing.cpp new file mode 100644 index 0000000..6667dbd --- /dev/null +++ b/scripts/reference-solutions/driver-rebalancing.cpp @@ -0,0 +1,26 @@ +#include +#include +#include +using namespace std; + +int main() { + ios::sync_with_stdio(false); + cin.tie(nullptr); + + int zones; + cin >> zones; + vector current(zones); + vector target(zones); + for (long long& drivers : current) cin >> drivers; + for (long long& drivers : target) cin >> drivers; + + long long imbalance = 0; + long long cost = 0; + for (int index = 0; index + 1 < zones; ++index) { + imbalance += current[index] - target[index]; + cost += llabs(imbalance); + } + + cout << cost << '\n'; + return 0; +} \ No newline at end of file diff --git a/scripts/reference-solutions/driver-rebalancing.py b/scripts/reference-solutions/driver-rebalancing.py new file mode 100644 index 0000000..1675707 --- /dev/null +++ b/scripts/reference-solutions/driver-rebalancing.py @@ -0,0 +1,19 @@ +import sys + + +def main(): + data = list(map(int, sys.stdin.buffer.read().split())) + zones = data[0] + current = data[1 : zones + 1] + target = data[zones + 1 : 2 * zones + 1] + + imbalance = 0 + cost = 0 + for index in range(zones - 1): + imbalance += current[index] - target[index] + cost += abs(imbalance) + + print(cost) + + +main() \ No newline at end of file diff --git a/scripts/simulate-contest-ratings.ts b/scripts/simulate-contest-ratings.ts index 342ce4d..36571ca 100644 --- a/scripts/simulate-contest-ratings.ts +++ b/scripts/simulate-contest-ratings.ts @@ -36,7 +36,17 @@ function sub( verdict: SubmissionVerdict, minutes: number, ): ContestSubmissionRow { - return { userId, contestProblemId, verdict, createdAt: at(minutes) }; + const accepted = verdict === SubmissionVerdict.ACCEPTED; + return { + userId, + contestProblemId, + verdict, + passedCount: accepted ? 1 : 0, + totalCount: 1, + earnedPoints: accepted ? 1 : 0, + possiblePoints: 1, + createdAt: at(minutes), + }; } /** Scripted contest: Alice solves both (with a WA on B), Bob solves A fast, Cara solves B after WA, Dan DNF. */ diff --git a/tests/unit/lib/contest.test.ts b/tests/unit/lib/contest.test.ts index e9b0e20..88f7f33 100644 --- a/tests/unit/lib/contest.test.ts +++ b/tests/unit/lib/contest.test.ts @@ -17,8 +17,19 @@ function submission( contestProblemId: string, verdict: SubmissionVerdict, createdAt: Date, + passedCount = verdict === SubmissionVerdict.ACCEPTED ? 1 : 0, + totalCount = 1, ) { - return { userId, contestProblemId, verdict, createdAt }; + return { + userId, + contestProblemId, + verdict, + passedCount, + totalCount, + earnedPoints: passedCount, + possiblePoints: totalCount, + createdAt, + }; } describe("computeStandings", () => { @@ -59,6 +70,43 @@ describe("computeStandings", () => { expect(standings).toEqual([]); }); + it("uses only the best partial score for each problem", () => { + const standings = computeStandings( + [ + submission( + "alice", + "p1", + SubmissionVerdict.WRONG_ANSWER, + new Date("2026-07-01T10:05:00.000Z"), + 3, + 10, + ), + submission( + "alice", + "p1", + SubmissionVerdict.WRONG_ANSWER, + new Date("2026-07-01T10:20:00.000Z"), + 7, + 10, + ), + submission( + "bob", + "p1", + SubmissionVerdict.WRONG_ANSWER, + new Date("2026-07-01T10:10:00.000Z"), + 6, + 10, + ), + ], + startsAt, + ); + + expect(standings).toEqual([ + expect.objectContaining({ userId: "alice", score: 70, solvedCount: 0, rank: 1 }), + expect.objectContaining({ userId: "bob", score: 60, solvedCount: 0, rank: 2 }), + ]); + }); + it("uses participant start times for demo contest penalties", () => { const standings = computeStandings( [ diff --git a/tests/unit/lib/judge.test.ts b/tests/unit/lib/judge.test.ts index 5bc16a4..149c891 100644 --- a/tests/unit/lib/judge.test.ts +++ b/tests/unit/lib/judge.test.ts @@ -44,13 +44,17 @@ describe("judgeSubmission", () => { verdict: SubmissionVerdict.ACCEPTED, passedCount: 2, totalCount: 2, + earnedPoints: 2, + possiblePoints: 2, runtimeMs: 6, + failureMessage: null, }); }); - it("returns wrong answer on the first mismatched output", async () => { + it("continues after a wrong answer and awards later weighted tests", async () => { + let execution = 0; const executor: CodeExecutor = async () => ({ - stdout: "0\n", + stdout: execution++ === 0 ? "0\n" : "6\n", stderr: "", exitCode: 0, signal: null, @@ -61,11 +65,20 @@ describe("judgeSubmission", () => { code: "", executor, language: "python", - testCases, + testCases: [ + { ...testCases[0], points: 20 }, + { ...testCases[1], points: 80 }, + ], timeLimitMs: 2000, }); - expect(result).toMatchObject({ verdict: SubmissionVerdict.WRONG_ANSWER, passedCount: 0 }); + expect(result).toMatchObject({ + verdict: SubmissionVerdict.WRONG_ANSWER, + passedCount: 1, + totalCount: 2, + earnedPoints: 80, + possiblePoints: 100, + }); }); it("maps compile errors before output comparison", async () => { From c89ce013eb8a0c90808737818b3ab9941b664495 Mon Sep 17 00:00:00 2001 From: Navneet Date: Sat, 1 Aug 2026 15:38:47 +0530 Subject: [PATCH 2/3] refactor(contests): make partial credit generic for all contests Every contest now scores with one rule instead of a HireUp-only rubric: each problem is worth 100 points and a submission earns round(100 * passedCount / totalCount) from its best attempt per problem. - Drop per-test 'points' weighting and the derived earnedPoints / possiblePoints columns; score is computed from pass counts alone. - Rename StandingRow.lastAcAt to lastScoredAt, since the tiebreaker now tracks the best scoring attempt rather than an accepted one. - Add the migration that drops the now-unused weighting columns so the schema and migration history stay in sync. --- app/(protected)/admin/contests/actions.ts | 2 - app/(protected)/admin/problems/actions.ts | 4 +- app/contests/[slug]/actions.ts | 26 ++-------- app/contests/[slug]/page.tsx | 2 - app/contests/[slug]/problems/[label]/page.tsx | 2 - app/problems/[slug]/actions.ts | 13 +---- app/problems/[slug]/page.tsx | 2 - components/practice/problem-workspace.tsx | 17 +++---- lib/contest.ts | 24 +++++----- lib/judge/core.ts | 10 ---- lib/judge/types.ts | 3 -- prisma/hireup-problem-set.js | 48 +++++-------------- .../migration.sql | 10 ++++ prisma/schema.prisma | 5 -- prisma/seed.mjs | 2 - scripts/simulate-contest-ratings.ts | 2 - tests/unit/lib/contest.test.ts | 2 - tests/unit/lib/judge.test.ts | 11 +---- 18 files changed, 46 insertions(+), 139 deletions(-) create mode 100644 prisma/migrations/20260801120000_simplify_contest_partial_credit/migration.sql diff --git a/app/(protected)/admin/contests/actions.ts b/app/(protected)/admin/contests/actions.ts index e168448..52be354 100644 --- a/app/(protected)/admin/contests/actions.ts +++ b/app/(protected)/admin/contests/actions.ts @@ -221,8 +221,6 @@ export async function finalizeContest(formData: FormData) { verdict: true, passedCount: true, totalCount: true, - earnedPoints: true, - possiblePoints: true, createdAt: true, }, }, diff --git a/app/(protected)/admin/problems/actions.ts b/app/(protected)/admin/problems/actions.ts index b9572a6..f15111c 100644 --- a/app/(protected)/admin/problems/actions.ts +++ b/app/(protected)/admin/problems/actions.ts @@ -10,8 +10,6 @@ type RunResult = verdict: string; passedCount: number; totalCount: number; - earnedPoints: number; - possiblePoints: number; runtimeMs: number | null; failureMessage?: string | null; } @@ -44,7 +42,7 @@ export async function runReferenceSolution(slug: string, language?: string): Pro timeLimitMs: true, testCases: { orderBy: { order: "asc" }, - select: { input: true, expectedOutput: true, points: true }, + select: { input: true, expectedOutput: true }, }, }, }); diff --git a/app/contests/[slug]/actions.ts b/app/contests/[slug]/actions.ts index 655a333..a33b744 100644 --- a/app/contests/[slug]/actions.ts +++ b/app/contests/[slug]/actions.ts @@ -13,8 +13,6 @@ type ContestPreviewResult = { verdict: string; passedCount: number; totalCount: number; - earnedPoints: number; - possiblePoints: number; runtimeMs: number | null; failureMessage?: string | null; }; @@ -158,7 +156,7 @@ export async function submitContestSolution(formData: FormData) { timeLimitMs: true, testCases: { orderBy: { order: "asc" }, - select: { input: true, expectedOutput: true, isSample: true, points: true }, + select: { input: true, expectedOutput: true, isSample: true }, }, }, }, @@ -186,10 +184,6 @@ export async function submitContestSolution(formData: FormData) { language: parsed.data.language, code: parsed.data.code, totalCount: contestProblem.problem.testCases.length, - possiblePoints: contestProblem.problem.testCases.reduce( - (total, testCase) => total + testCase.points, - 0, - ), }, select: { id: true }, }); @@ -213,11 +207,6 @@ export async function submitContestSolution(formData: FormData) { verdict: SubmissionVerdict.RUNTIME_ERROR, passedCount: 0, totalCount: contestProblem.problem.testCases.length, - earnedPoints: 0, - possiblePoints: contestProblem.problem.testCases.reduce( - (total, testCase) => total + testCase.points, - 0, - ), failureMessage: failureMessageFromError(error), }, }); @@ -286,7 +275,7 @@ export async function runContestSolution(formData: FormData): Promise total + testCase.points, - 0, - ), runtimeMs: null, failureMessage: failureMessageFromError(error), }; @@ -325,8 +309,6 @@ function previewError(message: string): ContestPreviewResult { verdict: SubmissionVerdict.RUNTIME_ERROR, passedCount: 0, totalCount: 0, - earnedPoints: 0, - possiblePoints: 0, runtimeMs: null, failureMessage: message, }; @@ -358,7 +340,7 @@ export async function runContestPreview(formData: FormData): Promise total + testCase.points, 0), runtimeMs: null, failureMessage: failureMessageFromError(error), }; diff --git a/app/contests/[slug]/page.tsx b/app/contests/[slug]/page.tsx index fc8004c..17a1eb6 100644 --- a/app/contests/[slug]/page.tsx +++ b/app/contests/[slug]/page.tsx @@ -38,8 +38,6 @@ export default async function ContestDetailPage({ verdict: true, passedCount: true, totalCount: true, - earnedPoints: true, - possiblePoints: true, createdAt: true, }, }, diff --git a/app/contests/[slug]/problems/[label]/page.tsx b/app/contests/[slug]/problems/[label]/page.tsx index f9ae76f..54f2457 100644 --- a/app/contests/[slug]/problems/[label]/page.tsx +++ b/app/contests/[slug]/problems/[label]/page.tsx @@ -115,8 +115,6 @@ export default async function ContestProblemPage({ verdict: true, passedCount: true, totalCount: true, - earnedPoints: true, - possiblePoints: true, runtimeMs: true, failureMessage: true, }, diff --git a/app/problems/[slug]/actions.ts b/app/problems/[slug]/actions.ts index 3413168..0c37da6 100644 --- a/app/problems/[slug]/actions.ts +++ b/app/problems/[slug]/actions.ts @@ -19,8 +19,6 @@ type RunResult = { verdict: string; passedCount: number; totalCount: number; - earnedPoints: number; - possiblePoints: number; runtimeMs: number | null; failureMessage?: string | null; }; @@ -44,8 +42,6 @@ function runError(message: string): RunResult { verdict: SubmissionVerdict.RUNTIME_ERROR, passedCount: 0, totalCount: 0, - earnedPoints: 0, - possiblePoints: 0, runtimeMs: null, failureMessage: message, }; @@ -76,7 +72,7 @@ export async function submitSolution(formData: FormData) { timeLimitMs: true, testCases: { orderBy: { order: "asc" }, - select: { input: true, expectedOutput: true, isSample: true, points: true }, + select: { input: true, expectedOutput: true, isSample: true }, }, }, }); @@ -101,7 +97,6 @@ export async function submitSolution(formData: FormData) { language: parsed.data.language, code: parsed.data.code, totalCount: problem.testCases.length, - possiblePoints: problem.testCases.reduce((total, testCase) => total + testCase.points, 0), }, select: { id: true }, }); @@ -159,8 +154,6 @@ export async function submitSolution(formData: FormData) { verdict: SubmissionVerdict.RUNTIME_ERROR, passedCount: 0, totalCount: problem.testCases.length, - earnedPoints: 0, - possiblePoints: problem.testCases.reduce((total, testCase) => total + testCase.points, 0), failureMessage: failureMessageFromError(error), }, }); @@ -195,7 +188,7 @@ export async function runSolution(formData: FormData): Promise { testCases: { where: { isSample: true }, orderBy: { order: "asc" }, - select: { input: true, expectedOutput: true, isSample: true, points: true }, + select: { input: true, expectedOutput: true, isSample: true }, }, }, }); @@ -216,8 +209,6 @@ export async function runSolution(formData: FormData): Promise { verdict: SubmissionVerdict.RUNTIME_ERROR, passedCount: 0, totalCount: problem.testCases.length, - earnedPoints: 0, - possiblePoints: problem.testCases.reduce((total, testCase) => total + testCase.points, 0), runtimeMs: null, failureMessage: failureMessageFromError(error), }; diff --git a/app/problems/[slug]/page.tsx b/app/problems/[slug]/page.tsx index 34f8804..08c6efe 100644 --- a/app/problems/[slug]/page.tsx +++ b/app/problems/[slug]/page.tsx @@ -55,8 +55,6 @@ export default async function ProblemDetailPage({ verdict: true, passedCount: true, totalCount: true, - earnedPoints: true, - possiblePoints: true, runtimeMs: true, failureMessage: true, }, diff --git a/components/practice/problem-workspace.tsx b/components/practice/problem-workspace.tsx index 1db36c9..bb6099a 100644 --- a/components/practice/problem-workspace.tsx +++ b/components/practice/problem-workspace.tsx @@ -18,8 +18,6 @@ type Submission = { verdict: string; passedCount: number; totalCount: number; - earnedPoints: number; - possiblePoints: number; runtimeMs: number | null; failureMessage: string | null; note?: string; @@ -35,8 +33,6 @@ type EphemeralRunResult = { verdict: string; passedCount: number; totalCount: number; - earnedPoints: number; - possiblePoints: number; runtimeMs: number | null; failureMessage?: string | null; }; @@ -67,6 +63,11 @@ const starterCode: Record = { }; function SubmissionMeta({ submission }: Readonly<{ submission: Submission }>) { + const score = + submission.totalCount > 0 + ? Math.round((submission.passedCount * 100) / submission.totalCount) + : 0; + return ( <> @@ -76,11 +77,7 @@ function SubmissionMeta({ submission }: Readonly<{ submission: Submission }>) { {submission.passedCount}/{submission.totalCount} tests - {submission.possiblePoints > 0 ? ( - - {submission.earnedPoints}/{submission.possiblePoints} points - - ) : null} + {submission.totalCount > 0 ? {score}/100 points : null} {submission.runtimeMs ?? 0}ms {submission.note ? {submission.note} : null} @@ -274,8 +271,6 @@ export function ProblemWorkspace({ verdict: result.verdict, passedCount: result.passedCount, totalCount: result.totalCount, - earnedPoints: result.earnedPoints, - possiblePoints: result.possiblePoints, runtimeMs: result.runtimeMs, failureMessage: result.failureMessage ?? null, note, diff --git a/lib/contest.ts b/lib/contest.ts index d99a4e2..1f8f8da 100644 --- a/lib/contest.ts +++ b/lib/contest.ts @@ -61,8 +61,6 @@ export type ContestSubmissionRow = { verdict: SubmissionVerdict; passedCount: number; totalCount: number; - earnedPoints: number; - possiblePoints: number; createdAt: Date; }; @@ -71,7 +69,7 @@ export type StandingRow = { score: number; solvedCount: number; penalty: number; - lastAcAt: Date | null; + lastScoredAt: Date | null; rank: number; }; @@ -198,9 +196,9 @@ function minutesFromStart(startsAt: Date, at: Date) { } function submissionScore(submission: ContestSubmissionRow) { - const possible = submission.possiblePoints || submission.totalCount; - const earned = submission.possiblePoints ? submission.earnedPoints : submission.passedCount; - return possible > 0 ? Math.round((earned * 100) / possible) : 0; + return submission.totalCount > 0 + ? Math.round((submission.passedCount * 100) / submission.totalCount) + : 0; } export function computeStandings( @@ -219,7 +217,7 @@ export function computeStandings( const stats = new Map< string, - { score: number; solvedCount: number; penalty: number; lastAcAt: Date | null } + { score: number; solvedCount: number; penalty: number; lastScoredAt: Date | null } >(); for (const [key, attempts] of Array.from(byUserProblem.entries())) { @@ -254,17 +252,17 @@ export function computeStandings( score: 0, solvedCount: 0, penalty: 0, - lastAcAt: null, + lastScoredAt: null, }; current.score += bestScore; if (bestAttempt.verdict === SubmissionVerdict.ACCEPTED) { current.solvedCount += 1; } current.penalty += problemPenalty; - current.lastAcAt = - !current.lastAcAt || bestAttempt.createdAt > current.lastAcAt + current.lastScoredAt = + !current.lastScoredAt || bestAttempt.createdAt > current.lastScoredAt ? bestAttempt.createdAt - : current.lastAcAt; + : current.lastScoredAt; stats.set(userId, current); } @@ -275,7 +273,7 @@ export function computeStandings( right.score - left.score || right.solvedCount - left.solvedCount || left.penalty - right.penalty || - (left.lastAcAt?.getTime() ?? 0) - (right.lastAcAt?.getTime() ?? 0), + (left.lastScoredAt?.getTime() ?? 0) - (right.lastScoredAt?.getTime() ?? 0), ); return ranked.map((row, index) => ({ @@ -283,7 +281,7 @@ export function computeStandings( score: row.score, solvedCount: row.solvedCount, penalty: row.penalty, - lastAcAt: row.lastAcAt, + lastScoredAt: row.lastScoredAt, rank: index + 1, })); } diff --git a/lib/judge/core.ts b/lib/judge/core.ts index ec827c8..0d5277d 100644 --- a/lib/judge/core.ts +++ b/lib/judge/core.ts @@ -55,14 +55,9 @@ export async function judgeSubmission({ timeLimitMs: number; }): Promise { let passedCount = 0; - let earnedPoints = 0; let runtimeMs = 0; let verdict: SubmissionVerdict = SubmissionVerdict.ACCEPTED; let failureMessage: string | null = null; - const possiblePoints = testCases.reduce( - (total, testCase) => total + Math.max(0, testCase.points ?? 1), - 0, - ); for (const testCase of testCases) { const result = await executor({ language, code, stdin: testCase.input, timeLimitMs }); @@ -76,8 +71,6 @@ export async function judgeSubmission({ verdict: SubmissionVerdict.COMPILE_ERROR, passedCount, totalCount: testCases.length, - earnedPoints: 0, - possiblePoints, runtimeMs, failureMessage: truncateFailureMessage(result.compileError), }; @@ -104,15 +97,12 @@ export async function judgeSubmission({ } passedCount += 1; - earnedPoints += Math.max(0, testCase.points ?? 1); } return { verdict, passedCount, totalCount: testCases.length, - earnedPoints, - possiblePoints, runtimeMs, failureMessage, }; diff --git a/lib/judge/types.ts b/lib/judge/types.ts index 8f458a4..693b7af 100644 --- a/lib/judge/types.ts +++ b/lib/judge/types.ts @@ -5,7 +5,6 @@ export type JudgeTestCase = { input: string; expectedOutput: string; isSample?: boolean; - points?: number; }; export type ExecutionResult = { @@ -29,8 +28,6 @@ export type JudgeResult = { verdict: SubmissionVerdict; passedCount: number; totalCount: number; - earnedPoints: number; - possiblePoints: number; runtimeMs: number | null; failureMessage?: string | null; }; diff --git a/prisma/hireup-problem-set.js b/prisma/hireup-problem-set.js index 816ce09..508a050 100644 --- a/prisma/hireup-problem-set.js +++ b/prisma/hireup-problem-set.js @@ -37,26 +37,14 @@ const driverRebalancing = { difficulty: "MEDIUM", timeLimitMs: 2000, samples: [ - { input: "4\n0 3 0 2\n1 1 2 1\n", expectedOutput: "3\n", points: 0 }, - { - input: "5\n10 0 0 0 0\n0 0 0 0 10\n", - expectedOutput: "40\n", - points: 0, - }, + { input: "4\n0 3 0 2\n1 1 2 1\n", expectedOutput: "3\n" }, + { input: "5\n10 0 0 0 0\n0 0 0 0 10\n", expectedOutput: "40\n" }, ], hidden: [ - { input: "1\n7\n7\n", expectedOutput: "0\n", points: 10 }, - { input: "5\n1 2 3 4 5\n1 2 3 4 5\n", expectedOutput: "0\n", points: 10 }, - { - input: "6\n5 0 4 0 0 3\n0 3 0 4 2 3\n", - expectedOutput: "15\n", - points: 20, - }, - { - input: "3\n0 0 1000000000\n1000000000 0 0\n", - expectedOutput: "2000000000\n", - points: 15, - }, + { input: "1\n7\n7\n", expectedOutput: "0\n" }, + { input: "5\n1 2 3 4 5\n1 2 3 4 5\n", expectedOutput: "0\n" }, + { input: "6\n5 0 4 0 0 3\n0 3 0 4 2 3\n", expectedOutput: "15\n" }, + { input: "3\n0 0 1000000000\n1000000000 0 0\n", expectedOutput: "2000000000\n" }, ], }; @@ -71,25 +59,16 @@ const bestDispatchHub = { difficulty: "HARD", timeLimitMs: 3000, samples: [ - { - input: "4\n0 1 1\n0 2 1\n2 3 1\n", - expectedOutput: "4 6 4 6\n", - points: 0, - }, - { input: "2\n0 1 7\n", expectedOutput: "7 7\n", points: 0 }, + { input: "4\n0 1 1\n0 2 1\n2 3 1\n", expectedOutput: "4 6 4 6\n" }, + { input: "2\n0 1 7\n", expectedOutput: "7 7\n" }, ], hidden: [ - { input: "1\n", expectedOutput: "0\n", points: 10 }, - { input: "3\n0 1 2\n1 2 3\n", expectedOutput: "7 5 8\n", points: 15 }, - { - input: "5\n0 1 1\n0 2 2\n0 3 3\n0 4 4\n", - expectedOutput: "10 13 16 19 22\n", - points: 15, - }, + { input: "1\n", expectedOutput: "0\n" }, + { input: "3\n0 1 2\n1 2 3\n", expectedOutput: "7 5 8\n" }, + { input: "5\n0 1 1\n0 2 2\n0 3 3\n0 4 4\n", expectedOutput: "10 13 16 19 22\n" }, { input: "4\n0 1 1000000\n1 2 1000000\n2 3 1000000\n", expectedOutput: "6000000 4000000 4000000 6000000\n", - points: 10, }, ], }; @@ -104,7 +83,6 @@ function buildHireupStressTests() { const aLongTransfer = { input: `${bigN}\n100000000 ${repeatValues(bigN - 1, 0)}${repeatValues(bigN - 1, 0).trim()} 100000000\n`, expectedOutput: `${(bigN - 1) * 100_000_000}\n`, - points: 20, }; // A: each adjacent pair starts with its driver in the left zone and needs it @@ -112,13 +90,11 @@ function buildHireupStressTests() { const aAlternating = { input: `${bigN}\n${sequenceLine(bigN, (index) => (index % 2 === 0 ? 1 : 0))}${sequenceLine(bigN, (index) => (index % 2 === 0 ? 0 : 1))}`, expectedOutput: `${bigN / 2}\n`, - points: 15, }; const aAlreadyBalanced = { input: `${bigN}\n${repeatValues(bigN, 1000000)}${repeatValues(bigN, 1000000)}`, expectedOutput: "0\n", - points: 10, }; // B: a long chain forces linear-time traversal and reroot propagation. For a @@ -131,7 +107,6 @@ function buildHireupStressTests() { const right = (rightNodes * (rightNodes + 1)) / 2; return left + right; }), - points: 30, }; // B: in a unit-weight star the center costs N-1 and every leaf costs 2N-3. @@ -139,7 +114,6 @@ function buildHireupStressTests() { const bStar = { input: `${bigN}\n${starEdges}\n`, expectedOutput: `${bigN - 1} ${repeatValues(bigN - 1, 2 * bigN - 3)}`, - points: 20, }; return { diff --git a/prisma/migrations/20260801120000_simplify_contest_partial_credit/migration.sql b/prisma/migrations/20260801120000_simplify_contest_partial_credit/migration.sql new file mode 100644 index 0000000..a9c40ef --- /dev/null +++ b/prisma/migrations/20260801120000_simplify_contest_partial_credit/migration.sql @@ -0,0 +1,10 @@ +-- All contests use equal credit per test case, so only pass/total counts are needed. +ALTER TABLE "TestCase" DROP COLUMN "points"; + +ALTER TABLE "Submission" +DROP COLUMN "earnedPoints", +DROP COLUMN "possiblePoints"; + +ALTER TABLE "ContestSubmission" +DROP COLUMN "earnedPoints", +DROP COLUMN "possiblePoints"; \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 50714f0..8b159a3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -325,7 +325,6 @@ model TestCase { input String expectedOutput String isSample Boolean @default(false) - points Int @default(1) order Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -345,8 +344,6 @@ model Submission { runtimeMs Int? passedCount Int @default(0) totalCount Int @default(0) - earnedPoints Int @default(0) - possiblePoints Int @default(0) failureMessage String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -550,8 +547,6 @@ model ContestSubmission { runtimeMs Int? passedCount Int @default(0) totalCount Int @default(0) - earnedPoints Int @default(0) - possiblePoints Int @default(0) failureMessage String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/prisma/seed.mjs b/prisma/seed.mjs index 42d88fa..a812afd 100644 --- a/prisma/seed.mjs +++ b/prisma/seed.mjs @@ -1051,14 +1051,12 @@ async function main() { input: testCase.input, expectedOutput: testCase.expectedOutput, isSample: true, - points: testCase.points ?? 1, order: index, })), ...[...problem.hidden, ...(hireupStress[problem.slug] ?? [])].map((testCase, index) => ({ input: testCase.input, expectedOutput: testCase.expectedOutput, isSample: false, - points: testCase.points ?? 1, order: problem.samples.length + index, })), ]; diff --git a/scripts/simulate-contest-ratings.ts b/scripts/simulate-contest-ratings.ts index 36571ca..76d01aa 100644 --- a/scripts/simulate-contest-ratings.ts +++ b/scripts/simulate-contest-ratings.ts @@ -43,8 +43,6 @@ function sub( verdict, passedCount: accepted ? 1 : 0, totalCount: 1, - earnedPoints: accepted ? 1 : 0, - possiblePoints: 1, createdAt: at(minutes), }; } diff --git a/tests/unit/lib/contest.test.ts b/tests/unit/lib/contest.test.ts index 88f7f33..de8a0cd 100644 --- a/tests/unit/lib/contest.test.ts +++ b/tests/unit/lib/contest.test.ts @@ -26,8 +26,6 @@ function submission( verdict, passedCount, totalCount, - earnedPoints: passedCount, - possiblePoints: totalCount, createdAt, }; } diff --git a/tests/unit/lib/judge.test.ts b/tests/unit/lib/judge.test.ts index 149c891..8e96a89 100644 --- a/tests/unit/lib/judge.test.ts +++ b/tests/unit/lib/judge.test.ts @@ -44,14 +44,12 @@ describe("judgeSubmission", () => { verdict: SubmissionVerdict.ACCEPTED, passedCount: 2, totalCount: 2, - earnedPoints: 2, - possiblePoints: 2, runtimeMs: 6, failureMessage: null, }); }); - it("continues after a wrong answer and awards later weighted tests", async () => { + it("continues after a wrong answer and counts later passing tests", async () => { let execution = 0; const executor: CodeExecutor = async () => ({ stdout: execution++ === 0 ? "0\n" : "6\n", @@ -65,10 +63,7 @@ describe("judgeSubmission", () => { code: "", executor, language: "python", - testCases: [ - { ...testCases[0], points: 20 }, - { ...testCases[1], points: 80 }, - ], + testCases, timeLimitMs: 2000, }); @@ -76,8 +71,6 @@ describe("judgeSubmission", () => { verdict: SubmissionVerdict.WRONG_ANSWER, passedCount: 1, totalCount: 2, - earnedPoints: 80, - possiblePoints: 100, }); }); From 0a564717b0a1b1930d6cc0727b6e18f6babfd64c Mon Sep 17 00:00:00 2001 From: Navneet Date: Sat, 1 Aug 2026 16:24:58 +0530 Subject: [PATCH 3/3] fix(hireup): keep Best Dispatch Hub output within the judge's limit The chain stress test at N=200,000 made the expected answer 2.4 MB, above the judge's 2 MB OUTPUT_LIMIT (lib/judge/piston.ts). Piston truncates at that limit, so even a correct submission would have been marked wrong, and the CI reference validator failed 2/8 tests on it. - Cap Best Dispatch Hub at N <= 5*10^4 in both the constraints and the stress tests, which drops the largest answer to ~0.5 MB. An O(N^2) solution still times out at that size. Driver Rebalancing keeps N = 200,000 since it prints a single number. - Give the validator an 8 MB spawnSync buffer. Node's 1 MB default silently truncated large answers and reported them as mismatches instead of surfacing the real problem. --- prisma/hireup-problem-set.js | 19 ++++++++++++------- scripts/validate-problems.mjs | 4 ++++ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/prisma/hireup-problem-set.js b/prisma/hireup-problem-set.js index 508a050..ba32bc7 100644 --- a/prisma/hireup-problem-set.js +++ b/prisma/hireup-problem-set.js @@ -54,7 +54,7 @@ const bestDispatchHub = { title: "Best Dispatch Hub", statement: "Uber operates in a city whose road network forms a tree. There are N intersections numbered 0 through N-1 and exactly N-1 bidirectional roads. Every intersection is reachable from every other intersection.\n\nEach road connects intersections u and v and has a positive travel time w. If a driver dispatch hub is placed at intersection r, its dispatch cost is the sum of the shortest travel times from r to all N intersections, including a travel time of 0 from r to itself.\n\nFor every possible hub location r, compute its dispatch cost.\n\nInput format:\n- First line: N\n- Next N-1 lines: three integers u, v, and w describing a bidirectional road\n\nPrint N space-separated integers. The value at index r must be the dispatch cost when the hub is placed at intersection r. Use 64-bit arithmetic.", - constraints: "1 <= N <= 2 * 10^5\n0 <= u, v < N\n1 <= w <= 10^6", + constraints: "1 <= N <= 5 * 10^4\n0 <= u, v < N\n1 <= w <= 10^6", tags: ["Tree", "Dynamic Programming", "Rerooting", "DFS", "Uber", "HireUp"], difficulty: "HARD", timeLimitMs: 3000, @@ -77,6 +77,11 @@ const bestDispatchHub = { // and independently verified against the reference solutions. function buildHireupStressTests() { const bigN = 200_000; + // Best Dispatch Hub prints one cost per intersection, so its answer grows with + // N. Cap it well under the judge's 2 MB output limit (lib/judge/piston.ts) — + // otherwise a correct solution would have its output truncated and be marked + // wrong. At 50k nodes an O(N^2) solution still times out comfortably. + const treeN = 50_000; // A: moving one large group from the first zone to the last makes every driver // cross every boundary. @@ -100,20 +105,20 @@ function buildHireupStressTests() { // B: a long chain forces linear-time traversal and reroot propagation. For a // node r, the sum is 1 + ... + r plus 1 + ... + (N-1-r). const bChain = { - input: weightedChain(bigN, 1), - expectedOutput: sequenceLine(bigN, (root) => { + input: weightedChain(treeN, 1), + expectedOutput: sequenceLine(treeN, (root) => { const left = (root * (root + 1)) / 2; - const rightNodes = bigN - 1 - root; + const rightNodes = treeN - 1 - root; const right = (rightNodes * (rightNodes + 1)) / 2; return left + right; }), }; // B: in a unit-weight star the center costs N-1 and every leaf costs 2N-3. - const starEdges = Array.from({ length: bigN - 1 }, (_, index) => `0 ${index + 1} 1`).join("\n"); + const starEdges = Array.from({ length: treeN - 1 }, (_, index) => `0 ${index + 1} 1`).join("\n"); const bStar = { - input: `${bigN}\n${starEdges}\n`, - expectedOutput: `${bigN - 1} ${repeatValues(bigN - 1, 2 * bigN - 3)}`, + input: `${treeN}\n${starEdges}\n`, + expectedOutput: `${treeN - 1} ${repeatValues(treeN - 1, 2 * treeN - 3)}`, }; return { diff --git a/scripts/validate-problems.mjs b/scripts/validate-problems.mjs index 39d5767..6b03c17 100644 --- a/scripts/validate-problems.mjs +++ b/scripts/validate-problems.mjs @@ -131,6 +131,10 @@ try { input: test.input, encoding: "utf8", timeout: Math.max(5000, problem.timeLimitMs + 3000), + // Node defaults to 1 MB, which silently truncates large answers and + // reports them as mismatches. Stay above the judge's own output limit + // so oversized answers surface as real diffs instead of buffer errors. + maxBuffer: 8 * 1024 * 1024, }); if (run.status !== 0) {