|
| 1 | +--- |
| 2 | +name: Dataset Lifecycle |
| 3 | +route: /dataset-lifecycle |
| 4 | +--- |
| 5 | + |
| 6 | +# Dataset Lifecycle |
| 7 | + |
| 8 | +Datasets are git/git-annex repositories managed by datalad-service. This |
| 9 | +document describes the states a dataset, its snapshots, and their DOIs |
| 10 | +pass through, and the mutations that drive transitions between them. |
| 11 | + |
| 12 | +## Dataset states |
| 13 | + |
| 14 | +A dataset's state is determined by two fields: whether it has any |
| 15 | +snapshots and whether it is public. |
| 16 | + |
| 17 | +```mermaid |
| 18 | +stateDiagram-v2 |
| 19 | + [*] --> Draft |
| 20 | + Draft --> Embargoed : createSnapshot() |
| 21 | + Embargoed --> Public : publishDataset() |
| 22 | + Public --> Embargoed : updatePublic(false) [admin] |
| 23 | + Draft --> Deleted : deleteDataset() |
| 24 | + Embargoed --> Deleted : deleteDataset() |
| 25 | + Public --> Deleted : deleteDataset() |
| 26 | +``` |
| 27 | + |
| 28 | +| State | Determination | DB representation | |
| 29 | +| ------------- | -------------------- | -------------------------------------------------- | |
| 30 | +| **Draft** | Unversioned, private | `Dataset.public = false`, no `Snapshot` docs exist | |
| 31 | +| **Embargoed** | Versioned, private | `Dataset.public = false`, ≥1 `Snapshot` doc exists | |
| 32 | +| **Public** | Versioned, public | `Dataset.public = true`, `Dataset.publishDate` set | |
| 33 | +| **Deleted** | Dataset removed | `Deletion` doc created | |
| 34 | + |
| 35 | +These states are implicit — there is no `Dataset.status` field. The code |
| 36 | +does not use the term "embargoed"; both Draft and Embargoed datasets have |
| 37 | +`public = false` and are distinguished only by the presence of snapshots. |
| 38 | + |
| 39 | +**Key constraints:** |
| 40 | + |
| 41 | +- Draft → Embargoed is irreversible. Once a snapshot exists, the dataset |
| 42 | + cannot return to Draft. |
| 43 | +- Public → Embargoed requires admin privileges. |
| 44 | + |
| 45 | +### Creation |
| 46 | + |
| 47 | +`createDataset()` creates a new `Dataset` document with `public: false` |
| 48 | +and fires a `{ type: "created" }` event. |
| 49 | + |
| 50 | +- **JS:** `packages/openneuro-server/src/datalad/dataset.ts` |
| 51 | + |
| 52 | +### Snapshotting (Draft → Embargoed, or new version) |
| 53 | + |
| 54 | +`createSnapshot()` tags the current HEAD as a new version. The first |
| 55 | +snapshot transitions the dataset from Draft to Embargoed. Subsequent |
| 56 | +snapshots do not change the dataset state. |
| 57 | + |
| 58 | +- **JS:** `packages/openneuro-server/src/datalad/snapshots.ts` |
| 59 | +- **Python:** `services/datalad/datalad_service/tasks/snapshots.py` |
| 60 | +- **Event:** `{ type: "versioned", version: tag }` |
| 61 | + |
| 62 | +**Sequence:** |
| 63 | + |
| 64 | +1. Acquire distributed lock (`lockSnapshot()`, 30-min TTL) |
| 65 | +2. Create `"versioned"` event |
| 66 | +3. Mint DOI via DataCite MDS API (`createIfNotExistsDoi()`) |
| 67 | + — format: `doi:{prefix}/openneuro.{datasetId}.v{tag}` |
| 68 | +4. Update `dataset_description.json` with `DatasetDOI` field |
| 69 | +5. Update `CHANGES` file with version and changelog entries |
| 70 | +6. POST to datalad-service to create git tag |
| 71 | +7. Store `Snapshot` document in MongoDB |
| 72 | +8. Clear Redis caches |
| 73 | +9. Queue for Elasticsearch indexing |
| 74 | +10. Notify dataset followers |
| 75 | + |
| 76 | +**Preconditions:** BIDS validation and git-annex fsck should pass before |
| 77 | +creating a snapshot. This is currently enforced in the UI, not in the |
| 78 | +`createSnapshot()` API. |
| 79 | + |
| 80 | +### Publishing (Embargoed → Public) |
| 81 | + |
| 82 | +`publishDataset()` sets `Dataset.public = true` and triggers export to |
| 83 | +S3 and GitHub. |
| 84 | + |
| 85 | +- **JS resolver:** `packages/openneuro-server/src/graphql/resolvers/publish.ts` |
| 86 | +- **JS function:** `packages/openneuro-server/src/datalad/dataset.ts` — `updatePublic()` |
| 87 | +- **Python task:** `services/datalad/datalad_service/tasks/publish.py` |
| 88 | +- **Event:** `{ type: "published", public: true }` |
| 89 | + |
| 90 | +**Sequence:** |
| 91 | + |
| 92 | +1. Check write permissions |
| 93 | +2. Set `Dataset.public = true` and `Dataset.publishDate = new Date()` |
| 94 | +3. Create `"published"` event |
| 95 | +4. POST to datalad-service `/datasets/{id}/publish` |
| 96 | +5. Create S3 and GitHub remotes (`create_remotes_and_export()`) |
| 97 | +6. Export dataset to S3-PUBLIC and GitHub (`export_dataset()`) |
| 98 | +7. Set S3 access tags to "public" |
| 99 | +8. Run remote fsck to verify exported data |
| 100 | +9. Drop local annexed files after verification |
| 101 | + |
| 102 | +### Re-embargo (Public → Embargoed) |
| 103 | + |
| 104 | +`updatePublic(datasetId, false, user)` sets `Dataset.public = false` and |
| 105 | +updates S3 access tags to "private". |
| 106 | + |
| 107 | +- **JS:** `packages/openneuro-server/src/datalad/dataset.ts` — `updatePublic()` |
| 108 | +- **Event:** `{ type: "published", public: false }` |
| 109 | + |
| 110 | +**Known issue:** This also sets `Dataset.publishDate = new Date()` rather |
| 111 | +than clearing it. |
| 112 | + |
| 113 | +### Deletion |
| 114 | + |
| 115 | +`deleteDataset()` removes the dataset from the git backend and |
| 116 | +Elasticsearch. Currently, datasets with snapshots require admin action to |
| 117 | +delete. |
| 118 | + |
| 119 | +- **JS:** `packages/openneuro-server/src/datalad/dataset.ts` — `deleteDataset()` |
| 120 | +- **Event:** `{ type: "deleted" }` |
| 121 | +- Creates a `Deletion` document (datasetId, user, reason, optional |
| 122 | + redirect URL) |
| 123 | + |
| 124 | +## Snapshot status |
| 125 | + |
| 126 | +Snapshots can be independently deprecated without changing the dataset's |
| 127 | +state. |
| 128 | + |
| 129 | +```mermaid |
| 130 | +stateDiagram-v2 |
| 131 | + [*] --> Active : createSnapshot() |
| 132 | + Active --> Deprecated : deprecateSnapshot() |
| 133 | + Deprecated --> Active : undoDeprecateSnapshot() |
| 134 | +``` |
| 135 | + |
| 136 | +| Status | DB representation | |
| 137 | +| -------------- | ----------------------------------------------------------- | |
| 138 | +| **Active** | `Snapshot` doc exists, no matching `DeprecatedSnapshot` doc | |
| 139 | +| **Deprecated** | `DeprecatedSnapshot` doc with user, reason, timestamp | |
| 140 | + |
| 141 | +- **JS:** `packages/openneuro-server/src/graphql/resolvers/snapshots.ts` |
| 142 | + |
| 143 | +## DOI assignment |
| 144 | + |
| 145 | +Each snapshot is assigned a DOI via the DataCite MDS API at creation |
| 146 | +time. DOIs are minted immediately as findable — there is no state |
| 147 | +management after creation. |
| 148 | + |
| 149 | +| Field | Value | |
| 150 | +| ------- | ------------------------------------------- | |
| 151 | +| Format | `doi:{prefix}/openneuro.{datasetId}.v{tag}` | |
| 152 | +| API | DataCite MDS (XML metadata + `PUT /doi`) | |
| 153 | +| Created | During `createSnapshot()` (step 3) | |
| 154 | +| State | Not tracked locally | |
| 155 | + |
| 156 | +- **JS:** `packages/openneuro-server/src/libs/doi/index.ts` |
| 157 | +- **Handler:** `packages/openneuro-server/src/handlers/doi.ts` |
| 158 | +- **Model:** `packages/openneuro-server/src/models/doi.ts` |
| 159 | + — fields: `datasetId`, `snapshotId`, `doi` |
| 160 | + |
| 161 | +DOI metadata includes creators, title, publisher ("Openneuro"), |
| 162 | +publication year, and resource type. The DOI string is written into |
| 163 | +`dataset_description.json` before the git tag is created. |
| 164 | + |
| 165 | +DOI state is never updated after creation. The DOI remains findable at |
| 166 | +DataCite regardless of whether the dataset is later re-embargoed, the |
| 167 | +snapshot is deprecated, or the dataset is deleted. |
| 168 | + |
| 169 | +## Working tree |
| 170 | + |
| 171 | +The working tree (git HEAD) can diverge from the latest snapshot when |
| 172 | +users upload or edit files. Draft datasets are always diverged (no |
| 173 | +snapshot exists). For Embargoed and Public datasets: |
| 174 | + |
| 175 | +| Status | Determination | |
| 176 | +| ------------ | ------------------------------- | |
| 177 | +| **Clean** | `HEAD == latestSnapshot.hexsha` | |
| 178 | +| **Diverged** | `HEAD != latestSnapshot.hexsha` | |
| 179 | + |
| 180 | +Divergence does not change the dataset's lifecycle state. A new |
| 181 | +`createSnapshot()` call is required to tag the current HEAD. |
| 182 | + |
| 183 | +- **JS:** `packages/openneuro-server/src/datalad/draft.ts` |
| 184 | + — `getDraftRevision()`, `getDraftInfo()`, `resetDraft()` |
| 185 | + |
| 186 | +### Data retention |
| 187 | + |
| 188 | +When the working tree diverges from the latest snapshot, a notification |
| 189 | +schedule enforces data retention: |
| 190 | + |
| 191 | +- 14 days diverged: first warning email |
| 192 | +- 21 days: second warning |
| 193 | +- 28 days: deletion notice |
| 194 | +- No snapshot within 24h of first upload: one-time reminder |
| 195 | + |
| 196 | +- **JS:** `packages/openneuro-server/src/datalad/dataRetentionNotifications.ts` |
| 197 | + |
| 198 | +## Validation |
| 199 | + |
| 200 | +Validation is a precondition for snapshotting, not a persistent state. |
| 201 | +Two checks are involved: |
| 202 | + |
| 203 | +### BIDS schema validation |
| 204 | + |
| 205 | +Runs the BIDS validator (deno-based, v2.3.2) against a specific commit. |
| 206 | + |
| 207 | +- **JS resolver:** `packages/openneuro-server/src/graphql/resolvers/validation.ts` — `revalidate()` |
| 208 | +- **Python task:** `services/datalad/datalad_service/tasks/validator.py` — `validate_dataset()` |
| 209 | +- **Storage:** `Validation` collection |
| 210 | + |
| 211 | +### git-annex fsck |
| 212 | + |
| 213 | +Checks data consistency of annexed files. |
| 214 | + |
| 215 | +- **JS resolver:** `packages/openneuro-server/src/graphql/resolvers/fileCheck.ts` — `fsckDataset()` |
| 216 | +- **Python task:** `services/datalad/datalad_service/tasks/fsck.py` — `git_annex_fsck_local()` |
| 217 | +- Runs `git-annex fsck -J4` with incremental 45-day schedule |
| 218 | +- **Storage:** `FileCheck` collection |
| 219 | + |
| 220 | +## Data model reference |
| 221 | + |
| 222 | +### MongoDB collections |
| 223 | + |
| 224 | +| Collection | File | Key fields | |
| 225 | +| -------------------- | ------------------------------------------- | ---------------------------------------------- | |
| 226 | +| `Dataset` | `packages/.../models/dataset.ts` | `id, public, publishDate, created, modified` | |
| 227 | +| `Snapshot` | `packages/.../models/snapshot.ts` | `datasetId, tag, hexsha, created` | |
| 228 | +| `DeprecatedSnapshot` | `packages/.../models/deprecatedSnapshot.ts` | `id, user, reason, timestamp` | |
| 229 | +| `DatasetEvent` | `packages/.../models/datasetEvents.ts` | `datasetId, userId, event, success, timestamp` | |
| 230 | +| `Doi` | `packages/.../models/doi.ts` | `datasetId, snapshotId, doi` | |
| 231 | +| `Validation` | `packages/.../models/validation.ts` | `id (ref), datasetId, issues, codeMessages` | |
| 232 | +| `FileCheck` | `packages/.../models/fileCheck.ts` | `datasetId, hexsha, refs, annexFsck[]` | |
| 233 | +| `Deletion` | `packages/.../models/deletion.ts` | `datasetId, user, reason, redirect` | |
| 234 | + |
| 235 | +All model files are under `packages/openneuro-server/src/models/`. |
| 236 | + |
| 237 | +### GraphQL types |
| 238 | + |
| 239 | +Defined in `packages/openneuro-server/src/graphql/schema.ts`: |
| 240 | + |
| 241 | +- `Dataset` — core entity with `public`, `publishDate`, `draft`, `snapshots` |
| 242 | +- `Draft` — working tree with `modified`, `summary`, `validation`, `fileCheck` |
| 243 | +- `Snapshot` — tagged version with `tag`, `hexsha`, `deprecated` |
| 244 | +- `DeprecatedSnapshot` — deprecation record |
| 245 | +- `DatasetValidation` — validation results with error/warning counts |
| 246 | + |
| 247 | +### Mutations |
| 248 | + |
| 249 | +| Mutation | Effect | |
| 250 | +| ----------------------- | --------------------------------------------------- | |
| 251 | +| `createDataset` | Creates dataset in Draft state | |
| 252 | +| `createSnapshot` | Creates snapshot + DOI; Draft → Embargoed on first | |
| 253 | +| `publishDataset` | Embargoed → Public; exports to S3/GitHub | |
| 254 | +| `updatePublic(false)` | Public → Embargoed (admin only) | |
| 255 | +| `deprecateSnapshot` | Marks a snapshot as deprecated | |
| 256 | +| `undoDeprecateSnapshot` | Removes deprecation | |
| 257 | +| `deleteDataset` | Removes dataset (admin required if snapshots exist) | |
| 258 | +| `fsckDataset` | Triggers git-annex fsck | |
| 259 | +| `revalidate` | Triggers BIDS validation | |
| 260 | + |
| 261 | +### Event types |
| 262 | + |
| 263 | +Dataset events are stored in the `DatasetEvent` collection and track all |
| 264 | +state transitions: |
| 265 | + |
| 266 | +`created` · `versioned` · `deleted` · `published` · `permissionChange` · |
| 267 | +`git` · `upload` · `note` · `contributorRequest` · `contributorCitation` · |
| 268 | +`contributorRequestResponse` · `contributorCitationResponse` |
0 commit comments