3 Commits

Author SHA1 Message Date
CC Worker
c0458f5528 S6-1: /api/exam analyse endpoint — structure → ghost-region suggestions (async)
The canvas "Auto-detect" path (spec R3.1/R3.3/R3.5/R5.4). Async because extraction on
an image-only paper takes ~15-60s (Card-12 grounding):

  POST /api/exam/templates/{id}/analyse -> {job_id}   (BackgroundTasks)
  GET  /api/exam/jobs/{id}              -> {status, counts, suggestions}

suggestions = questions + response_areas + boundaries, each source:'ai',confirmed:false
with confidence + page-fraction bounds + deterministic uuids — rendered as ghosts the
teacher confirms/dismisses; never overwrites manual shapes; persistence stays PUT /templates/{id}.

Access is as-the-user (E1/E2): the template is read under RLS (unseen -> 404, IDOR-safe)
and a job is only visible to the user who started it.

The heavy pipeline stays OUT of the API behind modules/services/exam_extract.py — an HTTP
client (aiohttp) to the exam-structure extraction service (the docling-exam-spike pipeline,
GPU-attached, deterministic), mirroring how the app calls Docling Serve/Ollama. Configured
via EXAM_EXTRACT_URL; unset -> the job fails cleanly as 'extraction unavailable'.

Follow-on (S6-1b): stand up that extraction service (POST /api/extract: source -> suggestions)
wrapping scripts/structure.py + scripts/analyse.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 19:45:26 +00:00
CC Worker
5ad9c01cde feat(exam): batches, scans, marks, results, CSV (S4-6)
Adds routers/exam/batches.py (mounted alongside templates under /api/exam):
- POST/GET /batches — batch creation seeds the cohort from class_students AS
  THE USER (cs_read requires caller teaches/admins the class); each active
  enrollee becomes a student_submissions row (status='absent') so no student
  is ever dropped from results (A7). Display names denormalised via a
  documented service-role profiles read (deny-all as-user, E4).
- GET /batches/{id}/queue — submissions + per-submission mark counts + progress.
- GET /batches/{id}/results + /csv — every roster student incl. absent (blank
  marks/total); CSV row always present (A7 baked into the contract).
- PUT /marks/{id} — upsert; batch_id derived server-side from the submission
  (client never supplies the RLS scoping key).
- POST /batches/{id}/scans — E3 guards: MIME check, hard size ceiling (chunked
  read), %PDF magic-byte sniff; owner-only; stores via service-role storage;
  manual/ordered matching (QR-decode is a follow-on, no QR fixtures yet).

Unit tests cover batch/roster-seed/list, queue, results+CSV A7, mark upsert
round-trip, and all scan guards + owner check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 18:40:10 +00:00
CC Worker
f52c3267ca feat(exam): /api/exam template CRUD router (as-user RLS, E1 fix)
S4-5: new routers/exam/ package mounted at /api/exam (R5.1/E5, not under
/database/). Template CRUD with hybrid persistence (R5.2):

- POST/GET/GET{id}/PUT{id}/DELETE{id} /templates + PATCH /questions/{qid}
- Calls Supabase AS THE USER via SupabaseAnonClient.for_user (E1 fix), so the
  RLS in 72-exam-marker.sql is enforced; no service-role for user-facing ops.
- Institute resolved/validated via the user_institute_ids() SECURITY DEFINER
  RPC (institute_memberships is deny-all as-user per E4); client-supplied
  institute_id is validated, never trusted (R5.5).
- Ownership pre-checked before writes (E2); out-of-scope ids read back as 404
  under RLS (IDOR-safe). Soft-delete archives, never hard-deletes.
- PUT full-replace preserves client UUIDs as Neo4j join keys (spec §2).
- eb_exams.exam_code denormalised via a documented service-role catalogue
  lookup (eb_exams is shared reference data, deny-all as-user per E4).

Unit tests cover auth, CRUD, ownership/IDOR, institute validation, soft-delete.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 17:49:58 +00:00