Events
:::info Source
Sourced from services/assessment-service/EVENT_SCHEMAS.md in the documentation repo.
:::
Transport: NATS JetStream stream
ASSESSMENTper doc 04 §2 Envelope: doc 04 §4EventEnvelope<T>Retention: Regulated class — 180d hot / 7y cold Schema registry:schemas://assessment/…
All events use the mandatory envelope; this doc specifies payloads only.
1. Published events (outbound)
| Subject | Retention | Trigger | Consumers |
|---|---|---|---|
assessment.quiz_bank.created.v1 | Regulated | UC-01 | analytics, search, authoring-read |
assessment.quiz_bank.updated.v1 | Regulated | UC-02, UC-03, UC-04 | same |
assessment.quiz_bank.published.v1 | Regulated | UC-05 | delivery (cache prewarm), content-service (bundle build), search |
assessment.quiz_bank.archived.v1 | Regulated | UC-06 | delivery, search |
assessment.scenario.created.v1 | Regulated | UC-11 | analytics, search |
assessment.scenario.updated.v1 | Regulated | UC-12 | same |
assessment.scenario.published.v1 | Regulated | UC-13 | delivery, content, search |
assessment.attempt_result.scored.v1 | Regulated | UC-09, UC-10, UC-17, UC-18, UC-19 | progress (LRS), analytics, certification (indirect via progress), sync |
assessment.attempt.pending_human_review.v1 | Operational | UC-09, UC-17 | notification, analytics |
assessment.short_answer.ai_graded.v1 | Audit | UC-17 | analytics, audit-log |
assessment.attempt.superseded.v1 | Regulated | UC-19 | progress, certification, analytics |
assessment.score_mismatch_detected.v1 | Audit | UC-10 | analytics, compliance queue |
assessment.branching_scenario.completed.v1 | Regulated | UC-09 (scenario path) | progress, analytics |
2. Consumed events (inbound)
| Subject | Consumer type | Use-case | Idempotency key |
|---|---|---|---|
authoring.block.added.v1 | Durable | UC-20 (link bank to block) | eventId |
authoring.block.updated.v1 | Durable | UC-20 (update linkage) | eventId |
authoring.block.removed.v1 | Durable | UC-20 (unlink) | eventId |
delivery.play_session.started.v1 | Ephemeral (fan-out) | UC-22 (preload cache) | N/A |
gdpr.subject_request.received.v1 | Durable | UC-21 (redact learner responses) | eventId |
tenant.offboarded.v1 | Durable | UC-21 (purge tenant data) | eventId |
sync.offline_artifact.received.v1 | Durable | UC-10 | (attemptId, clientMutationId) |
ai.grading.completed.v1 | Durable (callback) | UC-17 (async AI response) | (attemptId, questionId, gradingRequestId) |
3. Payload schemas (JSON Schema draft-2020-12 style)
3.1 assessment.quiz_bank.published.v1
{
"$id": "schemas://assessment/quiz_bank/published/v1",
"type": "object",
"required": ["quizBankId", "tenantId", "version", "courseVersionId", "questionCount", "gradingRule"],
"properties": {
"quizBankId": { "type": "string", "pattern": "^qbk_[0-9A-HJKMNP-TV-Z]{26}$" },
"tenantId": { "type": "string", "pattern": "^tnt_" },
"version": { "type": "integer", "minimum": 1 },
"courseVersionId":{ "type": "string" },
"title": { "type": "object" },
"questionCount": { "type": "integer", "minimum": 1 },
"gradingRule": {
"type": "object",
"required": ["passThreshold"],
"properties": {
"passThreshold": { "type": "number", "minimum": 0, "maximum": 1 },
"wrongPenalty": { "type": "number", "minimum": 0, "maximum": 1 },
"partialCreditDefault": { "enum": ["none", "proportional", "all_or_nothing"] },
"showCorrectAnswers": { "enum": ["never", "after_attempt", "after_close"] },
"maxAttempts": { "type": "integer", "minimum": 1 }
}
},
"publishedAt": { "type": "string", "format": "date-time" },
"publishedBy": { "type": "string", "pattern": "^usr_" },
"aiProvenance": { "$ref": "schemas://common/ai_provenance/v1" }
},
"additionalProperties": false
}
Partitioning: partitionKey = quizBankId → consumer ordering per bank.
3.2 assessment.attempt_result.scored.v1
The highest-stakes event this service emits. Drives LRS, certification, analytics.
{
"$id": "schemas://assessment/attempt_result/scored/v1",
"type": "object",
"required": [
"attemptId", "tenantId", "userId", "scaledScore", "passed",
"scoringMode", "rawScore", "maxScore", "startedAt", "scoredAt",
"state"
],
"properties": {
"attemptId": { "type": "string", "pattern": "^att_" },
"tenantId": { "type": "string", "pattern": "^tnt_" },
"userId": { "type": "string", "pattern": "^usr_" },
"quizBankId": { "type": "string", "pattern": "^qbk_" },
"scenarioId": { "type": "string", "pattern": "^scn_" },
"courseVersionId":{ "type": "string" },
"quizBankVersion":{ "type": "integer" },
"rawScore": { "type": "number" },
"maxScore": { "type": "number", "minimum": 0 },
"scaledScore": { "type": "number", "minimum": 0, "maximum": 1 },
"passed": { "type": "boolean" },
"durationSeconds":{ "type": "integer", "minimum": 0 },
"startedAt": { "type": "string", "format": "date-time" },
"scoredAt": { "type": "string", "format": "date-time" },
"scoringMode": { "enum": ["deterministic", "ai_graded", "mixed"] },
"offlineScored": { "type": "boolean" },
"state": { "enum": ["final", "pending_human_review", "superseded"] },
"regradeOf": { "type": "string", "pattern": "^att_" },
"responses": {
"type": "array",
"items": { "$ref": "#/$defs/responseSummary" }
},
"aiProvenance": { "$ref": "schemas://common/ai_provenance/v1" },
"integrityFlags": {
"type": "array",
"items": { "enum": ["nav_lock_bypassed", "tab_switched", "dev_tools_open", "time_anomaly"] }
},
"scoreReconciliation": {
"type": "object",
"properties": {
"clientScaledScore": { "type": "number" },
"serverScaledScore": { "type": "number" },
"diffAbs": { "type": "number" },
"mismatch": { "type": "boolean" },
"resolution": { "enum": ["server_wins", "equal"] }
}
}
},
"oneOf": [
{ "required": ["quizBankId"] },
{ "required": ["scenarioId"] }
],
"$defs": {
"responseSummary": {
"type": "object",
"properties": {
"questionId": { "type": "string", "pattern": "^qst_" },
"kind": { "type": "string" },
"pointsEarned": { "type": "number" },
"pointsPossible": { "type": "number" },
"correct": { "enum": [true, false, "partial", "pending"] },
"durationMs": { "type": "integer" },
"gradedBy": { "enum": ["deterministic", "ai", "human"] },
"aiConfidence": { "type": "number", "minimum": 0, "maximum": 1 }
}
}
},
"additionalProperties": false
}
PII rules:
- Does not contain raw response text for short-answer items (that stays in service DB and is only sent to progress-service via a hashed content reference).
- Does not contain correct-answer keys (even conceptually — this is a post-hoc summary, not a presentation form).
userIdis the internal ULID; identity-service is the only service that can resolve it to PII.
Ordering: partitionKey = attemptId. Consumers process per-attempt in order.
3.3 assessment.attempt.pending_human_review.v1
{
"$id": "schemas://assessment/attempt/pending_human_review/v1",
"type": "object",
"required": ["attemptId", "tenantId", "userId", "pendingResponses", "createdAt"],
"properties": {
"attemptId": { "type": "string" },
"tenantId": { "type": "string" },
"userId": { "type": "string" },
"pendingResponses": {
"type": "array",
"items": {
"type": "object",
"required": ["questionId", "aiConfidence"],
"properties": {
"questionId": { "type": "string" },
"aiConfidence": { "type": "number" },
"reason": { "enum": ["below_threshold", "prompt_injection_suspected", "rubric_ambiguous"] }
}
}
},
"slaDueAt": { "type": "string", "format": "date-time" },
"assignedQueue": { "type": "string" },
"createdAt": { "type": "string", "format": "date-time" }
}
}
Triggers notification-service to alert reviewer queues.
3.4 assessment.short_answer.ai_graded.v1
Audit-class event — records every AI grading decision for compliance review.
{
"$id": "schemas://assessment/short_answer/ai_graded/v1",
"type": "object",
"required": ["attemptId", "questionId", "aiProvenance", "grade"],
"properties": {
"attemptId": { "type": "string" },
"questionId": { "type": "string" },
"tenantId": { "type": "string" },
"gradingRequestId": { "type": "string" },
"aiProvenance": { "$ref": "schemas://common/ai_provenance/v1" },
"grade": {
"type": "object",
"required": ["pointsEarned", "pointsPossible", "confidence"],
"properties": {
"pointsEarned": { "type": "number" },
"pointsPossible": { "type": "number" },
"confidence": { "type": "number" },
"rubricBreakdown": { "type": "object", "additionalProperties": { "type": "number" } },
"rationale": { "type": "string" },
"humanReviewRequired": { "type": "boolean" }
}
},
"responseFingerprint": { "type": "string", "description": "SHA-256 of the learner response text for traceability without storing the text on this event" }
}
}
Retained 7 years (regulated) for AI Act audit trail.
3.5 assessment.branching_scenario.completed.v1
{
"$id": "schemas://assessment/branching_scenario/completed/v1",
"type": "object",
"required": ["attemptId", "scenarioId", "pathNodeIds", "scaledScore", "passed", "terminalClassification"],
"properties": {
"attemptId": { "type": "string" },
"scenarioId": { "type": "string" },
"tenantId": { "type": "string" },
"userId": { "type": "string" },
"pathNodeIds": { "type": "array", "items": { "type": "string" } },
"pathChoiceIds": { "type": "array", "items": { "type": "string" } },
"scaledScore": { "type": "number" },
"passed": { "type": "boolean" },
"terminalClassification": { "enum": ["pass", "fail", "conditional"] },
"durationSeconds": { "type": "integer" },
"completedAt": { "type": "string", "format": "date-time" }
}
}
Paired with (not replacing) attempt_result.scored.v1 — the scenario event is analytics-friendly, the attempt-result event is LRS-canonical.
3.6 assessment.score_mismatch_detected.v1
Audit event raised when offline client score disagrees with server score beyond tolerance.
{
"$id": "schemas://assessment/score_mismatch_detected/v1",
"type": "object",
"required": ["attemptId", "diffAbs", "clientScaledScore", "serverScaledScore", "detectedAt"],
"properties": {
"attemptId": { "type": "string" },
"tenantId": { "type": "string" },
"clientScaledScore": { "type": "number" },
"serverScaledScore": { "type": "number" },
"diffAbs": { "type": "number" },
"toleranceAbs": { "type": "number" },
"suspectedCause": { "enum": ["clock_skew", "stale_bundle", "tampering_suspected", "bug"] },
"detectedAt": { "type": "string", "format": "date-time" },
"deviceId": { "type": "string" }
}
}
4. Consumed event contracts
4.1 authoring.block.added.v1 (subset we care about)
{
"block": {
"kind": "quiz",
"blockRef": { "quizBankId": "qbk_01HN…" }
}
}
Handler (UC-20) links the authoring block to the quiz bank, creating a bidirectional reference for bundle build and analytics rollups.
4.2 sync.offline_artifact.received.v1
{
"artifact": {
"type": "attempt_result_claim",
"attemptId": "att_01HN…",
"clientMutationId": "cmu_01HN…",
"payload": { /* ClaimedAttempt */ }
}
}
Handler (UC-10) invokes reconciliation.
4.3 gdpr.subject_request.received.v1
{
"subjectType": "user",
"userId": "usr_01HN…",
"tenantId": "tnt_01HN…",
"requestType": "erasure" | "export" | "rectification",
"requestId": "gdpr_01HN…",
"deadline": "2025-05-15T00:00:00Z"
}
Handler (UC-21):
- For
erasure: scrub response text, regenerate attempt result withuserId → anonymizedandresponses[].text = null. - For
export: emit subject's attempts to sync-service's export channel. - For
rectification: only applicable to author metadata; rejected otherwise.
5. Schema evolution rules
| Change type | Allowed without new vN? | Procedure |
|---|---|---|
| Add optional field | ✅ Yes | Bump minor; update schemaUri content hash |
| Remove any field | ❌ No | New v2 + dual-publish window (minimum 1 release cycle) |
| Narrow enum | ❌ No | New v2 |
| Widen enum | ⚠️ With caution | Only if consumers use exhaustive-default; prefer new v2 |
| Tighten type | ❌ No | New v2 |
| Rename field | ❌ No | New v2 |
Dual-publish window: producers emit both v1 and v2 for ≥ 1 release cycle; consumers migrate; then v1 is deprecated, next release drops it. See doc 04 §9.
6. Dead-letter queue policy
- DLQ subject:
ASSESSMENT.dlq - Max redelivery: 5 with exponential backoff
1s, 5s, 25s, 2m, 10m. - On DLQ entry: alert
assessment-pagerdutycritical ifattempt_result.scored.v1(LRS impact); warn otherwise. - DLQ inspection tool exposed at
/admin/dlq(platform-admin only).
7. Pact contracts (consumer-driven)
Registered consumers (each has a Pact file pinned in /contracts/assessment/consumers/):
| Consumer | Events consumed | Contract file |
|---|---|---|
| progress-service | attempt_result.scored.v1, attempt.superseded.v1 | /contracts/assessment/consumers/progress-service.pact.json |
| certification-service | (indirect) — consumes progress events | — |
| analytics-service | all regulated + audit events | /contracts/assessment/consumers/analytics-service.pact.json |
| search-service | quiz_bank.published.v1, scenario.published.v1 | /contracts/assessment/consumers/search-service.pact.json |
| notification-service | attempt.pending_human_review.v1 | /contracts/assessment/consumers/notification-service.pact.json |
CI pipeline fails if a change breaks any registered consumer's pact.
8. Replay & backfill
- Consumers can be reset to
timestamporseqvianats consumer reset. - Replay is idempotent because every handler uses
inbox_eventsdedup oneventId. - Full rebuild of
progress-servicestate fromassessment.attempt_result.scored.v1is a supported recovery path (documented in progress-service runbook).
9. References
- Platform event doc: docs/04-event-driven-architecture.md
- AI provenance schema:
schemas://common/ai_provenance/v1(shared across services) - Application-layer producers/consumers: APPLICATION_LOGIC.md