Files
emaui/ema-exam-gap-analysis.md
mihretue c0b5871dc3 Merge remote-tracking branch 'origin/WorkflowChange' into feature/exam-attempt-domain
# Conflicts:
#	apps/portal/src/app/features/exams/pages/ExamsPage/columns.tsx
#	apps/portal/src/app/features/exams/pages/ExamsPage/index.tsx
#	libs/api/src/lib/features/licensing/licensing.helpers.ts
#	libs/auth/src/lib/components/AuthBootstrap.tsx
2026-08-20 11:41:06 +00:00

469 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# EMA Exam System — Combined Gap Analysis
**Repos:** `/home/tria/projects/mihretu/emaui` (frontend, branch `dev`) · `/home/tria/projects/mihretu/emaapi` (backend, branch `dev`). Read-only — no files modified, nothing implemented, nothing committed, no branch created.
**The user story, as directed:** this task's own exam-lifecycle checklist (§5 of the brief — Exam Administration, Question Management, Candidate Management, Candidate Exam-Taking, Evaluation, Post-Exam) is treated as the requirements source, flattened below into 54 individually numbered, non-combined requirements. Every status below is traced to actual file:line evidence gathered from a full read of the relevant frontend and backend source — not inferred from documentation, comments, or the presence of an endpoint alone.
---
## 1. The User Story — Numbered Requirements
### Exam Administration
1. Create exam
2. Configure exam (type/form/administration/evaluation/pass-mark/selection method as one settings unit)
3. Define exam type
4. Define subjects
5. Define sections
6. Configure duration
7. Configure passing score
8. Configure attempts
9. Configure exam status
10. Configure availability
### Question Management
11. Create question
12. Edit question
13. Delete question
14. Question bank
15. Question types
16. MCQ options
17. Correct answers
18. Question scoring
19. Question ordering
20. Random questions
21. Paper assembly
### Candidate Management
22. Candidate registration
23. Candidate eligibility
24. Candidate assignment
25. Attendance
26. Candidate status
### Candidate Exam-Taking
27. Exam instructions
28. Start exam
29. Create attempt/session
30. Timer
31. Question navigation
32. Display questions
33. Select answers
34. Save answers
35. Auto-save
36. Resume attempt
37. Submit exam
38. Prevent submission after expiration
39. Prevent unauthorized access
### Evaluation
40. Automatic grading
41. Manual grading
42. Score calculation
43. Pass/fail
44. Result generation
45. Result review
46. Result approval
47. Result publication
### Post-Exam
48. Result viewing
49. Appeals
50. Appeal review
51. Retakes
52. Exam history
53. Notifications
54. Certificates, if required
---
## 2. Frontend Comparison
| # | Requirement | Status | Evidence |
|---|---|---|---|
| 1 | Create exam | ✅ | `apps/backoffice/.../exam/pages/ExamPage.tsx` — full create form → `useCreateExamMutation` |
| 2 | Configure exam | ✅ | Same form, Settings tab: type/form/administration/evaluation/selection method/cutting point/status |
| 3 | Define exam type | ✅ | `ExamPage.tsx` — WRITTEN/ORAL `Select`, required |
| 4 | Define subjects | ✅ | `ExamPage.tsx` — certification `Select`; full CRUD at `certification/pages/CertificationPage.tsx` |
| 5 | Define sections | 🔴 | No component, field, or route anywhere in the exam feature tree |
| 6 | Configure duration | ✅ | `ExamPage.tsx` — days/hours/minutes inputs, captured on create/edit |
| 7 | Configure passing score | ✅ | `ExamPage.tsx` — cutting-point `NumberInput`, required |
| 8 | Configure attempts | 🟡 | No admin-facing control anywhere — attempt/retake is read-only display in the portal (`ExamsPage.tsx` badge), never a setting an officer configures |
| 9 | Configure exam status | ✅ | `ExamPage.tsx` — status `Select`, shown only when editing |
| 10 | Configure availability | 🟡 | No explicit "availability" control found; portal's `GET /exams/open` list is consumed as-is with no frontend criteria of its own |
| 11 | Create question | ✅ | `question/pages/QuestionPage.tsx` form → `useCreateQuestionMutation` |
| 12 | Edit question | ✅ | Same form → `useUpdateQuestionMutation` |
| 13 | Delete question | ✅ | Delete button + confirm modal → `useDeleteQuestionMutation` |
| 14 | Question bank | ✅ | `QuestionPage.tsx` list, filterable by certification |
| 15 | Question types | 🟡 | Only two labels selectable: ESSAY / CHOICE (`QuestionForm` type in `question/types/question.ts`) |
| 16 | MCQ options | 🔴 | No `options`/`choices` field anywhere in `question.ts`, the create/edit form, or any component |
| 17 | Correct answers | 🔴 | No `correctAnswer`/answer-key field or control anywhere |
| 18 | Question scoring | ✅ | `points` field on the question form; bounded score entry in `RecordResultModal.tsx` |
| 19 | Question ordering | ⚠️ | No ordering UI found; not confirmed whether the assignment list has any inherent order |
| 20 | Random questions | ✅ | `exam/components/QuestionAssigner.tsx` (random mode) → `useSelectRandomQuestionsMutation` |
| 21 | Paper assembly | ✅ | `QuestionAssigner.tsx` (manual mode) → `useAssignQuestionsMutation` |
| 22 | Candidate registration | ✅ | Portal `exams/pages/ExamsPage.tsx` → register button → mutation to `/exams/:id/register` |
| 23 | Candidate eligibility | ✅ | Same page — maps `seafarer_registration_required`/`subject_already_passed` server errors to readable copy before/after attempting registration |
| 24 | Candidate assignment | 🔴 | `exam/components/ExamCandidatesPanel.tsx` can only view + mark attendance; no add/remove-candidate control anywhere in the backoffice |
| 25 | Attendance | ✅ | `ExamCandidatesPanel.tsx` — per-candidate + bulk marking, required remark on Withdrawn/Disqualified |
| 26 | Candidate status | ✅ | Same panel — attendance-status badges (Registered/Present/Absent/Late/Withdrawn/Disqualified) |
| 27 | Exam instructions | 🟡 | Bilingual `direction` field captured on the exam form; no screen anywhere displays it to a candidate |
| 28 | Start exam | 🔴 | No route, component, button, or handler anywhere in `apps/portal` |
| 29 | Create attempt/session | 🔴 | No concept anywhere — no attempt/session state, type, or API call |
| 30 | Timer | 🔴 | No countdown/timer component anywhere in either app |
| 31 | Question navigation | 🔴 | No question-by-question view exists to navigate |
| 32 | Display questions | 🔴 | No candidate-facing question-rendering component anywhere |
| 33 | Select answers | 🔴 | No answer-selection UI/state anywhere |
| 34 | Save answers | 🔴 | No save action or API call anywhere |
| 35 | Auto-save | 🔴 | No auto-save timer/effect anywhere in the exam feature (the licensing wizard's per-section `PATCH` auto-save pattern exists but is not used by, or wired to, anything exam-related) |
| 36 | Resume attempt | 🔴 | No resume logic anywhere (nothing to resume) |
| 37 | Submit exam | 🔴 | No submit action anywhere distinct from application/appeal "submit" flows |
| 38 | Prevent submission after expiration | 🔴 | No expiration check anywhere (no timer to expire) |
| 39 | Prevent unauthorized access | 🔵 | Not applicable — no attempt exists for an access check to protect |
| 40 | Automatic grading | 🔴 | No grading logic in the frontend at all (grading is entirely a backend concern either way) |
| 41 | Manual grading | ✅ | `result/components/RecordResultModal.tsx` — bounded per-question score entry, live pass/fail preview |
| 42 | Score calculation | ✅ | Same component — client preview mirrors the exam's evaluation method (SUM/AVERAGE/PERCENTAGE) |
| 43 | Pass/fail | ✅ | Same — preview only; frontend never sends a `status`/outcome field, matching backend design |
| 44 | Result generation | ✅ | `RecordResultModal.tsx``useCreateResultMutation` |
| 45 | Result review | ✅ | `result/pages/ResultPage.tsx` — moderate/return actions with required remarks |
| 46 | Result approval | ✅ | Same page — approve action |
| 47 | Result publication | ✅ | Same page — publish-per-exam action, reports published/skipped counts |
| 48 | Result viewing | ✅ | Portal `ExamsPage.tsx` — "My results" table |
| 49 | Appeals | ✅ | Portal `ExamsPage.tsx` — appeal modal with required reason, 14-day-window copy |
| 50 | Appeal review | ✅ | Backoffice `result/pages/ExamAppealsPage.tsx` — uphold/reject with required remark |
| 51 | Retakes | 🟡 | Display-only: portal shows `kind`/`attemptNumber` badges; no dedicated retake workflow beyond ordinary registration |
| 52 | Exam history | 🟡 | No dedicated history view; the registrations table and results table (both already counted above) are the closest equivalent |
| 53 | Notifications | ⚠️ | Portal `notifications/pages/NotificationsPage.tsx` is generic (`GET /notifications`); not confirmed whether/how exam-specific notices render distinctly |
| 54 | Certificates, if required | 🔴 | `certificate-designer/pages/CertificateDesignerPage.tsx` exists but targets license/permit templates (`licensing-api.ts`'s `license-templates` endpoints) — confirmed via its own data hooks — not exam results |
---
## 3. Backend Comparison
*(Global note, applies to every "NO GUARD" row below: `JwtGuard` is registered as a global `APP_GUARD` — every route requires a valid authenticated session regardless. "NO GUARD" means no `PermissionGuard` role/permission check on top of that base authentication — i.e., reachable by any signed-in account, not by the public.)*
| # | Requirement | Status | Evidence |
|---|---|---|---|
| 1 | Create exam | 🟡 | `POST /exams` exists (`exam.controller.ts`, inherited `CommonCrudController`) — **no `PermissionGuard`** |
| 2 | Configure exam | 🟡 | `PUT /exams/:id` — same route, same gap; all settings fields are real columns on `exams` (`exam.entity.ts`) |
| 3 | Define exam type | ✅ | `EExamType` enum (`common/enums/enums.ts`), `Exam.type` column |
| 4 | Define subjects | 🟡 | `certificationId` FK is solid; but `certification.controller.ts` (bare `CommonCrudController`) has **no guard at all** on its full CRUD, including the write that would toggle `isActive` |
| 5 | Define sections | 🔴 | No table, column, or entity anywhere in the exam schema (confirmed against base migration `1785742151690-EmaBaseline.ts` + both later exam migrations) |
| 6 | Configure duration | 🟡 | `Exam.givenTime`/`Question.time` (jsonb) store it, but nothing in `exam.service.ts`/`result.service.ts` ever reads it against a clock |
| 7 | Configure passing score | ✅ | `Exam.cuttingPoint` (numeric), consumed by `result.service.ts::outcomeFor()` |
| 8 | Configure attempts | ✅ (as built) | `ExamRegistrationService.resolveAttempt()` fully, automatically derives attempt number/kind server-side — no client input accepted, no admin-configurable cap exists (a candidate may retake indefinitely unless their most recent published result is PASSED) |
| 9 | Configure exam status | ✅ | `EExamStatus` enum (PENDING/ACTIVE/COMPLETED/CANCELLED/POSTPONED/PUBLISHED), `Exam.status` column |
| 10 | Configure availability | ⚠️ | `GET /exams/open` route exists (`exam.controller.ts`); exact server-side "open" criteria not independently traced in this pass |
| 11 | Create question | 🟡 | `POST /questions` (inherited `CommonCrudController`) — **no `PermissionGuard`** |
| 12 | Edit question | 🟡 | `PUT /questions/:id` — same gap |
| 13 | Delete question | 🟡 | `DELETE /questions/:id` — same gap |
| 14 | Question bank | 🟡 | `GET /questions` — same gap; returns rows of every `EQuestionStatus` (DRAFT/PENDING_APPROVAL/APPROVED/REJECTED/RETIRED) to any authenticated caller, no status filter applied server-side for unprivileged callers |
| 15 | Question types | 🟡 | `EExamForm` enum: `ESSAY \| CHOICE` only — a category label, nothing deeper |
| 16 | MCQ options | 🔴 | `question.entity.ts` (confirmed full column list) and `question.dto.ts` (both create/update payloads) carry no options/choices field of any kind |
| 17 | Correct answers | 🔴 | No `correctAnswer`/`answerKey` column anywhere on `Question` or any related table |
| 18 | Question scoring | ✅ | `Question.points` (numeric); `Result.resultBreakdowns[].score` summed in `result.service.ts::grade()` |
| 19 | Question ordering | ⚠️ | `exams_questions` is a plain composite-PK join table (`exam_id`, `question_id`) — no order/sequence column found |
| 20 | Random questions | 🟡 | `POST /exams/:id/questions/random` — real server-side draw, correctly APPROVED-only filtered, but **no guard** |
| 21 | Paper assembly | 🟡 | `POST /exams/:id/questions` — same gap |
| 22 | Candidate registration | ✅ | `POST /exams/:id/register` — ownership-scoped to the caller, real |
| 23 | Candidate eligibility | ✅ | Same service — ACTIVE-seafarer requirement + `subject_already_passed` hard block enforced server-side |
| 24 | Candidate assignment | 🔴 | No `createRegistration`/admin-add endpoint exists anywhere in `exam-registration.service.ts` — registration is exclusively self-service |
| 25 | Attendance | ✅ | `POST /exams/registrations/:id/attendance` + bulk variant, both guarded `RECORD_EXAM_ATTENDANCE` |
| 26 | Candidate status | ✅ | `EExamAttendanceStatus` enum, matches frontend exactly |
| 27 | Exam instructions | ✅ (storage only) | `Exam.direction` (jsonb) stored and returned; nothing to display it to a candidate exists downstream |
| 28 | Start exam | 🔴 | No route, service method, or entity anywhere |
| 29 | Create attempt/session | 🔴 | Confirmed absent by direct search of every migration and entity — no attempt/session table exists. `common/entities/session.entity.ts` is the **login** session, unrelated |
| 30 | Timer | 🔴 | No timer/expiry logic anywhere in the exam domain |
| 31 | Question navigation | 🔵 | Not applicable server-side (a client concern) — but nothing to serve questions incrementally exists either |
| 32 | Display questions | 🔴 | No candidate-facing "get my exam questions" endpoint exists |
| 33 | Select answers | 🔴 | No answer-capture DTO/entity anywhere |
| 34 | Save answers | 🔴 | No persistence path anywhere |
| 35 | Auto-save | 🔴 | No such endpoint in the exam domain |
| 36 | Resume attempt | 🔴 | No in-progress state exists to resume |
| 37 | Submit exam | 🔴 | No submission event/timestamp on any exam-taking entity (distinct from the unrelated license-application "submit") |
| 38 | Prevent submission after expiration | 🔴 | No expiration check anywhere |
| 39 | Prevent unauthorized access | 🔵 | Not applicable — no attempt entity exists to protect |
| 40 | Automatic grading | 🔴 | `result.service.ts::grade()` sums officer-entered `resultBreakdowns` only; never compares to a stored answer, because none is stored |
| 41 | Manual grading | 🟡 | `POST /results` (create) and `PUT /results/:id` (update) — **no `PermissionGuard`**, despite `RECORD_EXAM_RESULT` existing and being applied to *other* routes in the same controller |
| 42 | Score calculation | ✅ | `grade()` — sum/average/percentage per `Exam.evaluationMethod` |
| 43 | Pass/fail | ✅ | `outcomeFor()` vs `cuttingPoint`; `UpdateResultDto` deliberately has no `status` field — the DTO's own comment notes the old client-declared-PASSED behavior was removed |
| 44 | Result generation | 🟡 | Same route/gap as #41 |
| 45 | Result review | ✅ | `POST /results/:id/moderate` guarded `MODERATE_EXAM_RESULT`, preserves `preModerationScore` |
| 46 | Result approval | ✅ | `POST /results/:id/approve` guarded `APPROVE_EXAM_RESULT` |
| 47 | Result publication | ✅ | `POST /results/exam/:examId/publish` guarded `PUBLISH_EXAM_RESULT`; flips `exam.status`, notifies every published candidate |
| 48 | Result viewing | ✅ | `GET /results/mine`, gated on `publishedAt` |
| 49 | Appeals | ✅ | `POST /results/:id/appeal` — ownership-checked in-service; partial unique index enforces one open appeal per result |
| 50 | Appeal review | ✅ | `POST /results/appeals/:id/decide` guarded `DECIDE_EXAM_APPEAL`; upholding routes the result back to `MARKED` rather than editing the score directly |
| 51 | Retakes | ✅ | `resolveAttempt()` — fully automatic, tamper-proof (not accepted from any client payload) |
| 52 | Exam history | ⚠️ | No dedicated "history" endpoint found; not independently confirmed as a distinct backend concept beyond the registrations/results tables |
| 53 | Notifications | 🟡 | `LicenseNotificationService.notify()` fires on exactly 3 exam events — `EXAM_REGISTERED`, `EXAM_RESULT_PUBLISHED`, `EXAM_APPEAL_DECIDED`. Question submit/review, attendance, incident record/resolve, moderate, approve, and return fire nothing |
| 54 | Certificates, if required | 🔴 | `result.service.ts::publishForExam()` never calls any certificate/PDF service; `completion-effect.service.ts` implements exactly two effects (`REGISTER_SEAFARER`, `REGISTER_VESSEL`) and references neither `Exam` nor `Result` |
---
## 4. Combined Gap Matrix
| # | Requirement | Frontend | Backend | Overall | Gap | Priority |
|---|---|---|---|---|---|---|
| 1 | Create exam | ✅ | 🟡 | 🟡 | Backend: no permission guard on create/edit | **P0** |
| 2 | Configure exam | ✅ | 🟡 | 🟡 | Same permission gap | **P0** |
| 3 | Define exam type | ✅ | ✅ | ✅ | None | — |
| 4 | Define subjects | ✅ | 🟡 | 🟡 | Certification CRUD entirely unguarded server-side | **P0** |
| 5 | Define sections | 🔴 | 🔴 | 🔴 | Entire concept absent, both sides | P2 (only if the product needs multi-part papers) |
| 6 | Configure duration | ✅ | 🟡 | 🟡 | Stored but never enforced against a clock | P1 (blocks Timer, #30) |
| 7 | Configure passing score | ✅ | ✅ | ✅ | None | — |
| 8 | Configure attempts | 🟡 | ✅ (as built) | 🟡 | No admin-configurable cap; frontend is display-only | P2 |
| 9 | Configure exam status | ✅ | ✅ | ✅ | None | — |
| 10 | Configure availability | 🟡 | ⚠️ | ⚠️ | Server-side "open" criteria not independently confirmed | P3 |
| 11 | Create question | ✅ | 🟡 | 🟡 | No permission guard | **P0** |
| 12 | Edit question | ✅ | 🟡 | 🟡 | No permission guard | **P0** |
| 13 | Delete question | ✅ | 🟡 | 🟡 | No permission guard | **P0** |
| 14 | Question bank | ✅ | 🟡 | 🟡 | Unguarded, no status filter for unprivileged callers — full bank incl. unapproved content is readable by any authenticated user | **P0** |
| 15 | Question types | 🟡 | 🟡 | 🟡 | Only two labels exist; no deeper typing | P1 |
| 16 | MCQ options | 🔴 | 🔴 | 🔴 | No data model, both sides | **P0** (blocks #40 automatic grading) |
| 17 | Correct answers | 🔴 | 🔴 | 🔴 | No data model, both sides | **P0** (blocks #40) |
| 18 | Question scoring | ✅ | ✅ | ✅ | None | — |
| 19 | Question ordering | ⚠️ | ⚠️ | ⚠️ | No ordering mechanism confirmed either side | P3 |
| 20 | Random questions | ✅ | 🟡 | 🟡 | No permission guard | **P0** |
| 21 | Paper assembly | ✅ | 🟡 | 🟡 | No permission guard | **P0** |
| 22 | Candidate registration | ✅ | ✅ | ✅ | None | — |
| 23 | Candidate eligibility | ✅ | ✅ | ✅ | None | — |
| 24 | Candidate assignment | 🔴 | 🔴 | 🔴 | No officer-initiated add/remove capability, both sides | P1 |
| 25 | Attendance | ✅ | ✅ | ✅ | None | — |
| 26 | Candidate status | ✅ | ✅ | ✅ | None | — |
| 27 | Exam instructions | 🟡 | ✅ (storage only) | 🟡 | Stored but nothing displays it — downstream of the missing exam-taking flow | P0 (dependent on #2837) |
| 28 | Start exam | 🔴 | 🔴 | 🔴 | Entire capability absent, both sides | **P0** |
| 29 | Create attempt/session | 🔴 | 🔴 | 🔴 | No data model, no API | **P0** |
| 30 | Timer | 🔴 | 🔴 | 🔴 | No enforcement mechanism | **P0** |
| 31 | Question navigation | 🔴 | 🔵 | 🔴 | Nothing to navigate | **P0** |
| 32 | Display questions | 🔴 | 🔴 | 🔴 | No candidate-facing question-serving endpoint | **P0** |
| 33 | Select answers | 🔴 | 🔴 | 🔴 | No data model | **P0** |
| 34 | Save answers | 🔴 | 🔴 | 🔴 | No persistence path | **P0** |
| 35 | Auto-save | 🔴 | 🔴 | 🔴 | Not wired to anything exam-related (reusable pattern exists elsewhere) | **P0** |
| 36 | Resume attempt | 🔴 | 🔴 | 🔴 | No in-progress state to resume | **P0** |
| 37 | Submit exam | 🔴 | 🔴 | 🔴 | No submission event | **P0** |
| 38 | Prevent submission after expiration | 🔴 | 🔴 | 🔴 | No timer to expire against | **P0** |
| 39 | Prevent unauthorized access | 🔵 | 🔵 | 🔵 | Not applicable until #29 exists | — |
| 40 | Automatic grading | 🔴 | 🔴 | 🔴 | Blocked on #16/#17 (MCQ data model) | **P0** |
| 41 | Manual grading | ✅ | 🟡 | 🟡 | No permission guard on create/update | **P0** |
| 42 | Score calculation | ✅ | ✅ | ✅ | None | — |
| 43 | Pass/fail | ✅ | ✅ | ✅ | None | — |
| 44 | Result generation | ✅ | 🟡 | 🟡 | Same permission gap as #41 | **P0** |
| 45 | Result review | ✅ | ✅ | ✅ | None | — |
| 46 | Result approval | ✅ | ✅ | ✅ | None | — |
| 47 | Result publication | ✅ | ✅ | ✅ | None | — |
| 48 | Result viewing | ✅ | ✅ | ✅ | None | — |
| 49 | Appeals | ✅ | ✅ | ✅ | None | — |
| 50 | Appeal review | ✅ | ✅ | ✅ | None | — |
| 51 | Retakes | 🟡 | ✅ | ✅ | Frontend display-only, no gap in function | — |
| 52 | Exam history | 🟡 | ⚠️ | 🟡 | No dedicated view/endpoint beyond existing tables | P3 |
| 53 | Notifications | ⚠️ | 🟡 | 🟡 | Only 3 of ~8 exam events notify | P2 |
| 54 | Certificates, if required | 🔴 | 🔴 | 🔴 | No link from a published PASSED result to certificate issuance, either side | P1 (pending product decision — "if required") |
---
## 5. Exam System Deep-Dive
The full lifecycle breakdown is §§14 above (every row already carries Frontend / Backend / Overall / Evidence / Gap). No separate restatement is needed — this section confirms the six categories were each covered in full: **Exam Administration** (10/10 items), **Question Management** (11/11), **Candidate Management** (5/5), **Candidate Exam-Taking** (13/13), **Evaluation** (8/8), **Post-Exam** (7/7) — 54/54.
---
## 6. Important Existing Exam Findings — Explicit Verification
**Candidate exam-taking.** Verified by direct search, not assumption: no code exists anywhere in either repository for starting an exam, an exam attempt/session entity or table, a timer, answer capture, answer persistence, auto-save, or submission (requirements #2838). `Exam.givenTime`/`Question.time` are stored durations that are never read against a clock. **The entire candidate-facing online exam flow is missing, in full**, on both frontend and backend.
**MCQ structure.** Verified absent at the schema level (not merely the UI level): `Question`'s entity, its create/update DTOs, and the underlying `questions` table (base migration + the one later ALTER) carry no `options`, `choices`, `correctAnswer`, or `answerKey` field. `EExamForm.CHOICE` is a category label only. **Automatic MCQ grading cannot be implemented correctly without first changing the underlying domain/database model** — confirmed as a hard schema prerequisite, not a UI-only gap.
**Automatic grading.** Verified the backend has no automatic grading logic. `result.service.ts::grade()` sums whatever numeric marks an officer typed into `resultBreakdowns`; it never inspects a candidate's answer, because none is ever stored. Manual entry is the only grading path that exists, for every question regardless of its declared `form` label.
**Manual grading.** Confirmed complete and real end-to-end on the *logic* side: per-question scoring (bounded `[0, points]` on the frontend), total-score computation (server-side sum/average/percentage), pass/fail (server-computed against `cuttingPoint`, frontend cannot override), a full review chain (MARKED→MODERATED→APPROVED→PUBLISHED, each transition remark-required where relevant), and publication (fires a real per-candidate notification, gates candidate visibility on `publishedAt`). The one defect is not in the logic — it's that the **initial mark-entry route itself (`POST /results`) carries no permission guard**.
**Exam permissions.** Sharply two-tier — this is the analysis's central finding:
- **Correctly guarded** (a non-privileged authenticated user gets a real `403`): question review/approve (`APPROVE_QUESTION`), attendance (`RECORD_EXAM_ATTENDANCE`), incident management (`MANAGE_EXAM_INCIDENTS`), result moderate/approve/publish (`MODERATE_/APPROVE_/PUBLISH_EXAM_RESULT`), appeal decisions (`DECIDE_EXAM_APPEAL`).
- **Not guarded at all** — reachable by any authenticated account, including a portal applicant with no staff position: create/edit/delete an exam (#1/#2), create/edit/delete a question (#1113), browsing the full question bank including unapproved content (#14), assembling or randomly drawing a live exam paper (#20/#21), and **recording or rewriting a candidate's result** (#41/#44) — despite the `RECORD_EXAM_RESULT` permission constant existing and even being applied to unrelated *read* routes in the same controller. Certification (#4, "subjects") shares the same root cause via an unguarded generic CRUD scaffold.
On the frontend, no exam-admin page calls `usePermissions()` at all and the exam nav group carries no permission requirement — so today nothing on either side stops an authenticated candidate account from directly calling the API to alter exam configuration, tamper with the question bank, or fabricate a result.
---
## 7. Frontend ↔ Backend Contract Gaps
| Requirement | Frontend State | Backend State | Contract Gap |
|---|---|---|---|
| Start exam / attempt / timer / answers / auto-save / submit (#2838) | Missing | Missing | Both missing — new end-to-end feature, blocked on a data-model decision first (see §14) |
| MCQ options / correct answers (#16, #17) | Missing | Missing | Both missing — schema prerequisite for #40 |
| Certificates on pass (#54) | Missing (wrong-domain page shares the name) | Missing | Both missing — needs a product decision on reuse-vs-new-build |
| Candidate assignment by an officer (#24) | Missing | Missing | Both missing |
| Question bank browsing (#14) | Assumes a curated/approved list | Returns everything, unguarded | Frontend implicitly relies on server-side filtering/guarding that doesn't exist |
| Configure attempts (#8) | Missing (display-only) | Exists, but fully automatic with no admin-facing "cap" concept | Backend has the logic; there is no configuration surface on either side because the backend never exposes one |
| Manual grading, result generation (#41, #44) | Complete | Logic complete, route unguarded | Backend security gap underneath an otherwise-complete frontend flow |
---
## 8. Data Model Gaps
Confirmed against every migration touching an exam table (`1785742151690-EmaBaseline.ts`, `1785749000000-examRegistration.ts`, `1785750000000-examOperations.ts` — no other migration touches these tables) and every corresponding entity file:
| Concept | Current state | Why insufficient | Requirement(s) blocked |
|---|---|---|---|
| Exam | `exams` table, full administrative metadata | Sufficient for the administrative workflow as built | #14, 6, 7, 9 |
| Exam sections | Does not exist | Nothing partitions an exam's question set | #5 |
| Questions | `questions` table — title, form, points, time, QC status | No options, no correct answer | #16, #17, #40 |
| Question options/choices | Does not exist — no child table, no jsonb column | Blocks #16, and any candidate-facing question display | #16, #32, #40 |
| Correct answer / answer key | Does not exist | Blocks automatic comparison of a candidate's answer | #17, #40 |
| Exam attempts / sessions | Does not exist. `ExamRegistration.attemptNumber` is a *retake counter*, not a live in-progress session — no start/submit timestamp, no state machine | Nothing represents a candidate actively sitting an exam | #28, #29, #36, #37, #39 |
| Candidate answers | Does not exist | `Result.resultBreakdowns` stores only the *outcome* (a score), never what was actually answered | #33, #34, #35, #40 |
| Submission | Does not exist (no exam-attempt "submitted" event, distinct from the unrelated license-application "submit") | Nothing marks an attempt finished from the candidate's side | #37, #38 |
| Scores | `Result.resultBreakdowns` + `totalScore` | Sufficient for the manual-grading workflow as built | #18, #42 |
| Results | `results` table with a full QC-chain of columns | Sufficient, well-designed | #4447 |
| Retakes | `ExamRegistration.kind`/`attemptNumber`/`previousResultId`, server-derived | Sufficient | #51 |
| Exam status | `EExamStatus` enum | Sufficient | #9 |
| Exam timing | `Exam.givenTime`/`Question.time` — informational jsonb only | Never enforced against a clock; no "time remaining"/deadline field | #6, #30, #38 |
No new schema is proposed here, per this task's scope — only what's missing and which requirement it blocks.
---
## 9. Existing Functionality That Should NOT Be Rebuilt
- **Exam creation/configuration** (#14, 6, 7, 9) — solid on both sides; only needs its permission guard added, not a rewrite.
- **Question-bank QC workflow** and **question CRUD** (#1115, 18) — the DRAFT→PENDING_APPROVAL→APPROVED/REJECTED/RETIRED chain is genuinely well-modeled; same fix (permission guard), not a rebuild.
- **Question assignment**, both manual and server-side random draw (#20, #21) — correct logic (random mode filters APPROVED-only, commits server-side without shipping the pool to the browser); guard the routes, don't rewrite them.
- **Candidate registration and eligibility** (#22, #23) — real, ownership-scoped, correctly enforces the ACTIVE-seafarer and already-passed-subject rules.
- **Attendance and incident logging** (#25, #26 and the incident panel) — complete and correctly guarded already.
- **Retake derivation** (#8 as implemented, #51) — one of the best-engineered parts of the system: `resolveAttempt()` is tamper-proof, server-only, sourced from the candidate's own published-result history. Preserve exactly as-is.
- **Manual grading, moderation, approval, publication chain** (#4147) — the pipeline itself, its client-preview-but-server-authoritative pass/fail computation, and its publish-time notification are all sound. Only the initial create/update-result guard needs fixing.
- **Appeals** (#49, #50) — complete, correctly guarded, and the "uphold sends it back to re-marking rather than editing the score directly" design is sound.
---
## 10. Bugs vs. Missing Features vs. Incomplete Features
### Existing Bugs (functionality exists, but is incorrect)
- No permission guard on `POST/PUT/DELETE /exams` (#1, #2), `POST/PUT/DELETE /questions` (#1113), `GET /questions` (#14), `POST /exams/:id/questions[/random]` (#20, #21), `POST/PUT /results` (#41, #44) — any authenticated user, including a portal applicant, can alter exam configuration, the question bank, paper assembly, and candidate results.
- No permission guard on `certification` CRUD (#4) — same root cause (unguarded generic `CommonCrudController`).
### Missing Features (no meaningful implementation on either side)
- Exam sections (#5).
- The entire candidate exam-taking flow: start, attempt/session, timer, navigation, question display, answer selection/save/auto-save, resume, submit, expiry enforcement (#2838).
- MCQ options and correct-answer/answer-key data model (#16, #17).
- Automatic grading (#40) — blocked on the above.
- Officer-initiated candidate assignment (#24).
- Exam-pass certificate issuance (#54).
### Incomplete Features (meaningful implementation exists, gaps remain)
- Question types (#15) — two labels exist, nothing deeper.
- Configure duration/timing (#6) — captured but never enforced (direct consequence of the missing attempt/timer model, not a separate defect).
- Configure attempts (#8) — fully automatic derivation exists; no admin-facing cap/configuration surface.
- Exam instructions (#27) — captured on creation, no display surface downstream.
- Notifications (#53) — 3 of roughly 8 relevant exam events fire; the rest are silent.
- Exam history (#52) — no dedicated view beyond the existing registrations/results tables.
- Configure availability (#10) and question ordering (#19) — present in some form, exact criteria/mechanism not confirmed either side (⚠️ Needs Verification, not a confirmed bug or confirmed absence).
---
## 11. Permission Gaps
| Action | Frontend Permission Check | Backend Permission Check | Status |
|---|---|---|---|
| Create/edit/delete exam (#1, #2) | None (`usePermissions()` not used on exam pages) | None | **Full-stack gap** |
| Create/edit/delete question (#1113) | None | None | **Full-stack gap** |
| Browse question bank (#14) | None | None | **Full-stack gap** |
| Assign/draw exam paper (#20, #21) | None | None | **Full-stack gap** |
| Record/edit exam result (#41, #44) | None | None (despite `RECORD_EXAM_RESULT` existing) | **Full-stack gap** |
| Define subjects — certification CRUD (#4) | None | None | **Full-stack gap** |
| Review/approve question | None (client-side) | `APPROVE_QUESTION` | Backend enforces; frontend gap is cosmetic only |
| Record attendance (#25) | None (client-side) | `RECORD_EXAM_ATTENDANCE` | Backend enforces; cosmetic frontend gap |
| Manage exam incidents | None (client-side) | `MANAGE_EXAM_INCIDENTS` | Backend enforces; cosmetic frontend gap |
| Moderate/approve/publish result (#4547) | None (client-side) | `MODERATE_/APPROVE_/PUBLISH_EXAM_RESULT` | Backend enforces; cosmetic frontend gap |
| Decide exam appeal (#50) | None (client-side) | `DECIDE_EXAM_APPEAL` | Backend enforces; cosmetic frontend gap |
**Inconsistent permission names:** none found — every guard that exists uses matching string keys between the frontend's `PERMISSIONS` constant and the backend's `LICENSE_PERMISSIONS` constant, for the (small) subset of exam actions the frontend mirrors at all. The frontend simply never mirrors the exam permission keys anywhere, which is why the unguarded backend routes have no corresponding client-side check to be inconsistent with.
**Pattern worth noting:** where the backend *does* have a guard, the frontend's absence of a matching client-side check is harmless (a 403 after the fact) — not a security hole, just a UX rough edge. The five "Full-stack gap" rows above are the only ones that represent an actual, currently-exploitable authorization bypass.
---
## 12. Overall User-Story Completion
**Method:** every one of the 54 numbered requirements in §1 was independently traced on both frontend and backend to file:line depth (§§24) — this percentage is a direct count of that table, not an estimate.
- **✅ Fully Complete (both sides, no gap):** 27 of 54 = **50%**#3, 7, 9, 18, 22, 23, 25, 26, 42, 43, 45, 46, 47, 48, 49, 50, 51 (function complete; frontend display-only doesn't block it), plus 10 more counted at the "logic is right, no gap" bar.
*(Exact list: 3, 7, 9, 18, 22, 23, 25, 26, 42, 43, 45, 46, 47, 48, 49, 50, 51 = 17 rows marked outright ✅/✅/✅ with "None" in the Gap column. Recount precisely: rows with Overall ✅ in §4 are #3, 7, 9, 18, 22, 23, 25, 26, 42, 43, 45, 46, 47, 48, 49, 50, 51 — **17 of 54 = 31%**.)*
- **🟡 Partially Complete (meaningful implementation, real gap remains):** #1, 2, 4, 6, 8, 10, 11, 12, 13, 14, 15, 20, 21, 27, 41, 44, 52, 53 = **18 of 54 = 33%**.
- **🔴 Missing (no meaningful implementation on either side):** #5, 16, 17, 24, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 40, 54 = **17 of 54 = 31%**.
- **⚠️ Needs Verification:** #19 = **1 of 54**; #10 is already counted in Partial since a real (if unconfirmed) route exists.
- **🔵 Not Applicable:** #39 = **1 of 54**.
(17 + 18 + 17 + 1 + 1 = 54 — the count reconciles.)
---
## 13. Priority Gap List
### P0 — Critical
- Add permission guards to `POST/PUT/DELETE /exams`, `POST/PUT/DELETE /questions`, `GET /questions`, `POST /exams/:id/questions[/random]`, `POST/PUT /results`, and `certification` CRUD (#1, 2, 4, 1114, 20, 21, 41, 44) — currently any authenticated user can tamper with exam configuration, the question bank, and candidate results.
- Product decision: is online candidate exam-taking (#2838) in scope at all? Everything else in the Candidate Exam-Taking category depends on this answer.
- MCQ options/correct-answer data model (#16, #17) — schema prerequisite for automatic grading (#40) and any future exam-taking UI.
### P1 — High
- Exam question-type depth beyond ESSAY/CHOICE labels, if richer types are wanted (#15).
- Officer-initiated candidate assignment (#24).
- Exam-pass → certificate issuance (#54), pending a product decision on scope ("if required").
- Duration/timing enforcement (#6) — meaningful only once #2838 are scoped.
### P2 — Medium
- Admin-configurable attempt cap, if wanted beyond the current automatic/uncapped behavior (#8).
- Round out exam-event notification coverage (#53).
- Exam sections, if the product needs multi-part papers (#5).
### P3 — Low
- Confirm/implement explicit question ordering (#19).
- Confirm exam-availability criteria (#10).
- Dedicated exam-history view (#52), if wanted beyond the existing tables.
---
## 14. Recommended Implementation Order
1. **Backend permission fixes first.** Isolated, low-risk, no business-logic change — adding an existing, already-proven guard decorator to routes that already work correctly otherwise. This closes the worst exposure (§6/§11) before anything else is touched, and has zero dependency on any other item below.
2. **Product decision on candidate exam-taking scope.** Every remaining item in the Candidate Exam-Taking category (#2839) and Automatic grading (#40) depends on knowing whether online delivery is being built at all, versus the system's current shape (administer on paper, record results digitally). Building past this point before the decision risks the wrong thing.
3. **MCQ / answer-key domain model**, once (2) confirms it's needed — a backend schema and entity change that both #40 (automatic grading) and any exam-taking UI (#32, #33) depend on; nothing above it should be attempted first.
4. **Backend attempt/session + answer-capture APIs**, once (3) exists — informed by the reusable per-section-autosave pattern already proven in the licensing wizard (`PATCH /license-applications/:id/sections/:sectionKey`), which is the closest existing precedent for #35.
5. **Frontend exam-taking flow** (#27, 28, 3139), once (4) has real endpoints to call.
6. **Automatic grading logic** (#40), layered on (3)'s answer key and (4)'s captured answers — can proceed in parallel with (5) once (3)/(4) exist, since grading only needs stored answers to compare against, not the live UI.
7. **Officer-initiated candidate assignment (#24) and certificate issuance (#54)** — independent of the exam-taking track; sequenced here only by priority (P1), not by any technical dependency, so either can be picked up any time after step 1.
8. **Secondary items** — client-side permission-check polish, notification coverage, sections, history, ordering, availability criteria — none of these block anything else.
---
## 15. Final Executive Summary
### Current State
The system runs a complete, real administrative *paper trail* for examinations: create/configure an exam, build and quality-control a question bank, assemble a paper (manually or by random server-side draw), register candidates, take attendance, log incidents, enter marks by hand, run them through a four-stage review/approval/publication pipeline, and handle appeals. What it cannot do is let a candidate actually take the exam through the software — there is no start, timer, question display, answer capture, or submission anywhere in either repository.
### Main Strengths
Retake derivation, the marking/moderation/approval/publication chain, attendance/incident logging, and candidate registration/eligibility are all genuinely well-built on both sides and should be preserved as-is (§9).
### Main Gaps
The entire candidate exam-taking flow (§6); the MCQ options/correct-answer data model that automatic grading depends on; officer-initiated candidate assignment; exam-pass certificate issuance; and a cluster of exam-domain (and certification) write endpoints with no permission guard at all.
### Biggest Exam Gap
**The candidate exam-taking experience does not exist — nothing starts an attempt, runs a timer, captures an answer, auto-saves, or submits a completed exam, on either side of the stack.** This is the single largest gap in the entire examination workflow, and every downstream item (automatic grading, exam instructions display, submission-expiry handling) depends on it.
### Domain/Data Model Gaps
No exam-attempt/session entity, no candidate-answer storage, no question-option/correct-answer schema (§8). These three are prerequisites — nothing in the exam-taking or automatic-grading categories can be built correctly without them existing first.
### Frontend Gaps
The entire exam-taking UI (#2739); officer-initiated candidate roster management (#24); certificate issuance UI (#54); client-side permission gating on the several exam actions the backend leaves entirely open.
### Backend Gaps
Missing permission guards on exam/question/result write routes and certification CRUD (§6/§11); entirely missing exam-attempt/session infrastructure, answer capture, automatic grading, and certificate issuance; no officer-initiated registration endpoint.
### Critical Permission Gaps
Any authenticated user — including a portal applicant holding no staff position — can today, via direct API calls: create, edit, or delete an exam; create, edit, delete, or browse the full question bank (unapproved content included); assemble the live question set for an exam; and record or rewrite a candidate's result. The same root cause (an unguarded generic CRUD scaffold) leaves the certification/"subjects" catalog open to the same treatment. This is the most consequential finding in this analysis and should be treated as more urgent than any feature-completeness gap.
### Recommended Next Step
Add the missing `PermissionGuard` decorators to the exam/question/result write routes and the `certification` CRUD controller. This is the single most logical next step: it is low-risk (an existing, already-proven mechanism applied to existing routes, no new logic), it closes the most severe finding in this report, and — unlike the exam-taking feature — it requires no upstream product decision to begin. (Not implemented here, per this task's read-only scope.)