Canonicalization & equivalence
Two independent implementations only interoperate if they agree on when two records mean the same thing. OpenBody defines this precisely: equivalence is semantic, not byte-level — two documents are equivalent iff they denote the same set of records under the model’s defined shorthand equivalences (SPEC §8.3), regardless of JSON key order, whitespace, number spelling, or the permitted shorthands. The conformance suite judges this mechanically: reduce each record to a canonical byte string via an ordered, deterministic algorithm, then compare those strings.
This page explains that algorithm plainly. The normative version is the companion document
conformance/EQUIVALENCE.md,
grounded on RFC 8785 (JSON Canonicalization Scheme). It is normative for the
conformance suite — test tooling and the reference implementation implement it, acting as
the equivalence oracle. A conformant implementation is not required to implement it:
per SPEC §8.3, an implementation is judged by its inputs and outputs against the published
vectors. You may implement it (e.g. to self-test); the oracle must follow it exactly.
Round-trip = parse → canonicalize → serialize
A “lossless round-trip” is: parse the record, process it with no change of meaning, serialize. An implementation demonstrates conformance by round-tripping the test vectors; the suite checks each result by canonicalizing both sides with this algorithm and comparing the bytes.
The ordered algorithm
- Numbers → exact-decimal fixed-point. Every number is read from its decimal text,
never via binary float (so
37.4220is exactly37422 × 10⁻³, not afloat64approximation), and replaced by its lowest-terms fixed-point object with stringcoefficient/exponent. So72,72.0, and{coefficient: 720, exponent: -1}all become{"coefficient":"72","exponent":"0"}. Because they’re strings, there’s no 2⁵³ precision ceiling — arbitrary-precision decimals compare exactly. Timestamps are likewise canonicalized to a single spelling (uppercaseT/Z,Zfor zero offset, trailing-zero fractional seconds removed). - Canonicalize units. A metric
unitequal to the field’s default is removed (sotime: 120andtime: {absolute:{value:120, unit:"s"}}converge). Aunitinsideload.valueis moved to its canonical home,Load.unit; likewise aunitinsideIntensity.valuemoves toIntensity.unit. This applies to whicheverTargetvariant is present, includingramp— but aramp’sfrom/toare never canonicalized by value: they are preserved exactly as authored, with no reordering (order is meaning — a cooldown ramps down). - Expand scalar metrics. A bare scalar
nbecomes{ "absolute": { "value": n } }for every metric field and forload.value. - Expand & fold
ExerciseRef. A bare-string ref becomes{ "id": … }; an explicitopenbody:prefix on a canonical id is folded to the unprefixed form. - Expand
roundScheme, thensets. ABlockwith a ladderedroundScheme(e.g.[21,15,9]) is expanded to its enumerated rounds — each round’s descendantWorkUnits whose primary metric is absent take that round’s count — and then aprescriptionwithsets: Nis replaced by N siblingWorkUnits. Both are planned shorthands and must not appear with aperformance. - Assign deterministic ids (root-down). Any record still lacking an
idgets<nearestAncestorId>#<containerField>#<index>(e.g.ex-1#workUnits#3).#is reserved in producer ids, so assigned ids never collide. - Flatten containment. Each inlined child becomes a standalone record with an explicit
partOflink to its parent; containment arrays are removed.subjectand the nearest ancestor’sstartTime/endTimepropagate down (an explicit value on the child wins). - Default
status. Absentstatus→active. - Serialize canonically. Order the six set-valued arrays —
linksby(type, ref),effortLoadby(kind, method),intensityby(dimension),modifiersby(type),mediaby(url), andqualitiesby token value — with ties broken by canonical bytes; all other arrays keep their semantic order. Then serialize per RFC 8785: lexicographic key sort, canonical escaping, no insignificant whitespace.
The set of canonical record byte strings (one per flattened record) is compared as an unordered set.
Nested ≡ flat + partOf
A direct payoff of flattening (step 7): the §5 hierarchy can be encoded two equivalent ways —
a nested document (a child inlined in its parent, the recommended transmission form) or
flat + partOf (a child as a standalone record linking to its container). A consumer
MUST NOT treat them as different activities, and the conformance vectors assert one
structure’s two encodings equivalent.