Only one real form exists in the domain: the marks entry form (S1), whose
contract is EnterMarksDto (examination-subject.dto.ts:47-65). S5's publish
confirmation is a dialog, not a form. Field-by-field below, including the
server-side validation each field maps to. (forward-looking) client spec.
"Max {maximumMarks}" beside field (examination-subject.schema.ts:27-28)
Errors
Client: over-max inline error before submit (no round trip). Server rejects still possible (stale maximumMarks): 404 RESOURCE_NOT_FOUND → normalise to "marks cannot exceed maximum" (404-vs-422 quirk, 14_QA_Checklist.md §5). 400 if negative or non-numeric.
Focus
auto-advance Enter → grade field; commit-on-blur + Enter
A11y
label "Marks obtained — {student}"; errorText announced via SemanticsService
dropdown of A+, A, B+, B, C, D, F + "clear" — but server accepts any string (@IsOptional() @IsString(), examination-subject.dto.ts:57-60), stored verbatim (examination.service.ts:157)
Default
empty (server derives no per-subject grade; only the overall grade is computed, result.service.ts:130-138)
Note
Client-supplied; do not auto-derive from marks client-side unless the org's scheme is known — flag: server grading (planned) (blueprint Results.md:54-55)
Errors
400 if non-string
A11y
label "Grade — {student}"; selected value announced via R-GradeChip semantics ("grade A plus")
Per-row commit, not a whole-grid POST (no bulk endpoint — (planned)marks-import, IMPLEMENTATION_PLAN.md:219).
Re-submitting the same row is an update, never a duplicate (upsert examination.service.ts:147-175).
Partial payloads: grade/remarks omitted → cleared on update ($set with undefined fields is not applied — only provided keys are written, :152-161); send all three keys to avoid surprise.
Offline: rows queue in the offline draft and replay in row order (15_Flutter §Offline).