Appearance
exam-service Agent Tasks
Agent workflow: follow README.md for Audit -> Investigate -> Code -> Test -> Fix; keep this pack's writable scope and verification commands authoritative.
Dispatch type: per-service
Dispatch ID: SVC-009
Owns: services/exam-service.
Must read:
docs/agents/handoff-phase-7-exam-snapshots.md- legacy exam authoring/publish/assignment modules:
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/exams/exam-core.service.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/exams/exams.controller.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/exams/exams.controller.spec.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/exams/exam-core.service.spec.ts.
Legacy source evidence:
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/exams/**- exam/assignment/share-link Prisma models in
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/prisma/schema.prisma.
Writable files:
services/exam-service/**contracts/openapi/services/exam-service.yamldocs/api/exam-service.md
Orchestrator-owned files:
- attempt-service runtime-start contracts, question-service source contracts, gateway route cutovers, and shared deploy manifests unless delegated.
Delegated gateway verification scope:
- Only when the orchestrator assigns an exam route proof subtask, this service agent may update non-default
deploy/gateway/routes.exam-*.jsonexamples,scripts/test/exam-*.sh,docs/qa/exam-*.md, and rootMakefiletest-exam-*targets. - Attempt-start route proof remains delegated through
exam-start-adapter.md; the default gateway route table remains orchestrator-owned until rollback evidence exists.
Completed foundation:
GET /v1/exams/search-projectionsexposes copiedEXAMsearch documents for search-service rebuilds. It remains tenant/actor scoped and redacts access password hashes and access-link codes.- Done: migration
000011_exam_search_projection_outbox.sqladds a dedicated monotonic per-exam/per-organization search revision and durable outbox. The owner trigger captures canonical exam writes plus copied question-snapshot, access-link, and assignment count changes, coalesces one tuple within a transaction, and writes a redactedsearch.projection.changed.v1envelope.CLOSEDremains anUPSERTbecause the owner rebuild includes closed exams; physical deletion emits aDELETEtombstone. This remains separate from the existingexam_outbox_eventsresult-release relay. - Done locally:
EXAM_SEARCH_PROJECTION_EVENT_TRANSPORT=natsstarts the dedicated JetStream publisher only when explicitly configured. Its dispatcher leases due rows, retries transient failures, marks terminal rows dead, and publishes the persisted envelope unchanged. The default transport is off. Existing publish/snapshot workflows can commit multiple canonical transactions, so the outbox guarantee is per committed mutation, not a claim that the complete workflow is a single transaction. - Native publish now optionally emits best-effort analytics-service producer events when
ANALYTICS_SERVICE_URLorANALYTICS_SERVICE_BASE_URLis configured. The event isexam.published.v1withsourceService=exam-service,sourceEventId={examId}:published, and a bounded exam/question/access-link summary payload that excludes access-link codes and password hashes. Analytics outages must not fail publish writes. - Native assignment now exposes internal
POST /v1/exams/{id}/assignmentsfor published exam -> classroom assignment upsert. It stores classroom public ids only, keeps classroom-manager validation/fanout outside exam-service, updates native assignment counts, and optionally emits best-effortexam.assigned.v1analytics events withsourceEventId={assignmentId}:assigned. WhenNOTIFICATION_SERVICE_URLorNOTIFICATION_SERVICE_BASE_URLis configured and the internal request supplies already-resolvednotification.recipients, it also best-effort emits notification-serviceexam.assigned.notification.v1events split byEXAM_ASSIGNEDandEXAM_ASSIGNED_PARENTpreference keys. Exam-service still does not discover classroom members, parents, or profile display names. - Native result release now exposes internal
POST /v1/exams/{id}/release-resultsfor published/closed exam result visibility release. It updates service-ownedresultsReleasedAt, keeps attempt/profile/score data outside exam-service, and optionally emits best-effortexam.results_released.v1analytics events with stablesourceEventId=exam:{examId}:results-released. The attempt handoff is now a separate durable relay: the first release writes anexam_outbox_eventsrecord in the same transaction asresultsReleasedAt, then the worker delivers it throughATTEMPT_SERVICE_URLwith the shared internal-service token. Failures back off without changing result visibility; retries retain the source event id until attempt-service accepts the event. Native release requiresADMIN/TEACHERactor headers and preserves the first release timestamp on duplicate requests; exact legacy org-member teacher parity remains a later gateway/IAM verifier slice before public route promotion. - attempt-service now has internal
POST /v1/events/exam-results-releasedto consume the release handoff and update copied attempt result-policy timestamps for already-started attempts. It accepts only authenticatedexam-servicetransport and remains replay safe; exam-service does not read or write attempt-service storage directly. - Runtime access can now wire production verifier clients with
IAM_SERVICE_URL/IAM_SERVICE_BASE_URLandCLASSROOM_SERVICE_URL/CLASSROOM_SERVICE_BASE_URL. Organization membership is checked through IAM's internal single-account membership decision endpoint; assigned classroom membership is checked through classroom-service using student actor headers. Exam-service still never joins IAM/classroom storage. GET /v1/admin/exams/summarynow provides the owner API for admin dashboard/source-map exam counts. It reports only exam-service local exam, assignment, and access-link row counts; attempt progress, scores, answers, and analytics remain attempt-service/analytics-service ownership.GET /v1/examsbatches access-link, assignment-count, and question-snapshot summary reads by its already owner-scoped exam IDs. The response preserves access-link and snapshot child ordering without cross-service joins, avoiding the former1 + 3Nlocal read pattern.- Done: DOCX print normalizes formula provenance from both direct-import fields and canonical Question Bank
formulaRefs[].sourceJson. A formula markedreviewRequiredis emitted as a visible review fallback rather than silently appearing as verified OMML; no public schema or cross-service database access was added. - Done locally: real DOCX
EXAM_DRAFTapproval created 22 snapshots, then exercised an IAM-verified student runtime decision for bothONLINE(allowed=true) andOFFLINE(EXAM_OFFLINE). Snapshot generation maps Go Formula DOCXcorrect_answer.labelstooptions.isCorrectand answer keys. Evidence and remaining parser review limits are indocs/qa/docx-fixture-persistence-runtime-proof.md. - Done:
POST /v1/examsnow accepts an optional owner-scopedIdempotency-Key.exam_create_idempotency_keysstores the canonical create request digest and draft ID in the same Exam transaction; an equal retry returns the original draft and a changed payload returns409. This is not a distributed transaction with Question Bank or Import. - Done:
exam_print_templatesis now a service-owned Word template configuration store with tenant/owner-scoped internal CRUD,ADMIN/TEACHERactor enforcement, a per-owner quota of 20, configuration-versioning, memory and Postgres stores, migration000006_exam_print_templates.sql, OpenAPI, and focused use-case/HTTP tests. It intentionally does not reuse legacyexams.paper_template_id. - Done:
exam_blueprintsandexam_blueprint_rulesnow persist a tenant-scoped matrix with full rule replacement, actor visibility (ADMINall, teacher own plus system), system-matrix protection, and a referenced-exam delete guard. It has native HTTP/use-case tests, migration000008_exam_blueprints.sql, mirrored OpenAPI, direct PostgreSQL workflow evidence, and a bounded BFF/Gateway teacher rehearsal.POST /v1/exam-blueprints/{id}/generatenow selects canonical published questions only through Question Bank's internal API, creates ONLINE/OFFLINE local drafts, and writes immutable snapshots without a cross-service DB read. A fresh isolated Physics 28-question BFF rehearsal also covered the 18/6/4 matrix, preserved 28 answer keys with 7 media/84 formula references, rendered the OFFLINE draft through a service-owned Word template, and rejected an unavailable rule before draft creation. The current rich-package recheck produced seven embedded images, seven drawings, 62 OMML formulas, and 22 visible formula-review fallbacks, preserving all 84 formula references. The follow-up source-keyed solution regression also retained all five non-empty Physics source solutions in Question Bank, both delivery snapshots, and the generated Word XML. It is local non-default BFF evidence only; seedocs/qa/docx-fixture-persistence-runtime-proof.md. - Done: internal
GET /v1/exams/{id}/print-docx?templateId=...renders a real OpenXML.docxdirectly from service-owned exam question snapshots and the caller-owned versioned template. It covers deterministic snapshot order, HTML-to-text normalization, title/header/footer, margin/font controls, optional answer/explanation appendix, response content type, and template id/version response headers. It also resolves snapshotmediaAssetIdvalues through document-service's token- and organization-scoped internal content contract, packages PNG/JPEG/GIF bytes intoword/media, and renders the supported LaTeX subset as OMML. Missing, cross-tenant, unsupported, or invalid media fails explicitly instead of being omitted. Focused unit and HTTP tests validate ZIP parts, image relationships, OMML, and rendered XML. This is an ephemeral internal download, not a public/api/exams/:id/paper-exportpromotion. - Done: internal
POST /v1/exams/{id}/print-exportsrecords aPENDINGexport with an immutable template config/version copy, renders from service-owned snapshots, and sends binary DOCX only to document-service's trusted artifact boundary. It uses the same scoped media resolver as the direct download, so a referenced image cannot silently disappear from a durable artifact. It transitions toCOMPLETEDwith document asset/object metadata, or persists boundedFAILEDdetail when storage rejects the upload.GETlists only the current owner and organization's records for a caller-owned exam. This remains an internal service surface, not a public route. - Done locally: the non-default teacher Word print gateway rehearsal now reaches BFF template/export adapters and preserves
routes.json. It has static and hermetic gateway proof, but not a live authenticated browser download or default-route promotion. - Done: versioned print-template config now supports the generic native answer sheet.
includeAnswerSheet=truerenders option labels, true/false boxes, or write-in lines from service-owned snapshots;answerSheetMode=separate-pageinserts a DOCX page break and other values fall back toafter-questions. - Remaining: template-to-exam binding semantics, browser UX, authenticated artifact retrieval proof, and exam blueprints/generation are separate vertical slices. The renderer still lacks full source-DOCX equation-layout parity, unsupported media conversions, legacy answer sheet variants, and multi-paper shuffle. Do not claim this slice as default public-route or blueprint parity.
Initial tasks:
- Complete access-link/share-link workflow tests and export slices.
- Verify publish snapshots preserve question content/options/scoring/media refs.
- Add gateway rehearsal for next safe exam route group only after parity evidence. The current authoring rehearsal is
make test-exam-authoring-routes; add a new named route-coverage target for any later route table this pack promotes. - Add folders, sections, share-link CRUD, export, and blueprint/generation task slices.
- Add an exam runtime snapshot/access-decision API that attempt-service can use for safe start orchestration.
- Done:
SVC-009-exam-attempt-public-proofexercised a seeded published exam through real local exam-service, Gatewayexam_start_adapter, and attempt-service with a strict IAM/classroom verifier fixture. It captured list/start/detail/save/event/submit/result, post-grade idempotency replay, Chromium same-origin API reads, and default legacy rollback. The run also fixed start/replay student-response redaction for option correctness, answer keys, scoring metadata, source snapshots, and unpublished explanations. This is not deployed verifier or student-UI proof; default start-route promotion remains gated.
Acceptance:
- Exam stores snapshots by value and never reads question-service DB.
- Draft-only edit and publish rules remain intact.
- Attempt start depends on published exam runtime snapshot only.
- Search projection transport retains canonical ownership in exam-service, redacts snapshots/access data, and remains opt-in; Search pull rebuild must not overwrite a tuple after it accepts a versioned event.
- Native attempt start is not promoted through gateway until assignment/link/ password/open-window decisions are supplied by exam-service or a verified gateway aggregation.
Verification:
GOTOOLCHAIN=go1.25.11 go test ./services/exam-service/...EXAM_SEARCH_PROJECTION_OUTBOX_POSTGRES_CONFIRM=disposable-postgres make test-exam-search-projection-outbox-postgresproves the dedicated owner transaction, redaction, revision ordering, rollback, and lease/retry/dead state against disposable PostgreSQL.EXAM_SEARCH_PROJECTION_BROKER_SMOKE_CONFIRM=disposable-runtime make test-exam-search-projection-broker-liveproves isolated Exam -> NATS JetStream -> Search delivery, subscriber restart, duplicate/stale replay,CLOSEDowner projection, and physical-delete tombstone. It is not a Gateway, browser, deployed-runtime, or default-route proof.GOTOOLCHAIN=go1.25.11 go test ./services/iam-service/... ./services/exam-service/... -count=1when runtime-access verifier contracts change.GOTOOLCHAIN=go1.25.11 go test ./services/exam-service/internal/usecase ./services/exam-service/internal/http -run 'TestExamSearchProjection|TestExamSearchProjectionRoute' -count=1 -vmake test-exam-authoring-routesmake test-teacher-exam-blueprint-bff-routes- On an explicitly isolated Compose stack with the temporary BFF listener:
DOCX_PHYSICS_EXAM_BLUEPRINT_ISOLATED_CONFIRM=docx-physics-exam-blueprint-isolated make test-docx-physics-exam-blueprint-isolated. It imports the real Physics 28-question fixture, requires the 18/6/4 matrix type split, verifies immutable online/offline snapshots plus seven package images and every formula as OMML or a visible fallback, and rejects an unavailable rule before draft creation. It is not a default Gateway or browser-promotion check. DOCX_PHYSICS_EXAM_BLUEPRINT_ISOLATED_SELF_TEST=1 make test-docx-physics-exam-blueprint-isolatedvalidates the disposable Compose port/dependency boundary without starting Docker.DOCX_PHYSICS_EXAM_BLUEPRINT_LOCAL_POSTGRES_CONFIRM=local-postgres make test-docx-physics-exam-blueprint-local-postgresis the Docker-free owner/BFF proof. It starts fresh Document, Import, Question Bank, Exam, BFF, and mock AI services on loopback with distinct temporary PostgreSQL databases, then checks the real Physics import, online/offline matrix snapshots, owned-media Word export, and reviewed AI writes. It is not a default Gateway or browser promotion proof; Document's object bytes are deliberately in-memory.DOCX_CORPUS_EXAM_BLUEPRINT_LOCAL_POSTGRES_CONFIRM=local-postgres make test-docx-corpus-exam-blueprint-local-postgresrepeats the owner/BFF handoff for Math, English, and DGNL review-safe subsets. It locks the parser and subset baselines at22 -> 13,40 -> 34, and102 -> 37, verifies anEXAM_DRAFTfail-closed precondition and replay per fixture, then compares canonical Question Bank rows with direct plusONLINE/OFFLINEsnapshots and document-service Word export metadata/checksum. The harness uses fresh loopback listeners and temporary owner databases only; default Gateway, browser, provider, and production claims remain outside scope.docs/qa/exam-authoring-route-rehearsals.mdrecords the current proof gate: static route-table coverage is not public promotion evidence until a later live/browser smoke captures real teacher/admin authoring traffic through the gateway.- Before any route promotion that affects attempt start:
GOTOOLCHAIN=go1.25.11 go test ./services/exam-service/... ./services/attempt-service/...andmake test-attempt-routes - Gateway rehearsal for exam-only routes must include the new named route target added by that slice.