Skip to content

Contribution Bundle - Format Specification

Status: implemented (schema/format only) See: DR-0023 Local/Offline and Hosted Submission Model, contribution-bundle-format-spec roadmap

What this document covers

A contribution bundle is a portable scientific contribution package. It carries selected scientific payloads, source-instance metadata, exporter metadata, submission metadata, bundle-local references, and integrity metadata in a format that can later be validated and ingested by a hosted TCKDB instance.

This document defines bundle version 1 (bundle_version == 1, an integer) and the schemas implemented in backend/app/schemas/workflows/contribution_bundle.py.

What this version is

  • A schema/format milestone only.
  • Re-uses existing thermo and kinetics upload schemas as the bundle's scientific payload, so nested validation runs through the same code paths as a normal API upload.
  • Validatable without database access.

What this version is not

  • Version 1 does not export records from any database.
  • Version 1 does not import into any hosted instance.
  • Version 1 does not create submissions, jobs, or moderation events.
  • Version 1 does not support raw database synchronization between instances.
  • Version 1 does not ship artifact packaging — only the manifest shape.
  • Version 1 does not support full network bundles, arbitrary graph export, or mixed thermo+kinetics bundles.
  • Version 1 does not introduce any FastAPI route, service, or DB write.

Supported bundle families

Allowed bundle_kind values:

Value Meaning
thermo Bundle carries thermo upload-equivalent payloads only.
kinetics Bundle carries kinetics upload-equivalent payloads only.

Future families intentionally not allowed in version 1 (to avoid shipping bundles no exporter or importer is actually validating yet):

  • statmech
  • transport
  • network
  • mixed
  • computed_reaction

Top-level shape

{
  "bundle_format": "tckdb-contribution-bundle",
  "bundle_version": 1,
  "bundle_kind": "thermo",
  "created_at": "2026-04-25T00:00:00Z",
  "source_instance": { "...": "..." },
  "exporter": { "...": "..." },
  "submission": { "...": "..." },
  "records": { "thermo_uploads": [], "kinetics_uploads": [] },
  "local_refs": { "species:ethanol": { "record_type": "species", "label": "ethanol" } },
  "manifest": { "sha256": null, "files": [] }
}

bundle_format

Must be the literal string tckdb-contribution-bundle.

bundle_version

Must be the integer 1. bundle_format names the format and bundle_version is its integer version; they are separate fields.

bundle_kind

One of thermo | kinetics.

created_at

ISO-8601 UTC timestamp (Pydantic datetime).

source_instance

Identifies the TCKDB instance that produced the bundle.

Field Required Notes
instance_kind yes local or lab_server. hosted is not allowed in version 1.
instance_name yes Free-text label, e.g. calvin-laptop or pi-rmgteam-server.
schema_version yes The Alembic revision the local DB is at, e.g. d861dfd60891.
software_version no Optional TCKDB software version label.
created_by_local_user no Optional local username/label.
notes no Free-text.

exporter

Provenance-only metadata about who produced the bundle. This is not the hosted actor identity. Hosted actor identity comes from hosted authentication during the future import milestone.

Field Required Notes
local_user_label yes Local label such as calvin.
orcid no ORCID iD if known. Not validated for format in version 1.
affiliation no Free-text.
email no Free-text contact.
notes no Free-text.

submission

Bundle-level submission metadata. The hosted instance maps this to its own submission row at import time; the bundle does not create submissions.

Field Required Notes
title yes Short title for the contribution.
summary yes One-paragraph summary.
source_kind yes Must be local_bundle in version 1.
rights no Deposit-time license agreement for every record in the bundle; see below.

submission.rights

The rights object is the wire fragment tckdb_schemas.rights.DepositRights, the same one every direct upload request carries:

"rights": {
  "license": "CC-BY-4.0",
  "depositor_attests_right_to_license": true,
  "source_terms": null
}
Field Required Notes
license yes SPDX identifier (1–64 characters). A hosted release compares it with its own data_license by exact, case-insensitive match; there is no compatibility lattice.
depositor_attests_right_to_license yes Must be literally true. A false fails validation — the bundle cannot carry a stored "no"; it omits the object instead.
source_terms no Citation or quotation of the terms the records were taken under, for records that are not the depositor's own work.

On hosted import the fragment is recorded as a depositor_agreement rights attestation on the submission, attested by the authenticated submitter — never by the local exporter label. A bundle without rights imports normally, but its records cannot be selected into a dataset release until a curator records a basis for the submission.

scripts/export_contribution_bundle.py fills the object from the standing rights attestation of the submission that deposited the exported records on the source instance. If the records were deposited under different licenses the export refuses (one bundle, one agreement); if only some of the records are attested the export also refuses, rather than extending one deposit's agreement to records nobody licensed; only if nothing at all is attested is the object omitted — the exporter never invents consent.

Note on local_bundle and the database enum. The submission.source_kind field on the bundle uses a format-level enum (BundleSubmissionSourceKind). The database enum SubmissionSourceKind in backend/app/db/models/common.py does not yet include local_bundle. Adding it is deferred to the hosted-import milestone — the bundle format only states "this submission came from a local bundle", and the hosted importer will be responsible for translating that to its own submission machinery.

records

Container for the upload-equivalent scientific payloads. Reuses the existing workflow upload schemas directly:

  • thermo_uploads: list[ThermoUploadRequest]
  • kinetics_uploads: list[KineticsUploadRequest]

Family rules for version 1:

  • A thermo bundle must contain ≥ 1 thermo_uploads entry and must not contain any kinetics_uploads.
  • A kinetics bundle must contain ≥ 1 kinetics_uploads entry and must not contain any thermo_uploads.
  • Mixed bundles are explicitly rejected.

Kinetics declarations in a bundle

A kinetics_uploads entry may carry determination, applicability and protocol (see backend/schema_spec.md § "Kinetics determination, applicability and protocol declarations"). Uploads of one bundle that state the same determination content (key, direction, whole-reaction target, source and reaction) are imported under one reaction entry and share one determination, as the exporter grouped them. The exporter writes what a portable bundle can carry and reports what it cannot (a resolved-channel determination, a protocol's supporting calculations) as a declaration_pruned omission. Each exported determination carries a bundle-local determination.group handle (d1, d2, ... in export order): records of one import that state the same handle and content share a determination, so two determinations that share a key, source and reaction content stay two after a re-import. The key is always exactly what the depositor stated. The handle only groups records inside one import, is never stored, and is refused on /uploads/kinetics and on the fits of a computed-reaction upload. The export CLI prints every omission, so declaration_pruned is never silent.

Exporting legacy thermo rows (undeclared enthalpy)

ThermoUploadRequest.enthalpy_reference_kind declares what an exported enthalpy means (see docs/guides/depositing_a_thermo_record.md and schema_spec.md § "Enthalpy reference declaration"). A thermo row deposited before that field existed can carry an enthalpy with no declaration; the DB never backfills one onto such a row, and the import workflow refuses to accept that exact shape on the way back in (tckdb_schemas.enthalpy_reference.enthalpy_reference_error). app.services.contribution_bundle_export.export_thermo_bundle (used by scripts/export_contribution_bundle.py) therefore never emits enthalpy_reference_kind: null next to a real enthalpy. Instead it applies one of two dispositions per legacy row, chosen by where the enthalpy lives:

  • 298 K scalar, tabulated point enthalpies or Gibbs energies, or a Wilhoit h0_kj_mol — each is an independently optional field, so only the enthalpy value(s) are dropped from the export. A point g_kj_mol is dropped with them: it is H(T) − T·S(T) on the same undeclared zero, and the importer counts it as enthalpy content. Entropy, heat capacity, temperature range, and every other field on the record are carried over unchanged, and the record re-imports normally (now with no enthalpy content and no declaration, which is a self-consistent shape). A tabulated point left with no value at all (one that held only H and/or G) is dropped on its own; the other points are exported, and the dropped temperatures are reported in the omission's points_dropped_at_k. If no point survives, a stored model_kind of tabulated is left out so the importer infers the kind from what remains. The record is omitted only when no point, fit or scalar value survives at all, and the omission says so and names the enthalpy values that were the record's only content.
  • A NASA-7 or NASA-9 fit — ThermoNASACreate / ThermoNASA9IntervalCreate require every coefficient, the enthalpy term included, so there is no way to drop only the enthalpy without destroying the whole fit. Since the fit is the record's entire scientific content, the record is left out of the bundle entirely rather than exported mutilated. This holds even when the record also carries tabulated points: the fit decides, and no point is pruned or reported.

Either way the change is reported, never silent: export_thermo_bundle returns a ThermoBundleExport(bundle, omissions) instead of a bare bundle. Each BundleExportOmission names the affected record by its public_ref (never a database row id) and states which disposition applied and why. bundle is None only when every selected thermo row was omitted outright and nothing legitimate remains to export. The CLI prints one note: line per omission to stderr, and an error: line if nothing was left to write.

A record that already carries a declaration is completely unaffected by this and exports (and re-imports) exactly as described above.

Exporting a target and protocol declaration

ThermoUploadRequest.thermodynamic_target and ThermoUploadRequest.protocol (see schema_spec.md § "Thermodynamic target and protocol declarations") are optional attributed claims. A bundle must be importable on any instance, so the exporter carries what is portable and leaves out, with a report, what names a row of the exporting database:

  • An equilibrium_ensemble target, and the protocol's recipe, formation_reference, thermal_approximation and departures, are carried exactly (departures: [] stays []: "no departures" is a statement).
  • A single_conformer target names a conformer group of the exporting database, so it is left out; the protocol's supporting_calculations are public refs to calculations of the exporting database, so they are left out too (a protocol that said nothing else is left out entirely). Left out means absent in the bundle, never replaced: a dropped single-conformer target does not become an equilibrium one.
  • Each such record gets a declaration_pruned omission (or, if its enthalpy was also pruned, the enthalpy_pruned omission carries both sentences).

A legacy record that made no declaration exports none and reports nothing.

local_refs

A map from a bundle-local reference key to a small descriptor.

Local refs are not hosted IDs and not raw DB primary keys. They exist so that future bundle versions can name records inside the bundle (e.g. for cross-payload references) without claiming any hosted identity.

Local ref key rules

A local ref key has the form <namespace>:<label>:

  • Namespace: lowercase ASCII, may contain _, must start with a letter.
  • Label: alphanumeric plus _, -, .. Must start with alphanumeric.
  • Purely numeric labels (species:123) are rejected — they look like raw database primary keys and that is exactly what bundles must not carry as canonical identity.

Recommended namespaces (informational, validated as record_type):

species:ethanol
species_entry:ethanol-singlet
reaction:h_abstraction_001
transition_state:ts_h_abstraction_001
calculation:sp_001
thermo:ethanol_nasa_001
kinetics:h_abstraction_rate_001
literature:zhang_2024_jpca

Keys are unique by Python dict semantics — duplicate keys in the source JSON collapse to the last value, so the schema validates the deduped map.

Annotation-only in version 1

In version 1, local_refs is an annotation map, not a cross-reference graph. It may describe records, labels, or local identities that help a future exporter/importer reason about the bundle, but version 1 scientific payloads (ThermoUploadRequest, KineticsUploadRequest) do not yet cite local refs.

The version 1 schema therefore validates only:

  • local-ref key shape (namespaced <namespace>:<label>)
  • local-ref record_type
  • rejection of purely numeric (raw-DB-PK shape) labels
  • overall map shape

The version 1 schema does not enforce coverage between local_refs and records — having a thermo:* ref does not prove it refers to a specific ThermoUploadRequest, because nothing in the upload payload binds to it yet.

Promoting local_refs to a first-class cross-reference is deferred to a future bundle version, where scientific payloads can explicitly cite local refs and the hosted importer can define a deterministic local-ref contract for import preview/diff.

manifest

Integrity metadata for any external artifacts shipped alongside the bundle. Artifact packaging is not implemented in version 1; the manifest just defines the shape so future artifact bundling has a place to land.

Field Required Notes
sha256 no Optional 64-char lowercase-hex covering hash.
files yes (may be []) List of BundleManifestFile entries.
created_by_tool no Free-text.
notes no Free-text.

manifest.files[*]

Field Required Notes
path yes Path inside the bundle. Must be unique across files.
sha256 yes 64-char lowercase-hex SHA-256.
size_bytes no Non-negative integer.
content_type no Free-text MIME or label.
role no Free-text role hint (e.g. output_log, input_geometry).

A bundle without external artifacts can use:

{ "sha256": null, "files": [] }

Validation rules implemented in version 1

  1. bundle_format == "tckdb-contribution-bundle".
  2. bundle_version == 1.
  3. bundle_kind ∈ {thermo, kinetics}.
  4. All required top-level metadata is present (source_instance, exporter, submission, records, manifest, created_at).
  5. The selected bundle kind has at least one record of the matching family.
  6. Mixed thermo+kinetics record sets are rejected.
  7. manifest.files paths are unique.
  8. local_refs keys match the namespaced format rule above.
  9. Purely numeric local-ref labels (raw-DB-PK shape) are rejected.
  10. extra="forbid" on every bundle schema rejects unknown top-level fields, so a future field cannot accidentally slip in unnoticed.
  11. Nested ThermoUploadRequest / KineticsUploadRequest payloads run their full upload-time validators (e.g. thermo must carry actual scientific content, kinetics a_units must match molecularity).

DB-backed scientific resolution (species lookup, level-of-theory resolution, deduplication, etc.) is not done in version 1.

Examples

  • Thermo and kinetics bundle examples: see examples/bundles/.

Future extension points (not in version 1)

  • Adding local_bundle to the DB SubmissionSourceKind enum, when the hosted-import milestone needs to persist bundle-origin submissions.
  • Wider bundle_kind support (statmech, transport, network, etc.).
  • Cross-payload references that consume local_refs keys directly inside scientific upload payloads.
  • Real artifact packaging (tarball/zip layout, content-addressed storage).
  • Hosted dry-run, hosted import, and hosted submission lifecycle.
  • Frontend UX for bundle authoring and review.