Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ this file is about the package, whose version moves independently.

## Unreleased

- **A diver carries a portrait.** §6.1 of
[the specification](https://github.com/divejson/divejson/blob/main/spec/divejson.md) adds
`portrait_file`, a Stored File (§6.7), and `divejson validate` claims its uuid in the
document's one identifier space, so a portrait sharing a uuid with any other record is
refused (§5.3). UDDF carries no portrait in either direction: the reader takes no image
from `<owner>`, and the writer reports the member rather than writing it.

## 0.11.0

- **Breaking: a diver carries a date of birth, a phone, emergency contacts and insurances,
Expand Down
2 changes: 1 addition & 1 deletion SPEC_REF
Original file line number Diff line number Diff line change
@@ -1 +1 @@
c5034f083d178de0d4c4130dd989213e8031db92
929788950d59e3b8130ccdbd27811e44aeb074ff
3 changes: 3 additions & 0 deletions divejson/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ def _semantic_issues(doc: dict[str, Any]) -> list[Issue]:
seen_uuids: dict[str, str] = {}
if isinstance(diver, dict):
_claim_uuid(diver, "diver", seen_uuids, issues)
portrait = diver.get("portrait_file")
if isinstance(portrait, dict):
_claim_uuid(portrait, "diver/portrait_file", seen_uuids, issues)
_check_datetime(diver, "created_at", "diver", issues)

collections = {
Expand Down
8 changes: 8 additions & 0 deletions docs/uddf-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ mapped* below.
(§6.1). Nothing fills `diver.emergency_contacts` or an insurance's `number`: UDDF has no
element for either.

**Nothing fills `diver.portrait_file`, and a linked image is not read as one.** `<owner>`
has no image of its own. The one route to a picture, a `<notes><link ref>` to a
`<mediadata><image>`, carries no role: UDDF's own documentation of `<owner>` links an
equipment photo, a group photo, an audio clip and a video that way, so a reader taking the
first linked image for a portrait would import a boat photo as the diver's face. The
`<image>` also names its file by a path whose bytes the UDDF file does not carry, so a Stored
File's digest and size (§6.7) could only be invented.

**`@id` is never read as a name or a handle.** It is an XML id, and Subsurface's is the
literal string `owner`.

Expand Down
5 changes: 3 additions & 2 deletions docs/uddf-writing.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ its examples. It is `<dateoftrip>`'s answer (*Sites and trips* below) and it inv
the member is a date, so a reader takes the date back off the front and the midnight never
reaches a document.

An insurance's `number` and the diver's `emergency_contacts` have no element, and each is
reported — *What is never written* has why.
An insurance's `number` and the diver's `emergency_contacts` and `portrait_file` have no
element, and each is reported — *What is never written* has why.

**Differs from the reference writer**: `diver.email` is written to
`<contact><email>`. The reference writer deliberately omits it — a UDDF file is the thing a
Expand Down Expand Up @@ -645,6 +645,7 @@ once per record that carries it, and none of them has anywhere in UDDF to go:
| `diver.username` | `<owner id>` is an XML id and not a handle |
| `diver.emergency_contacts` | UDDF has no element for one, and `<owner>` describes the logbook's owner and nobody else |
| `diver.insurances[].number` | `insuranceType` holds a `name`, `aliasname`, `issuedate`, `validdate` and `notes`, with nothing for the identifier the insurer knows the diver by. `<notes>` would read back as a remark, and `<personal><membership memberid>` as a membership, which a reader cannot tell from a club's |
| `diver.portrait_file` | `<owner>` has no image element. A `<notes><link>` to a `<mediadata><image>` names a file by path, whose bytes a UDDF file does not carry, and reads back as a linked picture with no role — `uddf-mapping.md`'s *Diver* section says why no linked image is read as a portrait |
| a recording's `source_files`, `started_at` and its device's `firmware`, and every recording after the first | UDDF gives a dive one `<samples>`, and `equipmentPieceType` no firmware element — *Devices* above has each answer and why the device of a dropped recording is kept even so |
| `trips[].parts[].location.bbox` | `geographyType` carries a point, not a box |
| `sites[].location.full_name`, `position` and `bbox` | a site's `<name>` is its own, so the locality gets only `<geography><location>` and that slot holds `location.name`; `<geography>`'s coordinates are the site's pin, and the box has nowhere either — *Sites and trips* above has the asymmetry with a part |
Expand Down
5 changes: 3 additions & 2 deletions fixtures/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ what makes a fixture dropped into this tree impossible to ignore.
| `minimal.divejson` | The smallest conforming document: `format`, `version`, `exported_at` — no diver (a source that records nothing about its owner omits the member), no collections (absent ≡ empty). |
| `demo-logbook.divejson` | A real export of the reference writer's demo account (all names are seeded demo data; pulled 2026-09-02, courses-era writer): 8 dives, one carrying a recording with its device, its stored file and a full sampled profile, which is what a dive imported from a computer file looks like — sites, trips, an empty `courses` collection, gear with a service history, certifications, and producer extensions carrying application-specific values. Its device is written by hand: the export predates recordings, and the file it names was read by the `suunto_json` parser, so the device is what that reader takes from such a file. So are the diver's `phone`, `born_on`, `emergency_contacts` and `insurances`, one element in each array, which the export predates as well. Regenerate from a fresh export when the writer changes. |
| `two-computers.divejson` | Hand-built coverage of §6.4a, and of the half of it no converter can reach: a dive with **two** recordings that both carry a profile — `ssrf/two-computers.ssrf` reaches that much from one file — where the first holds **two** `source_files`, one recording the app exported twice, as JSON beside FIT, each file read by a different parser. Its second recording carries its own `started_at`, 31.33 s after the dive's, so its samples sit on their own axis. Its two devices differ in every way §6.4b allows two devices to differ, and the second computer's deepest sample is deeper than the dive's logged `max_depth`, which is the ordinary disagreement between two devices rather than a defect. It has no pair: §6.7's stored-file records are the application's and no reader produces one, which is the point of it. It is also where the two halves of §6.4c the other hand-built file cannot reach live: its first recording runs an RGBM model named in words with a **negative** `conservatism` and no gradient factors, the shape a Suunto states, while its second is in **`gauge`** mode and carries no `deco_model` at all — a computer run as a bottom timer runs no decompression model, and saying so is not the same as saying nothing. Between the two files every member of §6.4c's table and two of §6.4a's five `mode` values are exercised by a document no converter wrote; `freedive` comes from `suunto_xml/freedive.xml`, and `closed_circuit` and `semi_closed` wait for a file, no source in hand stating either. |
| `technical-dive.divejson` | Hand-built coverage of what the demo corpus lacks: trimix, a sidemount pair (two cylinders, one blend, `usage: "parallel"`), staged deco cylinders, gas-switch events, a ceiling channel with a gap, per-cylinder pressure channels, a `+12:45` UTC offset **and** an offset-less local `started_at` (§5.2's third state), a dive with no recorded duration, a common-name-only species, a trip of three parts — a place with dates, a place with none, and dates with no place — the second of them carrying an antimeridian-crossing bounding box, the corpus's only **dive site whose locality is filled in** — a full name, a centre and a box on `location`, beside the site's own pin at a different point, which is the pair §6.10 exists to keep apart — an `agency: "other"` certification with `front_file`/`back_file`, a dive-count service interval, three courses — a completed `"other"`-agency course linked from a dive and its certification, an unreferenced `"planned"` one with no dates, and one with **no `agency`**, the private-instructor case §6.17 leaves the member out for — a lowercase-`z`, one-digit-fraction `created_at` (both spellings the grammar allows and naive parsers reject), and a `bookmark` 85 s **past** `profile.duration` — the surface-marker case §6.4 blesses, which the pre-2026-09-04 validator rejected, and which also carries a `label` beside a `type`, the pairing §6.6 asks for on any value defined after 1.0. Its recording's device carries **all six** of §6.4b's members, hand-built like the rest of the file. Its diver carries an emergency contact and an insurance with a `number`, the two members of §6.1 UDDF has no element for. It is also the **decompression** fixture: `mode: "open_circuit"` and a Bühlmann `deco_model` with a name and a 30/70 gradient-factor pair, and all six of §6.4's readout channels sampled on the depth channel's own seconds — an `ndl` that runs from the 99-minute display cap down to zero and back, a zero `ndl` at the same second as a `ceiling` (the coexistence §6.4 blesses), a `tts` present only while there is an obligation, a `ppo2` that climbs through three gases, a `cns` whose ends are the dive's own `cns_start` and `cns_end`, and a `surface_gradient_factor` above 100 with the `gradient_factor` beside it far below. Its profile's `extensions` carries a tissue-loading array, which is what §6.4c says tissue state rides until a reader can compute from it. |
| `technical-dive.divejson` | Hand-built coverage of what the demo corpus lacks: trimix, a sidemount pair (two cylinders, one blend, `usage: "parallel"`), staged deco cylinders, gas-switch events, a ceiling channel with a gap, per-cylinder pressure channels, a `+12:45` UTC offset **and** an offset-less local `started_at` (§5.2's third state), a dive with no recorded duration, a common-name-only species, a trip of three parts — a place with dates, a place with none, and dates with no place — the second of them carrying an antimeridian-crossing bounding box, the corpus's only **dive site whose locality is filled in** — a full name, a centre and a box on `location`, beside the site's own pin at a different point, which is the pair §6.10 exists to keep apart — an `agency: "other"` certification with `front_file`/`back_file`, a dive-count service interval, three courses — a completed `"other"`-agency course linked from a dive and its certification, an unreferenced `"planned"` one with no dates, and one with **no `agency`**, the private-instructor case §6.17 leaves the member out for — a lowercase-`z`, one-digit-fraction `created_at` (both spellings the grammar allows and naive parsers reject), and a `bookmark` 85 s **past** `profile.duration` — the surface-marker case §6.4 blesses, which the pre-2026-09-04 validator rejected, and which also carries a `label` beside a `type`, the pairing §6.6 asks for on any value defined after 1.0. Its recording's device carries **all six** of §6.4b's members, hand-built like the rest of the file. Its diver carries an emergency contact, an insurance with a `number` and a `portrait_file`, the three members of §6.1 UDDF has no element for. It is also the **decompression** fixture: `mode: "open_circuit"` and a Bühlmann `deco_model` with a name and a 30/70 gradient-factor pair, and all six of §6.4's readout channels sampled on the depth channel's own seconds — an `ndl` that runs from the 99-minute display cap down to zero and back, a zero `ndl` at the same second as a `ceiling` (the coexistence §6.4 blesses), a `tts` present only while there is an obligation, a `ppo2` that climbs through three gases, a `cns` whose ends are the dive's own `cns_start` and `cns_end`, and a `surface_gradient_factor` above 100 with the `gradient_factor` beside it far below. Its profile's `extensions` carries a tissue-loading array, which is what §6.4c says tissue state rides until a reader can compute from it. |

## invalid/

Expand Down Expand Up @@ -66,6 +66,7 @@ validator does not look.
| `duplicate-uuid.divejson` | two records share a uuid | §5.3 |
| `dangling-reference.divejson` | a `site_uuids` entry resolves to nothing | §5.3 |
| `duplicate-file-uuid-across-recordings.divejson` | two recordings of one dive carry the same stored-file record | §5.3 |
| `duplicate-file-uuid-portrait.divejson` | the diver's `portrait_file` carries the uuid of a certification's `front_file` | §5.3 |
| `recording-without-content.divejson` | a recording carrying none of `device`, `profile`, `source_files` | §3, §6.4a |
| `dive-profile-outside-recording.divejson` | a `profile` on the dive rather than inside a recording — the retired shape | §6.2, §6.4a |
| `device-empty-member.divejson` | a device with an empty `serial` | §6.4b |
Expand Down Expand Up @@ -243,5 +244,5 @@ point of each row below is which part of `uddf-writing.md` it reaches.
| file | written from | what it covers |
| --- | --- | --- |
| `opendiving.divejson` | `uddf/opendiving.divejson`, plus a device | The round trip that matters most, and the pair that exercises almost none of the report: this document is itself the *reading* of a UDDF export, so there is nothing in it UDDF cannot hold, and the only finding is the `extensions` exclusion every written file carries. `dive-<uuid>` ids that come back as those uuids, a trip of one part, written back as the `<trippart>` with its dates and its place that it was read from, a kit list under `<equipment>` with per-dive `<equipmentused>` links, two cylinders on two gases with their pressure channels, a `<switchmix>` gas switch and a `<setmarker>`. And the **fold**: its dive lists that `computer` gear item, which satisfies the predicate's link leg, and its recording's device and the gear item carry the same serial, so the serial leg fires and the two become one `<divecomputer>` element carrying both halves, with the device's counter on the dive as `<internaldivenumber>`. It is the corpus's only pair that reaches the serial leg. **No pair reaches the link leg refusing** — a `computer` gear item the document carries and one of its own dives does not link — so that branch has no pair either way, and [`docs/uddf-writing.md`](../docs/uddf-writing.md) writes it down rather than leaving it to the first writer to meet one. Its diver's `born_on`, `phone` and insurance are written to `<birthdate>`, `<contact><phone>` and `<diveinsurances>`, which puts a `<contact>` between `<personal>` and `<equipment>` and the insurances after the kit list, the order `personType` fixes. |
| `technical-dive.divejson` | `valid/technical-dive.divejson` | Everything the first one cannot reach, being hand-built to hold what no UDDF export carries. The `dropped` half of the report: `courses`, `certifications`, `gear_sets`, gear service and `species`, which UDDF has no slot for; `role` and `usage` on a sidemount pair and its staged deco cylinders; a ceiling channel; a trip part's bounding box, and a dive site's locality entire but for its name — its full name, its centre and its box, which is three findings off one record where the part loses one; the gas numbering UDDF cannot record; `shears` landing in `<variouspieces>` and reading back as `other`; a trip part whose location has coordinates and no full name, which loses the coordinates rather than borrowing the name; a `bookmark` carrying a label, which keeps its type and loses the label; an event with a label and no type, which goes out as a `<setmarker>` carrying the label; the `deco_model`, `tts` and `surface_gradient_factor` its document now carries, each reported `dropped` — the first because UDDF's `<decomodel>` requires a tissue table this format has no member for, the other two because UDDF has no element at all; an empty note, which no UDDF file can spell; a device's `firmware`, for which `equipmentPieceType` has no element; and the diver's `emergency_contacts` and an insurance's `number`, which UDDF has no element for either — the insurance itself is written, as a `<diveinsurances>` entry carrying its provider. It is also the **other** half of the device fold: its gear list holds no computer, so the device matches nothing and gets a `<divecomputer>` of its own with a non-UUID id and a `<link>` from the dive — the one case where reading the written file back returns a gear item the input never had. That its device carries a `name` is what puts it inside that exception rather than beside it: a nameless device on an element of its own gets an empty `<name>`, which comes back as no gear item at all. The `absent` half is its second dive and its third trip part: no maximum depth, no duration and a cylinder with no start pressure, so `<greatestdepth>`, `<diveduration>` and `<tankpressurebegin>` are each written as the `0` a reader takes back off, and a part with no place gets the empty `<name>` `simpleNamedType` requires. Its trip is the corpus's only three-`<trippart>` one, and the three are the three shapes a part comes in: a place with dates, a place with none — which writes no `<dateoftrip>`, that being the one thing here that costs nothing — and dates with no place. |
| `technical-dive.divejson` | `valid/technical-dive.divejson` | Everything the first one cannot reach, being hand-built to hold what no UDDF export carries. The `dropped` half of the report: `courses`, `certifications`, `gear_sets`, gear service and `species`, which UDDF has no slot for; `role` and `usage` on a sidemount pair and its staged deco cylinders; a ceiling channel; a trip part's bounding box, and a dive site's locality entire but for its name — its full name, its centre and its box, which is three findings off one record where the part loses one; the gas numbering UDDF cannot record; `shears` landing in `<variouspieces>` and reading back as `other`; a trip part whose location has coordinates and no full name, which loses the coordinates rather than borrowing the name; a `bookmark` carrying a label, which keeps its type and loses the label; an event with a label and no type, which goes out as a `<setmarker>` carrying the label; the `deco_model`, `tts` and `surface_gradient_factor` its document now carries, each reported `dropped` — the first because UDDF's `<decomodel>` requires a tissue table this format has no member for, the other two because UDDF has no element at all; an empty note, which no UDDF file can spell; a device's `firmware`, for which `equipmentPieceType` has no element; and the diver's `emergency_contacts` and `portrait_file` and an insurance's `number`, which UDDF has no element for either — the insurance itself is written, as a `<diveinsurances>` entry carrying its provider. It is also the **other** half of the device fold: its gear list holds no computer, so the device matches nothing and gets a `<divecomputer>` of its own with a non-UUID id and a `<link>` from the dive — the one case where reading the written file back returns a gear item the input never had. That its device carries a `name` is what puts it inside that exception rather than beside it: a nameless device on an element of its own gets an empty `<name>`, which comes back as no gear item at all. The `absent` half is its second dive and its third trip part: no maximum depth, no duration and a cylinder with no start pressure, so `<greatestdepth>`, `<diveduration>` and `<tankpressurebegin>` are each written as the `0` a reader takes back off, and a part with no place gets the empty `<name>` `simpleNamedType` requires. Its trip is the corpus's only three-`<trippart>` one, and the three are the three shapes a part comes in: a place with dates, a place with none — which writes no `<dateoftrip>`, that being the one thing here that costs nothing — and dates with no place. |
| `owner-profile-only.divejson` | `uddf/owner-profile-only.divejson` | The owner that records a person and names nobody, going out: a diver with a uuid and a `born_on` and no name or email still gets a `<diver>`, under `<owner id="diver-<uuid>">` because the document records something about the person, with the empty names `personalType` requires beside the `<birthdate>`. The written file is its reader pair's input again, `<generator>` aside. |
Loading
Loading