Skip to content

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.yaml
  • docs/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-*.json examples, scripts/test/exam-*.sh, docs/qa/exam-*.md, and root Makefiletest-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-projections exposes copied EXAM search 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.sql adds 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 redacted search.projection.changed.v1 envelope. CLOSED remains an UPSERT because the owner rebuild includes closed exams; physical deletion emits a DELETE tombstone. This remains separate from the existing exam_outbox_events result-release relay.
  • Done locally: EXAM_SEARCH_PROJECTION_EVENT_TRANSPORT=nats starts 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_URL or ANALYTICS_SERVICE_BASE_URL is configured. The event is exam.published.v1 with sourceService=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}/assignments for 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-effort exam.assigned.v1 analytics events with sourceEventId={assignmentId}:assigned. When NOTIFICATION_SERVICE_URL or NOTIFICATION_SERVICE_BASE_URL is configured and the internal request supplies already-resolved notification.recipients, it also best-effort emits notification-service exam.assigned.notification.v1 events split by EXAM_ASSIGNED and EXAM_ASSIGNED_PARENT preference 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-results for published/closed exam result visibility release. It updates service-owned resultsReleasedAt, keeps attempt/profile/score data outside exam-service, and optionally emits best-effort exam.results_released.v1 analytics events with stable sourceEventId=exam:{examId}:results-released. The attempt handoff is now a separate durable relay: the first release writes an exam_outbox_events record in the same transaction as resultsReleasedAt, then the worker delivers it through ATTEMPT_SERVICE_URL with 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 requires ADMIN/TEACHER actor 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-released to consume the release handoff and update copied attempt result-policy timestamps for already-started attempts. It accepts only authenticated exam-service transport 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_URL and CLASSROOM_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/summary now 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/exams batches 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 former 1 + 3N local read pattern.
  • Done: DOCX print normalizes formula provenance from both direct-import fields and canonical Question Bank formulaRefs[].sourceJson. A formula marked reviewRequired is 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_DRAFT approval created 22 snapshots, then exercised an IAM-verified student runtime decision for both ONLINE (allowed=true) and OFFLINE (EXAM_OFFLINE). Snapshot generation maps Go Formula DOCX correct_answer.labels to options.isCorrect and answer keys. Evidence and remaining parser review limits are in docs/qa/docx-fixture-persistence-runtime-proof.md.
  • Done: POST /v1/exams now accepts an optional owner-scoped Idempotency-Key. exam_create_idempotency_keys stores the canonical create request digest and draft ID in the same Exam transaction; an equal retry returns the original draft and a changed payload returns 409. This is not a distributed transaction with Question Bank or Import.
  • Done: exam_print_templates is now a service-owned Word template configuration store with tenant/owner-scoped internal CRUD, ADMIN/TEACHER actor enforcement, a per-owner quota of 20, configuration-versioning, memory and Postgres stores, migration 000006_exam_print_templates.sql, OpenAPI, and focused use-case/HTTP tests. It intentionally does not reuse legacy exams.paper_template_id.
  • Done: exam_blueprints and exam_blueprint_rules now persist a tenant-scoped matrix with full rule replacement, actor visibility (ADMIN all, teacher own plus system), system-matrix protection, and a referenced-exam delete guard. It has native HTTP/use-case tests, migration 000008_exam_blueprints.sql, mirrored OpenAPI, direct PostgreSQL workflow evidence, and a bounded BFF/Gateway teacher rehearsal. POST /v1/exam-blueprints/{id}/generate now 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; see docs/qa/docx-fixture-persistence-runtime-proof.md.
  • Done: internal GET /v1/exams/{id}/print-docx?templateId=... renders a real OpenXML .docx directly 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 snapshot mediaAssetId values through document-service's token- and organization-scoped internal content contract, packages PNG/JPEG/GIF bytes into word/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-export promotion.
  • Done: internal POST /v1/exams/{id}/print-exports records a PENDING export 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 to COMPLETED with document asset/object metadata, or persists bounded FAILED detail when storage rejects the upload. GET lists 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=true renders option labels, true/false boxes, or write-in lines from service-owned snapshots; answerSheetMode=separate-page inserts a DOCX page break and other values fall back to after-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-proof exercised a seeded published exam through real local exam-service, Gateway exam_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-postgres proves 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-live proves isolated Exam -> NATS JetStream -> Search delivery, subscriber restart, duplicate/stale replay, CLOSED owner 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=1 when 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 -v
  • make test-exam-authoring-routes
  • make 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-isolated validates 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-postgres is 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-postgres repeats the owner/BFF handoff for Math, English, and DGNL review-safe subsets. It locks the parser and subset baselines at 22 -> 13, 40 -> 34, and 102 -> 37, verifies an EXAM_DRAFT fail-closed precondition and replay per fixture, then compares canonical Question Bank rows with direct plus ONLINE/OFFLINE snapshots 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.md records 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/... and make test-attempt-routes
  • Gateway rehearsal for exam-only routes must include the new named route target added by that slice.

Go-platform documentation is generated from repository Markdown.