Skip to main content

CDMObject (the union)

Source fileschemas/cdm_object.schema.json
$idurn:synapsecommand:cdm:3.0.0:cdm_object
CDM schema version3.0.0
SHA-256 of source02328749f45a17571e609aa547b048befef5f4201424715cd1ed069ee6956182

The union​

A mixed stream is validated against this schema without guessing: the object_kind discriminator names which of the four canonical shapes an object is, so a consumer validates one object at a time and never has to try all four.

Discriminator valueObject
entityEntity
eventEvent
plan_objectPlanObject
trackTrack

Referenced definitions​

Every $ref on this page resolves to one of these, inlined here so the page is a complete reference and not a starting point for chasing pointers.

Affiliation​

Maps to MIL-STD-2525 standard identity — see symbology.standard_identity().

Four members, not 2525's seven: PENDING, ASSUMED_FRIEND and SUSPECT are judgements a fusion layer makes, not facts an adapter can read off a wire format. An adapter that invented ASSUMED_FRIEND would be doing business logic, which adapters may not do. The source's own wording is preserved in attributes when it is finer than this.

Closed vocabulary — a value outside this list is invalid, and UNKNOWN is a

member rather than a null wherever the enum has one.

Value
FRIENDLY
HOSTILE
NEUTRAL
UNKNOWN

Area​

A region with lateral geometry, optional vertical limits and optional time validity.

The three together are what an airspace, a danger area, a jamming footprint and a search box all are, and the reason they are one model is that any two of them without the third is a different claim: a polygon with no ceiling is the whole column of sky above it, and a polygon with no validity is permanent.

geometry is a Polygon or a MultiPolygon and nothing else. A point or a line is not an area — a corridor drawn as a line has no width, and a consumer that buffered it would be choosing the width itself, which is an operational decision an adapter may not make (Rule 6).

FieldTypeRequiredDescription
boundsBoundingBox | nullnoThe coarse extent the SOURCE declared about this area, when it declared one. NEVER computed from geometry here: a sensor's declared coverage rectangle and its footprint's envelope are different facts and are allowed to differ, so a computed value would overwrite one with the other. Default null.
geometryPolygon | MultiPolygonyesLateral extent. WGS84, [lon, lat].
validityTemporalValidity | nullnoWhen the area applies. None = the source stated no times. Default null.
verticalVerticalExtent | nullnoFloor and ceiling. None = the source stated no vertical limits, which is not the same as surface-to-unlimited (that is a VerticalExtent with both bounds absent, and it says the source described the column). Default null.

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

BoundingBox​

A rectangular region in WGS84, bounds NAMED rather than positional.

Why not RFC 7946 §5's bbox array: it is [west, south, east, north] and nothing in a four-float array stops a reader from taking it as [south, west, north, east]. That transposed reading is legal for most of the globe and puts the box in the wrong hemisphere, which is the same silent defect _check_lonlat exists to catch and cannot catch inside an array of four. Named fields make it unspellable. An adapter that must emit RFC 7946's array builds it from these four, in that order, at the edge where it is writing GeoJSON anyway.

A box is not a Polygon and is not a substitute for one. It is the coarse extent a source states about itself — a coverage envelope, a search area, a tile — and a consumer that draws it as a shape is drawing something the source did not describe. Where the source really does describe a region, that region is a Polygon or a MultiPolygon.

FieldTypeRequiredDescription
max_latnumberyesNorth edge, WGS84 degrees. (≥ -90; ≤ 90)
max_lonnumberyesEast edge, WGS84 degrees. (≥ -180; ≤ 180)
min_latnumberyesSouth edge, WGS84 degrees. (≥ -90; ≤ 90)
min_lonnumberyesWest edge, WGS84 degrees. (≥ -180; ≤ 180)
verticalVerticalExtent | nullnoFloor and ceiling, when the source states a volume. Default null.

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

Entity​

Anything that exists on the map, at a stated time, with stated confidence.

FieldTypeRequiredDescription
affiliationAffiliationyes
attributesobjectnoSource-specific fields the CDM has no home for. The never-drop bag: park data here rather than discarding it.
confidencenumber | nullno0..1. None = unknown; 0 means certainty-that-not, which is a claim. Default null. (≥ 0; ≤ 1)
entity_idstring (uuid)yesStable across updates — derived, see ids.derive(). Never drawn at random.
entity_typeEntityTypeyes
integrityIntegrity | nullnoPQC signature block — designed, not yet populated. A DATA CONTAINER and nothing more: this package makes no signature and verifies none, no conformance check, harness column or evidence field reads it, and its presence on an object proves nothing about the object. A record carrying one is unverified until something outside this package verifies it. Default null.
kinematicsKinematics | nullnoDefault null.
object_kind"entity"no
ontology_typesarray<string>noOptional SC-OES semantic types — absolute ontology identifiers saying what this thing IS in operational terms. Empty = the producer asserted none. Never derived from entity_type, and entity_type is never derived from it.
positionPosition | nullnoNone = position unknown. NEVER a Position holding zeros. Default null.
qualityQuality | nullnoHow good the SOURCE says this object is. None = the source said nothing about quality, which is not the same as saying it is poor. Default null.
residualResidual | nullnoSource information the CDM does not model, under the name of the format it came from (§28). None = nothing was left over, or — for the fourteen adapters shipped before this container existed — the leftovers are parked in attributes / payload under source_extras, which ARCHITECTURE.md §5 rules they keep through Part 1. Default null.
schema_versionstringnoSemver of the CDM this object was written against. Default "3.0.0". (pattern ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$)
sourceSourceRefyesWhich adapter produced this object. Required on every kind.
source_idsarray<SourceId>yesEvery external identifier this object is known by. At least one, on EVERY kind — see the class docstring. (min items 1)
statusOperationalStatus | nullnoThe source's own operational state for this object, namespaced. None = the source stated none. Default null.
symbolstring | nullnoMIL-STD-2525D SIDC, 20 digits. None when the source states no symbol — see symbology.sidc_from_affiliation() for deriving one. Default null. (pattern ^[0-9]{20}$)
valid_fromstringyesWhen this state began. (pattern ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$)
valid_tostring | nullnoWhen it ceased. None = still current / open-ended. Default null. (pattern ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$)

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

EntityRelation​

What ROLE an entity already related to this event plays in it.

Event.related_entities remains the single answer to "which entities does this event concern"; this adds the role. The membership rule that keeps the two lists agreeing lives on Event (models.py), because it is the only place both lists are visible.

FieldTypeRequiredDescription
entity_idstring (uuid)yesMust also appear in the event's related_entities — see Event._oes_relations.
predicatestringyesAn absolute semantic identifier: a governed ontology term or a valid third-party one. A bare word or a local name is refused. (min length 1)

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

EntityType​

Closed vocabulary — a value outside this list is invalid, and UNKNOWN is a

member rather than a null wherever the enum has one.

Value
UNIT
PLATFORM
SENSOR
FACILITY
EVACUEE_GROUP
INTERFERENCE_SOURCE
OVERLAY_OBJECT
UNKNOWN

Event​

Anything that happens. The audit-bearing object: two timestamps and a source, always.

FieldTypeRequiredDescription
event_idstring (uuid)yes
event_typeEventTypeyes
geometryPoint | LineString | Polygon | MultiPoint | MultiLineString | MultiPolygon | nullnoGeoJSON, WGS84, [lon, lat] order — e.g. a jamming footprint. Default null.
integrityIntegrity | nullnoPQC signature block — designed, not yet populated. A DATA CONTAINER and nothing more: this package makes no signature and verifies none, no conformance check, harness column or evidence field reads it, and its presence on an object proves nothing about the object. A record carrying one is unverified until something outside this package verifies it. Default null.
object_kind"event"no
observed_atstringyesWhen the SOURCE saw it. Never receipt time. (pattern ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$)
oesOesMetadata | nullnoThe SC-OES wire-semantic block. None = the producer made no SC-OES assertion, which is NOT the producer asserting defaults. Optional so that every object written before SC-OES existed stays structurally valid and every producer that knows nothing about SC-OES keeps emitting objects these models accept. Default null.
payloadobjectnoEvent-specific fields. Validated against PAYLOAD_MODELS[event_type] when one is registered; free-form otherwise. Also the never-drop bag for events.
qualityQuality | nullnoHow good the SOURCE says this object is. None = the source said nothing about quality, which is not the same as saying it is poor. Default null.
received_atstringyesWhen WE took delivery. Never source time. (pattern ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$)
related_entitiesarray<string (uuid)>noentity_id values this event concerns. Empty when the event concerns no specific entity (a feed-level status change).
residualResidual | nullnoSource information the CDM does not model, under the name of the format it came from (§28). None = nothing was left over, or — for the fourteen adapters shipped before this container existed — the leftovers are parked in attributes / payload under source_extras, which ARCHITECTURE.md §5 rules they keep through Part 1. Default null.
schema_versionstringnoSemver of the CDM this object was written against. Default "3.0.0". (pattern ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$)
severitySeverityyes
sourceSourceRefyesWhich adapter produced this object. Required on every kind.
source_idsarray<SourceId>yesEvery external identifier this object is known by. At least one, on EVERY kind — see the class docstring. (min items 1)
statusOperationalStatus | nullnoThe source's own operational state for this object, namespaced. None = the source stated none. Default null.

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

EventClass​

What KIND of assertion an event is — the coarsest semantic distinction SC-OES makes.

Eight members, and a consumer that understands no event type at all can still act on this one. It is asserted by the producer and never inferred: 02-event-classes.md forbids deriving it from type_id, from the payload or from the producer's identity, because the whole value of the field is that somebody took responsibility for the claim.

Closed vocabulary — a value outside this list is invalid, and UNKNOWN is a

member rather than a null wherever the enum has one.

Value
OBSERVATION
STATE_CHANGE
CONSTRAINT
ASSESSMENT
IMPACT
RECOMMENDATION
DECISION
ACTION

EventRelation​

One typed reference from this event to another event.

Both halves are required: a predicate with no target says nothing, and a target with no predicate is the association CORRELATES_WITH exists to spell honestly.

FieldTypeRequiredDescription
event_idstring (uuid)yesThe event this relation points at. It need not be locally available — rejecting an event because its antecedent has not arrived would make delivery order part of the contract.
predicateEventRelationPredicateyesOne of the seven governed predicates. An unrecognised predicate is refused rather than read as a nearby one.

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

EventRelationPredicate​

The seven governed relationship predicates. Direction is part of the meaning.

Five are carried by the LATER event and point at the earlier one; CONTRADICTS and CORRELATES_WITH are conceptually symmetric. An unrecognised predicate is a failure and must not be interpreted as a nearby one — 08-event-relationships.md is explicit, because the nearby predicate is always a stronger claim than the one the producer could support.

Closed vocabulary — a value outside this list is invalid, and UNKNOWN is a

member rather than a null wherever the enum has one.

Value
DERIVED_FROM
UPDATES
SUPERSEDES
RESOLVES
RETRACTS
CONTRADICTS
CORRELATES_WITH

EventType​

Closed vocabulary — a value outside this list is invalid, and UNKNOWN is a

member rather than a null wherever the enum has one.

Value
DETECTION
GNSS_INTERFERENCE
TRACK_UPDATE
ALERT
STATUS_CHANGE
PLAN_INJECT
SIM_RESULT

EvidenceKind​

What an evidence reference points at: another assertion, a source record, or the outside.

Closed vocabulary — a value outside this list is invalid, and UNKNOWN is a

member rather than a null wherever the enum has one.

Value
EVENT
SOURCE_RECORD
EXTERNAL_ARTIFACT

EvidenceRef​

What stands behind an assertion. Descriptive: nothing here is fetched or verified.

One model with a declared kind rather than three, because evidence[] is a heterogeneous list and a discriminated union in the wire form would make a non-Python consumer negotiate a discriminator to read a citation. The per-kind field rules are a validator instead, so the published schema stays readable and the refusal names the kind and the field.

FieldTypeRequiredDescription
descriptionstring | nullnoDefault null. (min length 1)
event_idstring (uuid) | nullnoEVENT only. The cited event need not resolve locally, and a consumer that cannot find it must not reject the citing event for that reason. Default null.
hashstring | nullnoDESCRIPTIVE METADATA ONLY. Not an integrity guarantee, not a signature and not evidence of authenticity; no conformance dimension asserts anything about its value. SC-OES v0.1.0 implements no signing and no verification. Default null. (min length 1)
kindEvidenceKindyesWhich of the three kinds of reference this is.
media_typestring | nullnoDefault null. (min length 1)
source_idSourceId | nullnoSOURCE_RECORD only. The CDM's own source-identifier representation, reused rather than re-invented — never the pair flattened into one opaque string. Default null.
uristring | nullnoEXTERNAL_ARTIFACT only. Never retrieved, at validation time or any other. Default null. (min length 1)

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

Integrity​

DESIGNED, NOT IMPLEMENTED — the field the PQC signature will occupy.

No crypto happens in this package (tests/test_cdm_boundary.py asserts the package imports no crypto module). 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, and would arrive exactly when the signing work is already late.

algorithm is a free string rather than an enum, naming what the platform's ledger already uses — ML-DSA-87 for entry signatures, SLH-DSA for checkpoints. Free, because the algorithm that replaces those is not knowable now, and an enum would make the migration a MAJOR bump for a value nobody reasons over programmatically.

All three 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.

FieldTypeRequiredDescription
algorithmstringyese.g. ML-DSA-87, SLH-DSA-SHAKE-256s. (min length 1)
chain_hashstringyesHash binding this object to the chain. (min length 1)
signaturestringyes(min length 1)

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

Kinematics​

Motion. Every field optional, and absent means UNKNOWN, never zero.

This is the AIS sentinel lesson in schema form: 0 kt is measured stillness, 0 deg is a course due north, 0 m/s climb is level flight. All three are real measurements, so none of them can double as "no data" — the adapter translates the source's sentinel to None.

FieldTypeRequiredDescription
climb_mpsnumber | nullnoMetres per second, negative = descending. Default null.
course_degnumber | nullnoDegrees true, [0, 360). Default null. (≥ 0; < 360)
speed_mpsnumber | nullnoMetres per second. Default null. (≥ 0)

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

LifecycleStatus​

The lifecycle state of the represented CONDITION — not of the message, not of a workflow.

Optional, and absence is the ordinary case: most sensor sources report occurrences rather than managed conditions. 05-lifecycle.md forbids defaulting it to ACTIVE or to anything else, and forbids deriving it from the effective interval — an event whose effective_to has passed is not thereby EXPIRED, because the interval is what the producer said about the world and this is what the producer says about the assertion's own standing.

Closed vocabulary — a value outside this list is invalid, and UNKNOWN is a

member rather than a null wherever the enum has one.

Value
PLANNED
ACTIVE
RESOLVED
EXPIRED
CANCELLED
RETRACTED
SUPERSEDED

LineString​

FieldTypeRequiredDescription
coordinatesarray<array<number>>yes(min items 2)
type"LineString"no

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

MultiLineString​

Several lines that are one thing — a route with a gap, a corridor in two legs.

FieldTypeRequiredDescription
coordinatesarray<array<array<number>>>yes(min items 1)
type"MultiLineString"no

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

MultiPoint​

Several points that are ONE thing — a scatter of detections from one report.

Not a list of Point objects, because a list of geometries is a list of objects and this is one object with a discontinuous location. The difference is visible the moment a consumer counts contacts.

FieldTypeRequiredDescription
coordinatesarray<array<number>>yes(min items 1)
type"MultiPoint"no

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

MultiPolygon​

Several polygons that are one thing — an airspace in disjoint lateral parts.

The ring rules are Polygon's and are enforced per part, for the reason they are enforced there: a ring that arrives open is a source or adapter defect, and closing it invents an edge the source never stated.

FieldTypeRequiredDescription
coordinatesarray<array<array<array<number>>>>yes(min items 1)
type"MultiPolygon"no

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

ObjectType​

What we push OUT — the egress direction, e.g. to TAK as a drawing object.

Closed vocabulary — a value outside this list is invalid, and UNKNOWN is a

member rather than a null wherever the enum has one.

Value
COA_SKETCH
ROUTE
CONTROL_MEASURE
ANNOTATION

OesMetadata​

The wire-semantic block. §52's thirteen fields, and maturity is not among them.

Maturity is a property of a governed semantic DEFINITION, not of an occurrence: it lives in the event registry, in ontology-term metadata and in profile documents. Putting it on the wire would duplicate registry governance metadata on every message and give it a second authority, which would then go stale (12-versioning.md).

Three states are distinguished throughout and are not collapsed (01-core.md): a field present with a value is ASSERTED, an absent field or a null confidence is UNKNOWN, and a value whose meaning is "none" is ASSERTED-ABSENT. An unknown value is never represented as a zero, an empty string or a default enumeration member — which is why every optional field here defaults to None and none of the enumerations carries an UNKNOWN member the way the CDM's own vocabularies do (enums.py:3). The CDM's rule is right for a closed structural classification a map has to render; SC-OES's optional fields are assertions a producer either made or did not, and "not asserted" is exactly what absence already says.

FieldTypeRequiredDescription
confidencenumber | nullno0..1 on the producer's own scale. None = unknown, never 0.0, and this specification defines no universal confidence algorithm. Default null. (≥ 0; ≤ 1)
effective_fromstring | nullnoWhen the represented condition begins. NEVER defaulted to observed_at, received_at or the time of validation. Default null. (pattern ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$)
effective_tostring | nullnoWhen it ceases. None = the producer did not state an end; it does not mean the condition is permanent and it does not mean it is still in force. Default null. (pattern ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$)
entity_relationsarray<EntityRelation>noRoles for entities already in related_entities.
event_classEventClassyesAsserted by the producer. Never inferred from type_id or from the payload.
event_relationsarray<EventRelation>noEmpty or absent asserts nothing. No universal cap.
evidencearray<EvidenceRef>noWhat stands behind the assertion. No universal cap.
extensionsobjectnoThe one declared open bag. Keys are x.<namespace>.<name>; sc.* is reserved and rejected in v0.1; values are preserved and never interpreted.
securitySecurityMarking | nullnoTransported markings. Absence is never a conformance failure and is never proof of unclassified status. Default null.
spec_versionstringyesThe SC-OES version whose semantics the producer is claiming. Semver, on the same rule CDMBase applies to schema_version; NOT derived from SCHEMA_VERSION or PACKAGE_VERSION, and not required to equal this package's SC_OES_VERSION — a producer at a later spec version stays transportable.
statusLifecycleStatus | nullnoNone = the source says nothing about lifecycle. Default null.
type_idstringyesThe governed or third-party semantic type identifier, under one of the two frozen grammars. Syntax here; recognition is dimension C's.
verificationVerification | nullnoNone = nothing is asserted about corroboration. Default null.

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

OperationalStatus​

What state the source says a thing is in, in the SOURCE's vocabulary, namespaced.

THERE IS NO ENUM HERE AND THERE WILL NOT BE ONE. "SERVICEABLE", "DEGRADED", "MISSION CAPABLE", "RED" and "U/S" come from five different domains and mean five different things, and a closed CDM vocabulary would have to map each of them onto a member — which is a judgement about somebody else's operational language, made inside a translator, invisible in the output, and exactly what Rule 6 forbids. namespace says whose vocabulary state belongs to, so a consumer meeting an unfamiliar value can find out what it means instead of guessing; a consumer MUST NOT compare state across namespaces.

since is when the state began, not when it was reported. Absent means the source did not say — never the receipt time, which would make every restart look like a state change.

FieldTypeRequiredDescription
attributesobjectnoStatus-specific source fields with no canonical home.
namespacestringyesWhose vocabulary state is in — normally the source format's name. Required: an unnamespaced status is a word with no owner. (min length 1)
sincestring | nullnoWhen the state began. None = the source did not say. Default null. (pattern ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$)
statestringyesThe source's own token, verbatim and untranslated. (min length 1)

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

Period​

A closed or open-ended interval. start required, end optional and open-ended if absent.

start is required and end is not, because that asymmetry is what the sources state: an activation begins at a stated instant and ends "until further notice" far more often than the reverse. An interval with neither bound is not a period, it is the absence of one, and the field holding a Period is optional for exactly that case.

FieldTypeRequiredDescription
endstring | nullnoWhen it closes. None = open-ended, never 'unknown'. Default null. (pattern ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$)
startstringyesWhen the interval opens. (pattern ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$)

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

PlanObject​

What we push OUT: a drawing a commander's plan puts on someone else's map.

Geometry is REQUIRED here, unlike on Event. An overlay with no geometry cannot be drawn, so an egress adapter would have to either invent a location or silently drop the object — and a COA sketch that quietly fails to appear on the TAK client is the worst of the three outcomes, because everyone assumes it arrived.

FieldTypeRequiredDescription
areaArea | nullnoLateral geometry with vertical limits and time validity, when this object is a region. geometry above stays the required projection of its lateral extent. Default null.
expires_atstring | nullnoWhen the drawing should disappear. None = until explicitly removed — which for a stale COA sketch on a live map is a decision, so state it. KEPT beside validity below, of which it is the projection: a receiving client that only knows how to expire an overlay reads this one field. Default null. (pattern ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$)
geometryPoint | LineString | Polygon | MultiPoint | MultiLineString | MultiPolygonyesGeoJSON, WGS84, [lon, lat] order. Required.
integrityIntegrity | nullnoPQC signature block — designed, not yet populated. A DATA CONTAINER and nothing more: this package makes no signature and verifies none, no conformance check, harness column or evidence field reads it, and its presence on an object proves nothing about the object. A record carrying one is unverified until something outside this package verifies it. Default null.
labelstring | nullnoWhat a client shows next to the drawing. None = unlabelled; never an empty string, which renders as a blank callout. Default null. (min length 1)
object_idstring (uuid)yes
object_kind"plan_object"no
object_typeObjectTypeyes
qualityQuality | nullnoHow good the SOURCE says this object is. None = the source said nothing about quality, which is not the same as saying it is poor. Default null.
residualResidual | nullnoSource information the CDM does not model, under the name of the format it came from (§28). None = nothing was left over, or — for the fourteen adapters shipped before this container existed — the leftovers are parked in attributes / payload under source_extras, which ARCHITECTURE.md §5 rules they keep through Part 1. Default null.
routeRoute | nullnoOrdered waypoints and legs, when this object IS a route (ObjectType.ROUTE). geometry above stays REQUIRED and stays the LineString projection of the waypoints, so every consumer written before routes existed still draws it. Default null.
schema_versionstringnoSemver of the CDM this object was written against. Default "3.0.0". (pattern ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$)
sourceSourceRefyesWhich adapter produced this object. Required on every kind.
source_idsarray<SourceId>yesEvery external identifier this object is known by. At least one, on EVERY kind — see the class docstring. (min items 1)
statusOperationalStatus | nullnoThe source's own operational state for this object, namespaced. None = the source stated none. Default null.
styleobjectnoRendering HINTS, not requirements — stroke, fill, opacity, dash. A receiving client is free to ignore them, so nothing that changes MEANING may live here (an affiliation belongs on the entity, not in a colour).
validityTemporalValidity | nullnoThe four times the source states about this object (§27). expires_at above is the projection of validity.valid_to for clients that read one field; when both are stated they must agree. Default null.

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

Point​

FieldTypeRequiredDescription
coordinatesarray<number>yes
type"Point"no

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

Polygon​

FieldTypeRequiredDescription
coordinatesarray<array<array<number>>>yes(min items 1)
type"Polygon"no

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

Position​

A fix. Both coordinates required — that is how the null-never-zero rule is structural.

An unknown position is the ABSENCE of this object (entity.position is None), never a Position holding zeros. Because lat and lon are required here, an adapter cannot express "unknown" as (0, 0) even by accident: it has to either omit the Position or state a real coordinate. Coordinate zero is a real point in the Gulf of Guinea, and a contact painted there is a contact that does not exist.

accuracy_m absent means unknown accuracy, NOT perfect accuracy. Zero would mean a fix with no error, which no sensor produces.

FieldTypeRequiredDescription
accuracy_mnumber | nullnoMetres, 1-sigma. None = unknown, never 0. Default null. (≥ 0)
alt_mnumber | nullnoMetres HAE. None = unknown. Default null.
latnumberyesWGS84 decimal degrees. (≥ -90; ≤ 90)
lonnumberyesWGS84 decimal degrees. (≥ -180; ≤ 180)
position_sourcePositionSourceyesHow the fix was obtained — the field that survives GNSS denial.
verticalVerticalPosition | nullnoThe height AS THE SOURCE STATED IT — unit and datum carried, never converted. alt_m above stays the canonical HAE-in-metres projection and is None whenever the source's datum is not HAE. Default null.

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

PositionSource​

How the position was obtained. Load-bearing in a GNSS-denied environment.

This is the field that lets a commander tell a fix from a guess. When PNTMAP reports jamming over an area, every GNSS-sourced position inside that area becomes suspect and every INERTIAL or MANUAL one does not — a distinction that is impossible to make after the fact if the adapter flattened them all to "position".

Closed vocabulary — a value outside this list is invalid, and UNKNOWN is a

member rather than a null wherever the enum has one.

Value
GNSS
INERTIAL
MANUAL
ESTIMATED

Quality​

How good the source says its own data is. Four ways of saying it, none derived.

confidence is 0..1 and comparable across sources; source_quality is the source's OWN grade, a free string, because ASTERIX's track quality, AIS's position accuracy flag and a NATO track's evaluation code are three ordinal scales with no defined mapping between them and inventing one would be a fusion decision made inside a translator.

uncertainty is a NAMED dict — {"along_track_m": 40.0, "cross_track_m": 12.0} — rather than a single number, because an error ellipse is not a radius and flattening it loses the orientation that made it worth sending. Names are the source's; the CDM does not fix a vocabulary here, and the unit belongs in the key exactly as signal_strength_dbm carries its own unit in its name.

FieldTypeRequiredDescription
accuracy_mnumber | nullnoMetres, 1-sigma. None = unknown, never 0. Default null. (≥ 0)
confidencenumber | nullno0..1. None = unknown; 0 means certainty-that-not, which is a claim. Default null. (≥ 0; ≤ 1)
source_qualitystring | nullnoThe source's own grade, verbatim. None = the source stated none. Default null. (min length 1)
uncertaintyobject<string, number>noNamed components, e.g. along_track_m / cross_track_m. The unit is in the key. Empty = the source named none.

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

Residual​

Source information the CDM does not model, kept under the name of the format it came from.

§28's container, and its two rules are load-bearing in opposite directions:

  1. It MUST identify its origin. namespace is the source format's own name — the same string as the adapter's metadata.format.name — so a reader meeting an unfamiliar key inside data can find out which standard's vocabulary it belongs to. An unnamespaced bag of leftovers is the free-form dict this model exists to replace.
  2. It MUST NOT be treated as semantically trusted merely because it survived translation. A residual says "the source said this and the CDM has no home for it". It is a fact about the source RECORD, not an assertion about the world. A consumer MUST NOT promote a residual value into a canonical field, and a later adapter MUST NOT read another adapter's residual as an input — ARCHITECTURE.md §5 states both as normative text.

data preserves the source's own STRUCTURE, which is why it is a dict and not a list of dotted paths: lossless.residual() learned that the hard way when a two-element list came back as two keys named affected_constellations[0] and [1], satisfying the never-drop rule in the letter while destroying the reader's ability to see a list.

THE FOURTEEN ADAPTERS IN THIS REPOSITORY DO NOT USE THIS YET, deliberately. ARCHITECTURE.md §5 rules that they keep their attributes / payload parking under source_extras through the whole of Part 1 and declare residual: legacy in their manifests, because the information is already preserved and paying for a placement change with every golden file and every downstream consumer buys nothing a reader can use (§29: no breaking change for stylistic cleanliness). Every Part 2 adapter declares residual: structured and uses this.

FieldTypeRequiredDescription
dataobjectnoThe unconsumed source structure, preserved as the source shaped it.
namespacestringyesThe source format's name — normally metadata.format.name. Required. (min length 1)

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

Route​

Ordered waypoints, the legs between them, and the route's own metadata.

At least two waypoints, because a route to one place from nowhere is a position. sequence values must be distinct — two waypoints numbered 3 make the order unrecoverable, which is the single thing sequence exists to preserve — and every leg must name sequences that exist, because a leg to a waypoint nobody sent is a route with a hole in it that renders as a line to the origin.

Legs may be EMPTY. A source that sends points and no segments has described a route whose legs are the implied consecutive pairs, and manufacturing those pairs here would publish segments the source never stated — including, for a route the source meant as a set of reporting points, segments that are not flyable.

FieldTypeRequiredDescription
legsarray<RouteLeg>noSegments the SOURCE stated. Empty = the source stated none; consecutive pairs are NOT invented here.
metadataobjectnoRoute-level source fields with no canonical home — a procedure name, a flight rules letter, an airway designator.
waypointsarray<Waypoint>yesAt least two. Ordered by sequence, not by list position. (min items 2)

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

RouteLeg​

One segment between two waypoints, addressed BY SEQUENCE and not by index.

distance_m and course_deg are optional because they are the SOURCE's numbers when the source states them, and nothing computes them here. Two waypoints determine a great-circle distance, so a computed value is always available — and a computed value that disagrees with the source's is a second truth in the same record, with nothing to say which one a consumer should plan against. The source's own leg lengths often encode a procedure the geometry does not (a DME arc, a holding pattern), which is exactly the information a computation destroys.

FieldTypeRequiredDescription
attributesobjectnoLeg-specific source fields with no canonical home.
course_degnumber | nullnoDegrees true, [0, 360), as the source states it. None = not stated. Default null. (≥ 0; < 360)
distance_mnumber | nullnoMetres, as the SOURCE states it. None = not stated. Default null. (≥ 0)
from_seqintegeryessequence of the waypoint this leg leaves. (≥ 0)
to_seqintegeryessequence of the waypoint this leg reaches. (≥ 0)

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

SecurityMarking​

Handling markings, TRANSPORTED. SC-OES does not interpret or enforce them.

Three propositions 10-security-markings.md states because each is routinely assumed away: presence of a marking is not authorization, absence is not proof of unclassified status, and transport is not enforcement. SC-OES is not a cross-domain guard and must not be represented as one; nothing in this package could enforce a marking, and a deployment that enforces one does so with its own accredited mechanism, which knows the scheme and is answerable for the decision.

Every value here is interpreted ONLY relative to scheme, which is why scheme is the one required field: two markings from different schemes are not comparable, and a classification with no scheme beside it is a string somebody will compare anyway.

FieldTypeRequiredDescription
caveatsarray<string>noAs that scheme spells them.
classificationstring | nullnoAs that scheme spells it. Transported verbatim. Default null. (min length 1)
marking_extrasobject<string, string>noScheme-specific values with no generic counterpart. Strings, because dimension B checks that marking values are strings and because the block has exactly one generic open bag and it is oes.extensions.
originatorstring | nullnoAs that scheme identifies them. Default null. (min length 1)
releasabilityarray<string>noAs that scheme spells them. Never reordered.
schemestringyesWhich marking system these values belong to. Required. (min length 1)

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

Severity​

Closed vocabulary — a value outside this list is invalid, and UNKNOWN is a

member rather than a null wherever the enum has one.

Value
INFO
ADVISORY
WARNING
CRITICAL

SourceHash​

A digest of the source record, as the ADAPTER computed it. Carried, never computed here.

Rule 5 asks for "source hash where appropriate", and the appropriateness is the adapter's judgement: a hash of a 3-byte AIS sentence identifies nothing an external id does not, while a hash of a 4 MB NITF segment is how an auditor proves the bytes on the wire were the bytes described. So it is optional, and its absence means the adapter did not compute one.

NO HASHING HAPPENS IN THIS PACKAGE. tests/test_cdm_boundary.py asserts that no module under synapse_cdm/ imports hashlib or any crypto library, and this model does not change that: it is a container for a value produced outside the contract layer, exactly as Integrity is a container for a signature this package does not make. algorithm is therefore required and free-form — a digest with no algorithm cannot be reproduced by anyone, and an enum would make the day SHA-3 arrives a MAJOR bump for a string nobody branches on.

FieldTypeRequiredDescription
algorithmstringyese.g. sha256. Named by the producer. (min length 1)
valuestringyesThe digest, in the producer's own encoding. (min length 1)

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

SourceId​

One external identifier for an object — the provenance mapping.

A list of these, not one, because the same object arrives from several systems: the same vessel is an MMSI to AIS, a track number to STANAG 4676 and a UID to TAK. Fusion joins them later; the adapter's job is to record which name its own system used, and never to overwrite another system's entry.

FieldTypeRequiredDescription
external_idstringyesThat system's own identifier. (min length 1)
systemstringyesThe external system, e.g. PNTMAP, TAK. (min length 1)

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

SourceRef​

Which adapter produced this object, from which system, and whether it is real.

synthetic is required and has no default. Every fixture in this repository is synthetic and every scenario package is too (TR-12), and the platform keeps the synthetic and live layers apart over one interface — so an object that does not say which layer it belongs to cannot be filed. A default of false would silently promote exercise data to operational data, which is the dangerous direction; a default of true would silently demote live data and hide it from an operator. There is no safe default, so there is no default.

FieldTypeRequiredDescription
adapterstringyesAdapter name, e.g. pntmap. (min length 1)
adapter_versionstringyesAdapter semver: MAJOR.MINOR.PATCH, no leading zeroes, nothing else. (pattern ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$; min length 1)
format_namestring | nullnoThe source STANDARD's name, e.g. 'ASTERIX'. Filled from the adapter's own declared metadata.format; None only for an adapter that declares none. Default null.
format_versionstring | nullnoThe edition of that standard, e.g. 'Cat 062 ed 1.18'. None = no document in this tree states which edition the adapter targets — a reading, never a gap filled in. Default null.
observed_atstring | nullnoThe instant the SOURCE RECORD states for itself, when it states one and no canonical field already carries it. Event.observed_at stays the event's own time; this is the record's. Default null. (pattern ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$)
original_idstring | nullnoThe source record's OWN identifier, as a first-class provenance field. Distinct from source_ids, which is the identity of the THING; this is the identity of the RECORD that described it. Default null.
record_indexinteger | nullnoWhich record of a multi-record payload this object came from, 0-based. None = the payload was not a sequence, or the adapter does not track it. Never -1 and never 0-as-unknown: 0 is the first record. Default null. (≥ 0)
source_hashSourceHash | nullnoDigest of the source record, computed by the adapter. Default null.
syntheticbooleanyestrue for anything not from a real source (TR-12).
systemstringyesThe external system this came from. (min length 1)
transformationsarray<string>noRule 5's transformation chain: the TRANSFORMS reasons this adapter applied, in the order it applied them. Empty = nothing was transformed, which is a claim the lossless check can contradict.

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

TemporalValidity​

The four times an object can have, separated because they answer different questions.

§27's four, and the separation is the point:

observed_at when the SOURCE saw the state valid_from when the state BEGAN, which may be long before anyone saw it valid_to when it ceased. None = still current, never "unknown" effective the period the source declares the object OPERATIVE — an airspace reservation published on Monday, effective Wednesday 0600 to 1200

An airspace restriction shows why one timestamp cannot do the work of four: it is observed when the NOTAM is read, valid from the moment it is published, and effective for a window that has not started yet. Collapsing those into "the time" is how a restriction gets drawn on a map twelve hours early.

EVERY FIELD IS OPTIONAL AND THAT IS DELIBERATE. A source that states only one of the four says one of the four; a model that required more would be filled by adapters copying one instant into three fields, and three copies of one reading look like corroboration. The PRESENCE of this block is itself information — the source described validity in time — and a block with nothing in it is the absence of the block.

§27's epoch rule, stated where it is enforced: unknown time is NOT 1970-01-01 and NOT now(). It is the absence of the field. Timestamp does not refuse the epoch instant, and that is a decision rather than an omission — 1970-01-01T00:00:00Z is a real instant, some sources legitimately carry it as a base epoch, and a validator that refused it would refuse real data in order to catch a defect that lives in the ADAPTER. The rule is therefore enforced where the substitution would be made, in review of the adapter and in the conformance suite's own reading, and it is written down here and in docs/docs/cdm/policies.

FieldTypeRequiredDescription
effectivePeriod | nullnoThe period the source declares the object operative. Default null.
observed_atstring | nullnoWhen the source saw it. None = the source did not say. Default null. (pattern ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$)
valid_fromstring | nullnoWhen the state began. None = the source did not say. Default null. (pattern ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$)
valid_tostring | nullnoWhen it ceased. None = still current / open-ended. Default null. (pattern ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$)

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

Track​

An entity's position history, in time order. The order is a contract, not a hope.

A scrambled sample list produces nonsense the moment anything differentiates it — speed from consecutive positions, a heading arrow, a predicted point. So non-decreasing timestamps are validated here, at the boundary, where the defect is one adapter's bug rather than a mystery in a fusion layer three hops downstream.

Equal timestamps are ALLOWED: two sensors reporting the same instant is real, and rejecting it would refuse legitimate multi-source data.

FieldTypeRequiredDescription
entity_idstring (uuid)yesThe Entity this history belongs to.
integrityIntegrity | nullnoPQC signature block — designed, not yet populated. A DATA CONTAINER and nothing more: this package makes no signature and verifies none, no conformance check, harness column or evidence field reads it, and its presence on an object proves nothing about the object. A record carrying one is unverified until something outside this package verifies it. Default null.
object_kind"track"no
qualityQuality | nullnoHow good the SOURCE says this object is. None = the source said nothing about quality, which is not the same as saying it is poor. Default null.
residualResidual | nullnoSource information the CDM does not model, under the name of the format it came from (§28). None = nothing was left over, or — for the fourteen adapters shipped before this container existed — the leftovers are parked in attributes / payload under source_extras, which ARCHITECTURE.md §5 rules they keep through Part 1. Default null.
samplesarray<TrackSample>yesTime-ordered, non-decreasing. At least one. (min items 1)
schema_versionstringnoSemver of the CDM this object was written against. Default "3.0.0". (pattern ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$)
sourceSourceRefyesWhich adapter produced this object. Required on every kind.
source_idsarray<SourceId>yesEvery external identifier this object is known by. At least one, on EVERY kind — see the class docstring. (min items 1)
statusOperationalStatus | nullnoThe source's own operational state for this object, namespaced. None = the source stated none. Default null.
track_idstring (uuid)yes
track_qualitynumber | nullno0..1. None = not assessed, never 0. Default null. (≥ 0; ≤ 1)

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

TrackSample​

One position at one instant. The unit STANAG 4676 calls a track point.

FieldTypeRequiredDescription
observed_atstringyesRFC 3339 UTC, exactly three decimal places, always Z. (pattern ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$)
positionPositionyes

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

Verification​

How corroborated the assertion is. NOT confidence — 06-verification-and-confidence.md.

They vary independently and every combination is meaningful: a single high-grade sensor can be highly confident and entirely uncorroborated, and three weak sources can corroborate each other and leave the producer unsure. Absence means nothing is asserted about corroboration; UNVERIFIED is the positive claim that the question was asked and the answer was "no", which is a different and more informative fact. DISPUTED records that a disagreement exists and is not a verdict on it.

Closed vocabulary — a value outside this list is invalid, and UNKNOWN is a

member rather than a null wherever the enum has one.

Value
UNVERIFIED
CORROBORATED
VALIDATED
DISPUTED

VerticalExtent​

Lower and upper bounds of a volume. An airspace's floor and ceiling.

Both optional and both may be absent: "surface to unlimited" is a real airspace and it is spelled by two absences, not by 0 and 999999. An extent with neither bound is still worth carrying, because its PRESENCE says the source described a volume rather than a surface.

A bound may be stated in a different unit or against a different datum from the other — a danger area really is published as "surface to FL195" — so no comparison between the two is attempted here. Comparing 0 AGL with FL195 would need a terrain model and a pressure, and a validator that guessed at either would refuse real airspace.

FieldTypeRequiredDescription
lowerVerticalPosition | nullnoFloor. None = unstated. Default null.
upperVerticalPosition | nullnoCeiling. None = unstated. Default null.

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

VerticalPosition​

A height, with the unit it was stated in and the datum it was measured from.

§26's rule made structural: value is never alone. Three fields are required together because any two of them without the third is the ambiguity the policy exists to prevent — 600 could be metres or feet, and 600 ft AGL over the Alps is not 600 ft MSL.

NO CONVERSION HAPPENS HERE. This model records; it does not normalise. models.Position keeps alt_m as the canonical HAE-in-metres projection and a validator (there, not here) requires the two to agree when both are stated and the reference really is HAE. When the source's altitude is anything else, alt_m stays None — Rule 2, unknown is not a substituted value — and this block carries what the source actually said. Converting MSL to HAE needs a geoid model, converting a flight level needs the local QNH, and an adapter that performed either silently would be publishing a number nobody measured.

uncertainty is in the SAME unit as value, and is one-sigma, matching Position.accuracy_m's convention. Absent means unknown, never zero: zero would assert a height with no error, which no altimeter and no GNSS receiver produces.

FieldTypeRequiredDescription
referenceVerticalReferenceyesThe datum. UNKNOWN is a member, not null.
uncertaintynumber | nullno1-sigma, in the same unit as value. None = unknown, never 0. Default null. (≥ 0)
unitVerticalUnityesNever inferred; see VerticalUnit.
valuenumberyesThe number the source stated, in unit.

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).

VerticalReference​

What a VerticalPosition is measured FROM. The datum, never assumed.

The six here are the ones the formats in scope actually state. HAE is the WGS84 ellipsoid — what a GNSS receiver computes natively and what Position.alt_m has always meant. MSL is a geoid model, and the separation between the two reaches 100 m in places, so a silent substitution moves an aircraft by more than its own vertical separation minimum. AGL is height above the terrain beneath the object, which is not a datum at all but a difference, and is therefore not convertible to either of the others without a terrain model. BARO is an altimeter reading against a stated or unstated pressure setting; FL is BARO against the standard setting.

UNKNOWN is a MEMBER, unlike VerticalUnit's absent one, and the asymmetry is the point: a number with no unit cannot be rendered at all, whereas a number whose datum is unstated can still be shown to an operator beside the words "reference unknown". That is a worse fix than a referenced one and a far better one than a fix silently labelled MSL.

Closed vocabulary — a value outside this list is invalid, and UNKNOWN is a

member rather than a null wherever the enum has one.

Value
HAE
MSL
AGL
BARO
FL
UNKNOWN

VerticalUnit​

The unit a VerticalPosition states its number in. §26's "value plus unit", closed.

Three members and no UNKNOWN, which is a departure from this module's opening paragraph and is the unit policy rather than an oversight. §26 forbids "ambiguous naked numeric fields where unit ambiguity matters"; a vertical measurement whose unit nobody knows IS that naked number wearing a wrapper, and recording it would let a consumer render 300 as metres when the source meant feet. An adapter that cannot read the unit therefore has no vertical position to state — it parks the raw number in the residual, where nothing reads it as a height. Absence is expressible (the whole VerticalPosition is optional); an unknown unit is not, deliberately.

FL is a unit as well as a reference because a flight level IS its own scale: FL350 is "35000 feet on the 1013.25 hPa isobaric surface", a number that is neither metres nor feet above anything on the ground. Converting it needs the local pressure, which the adapter does not have, so it is carried as it was stated.

Closed vocabulary — a value outside this list is invalid, and UNKNOWN is a

member rather than a null wherever the enum has one.

Value
m
ft
FL

Waypoint​

One ordered point of a route.

sequence is REQUIRED and is the route's order of record. List position would be the obvious alternative and it is the wrong one: a route arrives split across messages, is filtered, is re-sent with one leg amended, and every one of those operations preserves the numbers while destroying the positions. A waypoint that knows its own sequence can be reassembled; one that knows only where it happened to sit in an array cannot.

altitude_constraint is a VerticalPosition rather than a bare number for §26's reason: a crossing restriction published as "FL240" and one published as "8000 ft MSL" are different constraints and the difference is in the unit and the datum, not in the number.

FieldTypeRequiredDescription
altitude_constraintVerticalPosition | nullnoA crossing restriction, with its unit and datum. Default null.
etastring | nullnoEstimated time at this waypoint. None = not estimated. Default null. (pattern ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$)
namestring | nullnoThe source's own designator, e.g. an ICAO fix name. None = unnamed; never an empty string. Default null. (min length 1)
positionPositionyesWhere the waypoint is.
sequenceintegeryes0-based order of record. Required — see the class docstring. (≥ 0)

additionalProperties: false — unknown keys are rejected. Source-specific fields belong in the declared extension bags (Entity.attributes, Event.payload).