Skip to Content

Item Bank & Import Pipeline

The item bank (modules/items, WP-01) is the single source of truth for all content in Amal. Items are authored and validated offline, then imported through a controlled pipeline. The platform never generates, composes, or transforms item content at runtime.

All routes in this module are gated by RUN_BACKOFFICE. Students receive items only through the Task Delivery service, which runs its own scoped endpoint.

Item lifecycle

Every item moves through a four-status lifecycle. Status transitions are validated and logged; corrections to operational items are always new rows, never in-place edits (append-only rule, V-6).

draft → pilot → operational retired
StatusMeaning
draftJust imported; pending SME review
pilotApproved for limited pilot use; collects real response data
operationalFully approved; available for live session delivery
retiredRemoved from active selection; the row is preserved for audit

Synthetic items (those with source=synthetic) are permanently excluded from non-backoffice reads. They cannot be promoted to operational. The 409 response on the promote endpoint includes this as an explicit rejection reason.

The import batch

POST /api/items/import is the single audited write path for all new items. The request body carries the batch metadata (source, language, dialect, batch label). The actual item rows are supplied as a structured JSON payload that the server validates before writing anything.

The pipeline runs the following checks on every row, in order. A row that fails any check is rejected with a per-row reason; the rest of the batch continues to be processed.

Validation stepWhat it checks
SchemaRequired fields, types, enum values
Item-type structural rulesDistractor count, option count, and format rules for the declared item_type
Distractor compatibilityThe declared distractorType must be legal for the item type (compatibility matrix at GET /api/item-types/{code}/distractor-compat)
Language safetyItem content is scanned against the language-safety rule set; any substring match rejects the row
Arabic agreementGender, number, and case agreement across stem, options, and distractors (offline NLP check via the Arabic validator)
delta_prior computationA five-component difficulty prior is computed and stored; rows where SAMER level data is unavailable receive a samer_unavailable skip marker and remain importable at a reduced confidence level

A batch is idempotent on fileHash. Re-submitting the same file returns the existing batch report without writing duplicate rows.

delta_prior: the five components

The difficulty prior is a weighted sum of five content signals. The weights come from the active expected_difficulty_weights row in the database (versioned; the version is pinned on each item row for V-6 replay).

ComponentSource field(s)What it captures
TR (Text Readability)samerTextLevel (passage); falls back to samerAvgLevel (item-level)Passage difficulty on the SAMER scale
TW (Target Word)targetWordSamerLevelMean SAMER level of the target word(s)
CD (Cognitive Demand)dokLevel; pirlsProcess overrides when presentDepth-of-knowledge and comprehension-process load
SC (Sentence Complexity)avgSentenceLength, cliticDensityMorphological and syntactic complexity
AF (Answer Format)itemType, distractorSimilarity, requiresMultipleSelection, optionCountHow hard the response format makes the item

Default weight distribution (active weights row as shipped): TR 0.35, TW 0.25, CD 0.20, SC 0.10, AF 0.10. The weights are configurable in the database and do not require a code change to adjust.

The computed delta_prior is stored as Decimal(7,4) (same precision as θ) so that difficulty and ability live on a comparable scale and V-6 replay is byte-identical.

Item types

GET /api/item-types returns the full catalog: 15 active item types and 3 reserved codes. Each type has:

  • A structural rule set (which fields are required, how many options are valid, etc.)
  • An afBase value used as the AF component seed in delta_prior
  • A distractor-compatibility set, readable at GET /api/item-types/{code}/distractor-compat

The catalog is seeded at boot from catalog-seed.ts and does not change at runtime.

Approval and promotion

After import, items land as draft. Two separate approval paths exist.

Batch approval (POST /api/items/import/{batchId}/approve) is the standard path. An authorized backoffice user reviews the batch report and, if satisfied, approves the batch. All accepted rows in the batch are promoted to operational in one transaction. Rows with per-row rejection reasons are excluded. The endpoint is idempotent: re-approving an already approved batch returns 409.

Per-item SME approval (POST /api/items/{id}/sme-approve) applies to items with source=sme_authored. It marks the individual item as SME-reviewed without affecting the rest of its batch.

Promote (POST /api/items/{id}/promote) advances a single item one step along the lifecycle (draft → pilot → operational). The endpoint enforces the SME gate: a draft item cannot skip directly to operational without an SME approval stamp.

Retiring an item

POST /api/items/{id}/retire sets the status to retired. The item row is never deleted. It is excluded from all delivery selection queries but remains visible in the backoffice list (GET /api/items) and in the delta_prior log. This preserves the audit record for any sessions that served the item while it was operational.

delta_prior log

GET /api/expected-difficulty/log?itemBankId=<id> returns the full computation history for one item: every delta_prior recalculation, the weights version used, the five component values, and the resulting difficultyBand. This is the V-6 audit trail for difficulty prior changes. Use it when investigating why an item’s band changed after a weights update or a re-import.

API reference

All routes require RUN_BACKOFFICE.

MethodPathNotes
GET/api/itemsList items (filtered, tenant-scoped; synthetic excluded)
GET/api/items/{id}One item; add ?view=authoring for the full row
GET/api/item-typesFull type catalog (15 active + 3 reserved)
GET/api/item-types/{code}/distractor-compatDistractor compatibility matrix for one type
POST/api/items/importImport a batch; idempotent on fileHash
GET/api/items/importBatch history
GET/api/items/import/{batchId}Validation report for one batch (counts + per-row outcomes)
POST/api/items/import/{batchId}/approveSME batch approval; promotes all accepted rows
POST/api/items/{id}/sme-approvePer-item SME approval (for sme_authored items)
POST/api/items/{id}/promoteSingle-step status promotion (draft → pilot → operational)
POST/api/items/{id}/retireSet status to retired (the row is never deleted)
GET/api/expected-difficulty/logdelta_prior computation history for one item

What the import pipeline does not do

  • It does not generate items or content at runtime.
  • It does not invoke a language model at any point (V-5).
  • It does not silently accept rows with Arabic agreement failures; failing rows are rejected with per-row reasons.
  • It does not allow synthetic items to reach operational status.