# 10. Validation
Requirement prefix: `L10`.
## Two gates
- **L10.1** Constructors MUST enforce the invariants of the class they build, so
that data the program **builds** cannot be invalid.
- **L10.1a** Validation MUST enforce them on data the program **receives**.
`serde` writes fields directly and never calls a constructor, so a document
read from a wire has passed no gate but this one.
- **L10.1b** Documentation MUST state that a service which deserializes and
stores without validating has no invariant checking at all, whatever its
constructors do.
## What validation means, and what it does not
- **L10.2** Validation is **Reference-Model-level only**. Wherever it is
offered, the documentation MUST state that a passing composition can still
violate its archetype, because archetypes are out of scope (`S1.4`).
- **L10.3** Validation MUST collect every violation, not the first. A caller
fixing a rejected composition wants the whole list; one violation per round
trip turns a five-minute fix into five deployments.
- **L10.4** Every violation MUST name the path to the offending node, the RM
class, and the invariant **using openEHR's own invariant name**, so a reader
can find it in the class definition without a translation step.
- **L10.5** No violation may include node content (`X11.7`). Paths, class names,
and invariant names only.
- **L10.5a** A violation MUST be attributed to the class whose invariant it
actually breaks. An empty `LOCATABLE.name` breaks `DV_TEXT.Valid_value`, not
`LOCATABLE.Name_valid` — openEHR's `Name_valid` is only `name /= Void` — and
the wrong attribution sends a reader to the wrong class definition.
- **L10.9** A check the crate performs that openEHR does **not** state for that
class is an **addition**, not a rename, and MUST be declared in the register
below. `L10.4` cannot govern it: there is no openEHR name to use.
The distinction is not pedantry. A rename is a defect — the same rule reported
under a name the specification does not contain, so a reader cannot find it. An
addition is a *strengthening*: a rule openEHR does not require, which this
crate enforces anyway. Confusing the two makes both invisible, and it is what
made the first sweep for `A-20` over-report by treating every unmatched name as
wrong.
- **L10.10** An addition MUST NOT use a name openEHR uses for that class, even
for a different rule, and SHOULD avoid a name openEHR uses anywhere, so that a
future openEHR release cannot silently collide with it.
- **L10.11** *(added 2026-08-02)* An openEHR invariant the crate does **not**
enforce MUST be declared in the register below, with the reason. A rule that
is absent and undeclared is indistinguishable from one nobody noticed
(`C0.14`).
This is `L10.9`'s mirror. That one registers a rule openEHR does not state and
the crate enforces anyway; this one registers a rule openEHR states and the
crate does not. Both exist because the interesting cases are the ones where
the crate and the specification differ, and a difference nobody wrote down
reads as an oversight.
The register MUST agree with the **Not enforced** group of
[`assets/invariant-coverage.md`](../../assets/invariant-coverage.md), which is
generated. `openehr-assets` fails the build when the two disagree, so neither
can drift: a rule quietly abandoned appears in the report and not the
register, and a rule quietly enforced appears in the register and not the
report.
### Unenforced openEHR checks
Declared under `L10.11`. Ten of RM 1.1.0's 155 invariants, and none of them is
merely undone.
| `COMPOSITION` | `Language_valid` | ISO 639 is not carried (`S1.18`) |
| `COMPOSITION` | `Territory_valid` | ISO 3166 is not carried (`S1.18`) |
| `DV_ENCAPSULATED` | `Charset_valid` | IANA character sets are not carried |
| `DV_ENCAPSULATED` | `Language_valid` | ISO 639 is not carried |
| `DV_MULTIMEDIA` | `Media_type_valid` | IANA media types are not carried |
| `DV_TEXT` | `Encoding_valid` | IANA character sets are not carried |
| `DV_TEXT` | `Language_valid` | ISO 639 is not carried |
| `ENTRY` | `Encoding_valid` | IANA character sets are not carried |
| `ENTRY` | `Language_valid` | ISO 639 is not carried |
| `EHR_ACCESS` | `Scheme_valid` | an `EHR_ACCESS` may record no policy (`S1.20`) |
Nine are one decision wearing five hats: the crate does not carry external code
sets, so it cannot check a code against one. `S1.18` declares that for
`COMPOSITION`, where openEHR names the code sets itself and `S1.10` therefore
does not cover it; the other seven are the same departure on other classes.
The tenth is `S1.20`.
**What is *not* in this register.** The four demographic-graph invariants of
`S1.19` are excluded rather than unenforced — the crate does not model the thing
they constrain — and appear in the report's *out of scope* group. The
distinction matters: an exclusion is answered by scope, an omission by this
table.
### Crate-added checks
Declared under `L10.9`. Each is a rule openEHR does not state for that class,
which this crate enforces anyway.
| `ARCHETYPED` | `Archetype_id_rm_entity_matches` | the archetype id's RM entity matches the class it annotates | an `OBSERVATION` carrying a `COMPOSITION` archetype id is a document nobody can query correctly |
| `COMPOSITION` | `Is_persistent_validity` | a persistent composition has no `context` | openEHR states this on `VERSIONED_COMPOSITION`; enforcing it where the data is caught it earlier |
| `CONTACT` | `Addresses_valid` | a contact has at least one address | a contact with no address is a record of nothing |
| `DV_AMOUNT` | `Accuracy_finite` | accuracy is a finite float | openEHR assumes real numbers; IEEE 754 has `NaN` and `±∞`, and a `NaN` accuracy compares false against everything |
| `DV_MULTIMEDIA` | `Integrity_check_matches` | the recorded digest matches the inline data | openEHR requires a check to name its algorithm (`Integrity_check_validity`) but never says the digest must be *right*. Reported under openEHR's name until `A-22`, which sent a reader to an invariant about something else |
| `DV_PARSABLE` | `Value_valid` | the value is non-empty | openEHR constrains `formalism` and `size` but not the value |
| `DV_PROPORTION` | `Parts_finite` | numerator and denominator are finite | as `DV_AMOUNT.Accuracy_finite` |
| `DV_URI` | `Uri_well_formed` | an RFC 3986 scheme, and no space or control character | openEHR's `DV_URI.Value_valid` is only `not value.is_empty`. This crate's constructor has always required more (`D3.30`), and until `A-36` nothing required it of a URI that arrived as JSON |
| `EVENT` | `Time_after_origin` | an event's time is at or after its history's origin | openEHR states the offset relation but not the ordering |
| `EVENT_CONTEXT` | `End_time_valid` | end time is at or after start time | a consultation that ended before it began |
| `INSTRUCTION` | `Narrative_valid` | the narrative is non-empty | the narrative is what a human reads when the structured form is not understood |
| `INTERVAL_EVENT` | `Width_non_negative` | width is not negative | an interval of negative duration |
| `ITEM_TABLE` | `Rows_regular` | every row has the same column count | openEHR requires rows of `ELEMENT`; a ragged table renders as a table and is not one |
| `ORIGINAL_VERSION` | `Data_valid` | data is present unless the lifecycle state is `deleted` | a version claiming `complete` and supplying nothing |
| `ORIGINAL_VERSION` | `Lifecycle_state_valid` | the state is in the openEHR group | openEHR states this on `VERSION`; reported on the concrete class the caller constructed |
Two of these — `Accuracy_finite` and `Parts_finite` — exist because openEHR is
written against mathematical reals and Rust is not. A specification that says
"accuracy is a real number" does not anticipate `NaN`, and a `NaN` that reaches a
clinical comparison is false against every bound including itself.
## The checks
- **L10.6** Validation MUST check, at minimum: `ELEMENT`'s value/null-flavour
exclusivity (`R4.3`) and null-reason rule (`R4.4`); the null flavour's
membership of the openEHR group (`R4.5`); `CLUSTER` non-emptiness (`R4.8`);
`ITEM_TABLE` regularity (`R4.9`); `HISTORY` non-emptiness (`R4.11`) and event
ordering against origin (`R4.13`); `INTERVAL_EVENT` width sign (`R4.15`);
`LOCATABLE` node id and name (`M5.2`); `ARCHETYPED` class agreement (`M5.7`);
`DV_CODED_TEXT` rubric agreement for openEHR codes (`D3.7`); `DV_QUANTITY`
finiteness, units, and precision (`D3.19`, `D3.20`); `DV_PROPORTION`
denominator and integrality (`D3.23`); `DV_MULTIMEDIA` content presence and
integrity (`D3.26`); `PARTY_IDENTIFIED` basic validity (`M5.14`);
`COMPOSITION.category` group membership (`E6.6`); `EVENT_CONTEXT.setting`
group membership (`E6.11`); `ISM_TRANSITION.current_state` group membership
(`E6.21`); and `DV_URI` well-formedness with `DV_EHR_URI` scheme (`D3.30`,
`D3.31`), wherever a URI appears — as a `DATA_VALUE` **and** as a
`LINK.target` on any `LOCATABLE` (`M5.9`), which is where one actually
arrives from outside the process.
- **L10.7** Violation order MUST be stable across runs — depth-first, document
order — so that a report diffed between two runs shows real change.
- **L10.8** Where a check cannot be performed, the result MUST be reported as
*not checked* rather than as a pass (`D3.7`).