Skip to content

Data governance architecture

For the operator-and-administrator walk through one session, start with Consent, safety and dates; this page is the mechanism behind it.

This page describes data governance mechanisms: how this toolbox structurally prevents participant data from ending up somewhere it shouldn't, rather than asking an operator to remember not to put it there. If you are adopting the toolbox as a researcher under the authority of a research ethics/institutional review board, this is what you get for free, and what you still have to configure yourself.

The claim worth stating plainly: compliance here is an architectural property, not a policy document. A policy document describes what people should do. Every mechanism below describes what the software makes impossible, hard to do by accident, or impossible to do quietly — enforced by code and CI, not by an operator remembering a rule between a recording and a share. Your institution's ethics board, privacy law, and data-security office set the actual rules you must follow — that binding lives in your own governance document, not here. What follows is the toolbox half: the part that holds regardless of which rules you plug in.

Participant data cannot enter git by construction

The repository tracks code, schemas, and documentation. Raw signals, behavioural logs, questionnaires, and anything BIDS-shaped that came from a participant are excluded by the root .gitignore — a plain git add of a file under sourcedata/, derivatives/, or BIDS/sub-*/ cannot be staged in an ordinary workflow. A second, unrelated file, BIDS's own .bidsignore, is not a git mechanism at all: it only tells a BIDS validator which paths to skip, so it has no bearing on what git add will accept.

.gitignore alone is not the mechanism, because it is advisory: git add -f bypasses it outright, and once a path is tracked, the ignore rule is never consulted for it again. A contributor with a stale checkout has a stale guard, and an ignore file cannot see the tree that actually reaches the shared branch. The structural part is a CI check that is a whole-tree invariant, not a diff check: it runs git ls-files against a set of protected-path patterns on every pull request and on every push to the default branch, with no merge-base to evade and no dependency on which commit introduced the file. Because it checks the tree, not the change, a file that slipped in on some bypass path still fails the very next PR's build — it cannot merge, then hide. The check is deliberately pure-language-stdlib with no environment dependency, so a data governance gate cannot fail open because an unrelated package broke.

One file needs a second look because it is a growing artifact rather than a static one: the participants table (participants.tsv) that gains a row per participant. Tracking a file like that means recording a participant requires a pull request — friction with no safety payoff. The mechanism here is to untrack it and assert, in CI, that it stays untracked — a dedicated check for exactly that path, independent of the pattern check above, because git add -f can re-add a specific path just as easily as any other, and .gitignore does nothing once a path is tracked. The file still exists on disk, regenerated by the conversion step from the recording metadata; it is simply never the thing a pull request carries.

To configure for your institution: the protected-path patterns and the specific growing-table path are the only two things tied to a particular file layout. If your BIDS layout or your per-recording sidecar format differs, update the pattern list and the growing-artifact path; the CI structure (whole-tree check on every PR, pure-language dependency, no bypass that survives to the next build) carries over unchanged.

De-identification is enforced on shape, not on convention

A participant identifier is a numeric code and nothing else. The rule that matters is not "please use codes" — it's that the columns a participant table is allowed to carry are an explicit, fixed set, checked mechanically rather than left to convention, so a hand-edited or accidentally-richer file is caught rather than quietly carrying a name or a date of birth downstream. Two checks ride on this: an allowlist of exactly the columns a coarse-demographics table is permitted to carry, and a second pass that scans every remaining cell for the shape of an identifier — an email address, a date, a phone number — regardless of which column it turned up in.

This is enforced as a test suite check against the working tree, run in CI on every pull request and locally with the rest of the suite — not a write-time rejection inside the application itself, so nothing stops a person from hand-editing the file between test runs. What it does guarantee is that an unrecognised column or an identifier-shaped value cannot ride along unnoticed into a merged change: it fails the build loudly, once, and makes a human decide whether the new column belongs on the allowlist. It is the same principle as the git guard above, applied to content instead of paths: catch the shape of the problem, not a specific instance of it — implemented as a repository invariant rather than a runtime guard.

To configure for your institution: the allowed-columns set and the identifier-shape patterns are the two things to adapt to your own demographic reporting requirements (age brackets, sex/gender categories, whatever your board actually permits). The mechanism — an explicit allowlist checked as a repository invariant, plus a shape-based scan that runs independent of column naming — needs no institutional knowledge to carry over; if you want it enforced at write time too (not just in CI), that check has to be added at your own point of data entry.

The date shape is checked beyond the participants table too, because a recording date can leak through any file a session writes. A test records one synthetic session through the real writers (run sidecar, impedance log, questionnaire responses, the XDF itself), converts it to BIDS, and scans every file and file name it produced for a real date: today's, in any common written form, any date in the current year, or the current Unix time. It reads the real clock on purpose, since a writer that skips the date shift below writes the real date and a faked clock would hide exactly that. A second test lists every place the toolbox's source reads the wall clock or a file's timestamp and fails on any that nobody has classified, which covers the one writer no test can drive (the record loop, which needs an amplifier). Both run with the full suite (tests/test_compliance_dates.py) rather than in the stdlib-only guard, because the first has to run the converter.

Seeds derived from codes are only as de-identified as the codes. Two things are seeded from a participant's code: a randomized protocol group's run order, and an FPVS run's per-session item order (#530, docs/FPVS.md §2), whose seed is recorded in the recording. Each is a one-way hash of the protocol seed, design labels and the de-identified sub-/ses- codes, and nothing else (no date, time or operator), so it carries nothing the codes do not. But the space of codes is small enough to search, so a recorded seed can be matched back to the code it came from: an export that ever re-codes participants must drop such seeds, or the original code could be recovered.

Recording dates are shifted when they are written, and the shift is never kept

A recording date is an indirect identifier: whoever knows someone took part on a given day can pick their session out of a dataset that carries real dates, however carefully names were kept out of it. So the toolbox never writes a real date. Every wall-clock time it stamps into a participant's files (a run's start and end, each impedance reading, a questionnaire's start, finish and acknowledgement) goes through one function, ssvep.runtime.dates.stamp, which moves it back by a whole number of days.

  • The offset is random and exists only in memory. It is drawn the first time a session needs a timestamp and is written nowhere: not the site profile, not a file, not a log. Nobody, the lab included, can recover a real date from the data. The same design rules out withdrawal by date, just as having no identity key rules out withdrawal by name.
  • The shifted date carries no information about the real one. The offset is chosen so that the session's first shifted day falls uniformly between 1902-01-01 and 1924-12-31, whatever the real date is. A fixed offset would not do that, since anyone who knew the constant could add it back. Shifted years are ≤ 1925, which is the BIDS convention that marks a date as shifted. The window starts in 1902 because MNE stores a recording's date as signed 32-bit seconds, so it can't save anything before 1901-12-13; a dataset dated earlier would convert and then fail at every downstream raw.save().
  • Whole days, so the time of day survives, as does every interval inside a session. The day of the week does not survive.
  • One offset per participant's session. Two costs follow, and both are accepted. An app restart mid-session draws a new offset, so runs before and after it disagree about the date. And a multi-session study loses the interval between visits. Different participants' offsets are independent, so the files also don't say which participant came first.
  • Times are written in UTC. A local offset such as -03:00 would say whether daylight saving was in force, which reveals half the year. Anyone who knows the site's time zone can still recover the local time of day, to within an hour.
  • Every writer guards what it writes. A timestamp that isn't shifted, such as a real date passed in by mistake or an unparseable string, is dropped to null with a warning rather than written. A missing timestamp is a small loss, while a real date on disk is the thing this exists to prevent. A writer never fails over a timestamp, because failing would cost the data.
  • The BIDS view inherits the shift. meas_date, and so scans.tsv acq_time, is the run's shifted start in UTC. A sidecar written before date-shifting existed carries the real date, which the converter can neither shift (the offset was never kept) nor copy. The BIDS view of such a run has no date at all (n/a). The same applies to a start time with no timezone, which names no instant: it is left out rather than guessed at.
  • Session labels and subject codes can't carry a date either. Both go into every file name, and the subject code into participants.tsv as well, so ses-20310517 or sub-20310517 would put the real date on all of them. A session label must be ses- plus letters and digits (so upper-case SES-001 is refused, as BIDS requires). Either is refused if it reads as a date: a month name next to a number, a year from 1900 to 2099, or a run of four or six digits that parses as a day and month. The toolbox's own 3-digit ses-001 and sub-001 numbering never trips this check. A zero-padded 4-digit counter does, from ses-0101 (or sub-0101) on, because 0101 reads as 1 January. A false refusal costs a relabel; a missed date ends up in every file name.

What the shift does not hide:

  • Filesystem timestamps. Every file's created and modified times are real, and copy, backup and sync tools keep their own. They stay on the lab's storage. A shared dataset must not carry them, and the export sees to that: it regenerates every file rather than copying it, and gives each one the same fixed modified time before anything is zipped (docs/EXPORT.md). A real modified time next to a shifted timestamp from the same file reveals the offset, and so every date in the session.
  • The toolbox version. provenance.software pins the toolbox version, and commit dates are public, so a sidecar bounds when it was recorded to within days or weeks. It can't recover a date.
  • LSL sample times count seconds since the acquisition PC booted. They carry no date, but two recordings from one boot can be ordered and spaced against each other.
  • LSL stream identity. Each recording's XDF headers and its sidecar's lsl_streams name the acquisition PC (its hostname) and each stream's created_at, also seconds since boot, so a recording can be checked against the rig that should have made it (docs/ARCHITECTURE.md, "LSL stream identity and instance isolation"). A hostname identifies a machine, not a participant. Both stay on lab storage: they live only in sourcedata/, which the export never carries.
  • Free text. An operator's note or a participant's comment is written as typed. It stays on lab storage: an export never carries free text.
  • Analysis-time stamps under derivatives/ (the "rebuilt" line on report pages, pipeline logs) record when analysis ran, which may be soon after the session. They stay on lab storage, and the export does not ship derivatives/.

To configure for your institution: nothing. There is no key to set and no secret to copy between machines, which is the point. If your study needs the interval between visits, a keyed offset that your lab can reverse is a different trade: your ethics board has to be told that the lab can recover real dates. The toolbox doesn't provide one.

Data use is read from disk at the moment of transfer, never from a caller's memory

Every file a session writes carries its data-use stamp: the session's purpose and the participant's tier (next section). Two separate code paths move data — mirroring a finished run to internal storage, and exporting a subset for reuse or public release — and each reads the stamp from the sidecars on disk at the instant it acts, rather than accepting a list of what's eligible from whoever is calling it. Both ask the same matrix, for different actions: the internal-mirror path asks whether a recording may be pushed to the lab's own storage, which is not sharing; the export path asks whether it may be exported for the purpose requested (future use or public share). The stamp is re-read at transfer time rather than cached, so the check cannot be stale by construction, no matter how long after the recording the transfer happens or who initiates it. Every sidecar of the session counts, its questionnaire answers included, and the most restrictive one decides.

Both paths share the same default: a missing or unreadable stamp is treated as "no," never as "yes." A partially-written sidecar, an old file from before the stamp existed, or a corrupt read all fail the same direction — toward not transferring — rather than needing a human to notice the ambiguity and choose the safe answer under time pressure.

Observer only (tier 0) is the strictest case, and it is enforced by writing nothing at all: no recording, no questionnaire answer, no impedance log, not even temporarily, and no stamp is ever made for it. There is nothing for any path to transfer, and nothing downstream can re-derive a permission that was never written.

To configure for your institution: the wording your participants see (your protocol's data_use block) is yours. The mechanism — the stamp re-read at transfer time rather than cached, most-restrictive-wins on conflict or absence, and observer only meaning nothing is ever written — is the part that generalizes. See Export and sharing, and the push guide that ships with the lab deployment, for exactly which code paths this runs in.

Session purpose and data-use tier decide what may happen to a recording

Three independent consent flags allow combinations nobody can act on coherently, such as "may be shared publicly, may not be reused". The replacement is two facts, set when a session is locked and stamped into every file it writes, and one matrix that every gate reads instead of each deriving its own rule. The policy lives in one pure, stdlib-only module, ssvep.runtime.data_use. Every writer stamps it (a run's sidecar, a questionnaire answer, each impedance-log entry), and the push and export paths read it; a recording that carries only the older three consent flags is unstamped in this model (see below).

The purpose says why the session was run:

  • research: a participant gave consent and chose a tier.
  • pilot: a member of the team volunteered. There is no consent screen; instead the operator attests two things, that the volunteer is a team member and that they consented orally. No name is recorded.
  • teaching: a class. There is no consent form, and the tier is fixed: course use only.
  • bench: nobody is in the chair. Every participant-facing step is skipped.

Which purposes a machine offers depends on its site-profile kind: a lab offers research, pilot and bench; a teaching machine offers teaching and bench; a generic one offers all four.

The tier says what the participant agreed to. Tiers are cumulative: 0 observer only (the participant takes part, and nothing from the session is saved), 1 this study only, 2 this study and future research by this team, 3 share openly (a public repository, any future use). Only a research participant chooses one, from the tiers the protocol offers (docs/PROTOCOLS.md). Every other purpose has a single fixed tier, plus 0, so that "keep nothing" never requires leaving the session.

The matrix:

Purpose Tier Kept Pushed to lab storage Converted to BIDS Pooled into a contrast Exported for future use Exported for public share
observer only (every purpose) 0 no no no no no no
research 1 yes yes yes yes no no
research 2 yes yes yes yes yes no
research 3 yes yes yes yes yes yes
pilot operator-attested yes yes yes no no no
teaching course use only yes no yes yes no no
bench no participant yes yes yes no no no
  • Tier 0 means nothing is written, not written and deleted later. Pilot and bench sessions ask no tier question, so their tier 0 row is never reached at lock; it is there so that 0 means the same thing in every purpose.
  • Pooling takes exactly one purpose. A contrast pools research recordings, or, on a teaching machine, teaching recordings. Every other recording is excluded from it one by one, so a pilot, a bench test or another purpose's data is never aggregated into a claim. A generic machine pools research, so a class recorded on one can be pooled only once the recordings are on a teaching machine. The one other case is a dataset the synthetic generator made (it marks itself, and stamps every recording bench, because nobody was recorded): there the one purpose pooled is bench, since such a contrast checks the method and makes no claim about people. Each excluded recording is listed on the contrast's page with its reason.
  • Conversion follows its column, and analysis follows conversion. A recording that may not be converted (no stamp, a malformed one, only the older consent record) is never analysed either: the analysis outputs are derived from the BIDS dataset, so a recording kept out of it must not come back through a report of it. Each refused recording is named with its reason in the conversion's output and the analysis log. The BIDS view records the purpose: each session's session_type in sub-XXX_sessions.tsv, and the sorted purposes present (SSVEPPurposes) in both the raw and the derivatives dataset_description.json. A run report of a recording that is not research data says so at the top, and the derivatives index tags a report written before this gate, of a recording it now refuses, refused with the reason. The developer script scripts/analyze_recording.py goes through the same gate and stops on a recording with no *.session.json.
  • One rule for what is not study data. A session is held back from export under both purposes, skipped by the demographics fill, labelled in session_type, and left out of every contrast when any of its recordings was locked as a pilot, when its declarations file puts it in group pilot or test after it was recorded, or when that file cannot be read (it may say pilot). The same rule, in one place, serves all four, so a pilot declared afterwards is treated exactly like one locked as a pilot.
  • Only research leaves the lab. A pilot is never exported under either export purpose. A class's data is never pushed and never exported. A bench recording is pushed (it is test data, and the lab's storage is where test data goes) but never exported.
  • Before anything is recorded, the purpose also decides what the participant goes through. The policy calls for a photosensitivity safety screen whenever a human is present, so only bench skips it. A research session whose option the participant gives on screen (below) is shown it at lock, as the first participant screen; a "one or more of these applies to me" ends the session with nothing written and the answer recorded nowhere. A teaching session is shown the safety screen alone at lock (no consent form and no data-use question), with the same outcome when a statement applies. Pilot and operator-entry research sessions do not run the participant screens in this version, so for them screening still belongs in the study's own materials. Where a lab profile's approval envelope applies, it refuses a research session outside it, only warns on a pilot one, and does not apply to a bench session at all: nobody is at risk, and the safeguards are there to help an operator avoid a mistake, not to police them.
  • The flicker risk a session was run at is recorded, and a high one acknowledged at lock (#286 P5). The lock grades every run as the builder does, shows the operator the session's highest grade and what raised it, and, when that grade is high and a human is present (any purpose but bench), asks the operator to acknowledge it. The acknowledgement records the operator's id and a date-shifted time in every run's session_position, and nothing else about anyone. It is a step, not a refusal: Cancel leaves the session unlocked, and only the approval envelope refuses a stimulus. Each run's sidecar carries its own grade beside the data-use stamp (flicker_risk), taken from the manifest that sidecar embeds, so it is the grade of what that manifest declares was presented (the display's geometry is as declared; the toolbox cannot verify it against the physical screen). A run whose targets cannot be read (no stimuli list, a non-positive or non-finite frequency, a non-boolean pattern_reversal) is stamped high with problem: ungradable, never lower. The block also carries rule, the version of the grading rule, so a later change to the rule leaves earlier grades distinguishable. Every shipped example_teaching_* manifest grades high, so every teaching lock shows the acknowledgement; that is intended, not a bug.
  • The grade is a sibling of the data-use stamp, never part of it, and it travels as one closed value. The data-use stamp is read strictly, so a key added to it would make every new recording unreadable to an older toolbox; the grade has its own block, with its own format, and a reader takes only a format it knows. A recording made before the grade existed has no grade, which is read as "none recorded", never as low. The BIDS converter carries the run's grade into its _eeg.json as SSVEPFlickerRisk, and an export lets that key through only as one of low, moderate, elevated or high. The factors and the acknowledgement (who and when) stay in sourcedata, which a push mirrors and an export never carries.

A recording with no stamp is refused, by every path, with one exception: on a teaching machine, an unstamped recording counts as teaching, unless it says it was made on a lab machine. Its own site-profile stamp may say teaching, may say generic (what a recording made with no profile set says), or may name no kind at all (a copy made before the kind was stamped); all three count. A recording whose own stamp says lab is research data under its own rules and stays refused, wherever it is read. Off a teaching machine, only a recording that itself says teaching counts. Teaching is the most restrictive purpose (never pushed, never exported), so inferring it widens nothing, and the inference is never written back into the recording as if it were a stamp.

There is no migration. A sidecar's three legacy consent flags are never translated into a tier, because "public yes, future no" has no tier, and inventing one would record consent nobody gave. The legacy flags are read in one direction only: an observer-only flag (or one that cannot be read) still forbids everything, and a legacy pilot marker still excludes the recording from contrasts. A sidecar that carries both a stamp and a legacy consent record is refused as ambiguous.

The stamp is read strictly. An unknown key (perhaps written by a newer toolbox), a value of the wrong type ("2" or true for 2), a tier the purpose never carries, tier 0 or a research tier below the minimum it was stamped with (under either, nothing should have been written), or a pilot attestation that is not exactly two true values makes the stamp unreadable, and an unreadable stamp allows nothing. It is never inferred around, even on a teaching machine.

The words are recorded by hash. A research stamp says where the wording came from (the protocol, or the built-in generic wording a generic machine falls back to when the protocol has none) and the SHA-256 of that wording, so the exact words a participant read can be identified without copying them into every file. A lab machine never falls back: it refuses to lock a research session whose protocol carries no wording.

How the lock records it. Set up Session asks for the purpose, offering only those the machine's kind allows, and then only what that purpose needs. Research asks for the option the participant chose, from the tiers the protocol offers. Option 0 is observer only: the participant consents to take part but not to have their data used, so the session runs (the questionnaires still happen, because the person under the flicker is real) and nothing from it is written at all, not even temporarily. The toolbox's built-in wording has a minimum of 0 for that reason; a protocol may set a higher minimum, and an option below it means the participant has not consented, so the session does not run.

Who enters the option is a site setting (data_use.tier_entry in the site profile, docs/SITE_PROFILE.md §6b), because labs collect it differently. One rule decides who enters it: operator entry is honest only when the site says the operator records the participant's stated choice and the protocol does not administer its consent on screen. The other two cases:

  • the site's tier_entry is participant (the default): the participant gives the option themselves, on the participant screens;
  • the site's tier_entry is operator, but the protocol's consent_mode is on_screen: the lock is refused, and says why. On-screen consent runs only under participant entry, and an operator-entered stamp saying mode: "on_screen" would record a procedure that did not happen.

The participant screens (ssvep.runtime.participant_screens, shown by ssvep.ui.participant_screens) run at lock, on the stimulus monitor, after the protocol is loaded and resolved and every operator-side check has passed, and before anything is frozen: the safety screen, the consent form (on_screen only, scrolled to the end before "I consent" is enabled), then the data-use question in the resolved wording (the protocol's own, or the built-in wording where the machine allows it), in every consent mode. Until they have produced an answer the decision is a refusal (flagged as waiting for the screens, so Lock runs them rather than stopping). Esc at any point, a statement that applies, declining the form, or an option below the minimum ends the attempt with nothing written and the session unlocked; nothing is held in memory but the chosen option, and the stamp records it with entered_by: "participant". Under operator with a paper or none protocol, the operator enters the participant's stated choice and the stamp records who entered it (entered_by: "operator"). Teaching, pilot and bench sessions do not read the setting. Teaching asks for course use only or option 0, and its lock shows the student the safety screen only. Pilot asks for the two attestations. Bench asks nothing, and skips the questionnaires as well. Nothing is preselected in a new window. Unlock then relock of the same subject and session keeps the Purpose and data-use choices, still visible and editable, and the operator still locks explicitly. The first edit that changes the Subject or Session after an unlock clears every choice (typing the old value back does not restore them). So does changing the Output dir and changing the site profile. Loading a different protocol (another file path or another protocol name; not the fingerprint) under a Subject and Session that are set asks a question rather than clearing or keeping the choices silently (#491, docs/PROTOCOLS.md). If the session already holds another protocol's data, it cannot continue under the new one (#279): only the next session, or Cancel, which puts the earlier protocol back. Otherwise the operator chooses the next session, which clears the choices (a new session is decided afresh), or keeping the session. Keeping it keeps the Purpose and choices only if the new protocol's data-use wording is identical to the one they were made under; under other wording they are cleared, with a message, because the participant's option was given against the earlier wording and a stamp must name the wording the participant saw. Nothing is asked when the session holds no data and no purpose is chosen. Clear protocol does not forget the protocol loaded last. The same protocol file loaded again or saved again (#481) never asks; only if it was saved with different data-use wording are the choices cleared, with a message. A new window starts empty, so a purpose left from an earlier participant or session is never inherited. A bench session no longer needs a reserved subject code to say what it is: its stamp says so. When the relock follows recordings and the choices or the protocol changed, the lock asks whether to continue the session or start a new one (#490, docs/PROTOCOLS.md); choosing a new session moves Session to the next label and clears the choices (a new session is decided afresh) and stops without locking, a continue stamps the change into the runs' session_position, and neither happens across a subject change, which clears the choices first.

A participant may lower their option before they leave, never raise it, and never afterwards. While the session is locked, Lower data-use option… on Set up Session offers only the options below the current one (for research, those still at or above the protocol's minimum) and observer only. The operator enters the participant's new choice, as at lock. Unlocking ends the session, and after that nothing can be lowered: nothing links the recording to the person any more. A relock right after a lowering carries the lowered choice and nothing higher: research lowered to observer relocks as observer, and a pilot session lowered to observer carries nothing, so it must be declared afresh.

  • Lowering to another option rewrites the tier in every file the session has saved: each run's sidecar (a rescued one too), each questionnaire answer, each impedance-log entry. Nothing else in a file changes, and every file is checked before any is written, so one that cannot be lowered (an unreadable stamp, or one of another purpose) refuses the whole change. The tier lives only in these stamps, and every gate reads it back from them, so a BIDS view or an analysis made earlier follows without being rewritten. Every file written afterwards carries the lower option.
  • Switching to observer only after something was saved is a deletion, and the operator is told so before it happens. When that deletes a participant's last session, the code is kept as an empty sourcedata/sub-XXX/.retired marker so it is never offered to anyone else (#141): the marker holds no data, no date and no name, so "nothing is written" for the participant still holds, and it is the one thing left of them. A deletion made by hand outside the toolbox leaves no marker and cannot be caught. It removes the whole session: its recordings, sidecars, questionnaire answers, impedance log, rescued files and declarations, its BIDS view, its per-session analysis, its row in the participant's sessions table, every contrast report that pooled it, any other analysis page that names it, and its lines in the analysis logs and tables. Each contrast report lists the sessions it pooled, and that list is what finds it (the page itself shows subject and session in separate cells); run the contrasts again to rebuild them from what is left. A report written before that list existed is deleted when it belongs to one of the session's protocols, and named for a check by hand when the session's protocols cannot be read: none is kept silently. Logs and tables are records of everyone, so only the lines naming the session go (every line naming the participant, once nothing else of them is left). A participant with nothing left loses their participants.tsv row too. Copies outside this data folder (lab storage, an earlier export) are not reached. The session then carries on as observer only, saving nothing more.
  • Neither follows a symlink or junction, and neither accepts a subject or session label the recorder itself would refuse: a link in the way, or such a label, refuses the change before anything is touched.
  • Locking the same session again never raises it. If files already saved for that subject and session carry a lower research option, the lock is capped at it (and refused when the protocol cannot offer that option).

Pushes wait until the session ends wherever the option can still be lowered (research and pilot sessions; teaching is never pushed). A file already on lab storage is one a lowering here cannot reach, so nothing of the session leaves the machine while it is locked, through the automatic push or the Review dialog. Unlocking asks whether the session has ended, then pushes what was held, and the push gate re-reads every stamp as it goes. The operator can also unlock without pushing (to correct a detail and lock again), and the held files then wait for Review / push, which says how many research or pilot files are not yet on the server. A bench session pushes as each run ends. Copies outside the data folder, such as an export made earlier, are not reachable from here.

To configure for your institution: the wording. Put the words your ethics board approved into your protocol's data_use block, with the tiers your study offers and the minimum it requires. Build Protocol's Data use & consent… editor writes that block and checks it against these rules as you type (Session protocols, which has a placeholder worked example). The matrix itself has no configuration: no site-profile key can widen it.

BIDS conversion is the default analysis path, and analysis never infers the design

A run's design — frequencies, montage, trial structure, timing, marker meaning — is authored once, in a manifest, at build time. Analysis reads that manifest directly and never reverse-engineers a design fact from the recorded data by heuristic. This is a data-governance property as much as a scientific one: a hand-maintained, separately-typed description of what a recording contains is exactly the kind of place an identifying detail (a note, a free-text label, a participant's actual name used as a mnemonic) creeps in through convenience. Making the manifest the single source of truth for design removes the parallel, informal record that would otherwise exist beside it.

BIDS conversion itself is the default analysis path rather than an ad-hoc script someone runs sometimes, but it is not the only path: an explicit, logged opt-out exists for analysing straight from the raw recording when that is genuinely what's wanted, and a conversion failure is recorded rather than silently skipped. The governance property is that nothing infers design from data by heuristic on either path — the opt-out changes which artifact analysis reads, not whether the manifest stays authoritative.

To configure for your institution: the manifest-as-source-of-truth discipline itself needs no configuration. The BIDS conversion step does: this toolbox's converter hard-codes its own institution's authors, address, acknowledgements, license, and institution metadata into the dataset description it writes. An adopting lab must replace those values with its own before the converted output is theirs to use or share.

Storage-security layering, not replacement

Wherever this toolbox authenticates or authorizes anything — an operator identity stamped into provenance, a future remote-analysis login — it sits on top of your institution's existing storage security (encryption at rest, network access control, account lifecycle) and is designed to never weaken it. The toolbox's own auth layer is not a substitute for your server's controls; it is a thin addition that records who did what, for provenance, without loosening what your storage layer already enforces.

To configure for your institution: point the toolbox at your own storage and access-control stack; the layering principle — never bypass or relax the layer underneath — is what to preserve when you do.

What this page does not cover

It does not tell you what your ethics board will approve, what your privacy law requires, or which server is compliant for your data residency rules — those are institutional facts that belong in your own governance document, alongside file numbers, dates, and named approvals. This page is the half of that document that is true regardless of institution: the mechanisms above hold whether the rules plugged into them come from a Canadian TCPS2/FOIPOP regime, an American IRB/HIPAA regime, or anything else. Configure the specifics; keep the shape.