The project record¶
A project is the unit of work: a record of its own that outlives any one conversation
(Projects). The harness keeps each project as an
append-only log of what changed in it, in dd_harness.projects. The run store is separate and
unchanged. Conversations are still derived from it, and the project is not.
Where it is kept¶
Under DD_PROJECTS_DIR, which has no default (System and boundaries):
<project_id>/events.jsonl one event per line, in the order they happened
<project_id>/blobs/sha256/<digest> content an event added, kept once by its digest
A project_id is a UUIDv7. An event is never changed or removed: each is appended as one line
and flushed to disk before the call returns.
Events¶
Every event has these members:
| Member | What it is |
|---|---|
event |
What happened: project_created, conversation_started, inventory_added, repository_chosen, document_version_added, document_version_decided or document_change_refused. |
event_id |
A UUIDv7, so events sort in the order they happened. |
project_id |
The project. |
at |
When, in UTC. |
acted_for |
The Principal the change was made for (Policy and identity). |
caused_by |
What caused it: the command a person ran, such as dd-harness project add-inventory; the request, such as POST /api/projects/…/decisions; or the invocation_id of the run that drafted a version. |
project_created also gives title, if one was given: free text of at most TITLE_MAX_LENGTH
(200) characters that tells the project apart from others. A blank title is none.
conversation_started also gives the conversation_id (a UUIDv7) of a conversation started from
the project. ProjectStore.project_of(conversation_id) says which project, if any, a conversation
was started in. It reads every log once, when first asked, and is kept up to date after that,
so it holds after a restart. Reading every log does not scale past one deployment's projects,
TODO(#141).
inventory_added also gives:
| Member | What it is |
|---|---|
digest |
The SHA-256 of the inventory's canonical bytes, under which the blob is kept. |
canonicalisation |
dd-input-json-v1. The digest is therefore the input hash the conductor records when an agent is given this inventory. |
file_count |
How many files the inventory lists. |
inventory_made_at |
When dd-inventory made it. |
repository_chosen also gives:
| Member | What it is |
|---|---|
re3data_id, repository_name |
The repository chosen, by its re3data identifier. |
declared_standards |
The metadata standards its re3data record declares, as re3data names them. |
requirement_sets |
Each set the choice resolved to, as ref (<requirement_set_id>@v<version>) and the sha256 of its file. |
digest |
The SHA-256 of the merged requirements' canonical bytes, under which the blob is kept. |
canonicalisation |
dd-input-json-v1. |
warnings |
What the researcher was told: that the repository does not list DataCite, or that the deployment has no override for it. |
Changes of membership (Projects) will be events of the same log, TODO(#389).
The inventory¶
What a project holds of its research data is a FileInventory, made by
dd-inventory where the data live, and never the files. The store
checks an inventory against the core class schema before adding it, so a document of another
class, or one that gives an absolute path, is refused and nothing is recorded. The latest
inventory added is the project's inventory.
$ export DD_PROJECTS_DIR=… DD_PRINCIPAL_ID=… DD_PRINCIPAL_NAME=…
$ uv run dd-inventory path/to/dataset -o inventory.json
$ uv run dd-harness project create --title "Soil cores 2024"
0192f3a4-…
$ uv run dd-harness project add-inventory 0192f3a4-… inventory.json
$ uv run dd-harness project events 0192f3a4-…
$ uv run dd-harness project list
project list, and ProjectStore.projects(), give a summary of each project in the order they were
created. A summary has the title, who created it and when, the latest inventory (digest, file
count and when it was made), the repository chosen last and its warnings, how many conversations
were started in it, and how many documents have a latest version in each state.
The repository chosen¶
A researcher may choose any repository in re3data, usually one advisor.repository shortlisted.
What a deposit to it must or should have comes from the deployment's requirement sets under
DD_REPOSITORIES_DIR, loaded and checked by dd_harness.requirements
(Deposit): the repository's override, if there is one, merged on the set of the standard it names, or
else the set of a standard the repository declares, or DataCite's. The merged set is kept as a
blob, and the requirements of the repository chosen last are the project's. Sets that are
malformed or contradict one another stop the command before anything is recorded.
$ export DD_REPOSITORIES_DIR=app/config/repositories
$ uv run dd-harness project choose-repository 0192f3a4-… --re3data r3d100012124 \
--name ORDA --standard "Dublin Core"
Documents and decisions¶
A document is a sequence of versions; a person decides on each (Projects).
ProjectStore adds a version in one of two ways:
record_draft(project_id, envelope)takes a succeeded run'sDocumentpayload. A draft that revises nothing is version 1 of a new document; one whoserevisesnames a version of this project is the next version of that document. Anything else is refused and nothing is recorded. The event acts for whom the run acted for and itscaused_byis the run'sinvocation_id.edit(...)takes a person's text and the version and digest they edited. It names no run.
decide(project_id, decision, acted_for=…, caused_by=…) records a Decision: the version, the
digest of the text shown, approved or rejected, a reason, and to approve an edit, the edited
text. Whoever acted_for names decided: a Decision cannot name a decider. A rejection, or
an approval of an edit, needs a reason of at most REASON_MAX_LENGTH (2,000) characters;
review_rules() states that rule for the app to draw. An edit and its approval are appended
in one write.
document_version_added also gives:
| Member | What it is |
|---|---|
document_id, version |
The version: the document's UUIDv7, and a number counted from 1 within it. |
digest |
The SHA-256 of the text as UTF-8, under which the text is kept as a blob. |
document_kind, media_type |
As the Document gave them; a person's version keeps its parent's. |
document_title, conforms_to |
As the Document gave them, if it did. |
revises |
{document_id, version} of the version it replaces or was edited from, if any. |
drafted_by |
For an agent's version only: agent_id, agent_version, invocation_id and document_derivation. |
document_version_decided also gives document_id, version, digest, decision
(approved or rejected) and, if one was given, reason. Its acted_for is the decider.
document_change_refused records a decision or edit refused because it named no document of the
project (document-not-found), not the latest version (version-not-latest), a version
already decided (version-decided) or another text (digest-mismatch). It gives
document_id, version and digest as sent, the refusal, and the latest_version if
there is one. A decision malformed in itself, such as a rejection without a reason, is refused
with ProjectError and records nothing.
documents(project_id) folds the log into each document, in the order first drafted, with every
version oldest first, its text and its state (proposed, approved or rejected) with the
decision on it. It is what the app serves at GET /api/projects/{id}/documents.
Not yet built¶
- Only owners are to decide, and membership is not built, TODO(#389).
- The conductor fills only a
DraftRequest'sinventoryandrequirements, not itscurrent_versionnor any other class, and does not applyhosted_models(Projects), TODO(#394). - The app cannot add an inventory to a project, TODO(#395), or choose a repository, TODO(#400).
- Nothing checks a project against its merged requirements, TODO(#19, #202).
- How long a project is kept, and what deleting one removes, TODO(#141).