diff --git a/docs/development/v3-editor-handoff-2.md b/docs/development/v3-editor-handoff-2.md index dc8209b..d2ea06c 100644 --- a/docs/development/v3-editor-handoff-2.md +++ b/docs/development/v3-editor-handoff-2.md @@ -149,11 +149,12 @@ import, multi-doc YAML streams, pattern-path validation, a per-anchor ### Tier 5: polish -- **Phase 8** — write `docs/development/v3-matlab-validation.md` describing the - MCP-driven flow that confirms web output loads in MATLAB (the original Phase 1 - gate item 4, deferred). -- **Phase 9** — `experiment_designer_v3_quickstart.html`, a step-by-step - walkthrough modeled on `experiment_designer_quickstart.html` (the v2 one). +- **Phase 8** ✅ — `docs/development/v3-matlab-validation.md` describes the + MCP-driven flow that confirms web output loads in MATLAB (web side is CI-green; + the MATLAB-side script is documented as a manual gate, still to be committed in + maDisplayTools). +- **Phase 9** ✅ — `experiment_designer_v3_quickstart.html` shipped (step-by-step + walkthrough incl. the D4 cross-library import flow; linked from the editor header). - **Phase 7 leftovers** (optional): params-level anchor cases, deeper validation-error matrices. Core Phase 7 shipped in #78 (Suite 31). diff --git a/docs/development/v3-matlab-validation.md b/docs/development/v3-matlab-validation.md new file mode 100644 index 0000000..1d7664e --- /dev/null +++ b/docs/development/v3-matlab-validation.md @@ -0,0 +1,138 @@ +# Protocol v3 — Web → MATLAB Validation + +Validates that Protocol **v3** YAML produced/edited by the **v3 Experiment Designer** +(`experiment_designer_v3.html`) is loadable by the **MATLAB experiment runner** +(`ProtocolParser` + `ProtocolRunner`) in maDisplayTools. + +This is the v3 analogue of [`protocol-roundtrip-testing.md`](protocol-roundtrip-testing.md) +(v1/v2). It documents Phase 1 gate item 4 (the MATLAB cross-check, deferred to Phase 8). + +> **Status.** The **web side is implemented and CI-green** (576 checks in +> `tests/test-protocol-roundtrip-v3.js`). The **MATLAB side is a manual gate**: +> there is no v3-specific validation script in maDisplayTools yet (only the v2 +> `validate_web_protocol_roundtrip.m`). This doc describes the flow and how to run +> the cross-check via the MATLAB MCP tools until that script is written. + +## Architecture + +The v3 editor differs from v2 in one load-bearing way: **the YAML document is the +single source of truth.** The editor parses a v3 YAML into a `YAML.Document`, edits +it through node-level helpers, and exports via `_doc.toString()` — so anchors, +comments, and key order survive the round-trip. Validation therefore checks that +*the editor's exported YAML* (not a freshly-generated one) loads in MATLAB. + +``` +Web (JavaScript) MATLAB (maDisplayTools) +───────────────── ────────────────────── +experiment_designer_v3.html ProtocolParser.m + └─ parseV3Protocol() ── edits ──┐ └─ parse() (v3-aware) + (js/protocol-yaml-v3.js) │ │ + │ (YAML.Document model) │ ▼ + ▼ │ Parsed protocol struct + exported v3 YAML ───────────────┴───▶ (version 3, rig, plugins, + (anchors + comments preserved) variables/anchors resolved, + │ conditions, experiment[]) + ▼ │ + test-protocol-roundtrip-v3.js ▼ + (parse → re-emit → compare, ProtocolRunner constructor + 576 checks, anchors/comments) (dry-run, validates structure; + no hardware init) +``` + +## Test coverage + +| Layer | Test | Runs in CI? | +|-------|------|-------------| +| Web parse → re-emit round-trip (anchors + comments preserved) | `tests/test-protocol-roundtrip-v3.js` | Yes (Node.js) | +| Web node-edit helpers + D4 cross-library import (suites N1–N10) | `tests/test-protocol-roundtrip-v3.js` | Yes (Node.js) | +| MATLAB parse + anchor/plugin resolution (v3) | *manual gate — script TBD* | No (needs MATLAB) | +| MATLAB `ProtocolRunner` construction (v3, dry-run) | *manual gate — script TBD* | No (needs MATLAB) | + +## Running the validation + +### 1. Web-side (CI, no dependencies) + +```bash +cd webDisplayTools +npm run test:protocol-v3 # or: node tests/test-protocol-roundtrip-v3.js +``` + +576 checks across the v3 round-trip suites + the D4 cross-library-import suites +(N1–N10). Covers: parsing the two canonical v3 YAMLs (pinned from maDisplayTools +`origin/version3`) plus coverage-gap fixtures; re-emitting them byte-stably with +anchors and comments intact; the node-level edit helpers; and the cross-doc import +substrate. Exit code 0 = all passed. + +The canonical fixtures are kept in sync with upstream via +[`tests/refresh-v3-canonical.sh`](../../tests/refresh-v3-canonical.sh) — if upstream +changes the spec YAMLs, that surfaces the drift so the parser/tests can be updated. + +### 2. MATLAB-side cross-check (manual gate) + +The goal: confirm an editor-exported v3 YAML loads in MATLAB end to end. + +1. In the editor, load (or import-and-commit) a protocol that exercises the features + you care about, then **Export YAML** to a file under + `maDisplayTools/tests/web_generated_patterns/` (e.g. `test_protocol_v3.yaml`). +2. Run the MATLAB cross-check. Until a dedicated `validate_web_protocol_roundtrip('v3')` + path exists, drive it through the MATLAB MCP tools: + - `check_matlab_code` — static-analyze the parser entry point. + - `run_matlab_file` / `evaluate_matlab_code` — execute, e.g.: + ```matlab + p = ProtocolParser('tests/web_generated_patterns/test_protocol_v3.yaml').parse(); + assert(p.version == 3); + % anchors resolved to literal values; plugins present; conditions/experiment populated + r = ProtocolRunner(p); % dry-run: constructs + validates, no hardware + ``` +3. Confirm: version = 3; every `*alias` resolved to its anchor's literal value; + `plugins:` entries present; `conditions:` and `experiment:` populated; the + `ProtocolRunner` constructor succeeds. + +When this stabilizes, promote it into a committed +`maDisplayTools/tests/validate_web_protocol_roundtrip.m` v3 branch (mirroring the v2 +checks) so it becomes a repeatable gate. + +## Files + +### Web side (`webDisplayTools/`) +| File | Purpose | +|------|---------| +| `experiment_designer_v3.html` | The v3 editor; exports via `_doc.toString()` | +| `js/protocol-yaml-v3.js` | v3 parser/generator + node-level edit helpers | +| `js/v3-import.js` | D4 cross-library import substrate (dual-export) | +| `tests/test-protocol-roundtrip-v3.js` | v3 round-trip + import tests (576 checks) | +| `tests/fixtures/v3_*.yaml` | Canonical + coverage-gap fixtures | +| `tests/refresh-v3-canonical.sh` | Pull canonical v3 YAMLs from upstream | + +### MATLAB side (`maDisplayTools/`) +| File | Purpose | +|------|---------| +| `tests/validate_web_protocol_roundtrip.m` | v2 validator (v3 branch TBD) | +| `tests/web_generated_patterns/` | Drop exported v3 YAMLs here for the cross-check | +| Authoritative v3 spec | `docs/development/yaml_protocol_documentation_v3.md` on `origin/version3` | + +## What to update when the v3 format changes + +1. Refresh the canonical fixtures: `tests/refresh-v3-canonical.sh`, then + `git diff tests/fixtures/v3_canonical_*.yaml` to see upstream drift. +2. Update the parser/generator/helpers in `js/protocol-yaml-v3.js` (and + `js/v3-import.js` if the import substrate is affected) to match. +3. Run `npm run test:protocol-v3` — must stay green. +4. Re-export a sample and re-run the MATLAB cross-check (step 2 above). +5. Update `docs/development/v3-spec.md`'s pinned SHA if the spec moved. + +## Known constraints + +- **Dry-run only.** The MATLAB cross-check validates parse + construction, not + `run()` — it does not initialize hardware (PanelsController). +- **`ProtocolParser.m` is upstream code** (Lisa's) — DO NOT MODIFY. If the editor's + output doesn't parse, fix the web side or discuss the spec, don't patch the parser. +- **Anchors are document-local.** The editor resolves `*alias` to the anchoring + document only. Cross-document anchor reuse is not a thing; D4 import namespaces + imported anchors into the target document (see D4 design §3). +- **Complex anchors are read-only in the UI.** Map/sequence anchors render as a + badge and are edited in YAML directly; the round-trip still preserves them. +- **D4-imported snippets are runnable, not behavioral clones** (D4 design §12): + import copies conditions + their direct anchor/plugin dependencies and appends a + bare sequence ref. It does not reproduce the source's block membership, + repetitions, randomize, or intertrial placement. diff --git a/experiment_designer_v3.html b/experiment_designer_v3.html index cf8a016..b327de0 100644 --- a/experiment_designer_v3.html +++ b/experiment_designer_v3.html @@ -68,18 +68,6 @@ color: var(--accent); font-weight: 700; } - .beta-badge { - display: inline-block; - padding: 0.1rem 0.5rem; - font-size: 0.65rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - background: rgba(41, 182, 246, 0.15); - color: var(--beta); - border: 1px solid var(--beta); - border-radius: 3px; - } .header-spacer { flex: 1; } .header-btn { background: var(--surface-2); @@ -492,7 +480,7 @@ /* ── 3-zone main area ────────────────────────────────────────── */ .three-zone { display: grid; - grid-template-columns: 280px 1fr 360px; + grid-template-columns: 320px 1fr 360px; flex: 1; min-height: 0; border-bottom: 1px solid var(--border); @@ -504,6 +492,17 @@ min-height: 0; } .zone:last-child { border-right: none; } + /* Left column is split into two stacked panels: Conditions + Variables. */ + #libraryZone .lib-split { + display: flex; + flex-direction: column; + min-height: 0; + } + #libraryZone .lib-split.lib-split-conditions { flex: 1.5; } + #libraryZone .lib-split.lib-split-variables { + flex: 1; + border-top: 1px solid var(--border); + } .zone-header { padding: 0.5rem 0.75rem; background: var(--surface); @@ -1184,7 +1183,6 @@

v3 Experiment Designer

- Beta / Editor
- +
-
- Library - 0 - +
+
+ Library + 0 + +
+
+ +
+
Import a v3 YAML to populate the conditions library.
+
+
-
- -
-
Import a v3 YAML to populate the conditions library.
+
+
+ Variables + 0 +
+
+
Import a v3 YAML to view its variables (anchors).
@@ -1322,7 +1332,7 @@

v3 Experiment Designer

@@ -1921,38 +1931,55 @@

Import error

root.appendChild(pluginBox); } - // Variables — inline-editable (Phase 5, v0.10) - // Always render the section even when empty so the user can add - // the first variable. Complex anchors (maps/seqs) render as - // read-only badges; scalar anchors are name + value editable. - const varBox = el('div', { class: 'settings-section' }); - const varCount = experiment.variables ? experiment.variables.length : 0; - varBox.appendChild(el('h3', {}, 'Variables (' + varCount + ')')); + // Variables moved out of the drawer into the always-visible left-column + // panel (#variablesPanel) — see renderVariables(). The drawer now holds + // only Experiment Info, Rig, and Plugins. + } + + // Variables (anchors) editor — always-visible panel below the conditions + // library (was in the Settings drawer through v0.17). Renders into + // `hostEl` (#variablesPanel). Complex anchors (maps/seqs) show a read-only + // badge; scalar anchors are name + value editable. In import mode the + // panel is read-only (the target document is locked — design fix #8). + function renderVariables(hostEl) { + if (!hostEl) return; + hostEl.innerHTML = ''; + const varCount = experiment && experiment.variables ? experiment.variables.length : 0; + const countBadge = $('varCount'); + if (countBadge) countBadge.textContent = varCount; + + if (!experiment) { + hostEl.appendChild(el('div', { class: 'empty-state' }, + 'Import a v3 YAML to view its variables (anchors).')); + return; + } + + const locked = importMode; // target doc is read-only during import const table = el('table', { class: 'vars-table' }); - const thead = el('thead', {}, el('tr', {}, + table.appendChild(el('thead', {}, el('tr', {}, el('th', {}, 'Anchor'), el('th', {}, 'Value'), el('th', { style: 'width: 1.5em;' }, '') - )); - table.appendChild(thead); + ))); const tbody = el('tbody'); for (const v of experiment.variables || []) { const isComplex = variableIsComplex(experiment, v.name); const row = el('tr', { class: 'var-row' }); - // Name cell — always editable (rename cascades) + // Name cell — editable (rename cascades), read-only while importing. const nameInput = el('input', { type: 'text', value: v.name, class: 'var-name-input', - title: 'Rename anchor. Renaming cascades to every *alias reference.', + disabled: locked, + title: locked + ? 'Locked during import — finish or cancel the import first.' + : 'Rename anchor. Renaming cascades to every *alias reference.', }); - // change fires on blur, so each visit to the input produces - // at most one pushUndo (inside onVariableRename). nameInput.addEventListener('change', () => onVariableRename(v.name, nameInput.value.trim())); row.appendChild(el('td', {}, nameInput)); - // Value cell — editable scalar OR read-only badge for complex + // Value cell — editable scalar OR read-only badge for complex. if (isComplex) { row.appendChild(el('td', {}, el('span', { @@ -1966,62 +1993,68 @@

Import error

type: isNum ? 'number' : 'text', value: isNum ? String(v.value) : String(v.value ?? ''), class: 'var-value-input ' + (isNum ? 'num' : 'str'), - title: 'Edit the anchor value. Every *alias to this anchor will resolve to the new value.', + disabled: locked, + title: locked + ? 'Locked during import — finish or cancel the import first.' + : 'Edit the anchor value. Every *alias to this anchor will resolve to the new value.', }); valInput.addEventListener('change', () => onVariableValueEdit(v.name, valInput.value, isNum)); row.appendChild(el('td', {}, valInput)); } - // Delete cell - const delBtn = el('button', { - class: 'var-del-btn', - title: 'Delete this anchor. Blocked if it has *alias references; the prompt will offer cascade-unbind.', - onClick: () => onVariableDelete(v.name), - }, '✕'); - row.appendChild(el('td', {}, delBtn)); + // Delete cell — hidden while importing. + if (locked) { + row.appendChild(el('td', {}, '')); + } else { + row.appendChild(el('td', {}, el('button', { + class: 'var-del-btn', + title: 'Delete this anchor. Blocked if it has *alias references; the prompt will offer cascade-unbind.', + onClick: () => onVariableDelete(v.name), + }, '✕'))); + } tbody.appendChild(row); } - // + Add variable row (always at end) - const addRow = el('tr', { class: 'var-add-row' }); - const addNameInput = el('input', { - type: 'text', - placeholder: 'new_anchor_name', - class: 'var-name-input', - title: 'Anchor name (letters, digits, _, -). Pressing Enter or Tab creates the anchor with the value to the right.', - }); - const addValInput = el('input', { - type: 'text', - placeholder: 'value', - class: 'var-value-input', - title: 'Initial value. Numeric strings become numbers; everything else stays a string.', - }); - const addBtn = el('button', { - class: 'var-add-btn', - title: 'Create the new anchor.', - onClick: () => { - const name = addNameInput.value.trim(); - const val = addValInput.value; - if (!name) return; - onVariableAdd(name, val); - addNameInput.value = ''; - addValInput.value = ''; - addNameInput.focus(); - }, - }, '+ Add'); - // Enter in either input submits the add - const submitOnEnter = (e) => { if (e.key === 'Enter') { e.preventDefault(); addBtn.click(); } }; - addNameInput.addEventListener('keydown', submitOnEnter); - addValInput.addEventListener('keydown', submitOnEnter); - addRow.appendChild(el('td', {}, addNameInput)); - addRow.appendChild(el('td', {}, addValInput)); - addRow.appendChild(el('td', {}, addBtn)); - tbody.appendChild(addRow); + // + Add variable row (always at end) — omitted while importing. + if (!locked) { + const addRow = el('tr', { class: 'var-add-row' }); + const addNameInput = el('input', { + type: 'text', + placeholder: 'new_anchor_name', + class: 'var-name-input', + title: 'Anchor name (letters, digits, _, -). Pressing Enter or Tab creates the anchor with the value to the right.', + }); + const addValInput = el('input', { + type: 'text', + placeholder: 'value', + class: 'var-value-input', + title: 'Initial value. Numeric strings become numbers; everything else stays a string.', + }); + const addBtn = el('button', { + class: 'var-add-btn', + title: 'Create the new anchor.', + onClick: () => { + const name = addNameInput.value.trim(); + const val = addValInput.value; + if (!name) return; + onVariableAdd(name, val); + addNameInput.value = ''; + addValInput.value = ''; + addNameInput.focus(); + }, + }, '+ Add'); + const submitOnEnter = (e) => { if (e.key === 'Enter') { e.preventDefault(); addBtn.click(); } }; + addNameInput.addEventListener('keydown', submitOnEnter); + addValInput.addEventListener('keydown', submitOnEnter); + addRow.appendChild(el('td', {}, addNameInput)); + addRow.appendChild(el('td', {}, addValInput)); + addRow.appendChild(el('td', {}, addBtn)); + tbody.appendChild(addRow); + } table.appendChild(tbody); - varBox.appendChild(table); - root.appendChild(varBox); + hostEl.appendChild(table); } // ─── Variables editor handlers ───────────────────────────────────── @@ -2041,7 +2074,7 @@

Import error

if (oldName === newName) return; if (!newName) { showError('Rename failed', 'Anchor name cannot be empty.'); - renderSettings(); + renderVariables($('variablesPanel')); return; } if (!isValidAnchorName(newName)) { @@ -2049,12 +2082,12 @@

Import error

'Rename failed', 'Invalid anchor name: must contain only letters, digits, underscores, and hyphens.' ); - renderSettings(); + renderVariables($('variablesPanel')); return; } if (anchorExists(experiment, newName)) { showError('Rename failed', 'Anchor name "' + newName + '" is already in use.'); - renderSettings(); + renderVariables($('variablesPanel')); return; } const refs = findAliasesTo(experiment, oldName); @@ -2065,7 +2098,7 @@

Import error

list: refs.map((r) => '• ' + r.humanLabel), confirmLabel: 'Rename', }); - if (!proceed) { renderSettings(); return; } + if (!proceed) { renderVariables($('variablesPanel')); return; } } pushUndo(); try { @@ -2074,7 +2107,7 @@

Import error

renderAll(); } catch (err) { showError('Rename failed', err.message); - renderSettings(); + renderVariables($('variablesPanel')); } } @@ -2083,7 +2116,7 @@

Import error

const coerced = isNumericInput ? Number(rawValue) : _coerceVarValue(rawValue); if (isNumericInput && Number.isNaN(coerced)) { showError('Update failed', 'Value must be a number.'); - renderSettings(); + renderVariables($('variablesPanel')); return; } pushUndo(); @@ -2093,7 +2126,7 @@

Import error

renderAll(); } catch (err) { showError('Update failed', err.message); - renderSettings(); + renderVariables($('variablesPanel')); } } @@ -2708,6 +2741,7 @@

Import error

$('inspectorZoneTitle').textContent = 'Import inspector'; $('importStagedCount').textContent = staging.items.length; renderYoursLockedLibrary(); + renderVariables($('variablesPanel')); // read-only while locked renderTheirsLibrary(); renderImportInspector(); } @@ -4836,6 +4870,7 @@

Import error

function renderAll() { renderSettings(); renderLibrary(); + renderVariables($('variablesPanel')); renderSequence(); renderInspector(); renderTimeline(); diff --git a/experiment_designer_v3_quickstart.html b/experiment_designer_v3_quickstart.html new file mode 100644 index 0000000..6821ba5 --- /dev/null +++ b/experiment_designer_v3_quickstart.html @@ -0,0 +1,355 @@ + + + + + + v3 Experiment Designer Quick Start - PanelDisplayTools + + + + +
+ ← Back to v3 Experiment Designer + +

v3 Experiment Designer Quick Start

+

v0.18 | 2026-06-01 09:10 ET — Edit Protocol v3 YAML directly, with anchors and comments preserved on round-trip.

+ +

The v3 Experiment Designer is a YAML round-trip editor for Protocol v3. + Unlike the v2 designer (which builds a protocol from scratch in a form), v3 loads an existing + v3 YAML, lets you edit it through a structured UI, and exports it back — preserving your + anchors, comments, and key order. The on-disk YAML is the single source of truth.

+ +

The interface

+

Three columns over a flattened-timeline preview. The left column is split into a + Library of conditions and an always-visible Variables (anchors) panel.

+ +
┌───────────────┬──────────────────────┬─────────────────┐ +│ LIBRARYEXPERIMENT SEQUENCEINSPECTOR │ +│ conditions │ ▸ arena check │ selected │ +│ (search, │ ▾ main block │ condition / │ +│ + Add) │ trials, reps │ block / │ +├───────────────┤ ▸ posttrial │ ref details │ +│ VARIABLES │ │ (edit commands │ +│ &dur_short 3 │ │ inline) │ +│ &dur_long 10 │ │ │ +│ + Add │ │ │ +├───────────────┴──────────────────────┴─────────────────┤ +│ FLATTENED TIMELINE PREVIEW (every rep × trial expanded) │ +└─────────────────────────────────────────────────────────┘
+ +
    +
  • Library — every condition in the protocol. Click to inspect; drag onto the sequence; + Add creates one.
  • +
  • Variables — the variables: anchors. Edit a scalar value or rename an anchor and every *alias updates. Always visible now (no longer hidden in Settings).
  • +
  • Experiment Sequence — the ordered run: bare refs to conditions and blocks (groups of trials with repetitions / randomize / intertrial).
  • +
  • Inspector — edits the selected item: a condition's commands, a block's properties, a ref.
  • +
  • Timeline — a read-only preview of the flattened run order.
  • +
  • Settings ▾ (header) — experiment metadata, the rig path, and the read-only plugins list.
  • +
+ +

Walkthrough: edit and export a protocol

+ +
+ 1 +

Load a protocol. Use Import YAML to open your own file, or + Load demo ▾ to start from a bundled example (e.g. canonical_a).

+
+ +
+ 2 +

Inspect a condition. Click any row in the Library. Its commands appear in the + Inspector — controller (trialParams), wait, and plugin commands. + Edit fields inline; changes write straight to the YAML model.

+
+ +
+ 3 +

Use a Variable (anchor). In the Variables panel, edit &dur_long's + value — every command that references it via *dur_long resolves to the new value on export. + Click + Add to create a new anchor, or rename one to cascade the change across all references.

+

Complex anchors (maps / sequences) show a read-only badge — edit those in the YAML directly; renaming still works.

+
+ +
+ 4 +

Arrange the sequence. Drag a Library condition onto the Experiment Sequence to add a + ref, use + Ref / + Block, reorder by dragging, and edit block repetitions / + randomize / intertrial in the Inspector. The Timeline preview updates live.

+
+ +
+ 5 +

Export. Export YAML writes the file back out with your anchors and + comments intact. A non-blocking warnings strip flags unused conditions / anchors; a blocking modal + catches errors (duplicate names, dangling aliases) before you export.

+
+ +
+ Round-trip safe. Import → edit → export changes only what you touched. Anchors, + inline comments, and key order are preserved, so a diff against the original shows just your edits. +
+ +

Cross-library import (copy conditions between protocols)

+

Pull conditions out of a second v3 YAML — a sibling lab's protocol, or an older version of + your own — into the one you're editing, bringing along the anchors and plugin declarations they depend on.

+ +
+ 1 +

Open a source. With a protocol loaded, click Import from YAML… and pick + a second v3 file. The layout swaps: your protocol locks on the left, the source's conditions appear in + the middle, and an import inspector opens on the right.

+
+ +
+ 2 +

Stage conditions. Click ← Add on a source condition. It joins + "Pending additions" — its referenced anchors and plugins come along automatically (transitively).

+
+ +
+ 3 +

Adjust names. Imported anchors and plugins get a prefix__ (editable in + the banner) so they don't clash with yours; conditions keep their own names. The inspector flags any + name collision and suggests a fix. Plugins that match one of yours (same class + config) merge + instead of duplicating.

+
+ +
+ 4 +

Commit. Commit import applies the whole batch in one step (one + Undo reverts all of it). Imported nodes are stamped # imported from <source>. + Cancel discards everything and changes nothing.

+
+ +
+ Scope. Import copies conditions plus their direct dependencies and appends a + bare sequence ref so each is runnable. It does not reproduce the source's block membership, + repetitions, or intertrial placement — arrange those in your sequence afterward. +
+ +

Key concepts

+
    +
  • Condition — a named list of commands (the unit you reuse). Lives in conditions:.
  • +
  • Sequence ref vs. block — the experiment: list is the run order. A ref is a single condition; a block groups trials with repetitions, randomize, and an optional intertrial.
  • +
  • Anchor / alias&name value defines a reusable value in variables:; *name references it. Editing the anchor updates every reference.
  • +
  • Plugin — a runtime resource (camera, LED controller) declared in plugins: and called by plugin commands.
  • +
  • Controller modes — Mode 2 (constant rate, frame_rate set, gain 0) vs. Mode 4 (closed-loop, gain set, frame_rate 0).
  • +
+ +

Keyboard & history

+
    +
  • Ctrl/Cmd+Z — undo
  • +
  • Ctrl/Cmd+Y — redo (also Ctrl/Cmd+Shift+Z)
  • +
  • ⟲ Reset — clear to a blank minimum-valid skeleton (reversible with undo)
  • +
+

Edits are in-memory until you Export — the page warns before close/reload if you have unsaved + changes or an import in progress.

+ + +
+ +