Skip to main content

The four canonical objects

Everything an adapter emits is an Entity, an Event, a Track or a PlanObject. The split is by what a thing is — not by which system sent it, and not by which consumer wants it.

ObjectMeansKey fields
Entitysomething that existsentity_id, entity_type, affiliation, position, kinematics, valid_from/valid_to, confidence, attributes
Eventsomething that happenedevent_id, event_type, severity, related_entities, geometry, payload, observed_at, received_at
Trackan entity's position historytrack_id, entity_id, samples[], track_quality
PlanObjectsomething we push outobject_id, object_type, geometry, style, label, expires_at

What all four carry​

Every canonical object inherits CDMBase, and none of these fields is decoration.

This list grew on 2026-09-08, and it read "these four fields" until it did

schema_version, source, source_ids[] and integrity were the four. schema_version 2.1.0 adds three more — quality, status and residual — and all three are OPTIONAL, so an object written against 2.0.0 is still a valid object and a 2.0.0 reader still reads a 2.1.0 one. What each of them means, and the rule that keeps residual from being trusted, are on the CDM Policies page.

Corrected 2026-09-19 by the audit's F01 (the sentence above is left as written). The first half holds and the second does not: a 2.0.0 reader does NOT read a 2.1.0 object, because every published schema carries additionalProperties: false and refuses the three properties 2.1.0 introduced, populated or null. version.assess("2.1.0", read_by="2.0.0") answers REFUSED, WRITER_NEWER, and the frozen 2.0.0 schemas under tests/frozen/cdm/ are the evidence. See Compatibility is not equality.

schema_version​

Semver, in every serialised object. A consumer reading an object off a queue has no other way to know which shape it is holding, and "we will add versioning when we need it" means adding it at the moment two incompatible producers are already in the field.

Compared with version.compatible(), not by equality — see the Changelog.

source​

{system, adapter, adapter_version, synthetic} — which translator produced this object.

synthetic is required and has no default. Mislabelling exercise data as live can reach an operational picture; mislabelling live data as exercise hides it from an operator. There is no safe default, so there is no default.

source_ids[]​

Every external identifier the object is known by, at least one, on every kind.

This field moved, and the harness is why

source_ids was specified on Entity alone. The lossless check found the gap within a minute of first running: a PNTMAP alert whose emitter carries its own id produced an entity keyed on the emitter and an event keyed on nothing, so the alert's own identifier — PNTMAP-2026-04-29-0117 — appeared nowhere in the output.

Three consequences, none acceptable: a redelivery could not be recognised as a duplicate, because nothing held the identifier the source deduplicates on; an auditor holding the event could not get back to the source record, which is the one question an audit trail exists to answer; and the loss was silent — every other check passed.

It is a list rather than a single value because the same object arrives from several systems: one vessel is an MMSI to AIS, a track number to STANAG 4676 and a UID to TAK. Fusion joins them later; an adapter records the name its own system used and never overwrites another system's entry.

integrity​

The post-quantum signature block: designed, deliberately not implemented.

The field exists from day one so that turning signing on is a value change rather than a schema change — a schema change would be a MAJOR bump rippling through every store and every consumer, arriving exactly when the signing work is already late.

No crypto happens in this package, and that is enforced by AST rather than promised: an import of hashlib or cryptography here would mean somebody had started signing objects inside the translation layer, where the key material has no business being and where nothing audits it.

All three of its fields or none. A block holding a signature with no algorithm is unverifiable, and an unverifiable signature that looks present is worse than an absent one: it reads as assurance to everything downstream that does not check.

quality​

{source_quality, confidence, accuracy_m, uncertainty} — how good the source says its own data is. Absent means the source said nothing about quality, which is not the same as saying it is poor. uncertainty is a named dict rather than one number, because an error ellipse is not a radius and flattening it loses the orientation that made it worth sending.

status​

{state, namespace, since, attributes} — the source's own operational state, in the source's own words. There is no enum and there will not be one. "SERVICEABLE", "MISSION CAPABLE", "RED" and "U/S" come from different domains and mean different things; a closed CDM vocabulary would have to map each onto a member, which is a judgement about somebody else's operational language made inside a translator. namespace says whose vocabulary state belongs to, and a consumer must not compare state across namespaces.

residual​

{namespace, data} — source information the CDM does not model, kept under the name of the format it came from. It identifies its origin and it is never semantically trusted: it records that the source said something the CDM has no home for, which is a fact about the source record and not an assertion about the world. See CDM Policies.

Fourteen of the nineteen adapters shipped today do not use it — they keep parking leftovers in attributes.source_extras and declare residual: legacy, because the information is already preserved and moving the placement would cost every golden file and every consumer for nothing a reader can use. The five of the adapter expansion (geojson, geopackage, c2sim, aixm511, aixm52) declare residual: structured and carry their leftovers in it.

Why additionalProperties: false everywhere​

A canonical model whose objects accept unknown keys is not canonical — it is a dict with a docstring. The strictness is safe only because it is paired with a declared escape hatch: Entity.attributes and Event.payload accept anything, so an adapter never has to choose between dropping a field and failing validation.

Strict where the meaning is fixed, open where it is not, and the boundary between the two written down. The alternative — extra="allow" on the objects themselves — puts source-specific fields at the same level as canonical ones, and six months later nobody can tell which fields the model guarantees and which one adapter happens to send.