Chapter 3 dealt with the operations layer: messages that a vehicle emits while it works, each wrapped in the Phase 1 envelope and stamped with a sequence number. This chapter turns to the other layer of WIA-ENE-040, the science records. These are the durable documents a research programme keeps after the vehicle is back on deck. They say which cruise went where under which permit, what each dive did, which animals were seen, which physical samples were brought up and where they now sit, which vents were found, and which area was mapped at what resolution.
The records are defined twice in the sources. The core specification shows them as JSON examples in §12 (§12.1 expedition, §12.2 dive log, §12.3 observation, §12.4 sample, §12.5 vent). The TypeScript SDK @wia/ene-040 defines them as interfaces in types.ts. The two agree on the overall shape but differ in field names, enumeration spelling and completeness. The SDK interfaces are the more complete and the more precise of the two, so this chapter uses them as the reference and points out each place where a §12 example would fail validation against them. The chapter ends with the access tiers of §14.1, because a record model that cannot say who may see a coordinate is incomplete for deep-sea work.
All names, vessels, permits and positions in the examples are fictional. Numeric values in the examples are illustrative, chosen to be physically plausible and internally consistent, and are not measurements.
Every science record hangs off an expedition. An expedition contains dives, and a dive produces observations and samples. Two record types sit beside that tree rather than inside it. A multibeam survey belongs to an expedition but not to a dive, because surveys are usually run from the ship or from an AUV mission that is not logged as a dive. A hydrothermal vent belongs to no expedition at all, because a vent is a place that many cruises visit over many years. In v1.0 the vent record carries no expeditionId or diveId. It can be joined to the rest only by location. The diagram shows these relationships, with the one link this book proposes marked as such.
┌──────────────────────┐
│ Expedition │ expeditionId EXP-2025-004
│ vessel, permits[], │
│ scientists[] │
└──────────┬───────────┘
│ 1
│ 1 ┌──────────────────────┐
├──────────────┤ MultibeamSurvey │ surveyId (expeditionId FK)
│ N │ area.extent (bbox) │
│ N └──────────┬───────────┘
┌──────────┴───────────┐ : spatial join
│ DiveLog │ : (point in extent)
│ diveId │ :
│ timeline, track │ ┌──────────┴───────────┐
└──┬────────┬──────┬───┘ │ HydrothermalVent │ ventId
│ 1 │ 1 │ 1 │ location, ventField │ (no expeditionId/diveId
│ N │ N │ N └──────────┬───────────┘ in v1.0: linked by place)
┌──┴─────┐ ┌┴──────────────┐ ┌───────┴──────┐
│ Organism│ │ Samples │ │ (proposed) │
│ Obser- │ │ Biological │ │ visits[]: │
│ vation │ │ Rock │ │ diveId list │
│ obs.Id │ │ SedimentCore* │ └──────────────┘
└─────────┘ │ eDNA │
│ └─ IsobaricSampler.sample.sampleId
└───────────────┘
* SedimentCore is keyed by coreId, and its diveId is optional.
The Expedition interface groups its fields into blocks. The vessel block holds the ship's name and operator, its length in metres, an optional IMO number and a classification restricted to research_vessel or commercial. The location block gives a region, a named area, a representative coordinate and an optional exclusive economic zone. The duration block has start and end timestamps and a daysAtSea count. The scientific intent is carried by objectives[] and targetDepthRange (min, max, in metres). The submersibles[] and sensors[] arrays hold identifiers, not embedded specifications, so a vehicle description is written once in the submersible registry and referenced from every cruise that uses it. The expedition also carries permits[], scientists[] and a metadata block with createdAt, updatedAt and a lifecycle status of planning, active, completed or cancelled.
Three details deserve rules of their own.
daysAtSea can be computed from startDate and endDate. Store it for convenience, but define it (this book uses calendar days inclusive of both ends, in UTC) and check it. The §12.1 example, 1 March 00:00 to 30 March 23:59:59 with daysAtSea: 30, satisfies that definition."classification": "Research Vessel". The interface allows only research_vessel. A validator should reject the example rather than normalise it silently, since silent normalisation hides the fact that a producer is not following the schema.permits[].validUntil is typed as an ISO 8601 timestamp, but the example writes a bare date. A bare date has no time zone. When a permit expires at the end of a day, write the full UTC instant (2025-12-31T23:59:59Z) or agree on a date-only type and say so in the schema.The IMO number is optional and should stay optional: many research platforms, including small coastal vessels, do not have one. When it is present, it has seven digits, and the last digit is a check digit, so a validator can verify it. Do not put an invented seven-digit number in examples or test fixtures. A number that happens to pass the check digit may belong to a real ship, and the §12.1 placeholder does pass it. The examples in this book simply omit the field.
People appear in chiefScientist, scientists[], dive crew lists, identifiedBy and curator. These are personal data. Store names as the person wants them cited. Where the Phase 1 SAMPLE_METADATA message already carries an ORCID iD for the collector, carry that identifier into the science record as well, so that attribution survives name changes and transliteration.
{
"expeditionId": "EXP-2025-004",
"name": "Example Ridge Segment Survey 2025",
"chiefScientist": "Dr. Mina Example",
"vessel": {
"name": "R/V Example Explorer",
"operator": "Example Ocean Institute",
"length": 90.0,
"classification": "research_vessel"
},
"location": {
"region": "Example Ocean Basin",
"area": "Example Ridge, Segment A",
"coordinates": { "latitude": -17.4200, "longitude": -113.2100 },
"eez": "none (Area beyond national jurisdiction)"
},
"duration": {
"startDate": "2025-06-01T00:00:00Z",
"endDate": "2025-06-24T23:59:59Z",
"daysAtSea": 24
},
"objectives": [
"High-resolution multibeam survey of Segment A",
"ROV imaging transects across two vent fields"
],
"targetDepthRange": { "min": 2400, "max": 3800 },
"submersibles": ["ROV-EXAMPLE-01", "AUV-EXAMPLE-02"],
"sensors": ["CTD-SN-0421", "MBES-SN-1180"],
"permits": [
{
"type": "Marine scientific research consent",
"issuedBy": "Example Authority",
"permitNumber": "EXAMPLE-MSR-0001",
"validUntil": "2025-12-31T23:59:59Z"
}
],
"scientists": [
{ "name": "Dr. Mina Example", "affiliation": "Example University", "role": "chief_scientist" },
{ "name": "Jun Sample", "affiliation": "Example Ocean Institute", "role": "data_manager" }
],
"metadata": {
"createdAt": "2025-02-10T09:00:00Z",
"updatedAt": "2025-06-25T08:00:00Z",
"status": "completed"
}
}
The §12 examples use prefixed, year-stamped identifiers: EXP-2025-001, DIVE-HOV-2025-045, OBS-2025-001234, SAMPLE-2025-MT-001 and VENT-EPR-2025-003. The specification never states the grammar, so the patterns below are inferred from the examples and offered as a proposal. They accept every example in §12, which is the minimum requirement for a proposal of this kind. Two identifiers have no convention at all: the sediment core is keyed by coreId rather than sampleId, and the survey by surveyId, and neither appears in an example.
// Identifier patterns inferred from the ENE-040 §12 examples.
// v1.0 shows examples only; these regular expressions are this book's proposal.
export const ID_PATTERNS: Record<string, RegExp> = {
expeditionId: /^EXP-\d{4}-\d{3,}$/, // EXP-2025-004
diveId: /^DIVE-(HOV|ROV|AUV|LANDER|CRAWLER)-\d{4}-\d{3,}$/, // DIVE-ROV-2025-017
observationId:/^OBS-\d{4}-\d{6,}$/, // OBS-2025-000318
sampleId: /^SAMPLE-\d{4}-[A-Z0-9]{2,8}-\d{3,}$/, // SAMPLE-2025-ERA-012
ventId: /^VENT-[A-Z0-9]{2,8}-\d{4}-\d{3,}$/, // VENT-ERA-2025-003
};
export function checkId(field: string, value: string): string | null {
const re = ID_PATTERNS[field];
if (!re) return null; // no convention for coreId, surveyId
return re.test(value) ? null : `${field} "${value}" does not match ${re}`;
}
Two cautions apply. First, a readable ID is not a persistent identifier. The embedded year and area code make it easy to read in a spreadsheet, but they also tempt people to rename records when an area is renamed. Treat the string as opaque once it is issued. Second, IDs are local to a data system. For citation outside it, attach a DOI to the dataset (Chapter 7) and, for physical samples, consider a globally registered sample identifier such as an IGSN.
The DiveLog interface records one deployment of one vehicle. Its submersible block holds a vehicleId, a type from the SubmersibleType enumeration (hov, rov, auv, lander, crawler) and an optional crew list whose roles are pilot, scientist or observer. The §12.2 example differs in three ways: it writes the type as "HOV", the roles as "Pilot" and "Scientist", and it embeds the vehicle's name and maximum depth instead of a vehicleId. Follow the interface. Rated depth is a property of the vehicle, and copying it into every dive log invites the copies to disagree.
The timeline block has four instants and one duration: launch, onBottom, offBottom, recovery and bottomTime in seconds. bottomTime is derived: it equals offBottom − onBottom. It is still worth storing, because search and statistics queries use it, but a stored derived value needs a consistency check. The instants must also be in order, since a dive cannot leave the bottom before it arrives.
The §12.2 example shows why the check is needed. Its offBottom is written "2025-03-15-15:30:00Z", with a hyphen where the T separator should be. That is not a valid ISO 8601 timestamp, and a lenient parser may either reject it or read it in some unexpected way. Corrected to 2025-03-15T15:30:00Z, the example is consistent: 11:30 to 15:30 is 14,400 s, which matches its bottomTime. The validator below parses strictly first and then checks order and duration. The tolerance defaults to 1 s, matching the ±1 s time-synchronisation requirement of Phase 1.
interface Timeline {
launch: string; onBottom: string; offBottom: string;
recovery: string; bottomTime: number;
}
const ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/;
export function checkDiveTimeline(t: Timeline, toleranceS = 1): string[] {
const errors: string[] = [];
const keys = ["launch", "onBottom", "offBottom", "recovery"] as const;
const ms: number[] = [];
for (const k of keys) {
const v = t[k];
if (!ISO_UTC.test(v) || Number.isNaN(Date.parse(v))) {
errors.push(`timeline.${k}: not an ISO 8601 UTC timestamp: "${v}"`);
}
ms.push(Date.parse(v));
}
if (errors.length) return errors; // ordering is meaningless now
for (let i = 1; i < ms.length; i++) {
if (ms[i] < ms[i - 1]) {
errors.push(`timeline.${keys[i]} is earlier than timeline.${keys[i - 1]}`);
}
}
const derived = (ms[2] - ms[1]) / 1000; // offBottom - onBottom, seconds
if (Math.abs(derived - t.bottomTime) > toleranceS) {
errors.push(`timeline.bottomTime ${t.bottomTime} s != offBottom - onBottom ${derived} s`);
}
return errors;
}
// The spec's own example (ENE-040 §12.2) fails the first rule:
// checkDiveTimeline({ ..., offBottom: "2025-03-15-15:30:00Z", ... })
// -> ['timeline.offBottom: not an ISO 8601 UTC timestamp: "2025-03-15-15:30:00Z"']
The navigation block holds a launchPosition, a bottomTrack, the maxDepth and averageDepth in metres, and areaExplored in square kilometres. bottomTrack is typed as either a GeoJSON LineString or a string path to a file. Both forms are legitimate. A dive of several hours with navigation at 1 Hz produces tens of thousands of positions, which is too many to embed in a record that is fetched for every list query. A sensible rule is to embed a decimated track for display and reference the full-rate file.
GeoJSON has two conventions that trip people up. Positions are ordered longitude, latitude (RFC 7946), the reverse of the latitude/longitude order used everywhere else in ENE-040. The optional third element is an elevation, positive upwards, so a point 3,547 m below the surface is written as -3547. The spec says nothing about the vertical axis of the track, and positive-down depth is the convention in the rest of the record. State the choice in the dataset's metadata and check it: a validator can compare the track's deepest point with navigation.maxDepth and fail if they disagree in sign or by more than the navigation uncertainty.
A smaller inconsistency sits in the same block. The §12.2 example writes launchPosition as {"lat": …, "lon": …}, but the Coordinates type uses latitude and longitude. A consumer that follows the type will read the example's position as missing.
The observations block is a set of counters: organisms, newSpecies, samples, rocks, videos and photos. They are summaries of child records, not records themselves. The rule should be that the child records are the source of truth and the counters are a cache that is recomputed when a child is added or removed. newSpecies needs particular care. Whether a specimen belongs to an undescribed species is settled by taxonomic work that may take years, so at the time of the dive the honest value is a count of candidate new taxa, and many programmes will leave it at zero until a description is published.
The environmental block gives near-bottom conditions: bottomTemperature in °C, bottomSalinity in PSU, bottomOxygen in mg/L and an optional bottomPH. These are dive-level summaries. The underlying time series belongs in Phase 1 OCEANOGRAPHIC_DATA messages, where each value carries its sensor ID and calibration date. The dive log should say how its summary was formed (for example, the median over the bottom phase) in its notes or in dataset documentation. The incidents[] list records operational events with a severity of minor, moderate or major and a resolved flag. It is not a safety management system, but it is where a data user learns why a transect ended early.
The complete dive log below conforms to the interface and passes the timeline check. Its track has three points, a decimated display track in the sense described above.
{
"diveId": "DIVE-ROV-2025-017",
"expeditionId": "EXP-2025-004",
"submersible": {
"vehicleId": "ROV-EXAMPLE-01",
"type": "rov",
"crew": [
{ "name": "Pilot A (example)", "role": "pilot" },
{ "name": "Dr. Mina Example", "role": "scientist" }
]
},
"timeline": {
"launch": "2025-06-12T06:40:00Z",
"onBottom": "2025-06-12T08:35:00Z",
"offBottom": "2025-06-12T16:05:00Z",
"recovery": "2025-06-12T18:10:00Z",
"bottomTime": 27000
},
"navigation": {
"launchPosition": { "latitude": -17.4182, "longitude": -113.2071 },
"bottomTrack": {
"type": "LineString",
"coordinates": [
[-113.2069, -17.4185, -3531.0],
[-113.2061, -17.4190, -3547.2],
[-113.2050, -17.4197, -3539.8]
]
},
"maxDepth": 3547.2,
"averageDepth": 3538.5,
"areaExplored": 0.35
},
"observations": {
"organisms": 41, "newSpecies": 0, "samples": 6,
"rocks": 2, "videos": 9, "photos": 312
},
"environmental": {
"bottomTemperature": 2.1,
"bottomSalinity": 34.7,
"bottomOxygen": 5.9,
"bottomPH": 7.8
},
"notes": "Transect T2 aborted after 40 min: tether snag, cleared.",
"incidents": [
{ "type": "tether_snag", "severity": "minor",
"description": "Tether caught on talus block, freed by pilot.",
"resolved": true }
]
}
An OrganismObservation states that an organism was seen at a place and time during a dive. It holds a location (latitude, longitude, depth, depthZone and habitat), an organism block for identification, a morphology block, a behavior, an abundance, associatedSpecies[], media[], and two quality fields, quality (excellent to poor) and verified.
The organism block has an optional taxonId, which the interface describes as a WoRMS AphiaID or similar, an optional scientificName, the ranks from phylum to species, and three required fields: identificationLevel (phylum through subspecies), identifiedBy and identificationMethod (visual, genetic or morphological).
Keep taxonId and scientificName apart. A name is text that can be misspelt, revised or shared by homonyms in different kingdoms. An identifier from a taxonomic register is stable and resolvable. The §12.3 example uses "AMPHIPODA-SP001" as its taxon ID. That is a useful kind of label, since it is a working "morphospecies" code for an animal that has not yet been named, but it is not a register identifier and must not be exported as one. The recommended practice is to put a register identifier in taxonId when one exists (for WoRMS the LSID form is urn:lsid:marinespecies.org:taxname: followed by the AphiaID), to write the lowest confidently identified name in scientificName, and to keep internal morphospecies codes in a separate field or in the notes.
Deep-sea imagery is often identifiable only to family or order. That is not a defect. identificationLevel exists precisely to record it. An observation identified to genus has the genus as its scientificName and genus as its level. Qualifiers such as "cf." or "sp." belong in a qualifier field rather than inside the name string, which is how Darwin Core treats them. The identificationMethod matters to downstream users: a visual identification from video and a genetic identification from a specimen have very different reliability, and the difference should be filterable.
Morphological measurements are in millimetres (and grams for weight) according to the interface comments. The spec example gives "length": 34.0 with no unit. That is plausible as millimetres for a large amphipod, but the value is unusable unless the unit is fixed by the schema. Sizes estimated from video are usually derived from the laser scaler, two or more parallel lasers a known distance apart. Record the method alongside the value, because a laser-scaled size and a callipered specimen size carry very different uncertainty.
behavior is an enumeration: feeding, resting, swimming, scavenging, burrowing, attached or unknown. The §12.3 example writes "Feeding", which again fails a case-sensitive check. abundance is free text in both sources ("15-20 individuals", "dense aggregation"). Free text is honest about how abundance is estimated from video, but it cannot be summed or filtered. When exporting, put an exact count into a numeric field only when the count really is exact, and carry ranges and qualitative estimates as remarks.
Each media[] entry has a type (video or photo), a file, and optional timecode and resolution. The spec example gives a timecode of "02:35:22" for a file named after the wall-clock time 14-35-22. A reader cannot tell whether the timecode is an offset into the file or a time of day. Make the observation's timestamp the authoritative UTC instant, and define timecode as the offset from the start of the referenced file. If the video system writes SMPTE timecode (with frames), say so. A record that links an animal to the right second of a multi-hour video is worth far more than one that links it to the right file.
ENE-040 §11.2.2 names Darwin Core for biological metadata, ISO 19115 for geospatial metadata and OBIS as the target database. The standard does not map the two, and the table below fills that gap. Darwin Core terms are given without a namespace prefix. Three OBIS points need attention: occurrence records must state occurrenceStatus and basisOfRecord, which ENE-040 does not carry; depth goes into a minimum and a maximum term (equal for a point observation); and the geodetic datum should be stated even though ENE-040 implies WGS 84 throughout.
| ENE-040 field | Darwin Core / OBIS term | ISO 19115 element (dataset level) | Note |
|---|---|---|---|
observationId | occurrenceID | — | Must be globally unique once exported; prefix with the dataset namespace. |
diveId / expeditionId | eventID / parentEventID | Resource identifier, aggregation | The dive is the sampling event and the expedition is its parent. |
timestamp | eventDate | Temporal extent | ISO 8601 in UTC in both. |
location.latitude / longitude | decimalLatitude / decimalLongitude, geodeticDatum | Geographic bounding box | Datum WGS84 stated explicitly. |
| USBL / navigation accuracy | coordinateUncertaintyInMeters | Positional accuracy (data quality) | Not an ENE-040 field on observations; derive from the navigation solution. |
location.depth | minimumDepthInMeters = maximumDepthInMeters | Vertical extent | Positive-down metres. |
location.habitat | habitat | — | Enumeration value exported as text. |
organism.scientificName | scientificName | — | Lowest confident name, no qualifiers inside. |
organism.taxonId | scientificNameID | — | Only if it is a register identifier (e.g. a WoRMS LSID). |
organism.identificationLevel | taxonRank | — | Qualifiers go in identificationQualifier. |
organism.identifiedBy / identificationMethod | identifiedBy / identificationRemarks | — | Method as a controlled phrase in the remarks. |
behavior | behavior | — | Direct correspondence. |
abundance | individualCount or organismQuantity + organismQuantityType; occurrenceRemarks | — | Exact counts only in individualCount. |
associatedSpecies[] | associatedTaxa | — | Pipe-separated list in Darwin Core text files. |
media[] | associatedMedia | Browse graphic | Resolvable URLs, not local file names. |
| (not in ENE-040) | basisOfRecord, occurrenceStatus | — | Set on export: e.g. HumanObservation or MachineObservation, and present. |
Expedition name, objectives | (dataset metadata) | Title, abstract, purpose | From the expedition record. |
Expedition scientists[] | recordedBy (per record) | Point of contact / responsible party | Use ORCID iDs where available. |
The choice between HumanObservation and MachineObservation for annotated video is a project policy decision, and different programmes make it differently. Make it once for the dataset and document it.
An observation is a claim about the world. A sample is a physical object that someone must be able to find ten years later. For samples, the most important part of the record is therefore not the scientific description but the answers to three questions: where the object is now, who is responsible for it, and on what terms it may be used. The record-keeping view taken here covers identity, location, custody, availability and legal terms. How specimens are preserved, processed or analysed in a laboratory is outside the scope of this book and of the data model, which records only the outcome (a preservation type, a list of analyses).
types.ts defines five record types for physical material. They share a pattern (ID, parent links, timestamp, location, a storage block) but differ in their keys and in what their storage block can express.
| Record type | Key | Parent links | Type field | Storage block | Availability |
|---|---|---|---|---|---|
BiologicalSample | sampleId | expeditionId, diveId | sampleType: whole_organism, tissue, genetic, environmental_dna | facility, repository, catalogNumber, freezer?, box?, position? | available / restricted / depleted, plus curator |
RockSample | sampleId | expeditionId, diveId | rockType: basalt … manganese_nodule, crust | facility, catalogNumber, location | not modelled |
SedimentCore | coreId | expeditionId, diveId? | coreType: push, gravity, piston, box | facility, repository, archiveHalf, workingHalf | per subsample: available / analyzed / depleted |
EnvironmentalDNASample | sampleId | expeditionId, diveId? | water sample block | facility, freezer, box, position, remainingVolume (µL) | implied by remaining volume |
IsobaricSampler | samplerId | sample.sampleId | sampler specification | none (status and integrity) | sealed / opened / failed |
The table exposes three inconsistencies that an implementer has to resolve.
"sampleType": "sediment_core". That value is not in the BiologicalSample enumeration. In the SDK, cores are a separate record (SedimentCore, keyed by coreId) registered through /api/v1/geological/cores, and rocks go through /api/v1/geological/rocks. Follow the SDK. Where a single sample catalogue is needed, as it is for a request workflow, give every record a catalogue entry with a common key and a type discriminator."availability": "Available upon request", which is not an enumeration value, and rock samples have no availability field at all. Add the same three-value field to every sample type, and treat "on request" as the normal meaning of available.nagoyadCompliant. A boolean cannot show how a sample is compliant. Keep the field for SDK compatibility, but add references to the documents that establish compliance (see below).The storage block is a location hierarchy: facility, then repository or collection, then freezer, box and position. It supports a simple integrity rule: no two samples that are not depleted may occupy the same facility, freezer, box and position at the same time. It also supports a routine audit: pick a random set of records, go to the positions and confirm the labels. Both are cheap and catch the most common failure in sample collections, which is an object that exists but cannot be found.
What v1.0 does not have is a history. The storage block says where a sample is now and not how it got there. For material collected under a permit, subdivided for several laboratories and shipped between countries, that history is the chain of custody. The book proposes an append-only custodyEvents[] list as an extension. Each event has a UTC time, an action (collected, received, preserved, subsampled, transferred, returned, depleted), who performed it, and where applicable a from/to pair. Subsamples get their own IDs and a parentSampleId. The SedimentCore subsamples[] array can stay as a summary view, but its entries are then separate records. The current storage block becomes a projection of the latest events. This is an extension, not v1.0; the example marks it by placement at the end of the record.
{
"sampleId": "SAMPLE-2025-ERA-015",
"expeditionId": "EXP-2025-004",
"diveId": "DIVE-ROV-2025-017",
"timestamp": "2025-06-12T11:02:40Z",
"location": { "latitude": -17.4191, "longitude": -113.2058,
"depth": 3544, "habitat": "hydrothermal_vent" },
"sampleType": "whole_organism",
"organism": { "scientificName": "Gastropoda", "lifestage": "adult" },
"method": "slurp_gun",
"preservation": { "type": "ethanol", "concentration": 95 },
"storage": {
"facility": "Example Ocean Institute Repository",
"repository": "Invertebrate collection",
"catalogNumber": "EOI-INV-2025-0412",
"box": "ERA-2025-B", "position": "C4"
},
"analyses": ["Morphology", "DNA barcoding"],
"curator": "Collections Manager (example)",
"availability": "available",
"nagoyadCompliant": true,
"materialTransferAgreement": "MTA-EXAMPLE-2025-07",
"custodyEvents": [
{ "at": "2025-06-12T11:02:40Z", "action": "collected", "by": "ROV-EXAMPLE-01", "location": "seafloor" },
{ "at": "2025-06-12T18:40:00Z", "action": "received", "by": "Jun Sample", "location": "ship wet lab" },
{ "at": "2025-06-12T19:15:00Z", "action": "preserved", "by": "Jun Sample", "location": "ship wet lab" },
{ "at": "2025-07-03T10:00:00Z", "action": "transferred", "by": "courier (example)",
"from": "R/V Example Explorer", "to": "Example Ocean Institute Repository" }
]
}
An EnvironmentalDNASample is, at the record level, a water sample with a volume, a collection method (niskin_bottle, rov_sampler, ctd_rosette), a filter size and a storage position with a remainingVolume. The interface also has blocks that summarise laboratory processing and, optionally, sequencing results. This book does not discuss how those steps are carried out. For the data model it is enough to say that the eDNA sample record should link to its derived sequence data by identifier, and that exports should follow the Darwin Core and OBIS guidance for DNA-derived occurrence data rather than inventing new terms. The useful record-keeping rule is that remainingVolume behaves like a balance. It decreases with each subsample event and the sample becomes depleted when it reaches zero.
An IsobaricSampler record describes a pressure-retaining sampler and one sample it holds: collection depth, collection pressure and maintained pressure in MPa, temperature, a status of sealed, opened or failed, and an integrity of intact or compromised. The data point that matters is the ratio of maintained to collection pressure, which shows how well in-situ conditions were preserved. The interface does not say whether the pressures are absolute or gauge. At 3,547 m the difference is only about 0.1 MPa out of roughly 35.8 MPa, but it is still a difference between two numbers that claim to measure the same thing. Declare absolute pressure. Also note that maxPressure belongs to the sampler and the other fields to one deployment, so a reusable sampler needs one record per deployment or a deployment sub-record.
The records API exposes sample access through POST /api/v1/samples/{sampleId}/request, which the SDK wraps as requestSample(sampleId, {requester, institution, purpose}). getAvailableSamples() lists samples with availability=available. The specification defines no states for a request after it is submitted. A workable lifecycle is submitted → under_review (curator) → approved or declined → shipped → returned or consumed, with each shipment written as a custody event and each consumption reducing what remains. Chapter 5 covers the endpoint itself.
import { DeepSeaExplorationSDK } from '@wia/ene-040';
const sdk = new DeepSeaExplorationSDK({
apiKey: process.env.WIA_API_KEY!,
endpoint: 'https://api.example.org',
});
const res = await sdk.requestSample('SAMPLE-2025-ERA-015', {
requester: 'Dr. Requester Example',
institution: 'Example University',
purpose: 'Morphological comparison with specimens from Segment B',
});
if (!res.success) {
// e.g. HTTP_404 when the ID is unknown, HTTP_403 when the tier forbids it
console.error(res.error?.code, res.error?.message);
}
Legal terms travel with the sample, not with the request. ENE-040 §14.2 names the Nagoya Protocol's access and benefit-sharing (ABS), prior informed consent (PIC) and mutually agreed terms (MAT). As data, these become references: the permit from the expedition's permits[] under which the material was taken, the PIC and MAT documents, an internationally recognised certificate of compliance where one has been issued, and materialTransferAgreement for each transfer. A request that would take material outside those terms should be blocked by the system, not left to the curator's memory.
A HydrothermalVent record describes a place. It has a ventId, a ventField name, a discoveryDate, a location with a tectonicSetting (mid_ocean_ridge, back_arc_basin, volcanic_arc, intraplate), a ventType, a structure block (height and diameter in metres, material, and a chimney state of active, inactive or collapsed), a fluidChemistry block, a biology block and a monitoring block (lastVisit, and a status of active, waning or inactive).
The ventType enumeration in the SDK is black_smoker, white_smoker, diffuse_flow and lost_city_type. The specification text in §8 describes three of these (black smoker, white smoker, and the "Lost City type") and gives typical ranges for each. Those ranges are descriptive and should not be turned into validation rules for a single vent's type. Classification is an expert judgement based on the whole site, and a single measurement outside a textbook range is a finding, not an error. The generic Phase 1 range checks (for example, temperature −2 to 400 °C) still apply to each value.
The fluid chemistry block is where the two sources differ most. The interface gives temperature (°C), ph, h2s, ch4 and optional h2 (mmol/kg), and a metals object whose only unit comment is on fe (µmol/kg). The §12.5 example spells the pH key "pH", which is a different key from ph, and gives metal values with no units at all. Vent fluid concentrations routinely span several orders of magnitude between species, so a bare number is not interpretable.
The fix is the same one Phase 1 already applies to oceanographic measurements: every value carries its unit. The corrected example below writes each chemical value as {value, unit}, uses the UDUNITS-style unit strings that CF-NetCDF exports expect (Chapter 7), and states the reference conditions for pH, because pH measured on a recovered fluid sample at laboratory temperature and pressure is not the in-situ pH. It also adds two links that v1.0 lacks: sampleRef, pointing to the physical fluid sample the chemistry was measured on, and visits[], listing the dives that visited the vent. These two links replace the location-only join in the entity diagram with explicit references. They are proposals, and a v1.0 consumer that ignores unknown fields will still read the record.
{
"ventId": "VENT-ERA-2025-003",
"ventField": "Example Ridge, Segment A, field 2",
"discoveryDate": "2025-06-14T00:00:00Z",
"location": {
"latitude": -17.4193, "longitude": -113.2056, "depth": 3545,
"region": "Example Ocean Basin",
"tectonicSetting": "mid_ocean_ridge"
},
"ventType": "black_smoker",
"structure": {
"height": 12.0, "diameter": 1.8,
"material": "Sulfide (FeS, CuS, ZnS)", "chimneyType": "active"
},
"fluidChemistry": {
"temperature": { "value": 352, "unit": "degC" },
"ph": { "value": 3.6, "unit": "1", "reference": "measured at 25 degC, 1 atm" },
"h2s": { "value": 7.1, "unit": "mmol/kg" },
"ch4": { "value": 0.06, "unit": "mmol/kg" },
"metals": {
"fe": { "value": 1500, "unit": "umol/kg" },
"mn": { "value": 420, "unit": "umol/kg" }
},
"samplingMethod": "isobaric gas-tight sampler",
"sampleRef": "SAMPLE-2025-ERA-012"
},
"biology": {
"dominantSpecies": "Alvinocarididae (shrimp), not identified to species",
"abundance": "high",
"community": ["Gastropoda", "Polychaeta"]
},
"monitoring": {
"lastVisit": "2025-06-19T11:20:00Z",
"status": "active"
},
"visits": ["DIVE-ROV-2025-017", "DIVE-ROV-2025-021"]
}
Two further fields need tightening. discoveryDate is typed as a timestamp, while the spec example gives a date. And biology.abundance is an enumeration (low, medium, high) in the interface, but free text with a density in the example. Keep the enumeration for filtering. Put any quantitative density in the observation records it came from, where it has a method and a date.
A MultibeamSurvey describes one bathymetric survey and its deliverables. It has a surveyId, an expeditionId, a surveyDate, an area with a name, a bounding box (minLat, maxLat, minLon, maxLon) and totalArea in km², a sonarSystem (manufacturer, model, frequency in kHz, beam count, swath width in metres), a processing block, a deliverables block and depth statistics (minimum, maximum, mean, standard deviation).
The processing block records the software, the grid resolution in metres, the vertical and horizontal uncertainty, and two booleans for whether a sound velocity profile and a tide correction were applied. The booleans are the weakest part. A reprocessor needs to know which profile and which tide model were used. The Phase 1 BATHYMETRIC_DATA message already has a processingParameters block with an SVP reference, so the survey record should carry the same references rather than a yes/no.
The deliverables block names a bathymetryGrid file or URL, an optional backscatterMosaic, a format (geotiff, netcdf, xyz or bag, the last being the open Bathymetric Attributed Grid format) and a crs. Watch two points here:
EPSG:4326, a geographic system whose cells are measured in degrees. gridResolution is in metres. A 50 m grid is well defined in a projected system such as the appropriate UTM zone. In EPSG:4326 it is only an approximation that changes with latitude. Either deliver in a projected CRS or record the cell size in the grid's own units as well.statistics and the rest of ENE-040 use positive-down depth. A minDepth of 2,400 and a grid minimum of −3,800 can describe the same survey. Say which convention each artefact uses.A bounding box that crosses the antimeridian has minLon greater than maxLon. The SDK's getBathymetryGrid() passes the box through as a bbox string. Servers must either handle that case or reject it explicitly. They must not return the other 350° of the globe.
The survey links to vents and dives by location. A vent is "covered" by a survey when its position falls inside the survey's extent, and the survey grid is the base map on which dive tracks are drawn. That is why the survey's horizontal uncertainty and the dive's navigation uncertainty belong in the same unit and the same datum.
ENE-040 §14.1 defines three data-access tiers. The table restates them in record terms.
| Tier | What the spec places in it | Record-level effect |
|---|---|---|
| Public | Low-resolution multibeam (50 m grid); species-level observation lists; expedition metadata (overview, location, dates) | Expedition and dive summaries, observations with generalised or exact positions depending on the sensitivity of the taxon or site, survey grids at 50 m or coarser |
| Restricted | High-resolution multibeam (<10 m) with a 2-year embargo; genetic data from biological samples, which needs researcher consent; precise mineral-resource locations | Survey deliverables finer than the public grid until embargoUntil; eDNA and genetic sample records; sample request workflow gated by role |
| Confidential | Commercial exploration data; military-related data; exact coordinates of sensitive ecosystems | Exact positions of flagged vents and habitats never leave the system; public copies carry generalised coordinates only |
The specification states the tiers but gives no field to hold them. The SDK interfaces have none either. This book proposes a small access block on every science record: tier, embargoUntil (a UTC timestamp), license and a free-text reason. The embargo date should be computed from a stated event (for example, the end of the expedition) and stored, not recomputed on every read. Otherwise a change to the rule silently changes the date. Chapter 7 connects these fields to the Phase 4 roles and licences.
For the confidential tier, the exact position of a record must not appear in any public output, but the record itself should usually remain discoverable. The standard technique is generalisation: publish a coarser position and state its uncertainty. Darwin Core provides dataGeneralizations to say what was done and informationWithheld to say that more precise data exists.
The function below snaps a point to the centre of a fixed grid cell and rounds the depth. Snapping is deterministic, and that is the important design choice. Adding random noise to each request looks more private, but a user who can make many requests can average the noise away. A deterministic generalisation returns the same coarse point every time. The cell size and depth step are policy values, not values from the specification.
type Tier = 'public' | 'restricted' | 'confidential';
interface Point { latitude: number; longitude: number; depth: number; }
interface Generalized extends Point {
coordinateUncertaintyInMeters: number;
dataGeneralizations?: string;
}
const M_PER_DEG_LAT = 111_320; // approximate, WGS84 mean
// Snap to the centre of a fixed grid cell: deterministic, so repeated
// requests cannot be averaged back to the true position.
function snap(v: number, cellDeg: number): number {
return (Math.floor(v / cellDeg) + 0.5) * cellDeg;
}
export function generalizeForTier(p: Point, tier: Tier, usblSigmaM = 10): Generalized {
if (tier !== 'confidential') {
return { ...p, coordinateUncertaintyInMeters: usblSigmaM };
}
const cellDeg = 0.1; // policy value, not from the spec
const lat = snap(p.latitude, cellDeg);
const lon = snap(p.longitude, cellDeg);
const depth = Math.round(p.depth / 500) * 500; // depth also narrows the location
// upper bound on the half-diagonal: the latitude span is the longer side
const half = (cellDeg * M_PER_DEG_LAT) / 2;
return {
latitude: Number(lat.toFixed(4)),
longitude: Number(lon.toFixed(4)),
depth,
coordinateUncertaintyInMeters: Math.ceil(Math.SQRT2 * half + usblSigmaM),
dataGeneralizations: `snapped to ${cellDeg} deg grid cell centre; depth rounded to 500 m`,
};
}
For the vent in §4.5 the function returns latitude −17.45, longitude −113.25, depth 3,500 m and an uncertainty of about 7.9 km. That is enough to say that a vent field exists in this part of the ridge, but not enough to lead a vessel to it. Generalising the point is not the whole job. A confidential position leaks through other channels as well, and each needs its own rule:
bottomTrack, the launchPosition, observations and samples from the same dive, and the vent's visits[] all place the site. Generalise them together, or withhold the track.ventField such as "9°50'N" or a video file named after a position defeats the grid. Check free text and media names on export.types.ts are the reference. The §12 examples fail them in several places: case-sensitive enumerations, lat/lon keys, a malformed offBottom, a non-existent sediment_core sample type, and missing units in vent chemistry.bottomTime, daysAtSea, summary counters, remaining volume) are stored for convenience and must be checked against their sources.basisOfRecord and occurrenceStatus are handled explicitly.checkDiveTimeline, and name two other changes it needs to conform to the DiveLog interface.scientificName, identificationLevel, identificationMethod and the relevant Darwin Core terms."sampleType": "sediment_core". Which record type and endpoint should have been used, and how would you still make it requestable from a single catalogue?gridResolution: 25 with crs: "EPSG:4326". What is ambiguous about this, and what would you ask the producer to change?