This reference tracks current exported behavior of @experiments/core in this repo.
The framework uses a standardized adapter pattern to manage the execution of diverse experimental tasks.
All task adapters must implement this interface to be compatible with the unified shell and core lifecycle.
readonly manifest: TaskManifest: Metadata about the task (ID, label, available variants).initialize(context: TaskAdapterContext): Promise<void>: (Optional) Called to set up the task, parse configuration, and prepare resources.execute(): Promise<unknown>: (Optional) Called to run the main task logic. Should return the task results.terminate(): Promise<void>: (Optional) Called after execution (success or failure) to clean up resources like global listeners or timers.
Preferred way to define task adapters without per-task wrapper classes.
manifest: TaskManifest: Task metadata.run(context: TaskAdapterContext): Promise<unknown> | unknown: Main task entrypoint.initialize?(context): Optional setup hook.terminate?(context): Optional cleanup hook.
The factory returns a TaskAdapter compatible with LifecycleManager and keeps task context management centralized in core.
Context provided to task adapters during initialization and execution.
container: HTMLElement: The root element for the task UI.selection: SelectionContext: Metadata about the current task, variant, and participant.coreConfig: CoreConfig: The full core framework configuration.taskConfig: JSONObject: The task-specific configuration (automatically resolved at participant scope).resolver: VariableResolver: A pre-configured resolver for handling block and trial scoped variables.
Orchestrates the execution of a TaskAdapter.
constructor(adapter: TaskAdapter)run(context: TaskAdapterContext): Promise<unknown>: Executes the full lifecycle:initialize->execute(or legacylaunch) ->terminate.- Note:
run()automatically performs high-level variable resolution oncontext.taskConfigbefore callinginitialize. Onlyparticipantscoped variables are resolved at this stage;blockandtrialscoped variables remain as tokens for the adapter to handle.
A generalized RT trial runner that supports arbitrary phase sequences. Each phase can have a custom render function.
Provides utilities for generating non-overlapping spatial slots for stimuli.
generateSlots(args): Point[]: Supports"circular","grid", and"random"templates.
Standardized canvas renderer for SceneStimulus models. Handles rendering of shapes and supports slot-based positioning.
Utility to identify identity changes between two structured scenes.
Manages the loading, merging, and validation of experiment configurations.
load(path: string): Promise<JSONObject>: Fetches and parses a JSON config file.merge(base, taskDefault, variantOverride, runtimeOverride?): JSONObject: Sequentially deep-merges configuration levels.resolve(config: JSONObject, resolver: VariableResolver): JSONObject: Recursively resolves variable tokens in the configuration using the provided resolver.
Resolves task/variant/configPath/overrides/participant metadata from URL + JATOS.
When running under JATOS, URL-style parameters are resolved from:
window.location.search(if present)jatos.urlQueryParameters(fallback, preserves launch params across Publix redirects)
Task/variant precedence:
- JATOS (
taskId,variantId) - URL (
task,variant) coreConfig.selection
Overrides precedence:
- JATOS
overrides - URL
overrides
Accepted URL keys:
task,variant,config,overrides,cc- participant keys:
PROLIFIC_PID,STUDY_ID,SESSION_ID,SONA_ID,participant,survey_code - auto-responder toggle:
auto - auto-responder jsPsych mode:
auto_mode(visualordata-only)
Retries selection resolution for JATOS launches where selection payloads may arrive slightly after app boot.
- Default max wait:
10000ms - Stops early when JATOS selection becomes available
- Skips retry when URL already provides
task
Attempts to load JATOS runtime script from a candidate list (in order), returning:
loaded: whether a candidate succeededloadedFrom: source URL used when loadedattempts: all attempted candidate URLs
Waits for JATOS readiness via jatos.onLoad(...) when available, with polling fallback for
componentJsonInput / studySessionData.
Normalizes relative-vs-absolute launch paths for local/dev and JATOS:
- preserves absolute URLs (
https://...,data:..., etc.) - rewrites leading-slash app paths to component-relative paths under JATOS
- keeps local paths local-friendly outside JATOS
Runtime path tokens supported by templated stimulus/config helpers:
{runtime.assetsBase}{runtime.configsBase}
Deep merge order:
basetaskDefaultvariantOverrideruntimeOverride
Fetches JSON and enforces object-only payload.
Renders each prompt and waits for continue (button click or space).
Handles block envelopes and trial iteration.
Key behavior:
- Uses
getTrials(block)when provided, elseblock.trials. renderBlockStart/renderBlockEndreturning non-null HTML triggers continue gates.- Cursor policy is configurable:
- default hides cursor during each trial
- set
hideCursorDuringTrial: false(or function) for mouse-first tasks.
Linear timed stage runner with optional timed response capture.
Returns:
key,rtMstotalDurationMsstageTimings[]
Lowercases and normalizes " " | "spacebar" | "space" -> "space".
Captures first valid key in [startMs, endMs] window over totalDurationMs.
Displays HTML screen with continue button and space shortcut.
Maps canonical keys to jsPsych choices ("space" -> " ").
Maps and deduplicates keys for jsPsych plugin choices.
Returns .jspsych-content host if present, else container.
Pushes a jsPsych call-function timeline node that renders a continue screen through core waitForContinue.
Returns true when the given phase label indicates a trial execution phase where the cursor should be hidden (matches fixation, blank, stimulus, response, or feedback). Safe to call inside jsPsych on_trial_start callbacks — never applies during instruction or continue screens.
extractJsPsychTrialResponse(data: Record<string, unknown>): { key: string | null; rtMs: number | null }
Normalizes the response and rt fields from a jsPsych trial data object into canonical form. Returns null for each field when the raw value is absent or non-finite.
Encapsulates environment cleanup for tasks that install keyboard/scroll blockers or hide the cursor. Replaces the pattern of storing disposer references as module-level variables.
installKeyScrollBlocker(allowedKeys: string[]): void— installs a key scroll blocker and registers its disposer.installPageScrollLock(): void— locks page scroll and registers its disposer.addDisposer(fn: () => void): void— registers an arbitrary cleanup function.cleanup(): void— shows the cursor (setCursorHidden(false)) and runs all registered disposers.
Typical usage:
const taskEnvironment = new TaskEnvironmentGuard();
// in onTaskStart:
taskEnvironment.installKeyScrollBlocker(allowedKeys);
// in terminate:
taskEnvironment.cleanup();Resolves outer shell background with precedence:
taskConfig.ui.pageBackgroundcoreConfig.ui.pageBackgroundnull(caller may use CSS default)
Shared instruction-slot coercion used by task adapters.
Returned shape:
intro: string[]preBlock: string[]postBlock: string[]end: string[]
Accepted intro aliases (first key present wins):
pages(preferred)introPagesintroscreens
Accepted pre/post/end aliases:
- pre:
preBlockPages,beforeBlockPages,beforeBlockScreens - post:
postBlockPages,afterBlockPages,afterBlockScreens - end:
endPages,outroPages,end,outro
Behavior note:
- If a chosen key is explicitly present but blank (for example
""or[""]), the slot resolves to[](intentional clear) and does not fall back to defaults. - Blank entries inside arrays are ignored.
Shared helper that applies variable expansion across nested arrays/objects via resolver.resolveInValue(...) when a resolver is provided.
Useful for config fields that may be arrays/objects containing tokens (for example beforeBlockScreens: ["$between.preScreen"]).
Mode-aware callback bridge for local renderer/audio DRT presentation without embedding renderer logic into core.
Returns:
hasVisualModehasAuditoryModehasBorderModeonStimStart(stimulus)onStimEnd(stimulus)onResponseHandled()hideAll()
Typical usage:
- task-local DRT loop calls
onStimStart/onStimEndfrom engine/controller hooks - task-local key handler calls
onResponseHandledafter a handled DRT response - task cleanup calls
hideAll
The framework supports modular extensions that can be attached to specific scopes (task, block, or trial).
id: string: Unique identifier for the module.start(config, address, context): TaskModuleHandle: Called when a scope starts.
stop(): TResult: Called when the scope ends.step?(now: number): (Optional) Animation frame tick.handleKey?(key, now): (Optional) Keyboard event handler.getData?(): TResult: (Optional) Snapshot current active module data.controller?: unknown: (Optional) Runtime controller exposed for advanced task integrations.
Manages the lifecycle of active modules.
constructor(modules?: TaskModule[])setOptions(options: { onEvent?: (event) => void })start({ module, address, config, context }): Starts a new module instance at the specified address.stop(address): Stops the module instance at the specified address and records the result.stopAll(): Stops all active modules.getResults(): TaskModuleResult[]: Returns all results from stopped modules.getActiveData(criteria?): Returns live data snapshots from active modules.getActiveHandle(criteria): Returns the active handle at an exact scope address.
resolveScopedModuleConfig(raw, moduleId): read module overrides from eithermodules.<id>ortask.modules.<id>with local precedence.maybeExportStimulusRows({ context, rows, suffix }): centralizedexportStimuliOnlygate and export finalization path.parseSurveyDefinitions(entries): parse mixed survey configs (preset or inlinequestions[]) intoSurveyDefinition[].collectSurveyEntries(config, { arrayKey, singletonKey }): collect survey candidate entries fromsurveysarrays/scoped arrays and optional singleton aliases.runSurveySequence(container, surveys, buttonIdPrefix): run sequential surveys with standardized button id handling.attachSurveyResults(record, surveys): attach survey run payloads onto a trial/result record.findFirstSurveyScore(surveys, scoreKey): read first finite numeric score for a key across survey runs.renderSimpleInstructionScreenHtml(ctx, options): simple standardized instruction renderer for intro append + optional block label behavior.createInstructionRenderer(options): renderer factory for common task instruction patterns (intro append, block-label policy, optional summary-card section rendering, page resolver hook).buildTaskInstructionConfig(args): build standardized task instruction config (intro/pre/post/end, block intro template, block label policy) from raw task instructions + defaults.applyTaskInstructionConfig(taskConfig, instructions): apply standardized instruction config onto task instruction surfaces for orchestrator flows.
The DrtController provides a static helper to use the DRT engine as a task module:
DrtController.asTaskModule(config): Returns aTaskModuleinstance configured for DRT.
Common display helpers used by tasks:
computeCanvasFrameLayoutdrawCanvasTrialFramedrawCanvasFramedScenedrawCanvasCenteredTextdrawCenteredCanvasMessagecreateScaledCanvasHostmountCanvasElementensureJsPsychCanvasCenteredrenderCenteredNotice
Supports:
weighted(default)sequencequota_shuffleblock_quota_shuffle(alias)
Schedule options include withoutReplacement and without_replacement.
Core also exports generic helpers used by task adapters for block-level manipulation assignment:
createManipulationOverrideMap(value):- converts
[{ id, overrides }]into an id -> overrides map.
- converts
createManipulationPoolAllocator(value, seedParts):- creates a participant-seeded pool allocator from a config object like:
{ "poolA": [ ["manipA"], ["manipB"] ] }
resolveBlockManipulationIds(blockLike, allocator?):- resolves
manipulationPool,manipulation, andmanipulationsinto an ordered id list.
- resolves
applyManipulationOverridesToBlock(blockLike, manipulationIds, overrideMap, errorContext):- deep-merges referenced manipulation overrides into a block object.
These are intentionally task-neutral primitives. Whether a task allows one manipulation per block vs multiple is controlled by the task adapter.
hashSeed(...parts): numbercreateMulberry32(seed): () => numberSeededRandom(next,int,shuffle)
Core now exports task-neutral stimulus pool primitives:
- Source loading:
coerceCsvStimulusConfig(value)loadCategorizedStimulusPools({ inlinePools, csvConfig, resolver?, context? })loadTokenPool({ inline?, csv?, normalize?, dedupe? })
- Draw planning:
collectPoolCandidates(pools, categories, excludedCategories?)createPoolDrawer(candidates, rng, drawConfig?)createCategoryPoolDrawer(pools, categories, rng, options?)
- Config coercion:
coercePoolDrawConfig(value, defaults?)coerceCategoryDrawConfig(value, defaults?)
Supported draw modes:
ordered(source order, loops)with_replacement(independent random draw)without_replacement(shuffle/consume/recycle)- category drawers also support
round_robin
These helpers are used by PM/NBack for participant-seeded deterministic pool behavior.
Core now exports additive PM utilities in prospectiveMemory.ts:
generateProspectiveMemoryPositions(rng, { count, minSeparation, maxSeparation })resolveProspectiveMemoryCueMatch(context, rules)
Cue-rule primitives support:
category_intext_starts_withstimulus_colorflag_equals
Current shared policy for concurrent keyboard modules (primary task + DRT):
- Primary task keys remain task-owned and are handled in task runtime order.
- DRT uses controller-level capture listeners and only consumes configured DRT response key.
- If keys overlap by configuration, overlap is allowed and task adapters must explicitly prevent default/propagation where needed.
- Recommended practice is non-overlapping key maps per task/module pair.
Methods:
nextStimulus()update(response: 0 | 1)estimateMode()exportPosterior()
Helpers:
buildLinearRangeluminanceToDbdbToLuminance
Converts object rows to CSV with escaping.
- primitive cells are written directly
- object/array cells are JSON-serialized (instead of
[object Object])
Note: Task adapters do not call
finalizeTaskRundirectly. It is invoked internally byTaskOrchestratoras part of the session completion flow. This function is documented here for core reference only.
Behavior:
- Local save when
coreConfig.data.localSave !== false, usingcoreConfig.data.localSaveFormat:- default:
"csv"(CSV only) "json": JSON only"both": CSV + JSON CSV uses explicitargs.csv.contentswhen provided, else inferred tabular rows from payload where possible. Optionalargs.extraCsvsallows downloading additional CSV files in the same finalize pass.
- default:
- Submit to JATOS when available.
- When a core data sink handles JATOS incrementally, finalization does not overwrite streamed result data with a second full-payload submit.
endStudy()unlessendJatosOnSubmit === false.- Resolve and apply redirect template if enabled.
TaskOrchestrator can emit task/session data through a core-level TaskDataSink.
- Default behavior now installs a JATOS JSON-lines sink when JATOS is available.
- Session lifecycle events and trial results are emitted incrementally as envelopes.
- Local CSV/JSON save remains available for testing and debugging.
- Task adapters should not implement JATOS submission directly.
TaskOrchestrator.run(args) now auto-derives instruction UI from config unless explicitly overridden:
- task-level pages:
instructions.introPages,instructions.endPages - block-level defaults:
instructions.preBlockPages,instructions.postBlockPages - block intro template:
instructions.blockIntroTemplate - block flags:
instructions.showBlockLabel,instructions.preBlockBeforeBlockIntro - block page merging: global pre/post pages are merged with
block.beforeBlockScreens/block.afterBlockScreens
For adapters that parse/normalize instructions before orchestration, use args.instructionDefaults to provide the same surfaces once (instead of manually wiring introPages, endPages, and getBlockUi per task).
To avoid per-task orchestrator wiring, core also exposes:
applyResolvedTaskInstructionSurfaces(taskConfig, surfaces)
This hydrates normalized instruction surfaces back onto taskConfig.instructions, so TaskOrchestrator can consume them without adapter-level instructionDefaults.
TaskOrchestrator.run(args) supports a standardized pre-main staircase phase:
args.staircase.run: async callback executed after intro flow and before block/session executionargs.staircase.enabled(optional): explicit enable/disable override- if
args.staircase.enabledis omitted, orchestrator runs staircase only whentaskConfig.staircase.enabled === true
This keeps staircase as a core lifecycle slot (instead of task-specific intro hooks), so any task can use the same orchestration surface.
TaskOrchestrator.run(args) supports:
args.shouldAutoStartModule(ctx)args.resolveModuleContext(ctx)
This predicate is evaluated for each configured task module at block/trial scope before startScopedModules is invoked. It enables selective opt-out of orchestrator auto-start for specific modules without task-level raw config mutation.
Module config resolution is now layered centrally:
rawTaskConfig.task.modules(task-level baseline)block.modules/block.task.modules(block-level override)trial.modules/trial.task.modules(trial-level override)
Merging is deep and scope-aware (config.scope still determines whether a module starts at block or trial boundaries).
Provides .emit(eventType, eventData?, meta?) and accumulated .events.
DrtEngine: pure timing/scoring engine for DRT probes (presented/hit/miss/false_alarm, event log export).DrtController: browser runtime wrapper overDrtEnginewith:- scoped
start()/stop() - keyboard listener lifecycle
requestAnimationFramestepping- sampler-based ISI generation via shared core
createSamplerspecs. - independent probe
displayDurationMsandresponseWindowMs responseTerminatesStimuluscontrol- optional online parameter transforms (
parameterTransforms) that consumedrt_responseevents and emit per-update estimates viaonTransformEstimate. - transform persistence control (
transformPersistence):"scope": reset transform state at each DRT scope boundary (default)"session": persist transform state across all DRT scopes within one task run/session
- row-level export linking each
drt_responseto transform output (exportResponseRows()), including:estimate: primary estimate object for the response (ornull)transformColumns: flattened scalar columns for long-format analysis (drift_rate,threshold,t0, CI bounds, etc.)estimates: full estimate list (kept for backward compatibility)
- built-in presentation modes:
visual(default: top-center red square anchored to task display area when available; otherwise viewport)auditory(WebAudio tone)border(flash outline around target display element only; does not fall back to full-screen viewport border when target bounds are unavailable)
- scoped
Core now exports reusable tracking primitives (tracking.ts):
TrackingMotionController:waypointmotion (sampled destinations + linear traversal)chaoticmotion (heading jitter + wall reflections)
TrackingBinSummarizer:- accumulates per-window sample counts
- stores
insideCount,outsideCount, and boundary-distance moments for weighted aggregation
- geometry helpers:
computeTrackingDistance(point, target)for circle/square boundary distance with inside=0 convention.
OnlineParameterTransformRunner: generic runtime for event-driven, online parameter estimation modules.OnlineParameterTransform: minimal interface (observe,reset,exportState) for reusable model adapters.- Included first transform type:
wald_conjugate:- Moving-window analytic shifted-Wald fit from RT observations.
- Configurable priors (
mu0,precision0,kappa0,beta0), window sizes, and credible interval bounds. - Non-decision-time (
t0) supports:t0Mode: "fixed"(default): usest0as constant milliseconds.t0Mode: "min_rt_multiplier": usest0 = t0Multiplier * minObservedRtMs, whereminObservedRtMsis tracked across all finite observed RTs for that transform instance (not just the moving fit window).
- With
transformPersistence: "session", that min-RT tracking persists across DRT scope boundaries in a run; with"scope", it resets each scope. - Optional trial-varying prior mean shift mode (
priorUpdate.mode: "shift_means") matching the provided R/Python pattern. - Transform configs are object entries in
parameterTransforms[](for example{ "type": "wald_conjugate" }), not string shorthands.
Standardized correctness evaluation output used by task adapters.
Core exports shared feedback helpers used by task adapters:
parseTrialFeedbackConfig(value, fallback, defaults?)resolveTrialFeedbackView({ feedback, responseCategory, correct, vars?, resolver?, resolverContext? })drawTrialFeedbackOnCanvas(ctx, layout, feedback, view)
Supported feedback config fields:
enableddurationMs/duration_msmessages.correct|incorrect|timeout|invalidmessages.byResponseCategory/messages.by_response_categorystyle.correctColor|incorrectColor|timeoutColor|invalidColor(snake_case variants also accepted)style.byResponseCategoryColors/style.by_response_category_colorsstyle.fontSizePx|fontWeight|canvasBackground|canvasBorder(snake_case variants also accepted)
resolveTrialFeedbackView supports {placeholder} interpolation from vars and core variable resolver context.
Builds full-factorial cells from named factors and levels.
Builds an exact-quota condition sequence using full-factorial cells + optional weights + optional adjacency constraints:
maxRunLengthByFactormaxRunLengthByCellnoImmediateRepeatFactors
Useful for balanced block construction in any trial task.
Creates normalized term -> label mappings from grouped vocabularies.
Resolver API for normalized semantic label lookup:
resolve(term): string | nullhas(term): boolean
parseCsvDictionary(csvText, keyColumn, valueColumn, options?)loadCsvDictionary(spec)loadSemanticIndexFromCsvColumns(csvPath, keyColumn, labelColumns, args?)loadTokenListFromCsvColumn(path, column, args?)
These support generic dictionary/lexicon ingestion from CSV-backed assets.
Creates a normalized key-to-category resolver for RT tasks where physical keys map to abstract response categories.
Capabilities:
allowedKeys(categories?)for scoped key sets (for example block-specific key availability)responseCategoryFromKey(key)with built-intimeoutandinvalidcategoriesexpectedCategoryFromKey(key, fallback?)for key-coded expected responsesexpectedCategoryFromSpec(spec, fallback?)for expected responses that may be either:- a physical key (mapped to category), or
- a category label directly (including omission categories like
timeout)
keyForCategory(category)to resolve canonical key output for a category
This is used by current tasks (PM, Stroop, SFT) to avoid task-local key classification logic.
Creates a normalized token -> CSS color registry with validation and fallback support:
resolve(token): string | nullhas(token): booleanentries()
normalizeColorToken(token)
Core now exposes a generic extension hook runtime intended for cross-task overlays (for example embedding an auxiliary N-back stream into another primary task).
prepareTaskHooks(hooks, options?)- filters disabled hooks (
enabled: false) - resolves stable IDs
- orders hooks by
prioritythen declaration order
- filters disabled hooks (
createHookStateStore(initial?)- shared mutable state map for hook instances (
get,set,update,delete,entries)
- shared mutable state map for hook instances (
runTaskHookLifecycle(args)- async lifecycle fanout for
task_start | task_end | block_start | block_end | trial_start | trial_end
- async lifecycle fanout for
emitTaskHookEvent(args)- async custom event fanout (
TaskHookEvent) for task-specific signals - suitable for per-trial/per-stage side channels (audio onset, cursor stream ticks, etc.)
- async custom event fanout (
applyTrialPlanHooks(trial, context, hooks?)applyTrialPlanHooks({ trial, context, hooks, state?, options? })applyTrialPlanHooksAsync(args)
Hooks can transform trial plans and access shared hook state (state) plus per-hook IDs (hookId).
Sync API throws if a hook returns a Promise.
evaluateTrialOutcomeWithHooks(args)evaluateTrialOutcomeWithHooksAsync(args)
Hooks can patch:
- inputs before evaluation (
beforeEvaluate) - output after evaluation (
afterEvaluate)
Sync API throws if a hook returns a Promise.
Both APIs support HookExecutionOptions:
continueOnError(defaultfalse)onError(error, context)callback
Computes common RT phase durations from a timing config:
- fixation
- blank
- pre-response stimulus
- response window
- post-response stimulus
options.responseTerminatesTrial (default false) forces postResponseStimulusMs to 0.
Runs a generic single-trial RT lifecycle with:
- normalized timing decomposition
- shared response capture window
- user-provided render hooks for fixation/blank/stimulus
- optional
responseTerminatesTrialphase shaping (for fixed-trial tasks keep thisfalse)
Resolves an RT task config from:
- a required
baseTiming - optional
overrideobject (enabled,responseTerminatesTrial,timing.*) - default flags (
defaultEnabled,defaultResponseTerminatesTrial)
Useful for task-level defaults.
Merges a partial override onto an already-resolved RT config. Useful for per-block/per-condition overrides in plan-driven tasks.
Parses instructions.blockSummary into a normalized config.
Supports:
enabledat:before_post/after_post(aliases for block-end insertion slots)titlelines(string or string[])whenfilters (blockIndex,blockLabel,blockType,isPractice)wheretrial-result filters (field -> value or array of values; supports dotted paths)metrics.correctField,metrics.rtField(supports dotted paths)
Builds a computed block summary from block metadata and trial results. Template variables include:
{blockLabel},{blockIndex},{blockIndex1},{blockType},{isPractice}{total},{correct},{incorrect},{accuracyPct},{meanRtMs},{validRtCount}{blockSpawned},{blockCleared},{blockDropped},{blockPoints}{experimentSpawned},{experimentCleared},{experimentDropped},{experimentPoints}
Renders a simple HTML card from a summary model for tasks that use custom waitForContinue screens.
Computes filtered summary stats from trial results using where + metrics.
Both where keys and metric field names can use dotted paths (for example game.stats.cleared).
Useful when non-UI control flow (for example, retry logic) should use the same scoring semantics as block summary screens.
Parses a block-level repeatUntil object into normalized form.
Supports:
enabledmaxAttemptsminAccuracy(0..1) andminAccuracyPct(0..100 alias)minCorrect,minTotalmaxMeanMetric,minMeanMetric(mean absolute value ofmetrics.metricField)wheretrial-result filtering (supports dotted paths)metrics.correctField,metrics.metricField(supports dotted paths)
Evaluates pass/repeat decisions for one block attempt from trial results. Returns:
passedshouldRepeatreason(threshold_met,threshold_not_met,max_attempts_reached,disabled)- attempt-local
stats(total,correct,accuracy,meanMetric)