Basic computer-adaptive testing: in-process Rasch engine behind the CAT 1.0 seam
Status: accepted (supersedes ADR-0025 §4)
We will ship a basic but genuine Computerized Adaptive Test (CAT) before go-live: a small, owned TypeScript engine implementing common, simple psychometric algorithms, sitting behind the exact seam an industrial engine would later occupy. This reverses ADR-0025 §4 (“CAT out of scope; revisit via a dedicated ADR”) — this is that ADR.
CAT here is not a QTI feature. Per ADR-0005 it is the measurement-precision mode of the single Selection Policy: given learner state + an item pool + an objective, choose the next item. We build that one mode now; spaced repetition and experiment assignment are sibling modes, deferred.
The decisions (11)
Section titled “The decisions (11)”-
The seam is conform-ed’s
CatV1_0contract, called in-process. The engine consumesSectionData+AssessmentResultand returnsItemStage+CatEngineResultReport— the real Zod contracts from@conform-ed/contracts/cat/v1_0— but is invoked as a TS module, not over HTTP. Swapping an industrial engine in later is “point the same typed boundary at a REST adapter,” which is config, not a rewrite. This buys CAT 1.0 conformance now and the swap option without a premature network service. -
Rasch / 1PL is the measurement spine. One difficulty
bper item; abilityθon the same logit scale; item information peaks atb ≈ θ. The smallest model that is genuinely CAT, needs only one author-suppliable number per item, and speaks the same θ/info vocabulary as any engine we’d swap in. Multistage and staircase patterns can be added later as alternative selection strategies behind the same seam. -
Item parameters are author/import-supplied and frozen. No response data exists to calibrate from yet. A new immutable
item_parameter_set(b,provenance ∈ {author, imported, calibrated}) is bound to one Item Version; eachitem_sessionpins the exact set it used (params_snapshot_ref), so a θ is always reproducible. A CAT pool is gated to require a parameter set on every item-version — a missingbis an authoring error, never a silent default.calibratedis the scaffold for the future calibrate-from-interaction_eventpath. -
EAP is the estimator. Expected a posteriori with a
N(0,1)prior over a fixed quadrature grid. Chosen over MLE because MLE diverges on all-correct/all-incorrect patterns — i.e. every CAT’s first items — whereas EAP is robust from item one, never diverges, and the posterior SD is the SE the stopping rule needs. Cost: mild shrinkage toward 0, which vanishes as the test lengthens. Grid and prior are hardcoded sane defaults now, tunable later. -
Selection is max-information + randomesque. For Rasch, max-info = the unused item whose
bis nearest current θ. To avoid over-exposing mid-difficulty items we pick randomly among the top-k most-informative (default k=3), seeded by the attempt’s replay key so a session stays reproducible. The first item (θ = prior mean 0) starts at median difficulty. Sympson–Hetter, content-balancing and constraint-based selection are the industrial-engine carrot — deferred. -
Stopping combines three authored constraints:
stop when (SE ≤ target OR items ≥ max) AND items ≥ min, defaultsmin=5 / max=20 / SE≤0.3, with pool exhaustion as an implicit terminator. Carried inSectionData.constraints[]. Fixed-length ismin=max. The θ→reported-score/band transform is deferred; v1 reports raw θ + SE. -
Server-anchored only. CAT is structurally incompatible with offline-complete: it would require shipping the learner the whole pool + every answer key + every parameter, defeating key-withholding and enabling gaming. Each “respond → next item” round-trip is a server step the client blocks on — exactly what
server-anchoredalready means (ADR-0006). Authoring a CAT under an offline-complete profile is a validation error. -
CAT/measurement only, behind a generic
SelectionPolicyseam. Experiment assignment (arms, bandits) and its governance (Study/Consent — ADR-0008) are deferred;item_session.variant/conditionstay dormant this phase. A later experiment-mode policy lights them up. (condition= the experimental arm; distinct from QTI feedback’s “condition” predicate — see CONTEXT.md.) -
emergent claims the platform/delivery conformance role and ships a reference engine. Proven by an in-process interop lane (the ADR-0033 “interop proof = done” pattern): a full adaptive session drives the real engine against the real
CatV1_0Zod contracts, asserting both shape-validity and psychometric behaviour (θ converges to true-θ, SE shrinks monotonically, stop fires in bounds).cat/v1_0is pinned in the conformance overlay (ADR-0028); the REST/OAuth transport binding is deliberately deferred (it lights up only when an external engine is wired — the coverage map treats CAT operations as payload-constraining requirements, not a walked transport axis). -
A CAT is a first-class pool delivered via a Practice Session, scaled by a competency node. Not an
assessmentTestsection — that would entangle CAT with QTI sequencing and the §4 “separate spec layer” boundary. A CAT Pool is an explicit, author-curated set of Item Versions declared to measure one latent trait, anchored to a CASE competency node (which gives θ meaning and reuse). Unidimensional in v1.SectionDatais assembled 1:1 from the pool. -
The reference engine is stateless; caching is a sanctioned later optimisation. θ is a pure function of (persisted
item_sessionresponses + each row’sparams_snapshot_ref- pool +
selection_policy_version+ grid/prior config). The persisted rows are the source of truth — the engine holds nothing between round-trips, making it a pure function: trivially testable, perfectly reproducible, crash-safe, and clean to swap.SessionInfois a thin handle onto thepractice_session. A Dragonfly (Redis-compatible) memo of the computed posterior is allowed later as a non-authoritative, fully-keyed cache (key includes every input, so a rescore or recalibration can never serve a stale θ; audit/DIF/research always recompute from rows). It does not make the engine stateful in the problematic sense.
- pool +
Consequences
Section titled “Consequences”- New domain code (pure, no DB): a Rasch/EAP psychometrics module + a stateless CAT engine
implementing
CatV1_0+ a genericSelectionPolicyseam, all inpackages/domain. - New schema (Anton’s DB-apply step):
item_parameter_set(givesparams_snapshot_refa target) and a CAT-pool authoring surface; both regenerate through the migrations runbook. - The conformance board gains a pinned
cat/v1_0map with an honest triage: information model + operation semantics covered in-process; transport binding deferred; vocabularies verified as value-sets. - The swap to an industrial engine is reduced to writing a REST adapter behind the same contract — no change to session-writing, pool assembly, or persistence.
- Statelessness leaves a clean optimisation lever (Dragonfly) for if/when recomputation ever shows up in latency — not before.
Decisions for Anton
Section titled “Decisions for Anton”- CAT ships basic-but-real now, behind the CAT 1.0 seam, reversing ADR-0025 §4. (accepted)
- Rasch/1PL + EAP + max-info/randomesque + 3-way stopping, all sane-defaulted and tunable later. (accepted)
- Server-anchored only; offline-complete CAT is a validation error. (accepted)
- New tables
item_parameter_set+ CAT-pool surface regenerate via the migrations runbook; DB reset+apply remains Anton’s step. (pending operator step) - Experiment assignment + governance stay deferred; this phase is measurement only. (accepted)