Appearance
OA Simulator & Exam Integrity: Marking Schemes & Scoring
Part 2 of 5 in the OA Simulator & Exam Integrity series.
OA Structure & Timer Logic covered how an attempt is shaped and clocked. This page covers what happens to a finished attempt's raw answers: how they turn into marks. All of it lives in one dependency-free module, @edikylab/oa-engine (ediky-shared/packages/oa-engine/src/index.js), shared verbatim between the website and desktop builds — the scoring math cannot drift between the two the way a duplicated implementation could.
The marking scheme is data, not code
DEFAULT_MARKING is the fallback every OA scores against unless its creator overrides it in the builder:
javascript
export const DEFAULT_MARKING = Object.freeze({
codingMarks: Object.freeze({ Easy: 15, Medium: 25, Hard: 35 }),
correctMarks: Object.freeze({ Easy: 1, Medium: 2, Hard: 3 }),
negativeMarks: 0.5,
});Both codingMarks and correctMarks are tiered by difficulty rather than a flat rate — a Hard DSA problem is worth more than an Easy one, matching how real OAs actually weight sections rather than paying every question the same. negativeMarks is a single flat deduction, independent of difficulty. normalizeMarking() accepts either the tiered { Easy, Medium, Hard } shape or a bare legacy number (applied uniformly across all three tiers) — attempts and templates saved before difficulty-tiering existed keep scoring correctly with no migration step, and a creator can override only Hard in the builder and let Easy/Medium fall back to defaults.
Four question kinds, two scoring rules
questionKind(section) classifies every question as judge (DSA/coding, run through Judge0 — see Judge0 & Code Execution), concept (CS Fundamentals / Aptitude / DSA-Technical MCQ, AI- or MCQ-graded), sql (AI-graded SQL), or cf (a Codeforces problem, verified against the candidate's own CF submission history rather than judged locally — see Results & CP Comparison). Only two scoring rules exist across all four kinds:
- Coding-shaped (
judge,sql,cf) — never penalized. A solved question earns the fullcodingMarksweight for its difficulty; an unsolved-but-attemptedjudge/sqlquestion earns partial credit proportional to test cases passed.cfhas no partial credit (no local test cases to award it against) — it is binary: an Accepted submission was found on Codeforces, or the question scores 0. - Concept-shaped (
concept) — fullcorrectMarksif correct, proportional credit if an AI grader scores a free-text answer partially right, and−negativeMarksonly if the candidate answered and was fully wrong. An untouched question of either shape scores a flat 0, never negative — only an attempted-and-wrong MCQ/theory answer is penalized.
javascript
// Simplified real shape of questionMarks (oa-engine/src/index.js)
function questionMarks(q) {
if (!q.attempted) return 0; // skipped: flat zero, never negative
const weight = (q.kind !== "concept" ? codingMarks : correctMarks)[tierOf(q)];
if (q.solved) return weight;
if (q.kind !== "concept") {
// Coding: proportional credit, no penalty for a wrong run.
return q.bestTotal > 0 ? weight * (q.bestPassed / q.bestTotal) : 0;
}
// Concept/MCQ: proportional AI credit if partially right, else negative marking.
if (q.aiScore > 0) return weight * (q.aiScore / 100);
return -negativeMarks;
}Aggregation and the percentage
computeAttemptResult({ questions, perProblem, submissions, startedAtMs, marking }) runs this per question, then sums:
rendering diagram…
drag to pan · ctrl / ⌘ + scroll to zoom
marksRaw is rounded to 2dp (marks can be fractional — a 12/15-case partial credit isn't a whole number) and reported alongside marksMax so the result page can show "62.5 / 160" as well as a percentage. scorePct is separately clamped to [0, 100] — a sheet net-negative from heavy wrong-MCQ penalties never renders a nonsensical negative bar on the history chart, while score itself stays the honest, possibly-negative raw number underneath.
python
# Reference form: partial credit for an unsolved coding question
def coding_credit(passed, total, weight):
if total == 0:
return 0
return weight * (passed / total) # e.g. 12/15 cases -> 0.8 * weightBreakdown for weak-area feedback
topicBreakdown() and difficultyBreakdown() reduce the same perQuestion array into { topic, total, solved, solveRate } and { difficulty, total, solved } buckets — the numbers behind the result page's radar/weakness view. weakTopics() filters that breakdown to topics at or below a 60% solve rate, sorted worst-first; it accepts either one attempt's perQuestion array or several concatenated, which is what powers the "Retry Weak Areas" generator across a candidate's full history, not just their latest sitting.
Company presets weight selection, not scoring
It's worth restating from OA Structure & Timer Logic: COMPANY_PRESETS only bias which questions selectQuestions() picks (soft weighting, with graceful fallback when a company's tag pool is sparse — Uber has zero tagged problems in the live bank, Microsoft has one). Marking is a fully separate, orthogonal axis set independently in the builder. A Cisco-flavored question mix can be scored under a D.E. Shaw-flavored marking scheme, or any custom one — the engine never couples the two.
Self-assessment
Word count for this page: 875 words.
← Previous: OA Structure & Timer Logic→ Next: Risk Engine & the Hash-Chained Audit Log