Skip to content

Data governance architecture

This page describes mechanisms, not a policy binding to any one institution: 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 at another institution, 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: a participants table 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 per-recording metadata carries independent consent flags — this toolbox's are a future-use flag (may this team reuse the recording for another question), a public-share flag (may an anonymized copy leave the lab entirely), and an observer-only flag that means the opposite of both: this visit's data is deleted, never persisted past the session, and never transmitted anywhere. Two separate code paths exist — mirroring a finished run to internal storage, and exporting a subset for reuse or public release — and each reads consent from the sidecar on disk at the instant it acts, rather than accepting a list of what's eligible from whoever is calling it. The two paths do not gate on identical flags, because they answer different questions: the internal-mirror path only needs to ask "is this observer-only," since mirroring to the lab's own storage is not sharing; the export path additionally asks the flag specific to the purpose requested (future-use or public-share). Each path's relevant flag 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.

Both paths share the same default for the flag(s) they do check: a missing or unreadable flag is treated as "no," never as "yes." A partially-written sidecar, a schema migration that hasn't reached every old file yet, 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.

The observer-only flag is the strictest case, and it is enforced as forward-only: once it is set, nothing downstream re-derives or overrides it, and it blocks both transfer paths outright, regardless of what any other flag says. A session that is both "observer only" and (by whatever error) marked "public share" is still never shared — the more restrictive flag always wins, and there is no code path that lets a permissive flag override it.

To configure for your institution: the specific flag names, which path checks which flag, and what each one gates are yours to define around your own consent language and board-approved forms. The mechanism — each path's relevant flag re-read at transfer time rather than cached, most-restrictive-wins on conflict or absence, and a forward-only "never" state that nothing can override — is the part that generalizes. See BIDS conversion, Export and sharing, and Push to the lab server for exactly which code paths this runs in and which flags each one checks.

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.