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:
- 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).
- 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. - 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). - 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_acknowledgmentstamps anssvep_ack: {operator, acknowledged_at}block onto the already-saved_beh.jsonthe 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. - If the questionnaire declares
rest_after, a self-paced rest screen appears. No countdown, and nothing about it changed here — see the callout below. - 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).
RestScreendid 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.jsonis 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-NNNis the administration ordinal, which is precisely what BIDSrunmeans: repeated acquisitions of the same task. A once-per-session instrument gets norunentity.- The
.tsvis the data: a header of item names, one row of answers,n/afor unanswered. - The
.jsonis the BIDS sidecar (per-columnDescription+Levels) and carries the whole instrument, the responses, the flags the operator was shown, the timestamps, and the samesession_positionblock 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.xdfand does not seebeh/. 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 insourcedataonly; the converter does not carry it into the BIDS dataset proper.sourcedatais 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.