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
Config-path precedence:
- JATOS component input
config(aliasesconfigID/configId) - URL
config variantas a config alias — URLvariant, or JATOSvariantId/variant/variant_id— resolved task-scoped first (configs/<taskId>/<variant>.json)
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 participantIdis the canonical participant identifier after resolution;sonaIdis only populated from the rawSONA_IDparameter when present- 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}
Performs optional EEG bridge preflight:
- resolves effective EEG config from
coreConfig.eeg+taskConfig.eeg - if
enabled=trueandrequireBridge=true, checks{bridgeUrl}/health - throws on failure (caller can block experiment launch)
Resolves effective EEG bridge settings (enabled, bridgeUrl, requireBridge, event forwarding controls).
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[]
Runs any SubTaskHandle (see the concurrent runner) as a standalone orchestrated
experiment: each trial is a timed epoch during which scenario events are injected
according to a config-declared schedule. The MATB standalone adapters
(matb-comms, matb-sysmon, matb-resman) are thin EpochStandaloneTaskSpec
objects over this runner.
The spec supplies the irreducibly task-specific pieces:
createHandle— sub-task handle factoryparseScheduleEntry(raw)/toScenarioEvents(event)— schedule parsing and scenario-event expansiontrialRecordFields(result)/trialEndEventFields?(result)— per-trial CSV columns and trial_end event payloadauxMetadataKey?/collectAuxRecords?(result)— auxiliary per-trial records accumulated into task metadata- identity/config keys:
taskId,mode,runnerName,csvSuffix,buttonIdPrefix,subTaskConfigKey,scheduleKey, stage size, instruction defaults, duration/ITI defaults
parseEpochStandaloneConfig(taskConfig, spec) handles the shared config shape:
task.title, instructions, plan.blocks[] (label/phase/trials/trialDurationMs,
before/after block screens, per-block schedule + sub-task config overrides),
trialDefaults, and interTrialIntervalMs.
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.resolveUniformBlockScopedModuleConfig({ trialConfigs, moduleId, coerce, warnLabel? }): resolve the single block-scoped config for a module across all trial configs in a block; warns and uses the first resolved config when trials disagree.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
Builds the standard fixation/blank/stimulus/feedback canvas renderers used by framed RT tasks (stroop, flanker, go_no_go, sft). Every phase draws the shared cue-and-frame scene; only the stimulus content differs per task and is supplied as the drawStimulus(frame, trial) callback.
scene fields:
layout: aCanvasFrameLayoutfromcomputeCanvasFrameLayoutframeBackground/frameBorder/cueColorcueText: static string or(trial) => stringprovider for per-trial cues (e.g. sft rule cues)fixation:{ text?, color?, fontSizePx?, fontWeight? }— glyph defaults to"+"
Returned drawers: drawFixation(canvas, trial?), drawBlank(canvas, trial?), drawStimulus(canvas, trial), drawFeedback(canvas, feedback, view) (delegates to drawTrialFeedbackOnCanvas).
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.
- DRT ignores repeated
keydownevents for its response key until akeyupis observed. - 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])
toPrimitiveCell(value): coerce any value to a CSV-safe primitive (objects/arrays JSON-serialized, null/undefined → null).nonEmptyString(value): trimmed string or null.flattenUnknown(value, prefix, out): flatten nested objects intoprefix_keycolumns; arrays are JSON-serialized rather than expanded.pruneEmptyUnknown(value): recursively drop null/blank/empty entries; returnsundefinedwhen nothing remains.
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.
- The final task payload's
blocks[]entries containblockIndexandlabel, plus an optional semanticblockTypecopied from the planned block.
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 - repeat behavior flag:
instructions.skipBeforeBlockScreensOnRepeat(skip block-levelbeforeBlockScreenson retry attempts) - 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: "mix"(default): averages three robust estimators (method-of-moments, conservative min-RT fallback, and Gauss-Legendre MAP node selection over feasiblet0values).t0Mode: "fixed": 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).
- Drift posterior now uses the Gamma-Normal conjugate node moments directly (TMSA), replacing the earlier student-
t-based approximation. - 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.
Parses a per-condition trial quota map from config. When no quota is provided for any label, trials are split evenly across labels (remainder distributed from the first label onward). When provided, the quotas must sum to expectedTotal or an error is thrown; pass options.taskName to prefix the error message (for example "Stroop").
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[] (shared summary stats for all lines)
- line object or line object[] with:
text(orline/template) template- optional
wherefilters (field -> value or array; dotted paths supported; string values support*wildcard andregex:prefix) - optional
metricsoverrides (correctField,rtField,metricField)
whenfilters (blockIndex,blockLabel,blockType,isPractice)wheretrial-result filters (field -> value or array of values; supports dotted paths,*wildcard strings, andregex:string patterns)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)