Task #193
openHarshit's task list
15%
Description
Harshit's task list — Gantt W11 to W14 (21 Sep to 16 Oct 2026)¶
Read this first: your lane has changed¶
Until now you worked on the React frontend. From 21 September 2026 you own the Spring Boot backend and the admin panel. Nikita has taken over the student-facing React work on issue #194.
Everything you built in W9 and W10 stays — the Plans page, the FREE / PREMIUM badges, the payment page. Do not delete any of it. Nikita will build on top of it.
Tickets you will comment on: #95 (M4), #96 (M5), #97 (M6), #98 (M7), #99 (M8), #100 (M9), #101 (M10), #102 (M11), #103 (M12), #93 (M2), #94 (M3), #112 (QB-1). Do not create new tickets.
How to read every task below¶
Each task always has the same four parts:
- Goal — one sentence saying what the finished thing does.
- Why — the reason it matters, so you understand the task instead of just copying it.
- Steps — what to actually change, with the real file paths from our repository.
- Done when — how you prove to yourself it works before you say it is finished.
The calendar (no Saturdays, no Sundays)¶
| Week | Working days |
|---|---|
| W11 | Mon 21, Tue 22, Wed 23, Thu 24, Fri 25 September |
| W12 | Mon 28, Tue 29, Wed 30 September, Thu 1, Fri 2 October — Phase 1 MVP must be finished by this Friday |
| W13 | Mon 5, Tue 6, Wed 7, Thu 8, Fri 9 October — Phase 2 opens, JEE Main bank track opens |
| W14 | Mon 12, Tue 13, Wed 14, Thu 15, Fri 16 October |
That is 20 working days and 20 tasks.
Ground rules for all your work¶
- Backend runs on
http://localhost:8081/api. Frontend runs onhttp://localhost:5173. Never use port 8080. - In a controller write
@RequestMapping("/tests"), not@RequestMapping("/api/tests"). The/apipart is added automatically by the context path. - Constructor injection everywhere. Validate request bodies with
@ValidDTOs. Return errors as{"error": "..."}throughGlobalExceptionHandler. - Never commit a
.envfile, a password, an API key, or a webhook URL. - There are only two question stores and they must never be mixed: the shared catalog at
/admin/questions, and the six per-exam banks at/exam-questions.
Already built — do not rebuild these¶
A full code audit on 21 September found that several things our old notes call "pending" are actually finished. Skip them:
- Bonus marks, negative marks, partial marking for multiple-correct, and numerical tolerance are all live in
AssessmentService.gradeObjective. - The
AttemptAuditLogentity andGET /attempts/{id}/auditboth exist. - Admin already has Faculty vs Candidate tabs and a subscription-plan dropdown.
PlanRules.isPremiumexists on the backend.- Bulk candidate upload by CSV and Excel works.
The biggest problem we are fixing, and why W11 looks like it does¶
Our backend has a complete, working exam engine. It starts attempts, saves answers, submits, scores with negative and bonus marks, and calculates rank and percentile. But no student screen calls it. The only code that touches /attempts is an admin testing harness in TestAuthoringPanel.tsx. Nikita connects the student screens this week. Your job in W11 is to make that engine trustworthy: enforce the clock, enforce the navigation rules, and stop giving away premium accounts for free.
Week 11 — Mon 21 to Fri 25 September¶
Theme: make the exam engine trustworthy, and close the two Gantt modules that end this week — M5 auto evaluation and M7 institution management.
H1 — Monday 21 September: Make Razorpay payment verification actually verify something (M9, #100)¶
Goal. When someone finishes a payment, we confirm the payment really came from Razorpay before we give them a premium plan.
Why. Right now PaymentService.verify marks the order as PAID and upgrades the user to MONTHLY without checking anything at all. Anyone who knows the URL can call that endpoint and get a free premium account. This is a security hole, so it goes first.
Steps.
- Open
backend/src/main/java/com/teamtalks/backend/services/PaymentService.java. - Razorpay sends back three values:
razorpay_order_id,razorpay_payment_idandrazorpay_signature. Join the first two with a|between them, for exampleorder_abc|pay_xyz. - Run HMAC-SHA256 over that joined string using your Razorpay key secret as the key. Java has this built in:
Mac.getInstance("HmacSHA256"). Convert the result to lowercase hexadecimal. - Compare your hex string to the signature Razorpay sent. Compare the bytes using
MessageDigest.isEqual, notString.equals— that avoids a timing attack. - If they do not match, throw an error so the client gets
{"error": "Payment verification failed"}. Do not mark the order paid. - Stop hardcoding MONTHLY. Store which plan the user was buying on the
PaymentOrderrow when the order is created, and set that plan on success. - Keep the stub behaviour for local development: if
RAZORPAY_KEY_IDis not set, skip the signature check but return"stub": trueso it is obvious we are not in real mode.
Done when. Calling POST /api/payments/verify with a made-up signature returns an error and the user's plan does not change. With the correct signature the order becomes PAID and the plan becomes whatever was ordered. Never print the key secret in a log line.
H2 — Tuesday 22 September: Stop students submitting after time is up (M4, #95)¶
Goal. The server refuses to save or submit an attempt once the allowed time has passed, and closes off abandoned attempts by itself.
Why. Today the countdown clock only exists in the browser. A student can pause the JavaScript, reopen the page hours later, or call the API directly, and we will happily accept the answers. The clock must be enforced on the server, because the server is the only thing a student cannot edit.
Steps.
- Open
backend/src/main/java/com/teamtalks/backend/services/AssessmentService.java. - Write one small private helper, for example
assertNotExpired(AssessmentAttempt attempt). It takesstartedAt, adds the test'sdurationMinutes, and compares that deadline to the current time. - Call it at the top of
saveand at the top ofsubmit. - When the deadline has passed do not simply reject the call. Mark the attempt submitted, run the normal
evaluatescoring on whatever answers were already saved, write an audit row with actionAUTO_SUBMIT, and then return a clear error saying the time expired. - Allow about 30 seconds of grace, so a student whose answer is in flight when the clock hits zero does not lose it.
- Also check the test's own
startAtandendAtwindow. An attempt cannot begin before the test opens or after it closes. There is already anassertWindowmethod — reuse it, do not write a second one.
Done when. Create a test with a 1 minute duration, start it, wait two minutes, then try to save. You get an error, the attempt shows SUBMITTED with a score, and GET /api/attempts/{id}/audit shows the AUTO_SUBMIT row.
H3 — Wednesday 23 September: Enforce navigation modes and record tab switches (M4, #95)¶
Goal. The three navigation modes we already store actually restrict what a student can do, and switching browser tabs during an exam leaves a permanent record.
Why. AssessmentTest has a navMode column holding FREE, SEQUENTIAL or SECTION_LOCK. We send it to the browser and then ignore it completely. A real exam engine has to stop a student jumping back into a section they already closed. Separately, the audit code has a comment saying it supports a TAB_SWITCH action, but nothing in the codebase ever writes one, so that feature does not really exist yet.
Steps.
- Still in
AssessmentService.java, insidesave, look at the test'snavModebefore accepting an answer. - SEQUENTIAL: only accept an answer for the current question or one already reached. Track the furthest question index reached on the attempt row so you have something to compare against.
- SECTION_LOCK: once a student moves from section 1 to section 2, refuse any further answers for section 1. Add a small
completedSectionIdsfield onAssessmentAttemptand update it when the section changes. - FREE: accept everything, which is what happens today.
- When a rule is broken return a clear message such as
{"error": "This section is already closed"}. Do not silently ignore the answer. - The save request body already carries
tabSwitchCount. When that number is higher than what we stored, callauditService.log(attemptId, userId, "TAB_SWITCH", ...)once per new switch, then update the stored count.
Done when. On a SECTION_LOCK test, moving to section 2 and then answering a section 1 question returns an error. Switching tabs three times produces three TAB_SWITCH rows in the audit endpoint.
H4 — Thursday 24 September: Range-based numerical answers (M5, #96) — this closes M5¶
Goal. A numerical question can accept any answer inside a range, not only one exact number.
Why. JEE and NEET regularly publish answers as a band, for example "any value between 9.78 and 9.82". Today TestQuestion only has a single tolerance field, which means plus-or-minus the same amount on both sides of one number. That cannot express a band that is not symmetric. M5 ends in W11, so this is the last chance.
Steps.
- Open
backend/src/main/java/com/teamtalks/backend/entity/TestQuestion.javaand add two nullableDoublecolumns:answerMinandanswerMax. - Add the same two fields to the request DTO used when creating or updating a test, with validation: if one is given the other must be too, and
answerMinmust not be greater thananswerMax. - In
gradeObjective, in the NUMERICAL branch: when both are set, mark the answer correct if the student's number falls inside the band, including both ends. Otherwise fall back to the existing exact-value-plus-tolerance logic. Do not delete the old logic — existing questions depend on it. - Parse the student's typed answer safely. If they typed letters, treat it as wrong rather than letting a
NumberFormatExceptioncrash the request.
Done when. A question with answerMin 9.78 and answerMax 9.82 marks 9.8 correct and 9.9 wrong. An old question that only has tolerance still scores exactly as it did before.
H5 — Friday 25 September: Institution, Department, Class and Batch (M7, #98) — this closes M7¶
Goal. A real Institution record in the database, with departments, classes and batches underneath it, so a coaching centre can organise its students.
Why. Today "institution" is just a text field copied onto every user, plus an institutionCode string on tests. There is no Institution table. That means we cannot list a school's students reliably, cannot group them into batches, and cannot produce per-batch reports. The Gantt asks for department, class and batch management and this is the last week for M7.
Steps.
- Create
entity/Institution.javawithid,code(unique),name,address,contactEmail,active,createdAt. Thecodemust match theinstitutionCodestrings already stored on users and tests, so existing data keeps working. - Create
entity/Batch.javawithid, a@ManyToOnelink to Institution,name,department,classLevel,academicYear,active. - Add a nullable
@ManyToOnebatchfield onUsers. Leave the existing text fields alone so nothing breaks. - Create repositories for both, in the same style as the other repositories under
repository/. - Create
InstitutionServiceand add the endpoints to the existingAdminController, not a new controller:GET /admin/institutions,POST /admin/institutions,GET /admin/institutions/{id}/batches,POST /admin/institutions/{id}/batches,PUT /admin/users/{userId}/batch. - Write a small one-time migration that reads every distinct non-empty institution name already on
Usersand creates an Institution row for it, so we do not lose the data we already have. - Restrict these endpoints to Super Admin and Institution Admin, and make sure an Institution Admin can only ever see their own institution.
Done when. You can create an institution, create two batches under it, move a student into a batch, and list only that batch's students. Logged in as Institution Admin for institution A, you cannot see institution B.
Week 12 — Mon 28 September to Fri 2 October¶
Theme: finish Phase 1. Everything on the Gantt for M6, M8, M9 and M10 ends this Friday.
H6 — Monday 28 September: Let students upload answer files (M6, #97)¶
Goal. A student answering a descriptive question can attach a PDF, a Word file, or a photo of their handwritten answer.
Why. Descriptive questions today only accept typed text in AttemptAnswer.responseText. For long answers, essays and case studies, students need to attach work. This is the first of three descriptive tasks and the other two depend on it.
Steps.
- Create
entity/AnswerAttachment.java:id, link toAttemptAnswer,fileName,contentType,sizeBytes,storagePath,uploadedAt. - Add
POST /attempts/{attemptId}/answers/{testQuestionId}/attachmentonAttemptController, taking aMultipartFile. - Validate hard before saving anything. Allow only PDF, DOCX, PNG and JPEG. Cap the size at 10 MB. Check the real content type, not just the file extension — an extension is trivially faked.
- Never store the file under the name the student gave it. Generate a UUID filename. A name like
../../application.propertieswould otherwise let someone write outside our folder. - Save files under a configurable folder. Add
app.upload.dirtoapplication.propertieswith a sensible default, and create the folder at startup if it is missing. - Add a download endpoint. Only the student who owns the attempt, the test's author, and admins may download.
- Reject uploads once the attempt is submitted.
Done when. Upload a 2 MB PDF against a descriptive question and download it back unchanged. A 20 MB file is rejected. A .exe renamed to .pdf is rejected. A different student requesting the file gets an error.
H7 — Tuesday 29 September: Rubric-based marking (M6, #97)¶
Goal. An examiner marks a long answer against a list of named criteria instead of guessing one overall number.
Why. "Give this essay a mark out of 20" produces wildly different marks from different examiners. A rubric — structure 5, content 8, grammar 4, examples 3 — makes marking consistent and lets us explain the mark to the student.
Steps.
- Create
entity/Rubric.java(id,name,description,createdBy) andentity/RubricCriterion.java(id, link to rubric,name,description,maxMarks,displayOrder). - Add a nullable link from
TestQuestionto a Rubric, so a descriptive question can carry one. - Create
entity/RubricScore.java: link toAttemptAnswer, link toRubricCriterion,marksAwarded,comment,evaluatorId. - Add admin endpoints
GET /admin/rubrics,POST /admin/rubrics,PUT /admin/rubrics/{id}. - Extend the existing
PUT /attempts/{id}/manual-scoreso it can accept a list of per-criterion marks. Add the criteria up to get the total and write that into the answer'smarksAwarded. - Validate that no criterion mark is negative or above its
maxMarks, and that the criteria actually belong to the rubric attached to that question. - After saving, call
evaluateagain so the attempt total, rank and percentile all refresh.
Done when. Create a rubric with four criteria totalling 20, attach it to a descriptive question, mark a student's answer criterion by criterion, and confirm the attempt total went up by exactly the sum you entered.
H8 — Wednesday 30 September: Two independent examiners and a moderator (M6, #97) — this closes M6¶
Goal. Two examiners mark the same answer without seeing each other's marks, and if they disagree too much a senior reviewer decides the final mark.
Why. This is standard practice for high-stakes descriptive papers and it is the last piece of M6. Double marking catches examiner bias; moderation resolves genuine disagreement.
Steps.
- Change
RubricScoreso several evaluators can score the same answer. The pair of answer plus evaluator must be unique — one examiner, one set of marks. - Add to
AttemptAnswer:evaluationStatus(PENDING, ONE_DONE, TWO_DONE, MODERATION_NEEDED, FINAL),finalMarks,moderatedBy. - When the second examiner submits, compare the two totals. If they differ by more than a configurable threshold — start with 15 percent of the question's maximum — set MODERATION_NEEDED. If they agree, set
finalMarksto the average and the status to FINAL. - An examiner must never see the other examiner's marks while still marking. Filter them out of the API response by evaluator id.
- Add
GET /admin/evaluations/moderation-queueandPUT /admin/evaluations/{answerId}/moderate, where a Super Admin or Reviewer sets the final mark and writes a short reason. - Only once the status is FINAL should the mark count towards the attempt total.
Done when. Two examiners mark 12 and 13 out of 20 — the answer goes FINAL at 12.5 automatically. Two examiners mark 8 and 18 — it lands in the moderation queue and the attempt total does not move until a reviewer settles it.
H9 — Thursday 1 October: Fix invite-only notifications and add exam reminders (M10, #101) — this closes M10 for Phase 1¶
Goal. Students invited to a private test actually receive the email, and everyone gets a reminder before the exam starts.
Why. There is a real bug here: findAssignedCandidates returns an empty list for tests whose availability is INVITE, so invite-only tests email precisely nobody. Separately the Gantt asks for reminders and we only send mail at publish time and result time.
Steps.
- Create
entity/TestInvitation.java: link toAssessmentTest, link toUsers,invitedAt,notifiedAt. AddPOST /tests/{id}/invitationsandGET /tests/{id}/invitations. - Fix
findAssignedCandidatesinAssessmentService: for INVITE tests return the invited users instead of an empty list. Leave PUBLIC and PRIVATE behaving as they do now. - Create
ReminderSchedulerusing Spring's@Scheduled, running every 15 minutes. Add@EnableSchedulingto the application class. - On each run, find published tests starting within the next 24 hours, and again within the next hour, and email each assigned candidate who has not had that particular reminder. Record what was sent on the invitation row so nobody gets the same reminder twice.
- Reuse
AssessmentMailService.send. Do not build a second mail path. The console fallback when SMTP is missing must keep working so local development does not break. - Keep the subject and body text in one place so they are easy to change later.
- Do not build SMS, WhatsApp or push notifications. Those stay deferred.
Done when. Create an invite-only test, invite two students, publish, and both receive the email. Set a start time 50 minutes away and confirm the one-hour reminder goes out on the next run and does not repeat.
H10 — Friday 2 October: Institution and batch reports, then Phase 1 sign-off (M8, #99) — this closes M8 and Phase 1¶
Goal. An institution admin can see how a whole batch performed on a test and compare batches against each other.
Why. Everything we report today is about one student's single attempt. A coaching centre needs to know which batch is behind and which topic the whole class is failing. This is the last Phase 1 row on the Gantt.
Steps.
- Add
GET /reports/tests/{testId}/summary: number of candidates who attempted, average score, highest and lowest, average time, and topic-wise accuracy across everyone. - Add
GET /reports/batches/{batchId}/summaryusing the Batch entity from H5, plus a ranked list of students. - Add
GET /reports/institutions/{institutionId}/comparisonreturning one row per batch so batches sit side by side. - Do the arithmetic in the database with JPQL aggregate queries. Do not load every attempt into Java memory and loop — that will not survive real data volumes.
- Enforce access strictly. Institution Admin reads only their own institution. Super Admin reads anything. A Candidate reads none of these.
- Spend the rest of the day on Phase 1 sign-off. Run every endpoint you built in W11 and W12 once, then write a short comment on #95, #96, #97, #98, #99, #100 and #101 saying exactly what shipped and what is still deferred.
Done when. Two batches take the same test and the comparison endpoint shows two rows with averages you can verify by hand from the raw attempt rows.
Week 13 — Mon 5 to Fri 9 October¶
Phase 2 opens. Two Gantt tracks start: M11 Language Assessment, and QB-1 the JEE Main question bank.
Read this before you start. The audit found that CAT has about 10,092 questions across 36 chapter files in backend/data/cat_questions_json/, but JEE Main has only 9 sample questions in backend/data/jee_questions_json/. The Gantt wants 24,000 unit-wise JEE Main questions. Our only import script, backend/scripts/convert_and_import_cat_questions.py, is hardcoded to CAT. So this week is about building the machinery, not typing questions. Writing the actual content stays with Ayush Sharma on the Gantt.
H11 — Monday 5 October: One import script that works for every exam (QB-1, #112)¶
Goal. A single script that imports questions into any of our six exam banks, from CSV or JSON.
Why. The CAT script has the CAT folder path, CAT chapter names and CAT subject mapping baked in. Copying it five more times would leave six scripts to maintain and six places for the same bug. We need one script driven by a config file.
Steps.
- Create
backend/scripts/import_questions.py, starting from the existing CAT script so the working parts are preserved. - Take arguments:
--exam(JEE_MAINS, JEE_ADVANCED, NEET, CAT, JIPMAT, CUET),--input(file or folder),--mapping(config file),--dry-run,--import. - Move the filename-to-chapter and subject mapping into a JSON config per exam under
backend/scripts/mappings/. Adding a new exam then means adding a config file, not editing code. - Support both formats. CSV gets a column mapping in the config. JSON is expected in the
{exam, questions}shape the import endpoint already accepts. - Validate every row before sending anything: content not empty, at least two options, correct answer must be one of the options, marks and negative marks must be numbers. Collect all the problems and print a report. Do not stop at the first bad row.
--dry-runmust be the default. Nobody should write 4,000 rows into the database by accident.- Send in batches of 500 to
POST /api/exam-questions/import, with a progress line per batch. One request with 24,000 questions will time out. - Skip duplicates — the repositories already have
existsByContentIgnoreCase. Report how many were skipped. - Write a summary file next to the input, the way the CAT script writes
_import_summary.json.
Done when. Running the script against backend/data/jee_questions_json/ in dry-run reports 9 valid questions and no errors. Running with --import loads them, and running it again skips all 9 as duplicates.
H12 — Tuesday 6 October: Maths formulas and images on questions (M2, #93)¶
Goal. A question can contain a properly typeset mathematical formula and a diagram.
Why. We cannot build a serious JEE or NEET bank without this. Physics and Maths questions are full of integrals, fractions and vectors, and many refer to a circuit diagram or a graph. Today content is plain text and there is no image field anywhere, so those questions simply cannot be stored.
Steps.
- Add to
ExamQuestionBase:contentFormat(PLAIN or LATEX, default PLAIN),imageUrl,solutionImageUrl. - Add a latex flag and an image URL to each option too, since options often contain formulas. Options are stored as a JSON list, so extend that structure rather than adding columns.
- Add the same fields to
ExamQuestionItemRequestso the importer can populate them. - Add
POST /admin/questions/image, reusing the safe file handling from H6: whitelist content types, UUID filename, size cap. - Store LaTeX exactly as given. Do not parse or clean it on the server — the browser library renders it, and touching it here will only corrupt formulas.
- Update
ExamQuestionResponseso the new fields reach the frontend. - Because
ddl-auto=updateadds columns automatically, confirm existing rows still work withcontentFormatdefaulting to PLAIN.
Done when. Save a question with contentFormat LATEX and content like \int_0^1 x^2 dx, attach a diagram, read it back through GET /api/exam-questions, and everything is intact.
H13 — Wednesday 7 October: Generate many practice tests from a question pool (M3 and QB-1, #94 and #112)¶
Goal. One API call produces many different practice papers from an exam bank, following a blueprint.
Why. The Gantt asks for 100 practice tests and 100 mock tests per exam. Nobody will assemble 200 papers by hand in the admin panel. What we have today is ExamQuestionService.sample, which shuffles and takes N — that ignores subject balance and difficulty, so one paper might be all easy Chemistry. A real paper needs a blueprint.
Steps.
- Create
services/TestGenerationService.java. - Add
POST /tests/generatetaking: the exam bank, how many tests, a name pattern likeJEE Main Practice Test {n}, the duration, and a blueprint. - The blueprint is a list of rules. Each rule says subject, optional chapter, difficulty, and how many questions. For example 25 Physics medium, 25 Chemistry easy, 25 Maths hard.
- Before generating anything, check there are enough questions for every rule. If Physics-hard has 20 and the blueprint wants 25, fail immediately naming the shortfall. Do not generate half a paper.
- Make the papers genuinely different. Track which questions each paper used and cap how much any two papers may overlap — start at 20 percent.
- Create real
AssessmentTest,TestSectionandTestQuestionrows, so generated papers behave exactly like hand-made ones everywhere else. - Accept an optional random seed so a run can be reproduced when debugging.
- Run the whole thing in one transaction and return a summary: how many papers, their ids, and any warnings.
- Restrict to Super Admin and Test Admin. It creates a lot of data.
Done when. Generating 10 practice tests from the CAT bank with a VARC / DILR / QA blueprint produces 10 tests, each matching the blueprint exactly, with no two sharing more than a fifth of their questions.
H14 — Thursday 8 October: Audio for listening questions (M11, #102)¶
Goal. A question can have an audio clip attached, and the server controls how many times a student may play it.
Why. This is the first half of M11 Language Assessment, which starts this week. The play-count rule matters: in real language exams the audio plays a fixed number of times, and if we enforce that only in the browser a student can just reload the page.
Steps.
- Add to
ExamQuestionBase:audioUrl,audioDurationSeconds,maxPlayCount(null meaning unlimited). - Add
POST /admin/questions/audioaccepting MP3 and M4A up to 20 MB, with the same safe-filename and content-type checks as H6. - Serve audio through
GET /content/audio/{id}. Support HTTP range requests so the browser can stream and seek, otherwise large files will not play reliably. - Track plays server-side. Add a
PlaybackLogrecording attempt, question and time of each play. AddPOST /attempts/{id}/questions/{qid}/playwhich the player calls before each play; it returns how many plays remain, or an error at the limit. - Return the remaining play count in the attempt response so the player can show it from the start.
- Only allow audio access to a student with an active attempt containing that question. Do not leave exam audio publicly downloadable.
Done when. Upload a clip, set maxPlayCount to 2, and the third play request is rejected by the server. Refreshing the page does not reset the counter.
H15 — Friday 9 October: Recording answers for speaking tests (M11, #102)¶
Goal. A student can record a spoken answer and have it stored for an examiner to mark.
Why. This pairs with the listening work and completes the listening-and-speaking pair the Gantt schedules for W13 to W14. Nikita builds the microphone recorder the same week, so agree the audio format and field names with her first thing in the morning.
Steps.
- Add a SPEAKING value to the question type list on
ExamQuestionBase, plusmaxRecordingSecondsandpreparationSeconds. - Extend
AnswerAttachmentfrom H6 to acceptaudio/webmandaudio/mp4, which is what browser recording produces. Cap at 25 MB. - Add
POST /attempts/{attemptId}/answers/{testQuestionId}/recordingto receive the audio plus its length in seconds. - Reject recordings longer than
maxRecordingSecondsplus a small tolerance. - Only one recording per question. A second upload replaces the first and deletes the old file, so nobody is confused about which one counts.
- Add
GET /admin/evaluations/{answerId}/recordingso an examiner can listen while marking. Reuse the rubric scoring from H7 — speaking is marked against criteria like fluency and pronunciation, exactly like written work. - Do not attempt automatic speech scoring. That is M17 in Phase 3 and is out of scope.
Done when. Nikita's recorder uploads a 30-second clip, it appears in the examiner panel, plays back cleanly, and an examiner can score it against a rubric.
Week 14 — Mon 12 to Fri 16 October¶
Theme: M12 Adaptive Testing starts on the Gantt, M11 continues, and the first real JEE Main practice papers get generated. Friday is a quality day, because the whole backend currently has only two test files.
H16 — Monday 12 October: Groundwork for adaptive tests (M12, #103)¶
Goal. The system understands a new kind of test where the next question depends on how the student is doing.
Why. M12 runs W14 to W18 and this is its first week. An adaptive test has no fixed question list, which breaks several of our assumptions — so lay the foundations carefully before writing the algorithm tomorrow.
Steps.
- Add ADAPTIVE to the
testTypevalues onAssessmentTest, alongside PRACTICE, MOCK, EXAM and ASSIGNMENT. - Add settings on the test:
adaptiveQuestionBank,adaptiveTotalQuestions,adaptiveStartDifficulty,adaptiveStopOnConfidence. - Add to
AssessmentAttempt:abilityEstimate(decimal, starts at 0),abilityStandardError,currentDifficulty. - Make sure every difficulty value in the six exam banks maps to a number. Check what strings are actually in the data first with a query, then map them, for example easy to -1, medium to 0, hard to 1. Do not assume the data is clean.
- For an adaptive attempt, do not create all the
TestQuestionrows at the start. Create them one at a time as questions are served, because we do not know in advance which questions the student will see. - Confirm scoring, the audit log and the reports all still work when the question list grows during the attempt. This is the part most likely to break.
Done when. You can create an adaptive test and start an attempt. Only one question exists at the start and the ability estimate is 0. Nothing about normal fixed tests changes.
H17 — Tuesday 13 October: Choosing the next question (M12, #103)¶
Goal. After each answer the system picks a harder or easier question based on how the student is performing.
Why. This is the core of adaptive testing. The principle is simple: a right answer means the student is probably stronger than we thought, so ask something harder; a wrong answer means ask something easier. Done well this finds a student's true level in far fewer questions than a fixed paper.
Steps.
- Create
services/AdaptiveTestService.java. - Add
GET /attempts/{id}/next-question. It scores the previous answer, updates the ability estimate, picks the next question, creates itsTestQuestionrow, and returns it. - Start simple and correct. Do not attempt Item Response Theory — that is M14 in a later phase. Use a step rule: correct moves the target difficulty up one step, wrong moves it down one step, and the steps get smaller as the estimate settles.
- Pick the question whose difficulty is closest to the target, from the configured bank, excluding anything already seen in this attempt. If several tie, choose randomly so two students do not get identical papers.
- If the bank runs out at the target difficulty, widen the search rather than failing. If it runs out entirely, end the test early and record why.
- Stop when either the question count is reached, or the standard error drops below the confidence threshold.
- Never send the correct answer or the solution in the next-question response. This endpoint is called from the browser and a student can read the response.
- Write an audit row for every question served, with the difficulty and the ability estimate at that moment, so we can debug why a student got the paper they got.
Done when. Answer everything correctly and the difficulty climbs steadily. Answer everything wrongly and it falls. Answer randomly and it settles near the middle. No question repeats within one attempt.
H18 — Wednesday 14 October: Turning the ability estimate into a score (M12, #103)¶
Goal. An adaptive attempt produces a meaningful, comparable score.
Why. Adaptive scores cannot be "marks out of total". Two students answering 20 questions each may have faced completely different papers, so 15 out of 20 on a hard path is a much better performance than 18 out of 20 on an easy one. The score has to reflect the difficulty of the questions faced.
Steps.
- In
AssessmentService.evaluate, branch for ADAPTIVE tests. - Convert the final ability estimate into a scaled score on a fixed range, for example 0 to 100, so all adaptive attempts are comparable regardless of path.
- Weight each question by its difficulty when computing its contribution, so a correct hard answer is worth more than a correct easy one.
- Store both numbers: the scaled score in
score, and the raw correct count separately, so reports can show both. - Make
recomputeRankswork for adaptive attempts by ranking on the scaled score. - Add the ability estimate and its standard error to the report response, so the result page can show a confidence band rather than a single misleadingly precise number.
- Write the formula and the reasoning as a comment in the service. The next person to read this needs to understand why the arithmetic is what it is.
Done when. Two students each answer 15 of 20 correctly, one on a hard path and one on an easy path, and the hard-path student scores measurably higher. Ranks compute without error.
H19 — Thursday 15 October: Generate the first real JEE Main practice papers (QB-1, #112)¶
Goal. Use the importer from H11 and the generator from H13 together and produce genuine practice papers.
Why. This is the first end-to-end proof that our question-bank pipeline works. It will also expose whatever is wrong with both tools while they are still fresh in your mind, which is far cheaper than finding out during a 24,000-question import.
Steps.
- Talk to Ayush Sharma first and get whatever JEE Main Physics Class XI content exists. Do not invent questions yourself.
- Write the JEE Main mapping config under
backend/scripts/mappings/, following the CAT config as a model. Map unit names to chapters and to Physics, Chemistry and Mathematics. - Run the importer in dry-run first and fix every validation problem it reports. Expect problems — real content is always messier than sample data.
- Import for real, then verify by querying
jee_mains_questionsdirectly: count rows, check the subject and chapter spread, and read ten questions end to end to confirm nothing was mangled. - Build a practice blueprint matching the real JEE Main pattern: 25 questions per subject with a realistic easy / medium / hard mix. Look the pattern up rather than guessing.
- Generate 10 practice tests, not 100. Start small so mistakes are cheap.
- Take one of the generated papers yourself, all the way to the result page. This is the real test of whether the chain works.
- Write what you found, with counts and any problems, as a comment on #112. We need this before scaling to 24,000 questions.
Done when. 10 JEE Main practice tests exist, each matching the blueprint, and you have personally completed one and seen a correct result page.
H20 — Friday 16 October: Automated tests for the exam engine (M4 and M5, #95 and #96)¶
Goal. Automated tests that will catch it if someone breaks scoring or attempt handling.
Why. The whole backend has exactly two test files: BackendApplicationTests.java and rules/TopicAccessRulesTest.java. Scoring is the most important code we have — if it silently breaks, students get wrong marks and we may not notice for weeks. You have changed scoring, timing, navigation and payments over four weeks. Lock that behaviour down.
Steps.
- Create
AssessmentServiceTestcoveringgradeObjectivethoroughly: correct single MCQ, wrong one with negative marks, fully correct multi-correct with bonus, partially correct multi-correct, multi-correct with a wrong option selected, numerical within tolerance, numerical outside it, and numerical inside the new range from H4. - Add tests for the time limit from H2: saving before the deadline works, saving after it auto-submits and errors.
- Add tests for the navigation rules from H3: sequential rejects jumping ahead, section-locked rejects going back.
- Add a test for the payment signature check from H1: valid signature passes, invalid one is rejected, and the plan does not change on rejection. Use a fixed test key, never a real one.
- Use an in-memory H2 database so tests run without MySQL and anyone can run them.
- Confirm
mvn testpasses and add the command to the README. - Aim for meaningful coverage of
AssessmentService, not a coverage percentage. Test the paths that would hurt if they broke.
Done when. mvn test runs green, and deliberately breaking the negative-marking line makes a test fail with a clear message.
Do not do these¶
Say no if they come up. They are either scheduled for a later Gantt phase or explicitly deferred in our project rules.
- Google and Microsoft sign-in, and hardware device binding.
- Live PayU, Stripe and PayPal. Razorpay stays in stub mode locally; only the signature check becomes real.
- SMS, WhatsApp and push notifications (M10). Email only.
- Camera-based proctoring.
- Item Response Theory and the question analytics dashboard (M14, W15 to W19).
- White-label branding and custom domains (M15, W17 to W20).
- AI-assisted marking (M17, Phase 3).
- Multi-tenant architecture and the 10,000 concurrent user load test (M18 and M19, Phase 3).
- The coding assessment module (M20, optional).
- Writing the bulk question content itself. Per the Gantt the question bank tracks belong to Ayush Sharma — you build the tooling.