Skip to content

Commit 9607dcf

Browse files
committed
doc: Document current dataset lifecycle, propose DOI lifecycle
1 parent bf3a5e4 commit 9607dcf

2 files changed

Lines changed: 490 additions & 0 deletions

File tree

docs/dataset-lifecycle.md

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
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

Comments
 (0)