Changelog and versioning
Every serialised object carries schema_version. 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.
:::note Source of truth
This page mirrors packages/cdm/synapse_cdm/MIGRATIONS.md, which is the file the procedure
below tells you to edit. When they disagree, the file in the package wins — it sits next to the
version.py it describes.
:::
What each bump means
| Bump | Change | Consumer impact |
|---|---|---|
| MAJOR | a field removed or renamed; a type narrowed; an enum member removed; an optional field made required; ids.NAMESPACE changed | breaks readers; needs a migration entry below and a coordinated deployment |
| MINOR | an optional field added; an enum member added; a payload model registered; validation relaxed | old readers keep working, old data keeps validating |
| PATCH | descriptions, error-message wording, docs | none |
Compatibility is not equality
version.compatible(written_with="1.2.0", read_by="1.0.0") # True
version.compatible(written_with="2.0.0", read_by="1.0.0") # False
compatible() accepts the same major including a minor from the future: a 1.0.0 reader
accepts a 1.2.0 object, because MINOR additions are optional by definition and the alternative
is a fleet that stops ingesting the moment one adapter is upgraded. A different major is refused
outright.
Renaming a field is two releases, never one: add the new name in a MINOR, populate both, then remove the old one in the next MAJOR. One release that renames is an outage for every consumer that has not been redeployed in the same hour.
Changing the schema — the procedure
- Edit the Pydantic model. It is the single source; the files in
/schemasare a publication. - Bump
version.SCHEMA_VERSIONper the table above. - Re-export:
python -m synapse_cdm.schemas --out schemas.tests/test_cdm_schemas.pyfails the build if you forget, and--checkis the CI form. Then regenerate this site's reference pages:cd docs && npm run gen:schemas, whichnpm run check:schemasgates in the same way. - Add an entry below, naming the reason — not just the change.
- Re-run every adapter's golden files and read the diffs:
python -m synapse_cdm.harness --adapter <name> --fixtures <dir> --update-golden. A golden file updated without being read is how a defect becomes the expectation. - If a documented gap in
FORMAT_COVERAGE.mdis now closed, close it there too.tests/test_cdm_format_coverage.py::test_the_documented_gaps_are_still_gapsfails deliberately when a gap field appears, so the document cannot silently disagree with the code.
History
1.0.0 — initial contract
The four objects (Entity, Event, Track, PlanObject), Position, Kinematics,
SourceId, SourceRef, Integrity, TrackSample, and one registered payload model
(GnssInterferencePayload for GNSS_INTERFERENCE).
Two decisions in this release depart from the original specification, both because building the reference adapter surfaced the reason:
source_idsmoved fromEntitytoCDMBase, required on every kind. The harness's lossless check found the gap on its first run: 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 appeared nowhere in the output. A redelivery could not be recognised as a duplicate and an auditor holding the event could not get back to the source record.signal_strengthbecamesignal_strength_dbm. A baresignal_strengthhas been read as dBW, dBm and a 0–100 bar by three different consumers; the unit belongs in the name, as it already does inspeed_mps,alt_mandaccuracy_m.
Adapters that landed with no schema change
Recorded because "no entry" and "nobody wrote an entry" look identical from here, and the first is worth stating.
-
adapters/tak.py1.0.0 — Cursor-on-Target, bidirectional. Implements every row of the CoT table inFORMAT_COVERAGE.mdat schema version 1.0.0, with no field added, removed or retyped. Two temptations were declined and are listed below as 1.1.0 candidates instead: a canonical home for the CoT callsign, and one forpoint/@le. Both would have been MINOR, and both would have been added in passing — which is how a canonical model acquires two fields that mean nearly the same thing.What it needed instead already existed:
attributesfor the unmapped values,TRANSFORMSfor the nine paths whose value legitimately changes, andUNKNOWNas an enum member for the three CoT affiliation letters the CDM does not carry.
Proposed for 1.1.0 — MINOR, not yet implemented
Both come from FORMAT_COVERAGE.md's gap list, and both are deliberately deferred rather than
added in passing. Both are now confirmed by a shipped adapter rather than anticipated: the
TAK adapter parks a real value for each of them on every fixture it translates, which is the
evidence that was missing when they were first written down.
Entity.label— a canonical human-readable name. A CoT callsign and a STANAG 4676 track number are the strings an operator reads, and today they land inattributes, so every consumer that wants to label a contact needs private knowledge of which adapter's key to look under. Deferred because it needs one owner naming its precedence rules across sources, not a field added in passing.Position.alt_accuracy_m— vertical accuracy.accuracy_mis horizontal only, so CoT's@lehas no home. It matters for air tracks, where a 300 m vertical error decides whether two aircraft are deconflicted — and the TAK adapter'sair_track_due_northfixture is exactly that case:le="120.0"on a track at 7 620 m, parked inattributeswhere no consumer will look for it.
Until then, both values are carried in Entity.attributes by the adapters that receive them —
which is lossless but not canonical, and that difference is the whole reason these are listed as
gaps rather than as decisions.