Skip to main content

Events

:::info Source Sourced from services/assessment-service/EVENT_SCHEMAS.md in the documentation repo. :::

Transport: NATS JetStream stream ASSESSMENT per doc 04 §2 Envelope: doc 04 §4 EventEnvelope<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)

SubjectRetentionTriggerConsumers
assessment.quiz_bank.created.v1RegulatedUC-01analytics, search, authoring-read
assessment.quiz_bank.updated.v1RegulatedUC-02, UC-03, UC-04same
assessment.quiz_bank.published.v1RegulatedUC-05delivery (cache prewarm), content-service (bundle build), search
assessment.quiz_bank.archived.v1RegulatedUC-06delivery, search
assessment.scenario.created.v1RegulatedUC-11analytics, search
assessment.scenario.updated.v1RegulatedUC-12same
assessment.scenario.published.v1RegulatedUC-13delivery, content, search
assessment.attempt_result.scored.v1RegulatedUC-09, UC-10, UC-17, UC-18, UC-19progress (LRS), analytics, certification (indirect via progress), sync
assessment.attempt.pending_human_review.v1OperationalUC-09, UC-17notification, analytics
assessment.short_answer.ai_graded.v1AuditUC-17analytics, audit-log
assessment.attempt.superseded.v1RegulatedUC-19progress, certification, analytics
assessment.score_mismatch_detected.v1AuditUC-10analytics, compliance queue
assessment.branching_scenario.completed.v1RegulatedUC-09 (scenario path)progress, analytics

2. Consumed events (inbound)

SubjectConsumer typeUse-caseIdempotency key
authoring.block.added.v1DurableUC-20 (link bank to block)eventId
authoring.block.updated.v1DurableUC-20 (update linkage)eventId
authoring.block.removed.v1DurableUC-20 (unlink)eventId
delivery.play_session.started.v1Ephemeral (fan-out)UC-22 (preload cache)N/A
gdpr.subject_request.received.v1DurableUC-21 (redact learner responses)eventId
tenant.offboarded.v1DurableUC-21 (purge tenant data)eventId
sync.offline_artifact.received.v1DurableUC-10(attemptId, clientMutationId)
ai.grading.completed.v1Durable (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).
  • userId is 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 with userId → anonymized and responses[].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 typeAllowed without new vN?Procedure
Add optional field✅ YesBump minor; update schemaUri content hash
Remove any field❌ NoNew v2 + dual-publish window (minimum 1 release cycle)
Narrow enum❌ NoNew v2
Widen enum⚠️ With cautionOnly if consumers use exhaustive-default; prefer new v2
Tighten type❌ NoNew v2
Rename field❌ NoNew 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-pagerduty critical if attempt_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/):

ConsumerEvents consumedContract file
progress-serviceattempt_result.scored.v1, attempt.superseded.v1/contracts/assessment/consumers/progress-service.pact.json
certification-service(indirect) — consumes progress events
analytics-serviceall regulated + audit events/contracts/assessment/consumers/analytics-service.pact.json
search-servicequiz_bank.published.v1, scenario.published.v1/contracts/assessment/consumers/search-service.pact.json
notification-serviceattempt.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 timestamp or seq via nats consumer reset.
  • Replay is idempotent because every handler uses inbox_events dedup on eventId.
  • Full rebuild of progress-service state from assessment.attempt_result.scored.v1 is a supported recovery path (documented in progress-service runbook).

9. References