* fix(exercise): persist completion policy and pass threshold on save buildExerciseUpdateFields accepted completionPolicy and passThreshold in the validation schema but never mapped them to the database update, so exercise settings appeared to save while reverting on refresh. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(db): build workspace deps before compiling @cio/db imports @cio/utils/notifications, which only exists after @cio/utils is built. Building db in isolation failed when utils dist was missing or stale. - Add a db prebuild step for question-types, utils, and email - Clean dist and tsbuildinfo before utils and email builds so incremental tsc cannot skip emission after dist was removed Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * chore(dev): build api workspace deps before api:dev Run turbo build for @cio/api and @cio/jobs-worker dependencies before starting the dev servers, so missing dist/ folders do not break startup. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * feat(course): improve exercise progression grading UX --------- Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
@cio/question-types
Shared, framework-agnostic question type contracts for ClassroomIO exercise products.
This package is the source of truth for:
- Question type keys and DB id mappings
- Type metadata and capability flags
- Stable render contracts and list contracts
- Answer payload and serialization helpers
- Renderer registry helpers (without UI implementation)
- Form state primitives for question flows
It is intentionally pure TypeScript and contains no UI components.
Why this package exists
@cio/question-types centralizes exercise domain contracts so every app/package can use the same behavior:
apps/dashboarduses the same question keys/types as API and DB consumersapps/apican rely on stable question type IDs@cio/uican implement renderers against one shared contract- Future products (for example, forms/assessments) can reuse the same domain model
Package boundary
What belongs here:
- Domain constants and type-level contracts
- Pure mapping/lookup helpers
- Deterministic utilities (same input => same output)
- Serialization rules for answer payloads
What does not belong here:
- Svelte/React/Vue components
- Package-specific UI behavior
- Network calls
- DB query implementations
Architecture
flowchart LR
subgraph Product Apps
DASH[apps/dashboard]
API[apps/api]
FUTURE[future app e.g. forms]
end
subgraph Shared Packages
QT[@cio/question-types\ncontracts + registry + serializer]
UI[@cio/ui/custom/exercise-question\nrendering implementation]
DB[(DB question_types ids)]
end
DASH -->|question models, keys, lookups| QT
FUTURE -->|same contracts| QT
UI -->|imports types + registry helpers| QT
DASH -->|renders via props/contracts| UI
API -->|imports QUESTION_TYPE_IDS| QT
QT -->|stable id <-> key mapping| DB
DASH -->|answersByKey| QT
QT -->|serializeExerciseAnswerMap| API
Source layout
packages/question-types/src/
answer-serializer.ts
exercise-types.ts
form-state.ts
index.ts
question-answer-payload.ts
question-scoring-rules.ts
question-type-capabilities.ts
question-type-keys.ts
question-type-lookup.ts
question-type-registry.ts
question-type-types.ts
render-contract.ts
renderer-registry.ts
Supported question types
Registry source: src/question-type-registry.ts
| key | id | label | autoGradable | supportsPartialCredit | manualGradingRequired |
|---|---|---|---|---|---|
RADIO |
1 | Single answer | yes | no | no |
CHECKBOX |
2 | Multiple answers | yes | yes | no |
TEXTAREA |
3 | Paragraph | no | no | yes |
TRUE_FALSE |
4 | True/False | yes | no | no |
SHORT_ANSWER |
5 | Short answer | no | no | yes |
NUMERIC |
6 | Numeric answer | yes | yes | no |
FILL_BLANK |
7 | Fill in the blank | yes | yes | no |
FILE_UPLOAD |
8 | File upload | no | no | yes |
MATCHING |
9 | Matching | yes | yes | no |
ORDERING |
10 | Ordering | yes | yes | no |
HOTSPOT |
11 | Hotspot | yes | yes | no |
LINK |
12 | Links | no | no | yes |
Public API overview
1) Keys and metadata
QUESTION_TYPE_KEYQuestionTypeKeyQUESTION_TYPE_REGISTRYQUESTION_TYPE_IDSQUESTION_TYPE_BY_KEYQUESTION_TYPE_TYPENAME_TO_KEYQUESTION_TYPE_ID_TO_KEY
Lookup helpers:
getQuestionTypeByKey(key)getQuestionTypeByTypename(typename)getQuestionTypeById(id)getQuestionTypeId(key)
Capability helpers:
isAutoGradableQuestionType(key)supportsPartialCredit(key)requiresManualGrading(key)
2) Exercise rendering contracts
Core types (src/exercise-types.ts):
ExerciseQuestionModelExerciseQuestionOptionExerciseAnswerValueExerciseRendererMode(edit | take | preview | view)ExerciseRendererDefinition<TRenderer>ExerciseQuestionRendererProps
Render contracts (src/render-contract.ts):
ExerciseQuestionRenderContractExerciseQuestionListRenderContractExerciseQuestionImageUploadergetExerciseQuestionContractKey(question, fallbackIndex)
3) Renderer registry helpers (UI-agnostic)
From src/renderer-registry.ts:
ExerciseRendererRegistry<TRenderer>getRendererDefinition(registry, questionType, fallback)getRendererForMode(registry, questionType, mode, fallback)
Important behavior:
mode: 'view'resolves to'take'internally ingetRendererForMode.
4) Answer payload and serialization
Answer payload union (src/question-answer-payload.ts):
QuestionAnswerPayloadand per-type payload interfaces (RadioAnswerPayload,CheckboxAnswerPayload, etc.)
Serializer (src/answer-serializer.ts):
serializeExerciseAnswer(question, answer)serializeExerciseAnswerMap(questions, answersByKey)SerializedExerciseAnswer
5) Form state and scoring constants
Form state (src/form-state.ts):
ExerciseQuestionFormStatecreateExerciseQuestionFormState(initialQuestions)getQuestionStateKey(question, fallbackIndex)
Scoring constants (src/question-scoring-rules.ts):
QUESTION_SCORING_MODEATTEMPT_SCORING_RULESHOW_CORRECT_ANSWERS_POLICY
Deterministic contract guarantees
The following helpers are deterministic and should be treated as stable contracts:
getExerciseQuestionContractKeygetQuestionStateKeygetQuestionTypeById/getQuestionTypeIdgetRendererForModeserializeExerciseAnswer/serializeExerciseAnswerMap
If given the same inputs, they always return the same outputs.
Usage examples
Resolve question type from id
import { getQuestionTypeById } from '@cio/question-types';
const metadata = getQuestionTypeById(10);
// => { key: 'ORDERING', label: 'Ordering', ... }
Build a renderer registry in another package
import {
QUESTION_TYPE_KEY,
getRendererForMode,
type ExerciseRendererRegistry,
type ExerciseRendererDefinition
} from '@cio/question-types';
type Renderer = unknown;
const registry: ExerciseRendererRegistry<Renderer> = {
[QUESTION_TYPE_KEY.RADIO]: {
edit: 'RadioEditRenderer',
take: 'RadioTakeRenderer',
preview: 'RadioPreviewRenderer'
}
};
const fallback: ExerciseRendererDefinition<Renderer> = {
edit: 'FallbackEdit',
take: 'FallbackTake',
preview: 'FallbackPreview'
};
const renderer = getRendererForMode(registry, QUESTION_TYPE_KEY.RADIO, 'view', fallback);
// 'view' resolves to 'take'
Serialize answers for API submission
import { serializeExerciseAnswerMap } from '@cio/question-types';
const payload = serializeExerciseAnswerMap(questions, answersByKey);
// => Array<{ questionId, optionId?, answer?, payload? }>
Create question form state
import { createExerciseQuestionFormState } from '@cio/question-types';
const state = createExerciseQuestionFormState(initialQuestions);
Adding a new question type
See ADDING_A_QUESTION_TYPE.md for a step-by-step guide. At a high level:
Build
pnpm -C packages/question-types build
Notes for consumers
- Prefer importing keys/lookup helpers from this package instead of duplicating constants.
- Use
getExerciseQuestionContractKeyconsistently for stable answer maps. - Keep UI implementation in
@cio/ui; keep domain contracts in@cio/question-types.