Module-specific quality gates on top of 00-shared/10 (baseline applies to every
item here). Each checklist row is traceable to a server behaviour cited inline.
# Check Expected Server source
1.1 Duplicate admission number Create → 409 "Admission number "x" already exists."; import row → per-row error student.service.ts:56-62; students-import.adapter.ts:49-54
1.2 Duplicate admission via race (two clients) unique index {tenantId, admissionNumber} prevents both; one wins, other 409 student.schema.ts:68
1.3 Create auto-enroll response student + one ACTIVE enrollment with joinedAt ≈ now student.service.ts:71-78
1.4 Enroll closes previous old enrollment status:"transferred" + leftAt set; new ACTIVE; history append-only student.service.ts:125-139; COLLECTIONS.md:1674-1677
1.5 Transfer guard non-active status → 409 with exact status in message; no enrollment change happens student.service.ts:175-179
1.6 Graduate idempotency second graduate → 409 "already graduated"; status stays graduated student.service.ts:207-209
1.7 Archive/restore idempotency archive twice → 409; restore twice → 409 "already active" student.service.ts:228-230
1.8 Soft delete isolation after DELETE, student absent from list/detail/documents/history; isDeleted:true base.repository.ts:20-30,68-74
1.9 No cascade on delete documents/links/enrollments remain queryable by id student.service.ts:157-168 (no cascade)
1.10 Transfer writes profile classId/academicYearId updated; gradeId/sectionId only when provided student.service.ts:185-192
# Scenario Expected
2.1 Enroll same class twice in a row second enroll deactivates first (transferred), creates new ACTIVE — allowed by server; UI warns (no server 409) — flagged OQ-13
2.2 Enroll during active transfer UI server processes sequentially; last write wins (version increments)
2.3 Concurrent PATCH + transfer findOneAndUpdate + $inc version — no lost-update corruption (base.repository.ts:57-66); UI refreshes after both
# Check Expected Source
3.1 No-file upload request without file → 400 (multer) student.controller.ts:82-90
3.2 Oversized file no server limit today — must be enforced client + proxy (proposed) 10 MBOQ-9; student.service.ts:250-271
3.3 Unsupported mime no server allowlist — client (proposed) allowlist pdf/jpg/png/webp OQ-9
3.4 Empty category allowed (optional string) — UI shows uncategorized chip upload-student-document.dto.ts:4-9
3.5 Uploaded metadata correctness fileName/mimeType/size match actual file; uploadedBy = actor student.service.ts:262-270
3.6 List order newest first (createdAt desc) student.service.ts:273-279
# Check Expected
4.1 Valid 1000-row CSV report {totalRows:1000, imported:1000, failed:0, errors:[]}; 1000 students + users created; no timeout → request budget: set client timeout ≥ 120 s for import (00-shared/11 §5 )
4.2 Header row only (0 data rows) 400 "CSV must include a header row and data."
4.3 Malformed CSV (unbalanced quotes) 400 "Malformed CSV: could not parse file."
4.4 Missing required column per-row error "Missing required column "section"."; row number = index+2
4.5 Bad email format "Invalid email format."
4.6 Duplicate admission number in file (row 5 duplicates existing) row 5 failed "already exists"; other rows import
4.7 Duplicate email in file first row creates user; second row fails "already registered" (validate pre-check) — or falls back to existing user inside create if validate missed it (race)
4.8 Unknown academic year/grade/section per-row "Academic year "x" not found." / "Grade … not found." / "Section … not found."
4.9 Grade+section with no class "No class found for grade "g" section "s"."
4.10 Grade match by code grade matched on name or code
4.11 Row-level failure isolation one bad row never aborts the batch (try/catch per row)
4.12 Import from non-authorized user currently allowed (JWT-only) — verify when RBAC lands (OQ-4)
# Check
5.1 Cross-tenant :id on any endpoint → 404/403, no data leak (base.repository.ts:20-30)
5.2 tenantId never in request bodies from client
5.3 Duplicate email/user cross-tenant never resolves (users repo is tenant-scoped too)
5.4 uploadedBy/createdBy set from JWT, never from body
5.5 Soft-deleted students not returned by any scoped query
5.6 Parent link of tenant A student with tenant B parent → structurally impossible (both scoped)
Metric Budget
Roster first frame (cached) < 300 ms
Roster network content skeleton ≤ 200 ms, content ≤ 2 s (page 1, limit 20)
1000-row import completes; client timeout 120 s+; no UI freeze (parse on isolate)
Detail load profile immediate; tabs lazy (documents/history only on activation)
Scroll 0 dropped frames; ListView.builder + const tiles
Scenario Expected
Offline → roster cached rows + AppOfflineBanner; pull-refresh → Retry error state
Offline → create/transfer/upload/import/link blocked with guidance (no offline queue)
Network loss mid-upload DocumentUploadTile error + Retry; no silent corruption
Network loss mid-import request fails → wizard error; re-import is idempotent per-row (admission/email dedup)
Status badges: icon + text, never color-only (00-shared/09 §9 ).
Search results count in live region; "item X of Y" list position.
Wizard: focus first invalid field on Next; step index announced.
Import report: live-region summary "997 of 1000 imported".
Timeline: readable list semantics; badges announced.
Text scale 2×: no clipping in header card, tiles, report cards.
Dialogs trap focus; Esc/back closes; focus returns to trigger.
Dates via Intl (admissionDate, joinedAt/leftAt, createdAt); money none.
ADM numbers in mono, not localized.
RTL readiness (relationship chips, timelines mirror).
create → auto-enroll → history shows 1 ACTIVE.
enroll → old enrollment transferred + leftAt.
transfer on archived student → 409.
graduate → archive → restore sequence statuses.
delete → list excludes; detail 404.
import mixed file → report numbers + error strings.
link parent → student missing → 404; parent missing → no error today (OQ-11 — decide and test when resolved).
documents: upload → list order newest-first.
pagination: page 2 offset math (skip = (page-1)*limit, student.service.ts:104).
envelope: every response {success,message,data,meta?,timestamp,requestId}; every error carries code.
students.list.{view,search,filter,open}, students.create.{start,step,success,failure},
students.transfer.{submit,success,failure}, students.import.{template,upload,report},
students.document.{upload,open}, students.lifecycle.{graduate,archive,restore,delete} — per 00-shared/10 §8 .