Skip to content

Questionnaires — break-time symptom checkpoints & end-of-session (#20)

A participant stares at provocative flicker for a whole session. There has always been an always-available abort, but until now nothing structured asked them how they were doing. That is issue #20, and it is a prerequisite for the ethics-amendment requirement — "max session duration + mandatory breaks + symptom checkpoints" before multi-condition sweeps can be approved — being honest rather than aspirational (REB_AMENDMENTS.md).

This doc is the operator SOP and the design rationale. The instruments themselves come from Reference/SSVEP-Questionnaires-Ethics-Description.docx (Appendix B of the amendment).


1. The two instruments

Questionnaire 1 — post-condition Questionnaire 2 — end-of-session
When After every experimental condition Once, at the very end
Attached to A protocol group The protocol
Items 11: four engagement (Interest, Enjoyment, Challenged, Stimulated) + seven adverse (Headache, Lightheaded, Dizziness, Nausea, Disoriented, Mental fatigue, Visual fatigue) 2: a recommendation rating + an optional free-text comment
Scale 5-point: Very slightly or not at all · A little · Moderately · Quite a bit · Extremely 5-point, endpoint-anchored: Not at all likely … Extremely likely
Gating Every item required, then it finishes on its own The rating is required; the comment is not. Finishes once the rating is given
After A self-paced rest break Nothing — the session is over

Both are shipped verbatim as ssvep.runtime.questionnaire.post_condition() and end_of_session(). They are what the REB read — the items, the scale, and the wording. What changed under #97 is only the software mechanism for finishing: there is no longer a participant-facing "Confirm & Continue" / "Finish" button to click (ssvep.ui.questionnaire_dialog.QuestionnaireScreen finishes itself the instant every required item has an answer). Nothing about which questions are asked, their order, their scale, or their thresholds moved — that would be an amendment matter, and none of this is one. If a participant wants to add the (optional) end-of-session comment, they need to type it before giving the required rating, since the rating is what completes that instrument — noted here because it is the one place the new one-required-item-only instrument gives them less time than the old button did.

A group is the right home for the checkpoint because a group is one experimental condition — the thing the approved instrument is administered per. Nothing has to interpret "condition" at run time; the protocol already says where the boundaries are.


2. Authoring — the Build Protocol tab

Every new group gets the checkpoint automatically. That is the whole design decision: a symptom check that depends on someone remembering to add it is a symptom check that will be missing from the session where it mattered. The end-of-session questionnaire is likewise on by default.

  • Checkpoint… (with a group selected) → edit the questions, or remove the checkpoint.
  • End of session → Edit questions… in the protocol metadata box.
  • The tree's third column shows each group's checkpoint, and marks it (edited) when it is no longer the approved instrument.
  • The resolution preview shows checkpoints in the running order, flagged .

Removing one is deliberate and possible on purpose: this toolbox will serve groups under many ethics approvals — including deployments with none configured — and a protocol that does not require these instruments must be buildable. Where a deployment's own approval requires the checkpoint, removing it is a decision for that approval, not for this dialog.

Editing the questions

The editor pop-out changes the prompt, the scale anchors, the items, which items are adverse, and each one's flag threshold. It carries a standing warning, which is the honest position:

For a protocol running under a governing approval configured on this machine, changing the wording, the scale, or which symptoms are asked about is an amendment matter — check with your ethics board before editing. A protocol with no governing approval configured (a teaching or demo session) can be edited freely.

Restore REB defaults puts back the approved instrument exactly.

Protocols authored before #20

Untouched. Opening one in Build Protocol offers to add the defaults; nothing is written until you say yes and save. That matters because a protocol's fingerprint is stamped into every recording made under it — editing the file changes the fingerprint, so it has to be a decision, and it should come with a version bump.


3. Running — the Run Session tab

The session checklist is the literal running order: runs and checkpoints interleaved, a row after each group and the end-of-session instrument last. The checklist itself lives on Run Session, alongside the QA check (a collapsible pane below it) — moved there from Set up Session in

126. The Administer questionnaires toggle, along with loading/resolving the protocol and the

🔒 Lock, lives on Set up Session: #126 moved all of it to Run Session, and #147 moved the load/resolve/lock/toggle half back next to the identity fields the lock freezes, leaving only the checklist and the Start button on Run Session.

#   Group        Item                     Status
1   band-high    ssvep-high-a             ✓ recorded
2   band-high    ssvep-high-b             ✓ recorded
—   band-high    ⚑ post-condition         ✓ answered
3   band-med     ssvep-med-a              ○ pending
—   —            ⚑ end-of-session         ○ pending

A checkpoint follows its run on its own. When a run completes, the checkpoint that comes next is administered immediately — nobody has to press anything. The checkpoint belongs to the run that just ended: waiting on an operator click puts a person-shaped delay between the flicker and the symptom question meant to catch it, and pushes the break it precedes later and later into the session.

Two cases where it deliberately does not fire, and the button is left for the operator:

  • The run was aborted or errored. Its recording is partial and the operator is mid-problem. The checklist steps over partial runs, so without this exception an abort would ask the participant about a block that did not happen — and would mark done the checkpoint belonging to the re-run.
  • The run has no session position. Recording is protocol-only since #115, so in practice this is only a pre-#115 recording made outside a protocol (session_position: {"ad_hoc": true}).

In those cases the next-action button says what pressing it will do — Start selected run (named "Start next run" until #147 — renamed because the operator can select a different row to redo it) or Run questionnaire (one button since #126; it used to be a separate "Record next run" / "Re-run selected…" pair). An operator about to hand the mouse to a participant should not have been told "record". Once every run and questionnaire is done — and nothing is deliberately selected to redo — that button says Session complete and disables itself instead of staying up as a click that has nothing to do (#125) — it used to stay enabled/labelled "Record next run" after the session's last item was already recorded.

What happens, pressed or automatic:

  1. The questionnaire opens fullscreen on the participant's monitor — the highest-refresh display, the same rule the stimulus renderer picks by, so it lands on the screen the flicker was on. It is made application-modal before that first show (not after), which is what actually blocks the operator's own window for as long as it is up — a subtlety that matters, because setting modality after a widget's first show is a Qt no-op, and that gap is exactly what let an operator interact with the main window during a live questionnaire before this was fixed (#97).
  2. The participant answers, and there is no separate submit/continue step. The instant every required item has an answer, the screen finishes itself and control returns — nothing is left for the participant to press. Before #97 this was a "Confirm & Continue" / "Finish" button the operator had no way to know was still outstanding, so they could (and did) try to start the next run while the participant was still looking at an enabled button nobody had told them about. Esc does nothing; the operator's way out is operator: end, which records declined — its confirmation dialog is parented to the operator's own window, never shown on the participant's screen.
  3. Responses are written (§4). The questionnaire screen itself is then always closed and released — in a finally, regardless of how the administration ended — so it can never linger or reappear between later runs the way it did before this was fixed (#122).
  4. The operator sees a popup reporting this checkpoint's scores every single time it completes — not only when something is flagged (#97). Before, a dialog only ever appeared for the alarming case, so the ordinary one gave the operator nothing to look for and no reliable way to know the participant was actually done. When something is flagged, the popup keeps its warning styling and must be acknowledged before the checklist moves on (§5); the responses were already written in step 3 — participant first, operator interruption second — so the acknowledgement itself has nowhere to land unless something writes it back afterward: ssvep.runtime.questionnaire.record_acknowledgment stamps an ssvep_ack: {operator, acknowledged_at} block onto the already-saved _beh.json the moment the operator dismisses the dialog. Before this existed the acknowledgement happened but left no record at all — the 2026-08-28 pilot (sub-994) had a real flagged checkpoint the operator did check in on, with nothing on disk to show it.
  5. If the questionnaire declares rest_after, a self-paced rest screen appears. No countdown, and nothing about it changed here — see the callout below.
  6. When the participant ends their own break, the operator is told. The break screen is fullscreen on the participant's monitor, so "I'm ready to continue" is invisible from the operator's station — without this the session sits finished-and-waiting until somebody happens to look. The prompt names what is next and offers Set up next, which prepares that run (locks its manifest, expands the QA-check pane). Starting it stays a deliberate act on the Run Session tab (#86).

RestScreen did not change. #97/#122/#125 are all about the questionnaire screen and the operator's side of the handoff — the mandatory rest break that can follow it (step 5 above) is a separate class (ssvep.ui.questionnaire_dialog.RestScreen) and REB Appendix B describes it as self-paced: the participant decides when it ends, with no countdown. Making it operator-timed instead would be a new constraint on the participant nobody described to them or to the REB (docs/REB_AMENDMENTS.md), so it was deliberately left untouched by this fix and stays exactly as tested before.

Re-administering. Click the checkpoint in the checklist (it becomes the highlighted row) and press Start selected run — a confirmed overwrite, same as re-recording a run (one button since #126; it used to be a separate "Re-run selected…"). A questionnaire can go wrong in ways the checklist cannot see (answered by the operator by mistake, ended too early), and overwriting a real person's answers should be a decision.

Turning them off — "Administer questionnaires"

Unticking it skips every checkpoint for the session. Use it for a rig test with no real participant.

Two things happen, and the second is the point:

  • Checkpoints show as skipped in the checklist rather than disappearing, so nobody has to wonder later whether the protocol had any.
  • Every run recorded that session is stamped session_position.questionnaires: "skipped". A protocol that declares none stamps "none"; a normal session stamps "administered".

An absent response file has to mean exactly one thing ("not asked yet") for the checklist to resume correctly. So the reason there will never be one gets written down, not left to be inferred. A rig test and a session whose responses were lost must not read the same way six months later — that is the ses-002 lesson applied one artifact over.

The switch is disabled for a protocol that declares no questionnaires; there is nothing to skip.


4. Where the answers go

sourcedata/sub-XXX/ses-YYY/beh/ — the BIDS beh datatype, a sibling of the eeg/ folder the runs already write into, so one session directory describes itself:

sourcedata/sub-905/ses-001/
├── eeg/
│   ├── sub-905_ses-001_task-ssvep_run-001_eeg.xdf
│   └── sub-905_ses-001_task-ssvep_run-001_eeg.session.json
└── beh/
    ├── sub-905_ses-001_task-postcondition_run-001_beh.tsv   # after group 1
    ├── sub-905_ses-001_task-postcondition_run-001_beh.json
    ├── sub-905_ses-001_task-postcondition_run-002_beh.tsv   # after group 2
    ├── sub-905_ses-001_task-postcondition_run-002_beh.json
    ├── sub-905_ses-001_task-endofsession_beh.tsv
    └── sub-905_ses-001_task-endofsession_beh.json
  • One pair per administration, never appended to. File-exists is then the completion test — exactly as *.session.json is for a run — so an abandoned session resumes at the right point and nothing is rewritten mid-session. There is still no separate progress file to desync from the data.
  • run-NNN is the administration ordinal, which is precisely what BIDS run means: repeated acquisitions of the same task. A once-per-session instrument gets no run entity.
  • The .tsv is the data: a header of item names, one row of answers, n/a for unanswered.
  • The .json is the BIDS sidecar (per-column Description + Levels) and carries the whole instrument, the responses, the flags the operator was shown, the timestamps, and the same session_position block a recording gets. The file can say what it asked without anyone having to go and find the protocol version.
  • Free text is verbatim in the JSON only. In the TSV, tabs and newlines are flattened — a raw tab in a cell would silently shift every column after it, which is worse than an escaped newline.

Declined is recorded, not absent. A refusal writes a file with ssvep_state: "declined". A participant who could not finish and a checkpoint that never happened must not look the same.

Compliance

  • Responses are participant data: under sourcedata/, so git-ignored (R1), Canada-only (R3), and pushed only under consent (R5).
  • Observer-only: nothing is written at all. The questionnaire is still administered and the operator still sees any flags — the person under the flicker is real whether or not their data is kept, so the safety check is not theirs to lose. Only the file is withheld.
  • The free-text box carries an on-screen note asking for nothing identifying (R2). It is the one place a participant can type anything at all, and the only place that rule can act is before the typing.
  • Responses are not copied into the BIDS dataset proper yet — the converter reads sub-*/ses-*/eeg/*_eeg.xdf and does not see beh/. A follow-up.

5. Flags — what makes this a safety measure

Only adverse items can flag, and only at or above their own threshold:

Items Flags at
Headache, Lightheaded, Dizziness, Nausea, Disoriented 3 — "Moderately"
Mental fatigue, Visual fatigue 4 — "Quite a bit"
Interest, Enjoyment, Challenged, Stimulated never

The split is deliberate. An hour of flicker makes moderate visual and mental fatigue an ordinary outcome; an alert that fires at every single break is one an operator learns to dismiss without reading. Engagement items never flag — a participant who is bored is not a safety event.

The thresholds live in the protocol, not in the code, so a study that wants different ones sets them in its own protocol rather than needing a change here.

When something flags, the operator gets a dialog naming the symptom, the response, and the threshold, and must acknowledge it before the checklist offers the next item. Nothing stops the session automatically. Whether to continue, shorten, or stop is a judgement about a person in the room, and the toolbox is not in a position to make it. What it can guarantee is that the operator saw.


6. Where the code is

Piece Where
Model, REB defaults, flagging, the beh/ writer (pure, Qt-free) ssvep.runtime.questionnaire
Protocol blocks + resolution into checkpoints + the checklist ssvep.runtime.session
Schema (questionnaire on a group, end_questionnaire on the protocol) schemas/session_protocol.schema.json (v1.1)
Participant screen, rest screen, operator acknowledgement, editor ssvep.ui.questionnaire_dialog
Design-tab authoring ssvep.ui.session_builder
Run-tab checklist + administration ssvep.ui.session_record

A protocol carrying questionnaires stamps protocol_version: "1.1"; one without them round-trips byte-identically as "1.0", fingerprint unchanged. Loading a pilot protocol to look at it does not re-version it.


7. Not yet, and why

  • BIDS export. beh/ lives in sourcedata only; the converter does not carry it into the BIDS dataset proper. sourcedata is exempt from BIDS validation, so nothing is wrong — it is just not done.
  • A minimum break duration. A9 says "mandatory breaks"; the ethics appendix says the break is self-paced. A countdown that locks the Continue button would be a new constraint on the participant that nobody has described to them or to the REB. If A9 comes back requiring a number, it goes in the questionnaire block beside rest_after.
  • Cross-session symptom trends. Each administration stands alone. Comparing a participant's checkpoints across a session (or across visits) is an analysis question, and nothing reads these files yet.
  • Per-item scale editing in the GUI. The editor changes the questionnaire-wide scale; a per-item override (Questionnaire 2's endpoint-anchored one) is preserved but must be edited in JSON.