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

42 KiB
Raw Blame History

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

  1. Create question
  2. Edit question
  3. Delete question
  4. Question bank
  5. Question types
  6. MCQ options
  7. Correct answers
  8. Question scoring
  9. Question ordering
  10. Random questions
  11. Paper assembly

Candidate Management

  1. Candidate registration
  2. Candidate eligibility
  3. Candidate assignment
  4. Attendance
  5. Candidate status

Candidate Exam-Taking

  1. Exam instructions
  2. Start exam
  3. Create attempt/session
  4. Timer
  5. Question navigation
  6. Display questions
  7. Select answers
  8. Save answers
  9. Auto-save
  10. Resume attempt
  11. Submit exam
  12. Prevent submission after expiration
  13. Prevent unauthorized access

Evaluation

  1. Automatic grading
  2. Manual grading
  3. Score calculation
  4. Pass/fail
  5. Result generation
  6. Result review
  7. Result approval
  8. Result publication

Post-Exam

  1. Result viewing
  2. Appeals
  3. Appeal review
  4. Retakes
  5. Exam history
  6. Notifications
  7. 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.tsxuseCreateResultMutation
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.

  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.

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.)