Task Delivery
The task delivery service (WP-DTE-BE, modules/delivery) selects a stored, render-ready item from the item bank and serves it exactly as stored. The platform never composes, templates, or generates content at runtime.
Item-bank-only model
All items are authored and validated offline, then imported into the item bank through an import pipeline that runs Arabic agreement checks (gender, number, case), metadata validation, language-safety screening, and difficulty-prior (delta_prior) computation at import time. By the time an item enters the bank it is ready to be served without any runtime transformation.
When the delivery service runs, it does exactly three things: filter, serve, and log. No content is assembled, no language model is invoked, and no template is expanded.
Selection filters
Each serve request specifies a target context. The engine applies the following predicates in combination:
| Filter | Description |
|---|---|
subSkillId | Must match the sub-skill being practiced or assessed |
taskMode | practice, quick_check, or progress_monitoring_probe; each has distinct UX rules |
scaffoldTier | Level_1_Foundation, Level_2_Targeted, or Level_3_Independence (progress_monitoring_probe skips tier) |
difficultyBand | Difficulty band anchored to delta_prior |
dialect | MSA / LEV; items whose dialect tags are absent are treated as dialect-agnostic |
| Recently served | Items served to the same student in the recent-serve window are excluded |
When multiple items pass all filters the engine applies a fully deterministic tie-break: closest difficulty band first, then delta_prior ascending, then itemBankId lexicographic. The selection is byte-identical for identical inputs, which satisfies the V-6 determinism rule.
Scaffold tier resolution
The scaffold tier is per (student × skill × bundle × task type), never a global student-level attribute. For tiered task modes (practice or quick_check within an active bundle) the delivery service looks up the student’s student_scaffold_assignment row. If no assignment exists the serve returns no_eligible_item. A random fallback tier is never applied.
Probes (diagnostic quick_check outside a bundle) select tier-agnostically.
Serve outcomes
Every serve response is HTTP 200. When no item is served, served is null and reason
carries one of the non-ok outcome codes. The caller is responsible for choosing the correct UX
for each case.
| Outcome | served | Meaning | What to do |
|---|---|---|---|
ok | item payload | Item selected and passed all checks | Render the item |
no_eligible_item | null | No item passed the filter combination | Surface the gap UX; the outcome writes a gap-queue row |
blocked_by_visibility_rule | null | An item was selected but failed the student-visibility wrapper at serve time | Treat as a content-pipeline error; surface the same gap UX as no_eligible_item |
failed | null | Unexpected internal failure | Retry once; if it persists, escalate |
no_eligible_item
Every no_eligible_item outcome writes a gap-queue row. The admin gap summary endpoint
aggregates these rows by (sub_skill × tier × mode) so that offline content authoring teams know
exactly which combinations need new items.
no_eligible_item is not an error. It is a gap signal. Never treat it as a 4xx or 5xx. Surface it to the content pipeline so the gap is filled offline.
blocked_by_visibility_rule
The visibility wrapper runs a final check on the selected item’s content payload immediately
before the serve is written. If the check finds an internal label that must not reach a student,
the serve is aborted and logged as blocked_by_visibility_rule. The import gate is designed to
prevent such items from reaching operational status, so this outcome indicates a content-pipeline
gap that needs investigation.
From a caller perspective, blocked_by_visibility_rule should be treated the same as
no_eligible_item: served is null, the UX should reflect the absence of an item, and the
incident appears in the gap summary so the content team can investigate.
Served-task-instance audit
Every successful serve writes a served_task_instance row with a UUID primary key. This UUID is the join token for printed QR worksheets (session-gen and paper-scan flows). UUIDs are non-enumerable and globally unique, making them safe to embed on physical paper, unlike auto-increment integers.
The audit row pins the following for permanent reproducibility:
- Item-bank snapshot version and selection-policy version
- Scaffold tier and rule version used
- The recently-served exclusion window
- Request context (sub-skill, mode, band, dialect)
Dry-run replay (V-6 verification)
POST /api/admin/delivery/dry-run-replay re-runs item selection against the same pinned versions. It writes nothing. The response reports whether each replayed request returns the byte-identical item. A false result is a determinism regression.
API reference
| Method | Path | Permission | Notes |
|---|---|---|---|
POST | /api/delivery/task | VIEW_OWN_CLASSES or RUN_BACKOFFICE | Serve one item; served:null is 200 |
GET | /api/delivery/log/{taskInstanceId} | RUN_BACKOFFICE | Full serve audit for one instance |
GET | /api/admin/delivery/gap-summary | RUN_BACKOFFICE | Gap counts by (sub_skill × tier × mode) |
POST | /api/admin/delivery/dry-run-replay | RUN_BACKOFFICE | V-6 determinism assertion; read-only |
There is no student-facing delivery endpoint. All delivery routes require a teacher or admin permission. Students never make direct calls to this service (B.17).
What delivery does not do
- It does not generate or compose content at runtime.
- It does not invoke a language model at any point.
- It does not fall back to a substitute item when the requested tier has no match.
- It does not validate Arabic agreement at serve time (that check runs at import).
- It does not expose a student-facing endpoint.
Related
- Skills Taxonomy: sub-skill IDs used as selection keys
- Student Profiles: scaffold tier source
- Intervention Bundles: bundle context that governs tier selection
- Language Safety: import-time Arabic agreement and safety gate