1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
// @generated-from-template templates/openehr-rm/validate.rs — DO NOT EDIT; edit the source and re-run `openehr-codegen -- emit`.
// SPDX-FileCopyrightText: FerroEHR contributors
// SPDX-FileCopyrightText: openEHR Foundation
// SPDX-License-Identifier: MIT AND Apache-2.0
//! RM-level validation glue (hand-written spec behaviour).
//!
//! The module is preserved
//! across `openehr-codegen` regeneration — the generator does not emit or overwrite it, so
//! the generator's `declare_hand_written_modules` keeps it and `lib.rs`
//! auto-declares `pub mod validate;`.
//!
//! Everything here judges a canonical-JSON RM node **as a value** — no codec,
//! no template, no wire context — and reports [`InvariantViolation`]s relative
//! to that node:
//!
//! 1. **The allocation-free fast-path RM class-invariant check**
//! ([`try_fast_validate`] → the private `fast` path) over a live canonical-JSON node, plus
//! the **shared invariant helpers** used by the sibling `*_impl.rs`
//! behaviour files (the DV_AMOUNT / DV_QUANTIFIED accuracy + magnitude-status
//! rules, the LOCATABLE `Archetype_node_id_valid` rule, ISO-8601 value
//! checks).
//! 2. **The JSON-level per-node checks the typed model cannot express** —
//! [`check_mandatory_containers`] (model-driven container lower bounds),
//! [`check_archetyped_valid`] and [`check_data_structure_shapes`]. Each is
//! a pure function over the node's own value, run as its own layer beside
//! the fast/typed core pair so the equivalence property between those two
//! stays exactly the core property. (The `x /= Void implies not x.is_empty`
//! family holds BY CONSTRUCTION — `Option<NonEmptyVec<T>>`/`NonEmptyVec<T>`
//! shapes make the forbidden state unrepresentable, #1730/#2445.)
//! 3. **The terminology-backed invariants**, in the sibling [`terminology`]
//! module (they need the openEHR terminology bundle, not just the node).
//! 4. **The `553|incomplete|` presence relaxation predicates**, in the sibling
//! [`incomplete`] module: the pair of pure model-driven questions ("is
//! anything missing?" / "is anything wrong?") that RM common
//! `master06-change_control_package.adoc` §Incomplete Content splits a
//! node's structural judgement into. Used only by the relaxed
//! (`553|incomplete|`) commit path; the strict path never calls them.
//! 5. **The typed-dispatch tier**, in the sibling [`typed_dispatch`] module:
//! the `_type` → concrete-RM-type table that *deserializes* a node through
//! the emitted canonical-JSON `serde` impls (`crate::json_serde`) and runs
//! that class's `Validate` impl — the authoritative oracle the fast path may
//! only skip when its result is provably identical.
//!
//! Kept OUT of this crate, in `openehr-its`: the GENERATED five-crate
//! structural dispatch that [`typed_dispatch::dispatch_typed`] falls through to
//! (it spans `openehr-base`/`-rm`/`-am`/`-term`/`-lang` at once, so it can only
//! be emitted downstream of all of them), the thin wire-boundary entry points
//! that compose the tiers, and the walkers that recurse an instance and prefix
//! absolute RM paths. The wire-boundary entry point
//! `openehr_its::wire_validate::validate_rm_value` calls [`try_fast_validate`]
//! then falls back to [`typed_dispatch::dispatch_typed`].
//!
//! # The invariant source and the diagnostic form
//!
//! The RM class invariants realized here are the released class tables' own
//! invariant expressions, machine-classified from the vendored BMM — the
//! generated register at the head of the crate-private `generated` module
//! (`validate/generated.rs`) is the per-invariant
//! authority (venue + citation + adjudication). A failure renders the uniform
//! message `Invariant <Name> failed on type <RM_TYPE>` (see
//! `invariant_failed`): `<Name>` is the invariant's released class-table name
//! (`docs/specs/openehr/RM/docs/UML/classes/*.adoc` §Invariants), so a
//! violation is identifiable by the spec's own vocabulary. No openEHR spec
//! governs the message wording itself — the `invariant_failed` helper below
//! is its single home.
//!
//! What deliberately does **not** run in the *core/typed* tiers:
//! - **Terminology-bound invariants** (the class-table rules that invoke
//! `has_code_for_group_id` / `code_set (id).has_code` — `Language_valid`,
//! `Encoding_valid`, `Category_validity`, `Change_type_valid`, …). They
//! resolve a code against the openEHR terminology bundle rather than
//! inspecting the node alone, so they live in the sibling
//! [`terminology`] module (over `openehr-term`) and are run as a separate
//! post-core layer — never inside the fast/typed pair, whose equivalence
//! property is defined over the core invariants only.
//! - **Invariants adjudicated out of the per-node layer** — each carries a
//! citation-pinned `Excluded` adjudication in the generated register
//! (aggregate/cross-object rules owned by another layer, derived-function
//! constraints, and rules over undeclared attributes).
//! - **Cross-child recursion**: each `Validate` impl checks only its own class
//! invariants; the composition validator recurses into children (and prefixes
//! the absolute RM path onto each [`InvariantViolation`]).
use InvariantViolation;
use Value;
/// The generated RM class-invariant cores (`openehr-codegen -- emit-validate`):
/// one `pub(crate) fn <name>_core` per mechanically-shaped invariant group, the
/// single source both the typed `Validate` impls and [`fast`] call. This is the
/// ONE hand-declared module for that `// @generated` file — the runtime helpers
/// the cores call (`invariant_failed`, the ISO-8601 validators, the dialect
/// predicates) stay hand-written below.
pub
// The `553|incomplete|` presence relaxation predicates (its own `//!` module
// docs carry the detail — an outer doc attribute here would force rustdoc to
// resolve the module's intra-doc links in THIS module's scope instead of its
// own).
// The terminology-backed RM class invariants (its own `//!` module docs carry
// the detail — an outer doc attribute here would force rustdoc to resolve the
// module's intra-doc links in THIS module's scope instead of its own).
// The typed-dispatch tier (its own `//!` module docs carry the detail — an
// outer doc attribute here would force rustdoc to resolve the module's
// intra-doc links in THIS module's scope instead of its own).
/// Run the allocation-free fast-path RM class-invariant check for a single
/// canonical-JSON node, dispatching on its `_type`.
///
/// Returns `true` when the fast path vouched for (fully handled) the node —
/// nothing is appended on `false`.
///
/// This is the public seam the wire-boundary two-tier dispatcher
/// (`openehr_its::wire_validate::validate_rm_value`) calls before falling back to
/// the typed deserialize path. Kept here because the fast path is untyped
/// (walks `&serde_json::Value` against the generated RM model) and needs no
/// canonical-JSON codec — pure RM model semantics.
///
/// NOTE: no openEHR spec governs the fast path — it is our own performance
/// design; the *semantics* it realizes are exactly the RM class invariants of
/// the `*_impl.rs` siblings (see the private `fast` module).
/// The inherited LOCATABLE `Archetype_node_id_valid` violation for a
/// canonical-JSON node whose RM type is `ty`, or `None` when the node does not
/// violate it.
///
/// `LOCATABLE.Archetype_node_id_valid` (`not archetype_node_id.is_empty`,
/// `docs/specs/openehr/RM/docs/UML/classes/org.openehr.rm.common.locatable.adoc`
/// §Invariants) is inherited by **every** concrete LOCATABLE descendant, so the
/// set of RM types it applies to is read from the generated static RM model
/// ([`crate::v1_1::model::descendants`] of `LOCATABLE` — the transitive *concrete*
/// descendant closure of the BMM), never from a hand-maintained list: a class
/// the spec adds to the hierarchy is covered the moment the model is
/// regenerated. The violation itself is built by the generated core
/// (`generated::archetype_node_id_core`), so its text has one source.
///
/// Only a **present and empty** `archetype_node_id` violates the invariant. An
/// absent (or non-string) one is a structural defect of a mandatory attribute,
/// reported by the decode/mandatory-attribute layer instead — reporting it here
/// too would double-report the same defect.
///
/// The wire-boundary dispatcher (`openehr_its::wire_validate`) runs this for
/// every node after its class-invariant tier, appending only what that tier did
/// not already report (the concrete LOCATABLEs with a typed `Validate` impl
/// realize the inherited invariant themselves).
/// The model-driven mandatory-container lower-bound check.
///
/// Every attribute the
/// static RM model declares as a MANDATORY container must be present, and one
/// whose BMM cardinality has a lower bound ≥ 1 must be non-empty — e.g.
/// `CLUSTER.items: List<ITEM>` is `1..*`
/// (`docs/specs/openehr/RM/docs/UML/classes/org.openehr.rm.data_structures.cluster.adoc`
/// §Attributes), so a CLUSTER with an absent or empty `items` does not conform.
///
/// This is a distinct mechanism from the codec decode: a MANDATORY container
/// emits as a bare `Vec<T>` and the canonical-JSON reader treats an absent
/// array as an empty `Vec` (wire tolerance), so the omission is invisible to
/// typed decoding — the lower bound is enforced HERE, from the model, for every
/// class uniformly. The OPTIONAL-attribute family (`x /= Void implies not
/// x.is_empty`, e.g. `COMPOSITION.content`) needs no evaluator at all: those
/// attributes emit as `Option<NonEmptyVec<T>>`, so the forbidden
/// present-but-empty state is unrepresentable (#1730); this function judges
/// MANDATORY containers only.
/// `List<Octet>` is exempt by shape: canonical JSON renders it as an inline
/// base64 string, which presence-checks as a non-array member.
///
/// Kept OUTSIDE the fast/typed core pair (the caller runs it as its own layer),
/// so the fast-vs-typed equivalence property stays exactly the core property.
/// The declared-slot-type conformance rule, over the BMM-generated attribute
/// model.
///
/// A child node's wire `_type` must name the RM type the parent
/// attribute declares, or a subtype of it (`docs/specs/openehr/ITS-JSON`
/// discipline: `_type` names the instance's RM class; the attribute's
/// declared type comes from the RM UML/BMM — e.g. RM ehr
/// `composition.adoc` §Attributes types `content` `List<CONTENT_ITEM>`, so a
/// `DV_TEXT` member of `content` is a positive type contradiction). This is
/// the WRONGNESS half of slot typing — it never relaxes ("data may be
/// missing, but it may not be wrong", RM common
/// `master06-change_control_package.adoc` §Incomplete Content); the
/// presence/lower-bound half lives in [`check_mandatory_containers`].
///
/// Returns `None` (no judgement) when the slot is unknown to the model, the
/// declared type is not a modelled class (a primitive such as `String`), or
/// the attribute is a keyed map (`Hash` — its JSON object is a map, not an
/// RM node). An untagged child is judged elsewhere (the effective-type rule,
/// [`crate::v1_1::model::declared_concrete_type`]).
/// The scalar-member arm of the declared-slot-type rule.
///
/// A NON-OBJECT member
/// of a list slot whose declared element type is a modelled RM class is the
/// same positive contradiction a foreign `_type` is — no JSON scalar can be
/// an instance of an RM class (canonical JSON encodes every RM object as a
/// JSON object; ITS-JSON). Same guards as [`check_declared_slot_type`]:
/// `None` for unknown slots, primitive-typed slots (a `List<String>` member
/// IS legitimately a scalar), and keyed maps. Never relaxed.
/// `LOCATABLE.Archetyped_valid`: `is_archetype_root xor archetype_details =
/// Void`
/// (`docs/specs/openehr/RM/docs/UML/classes/org.openehr.rm.common.locatable.adoc`
/// §Invariants).
///
/// The enforceable arm on an instance is: a **non-root** node — one whose
/// `archetype_node_id` is an `at`/`id` term code
/// ([`crate::v1_1::paths::archetype_node_id_is_term_code`]), which per the node-id
/// format can never be the root of an archetyped structure — must NOT carry
/// `archetype_details`.
///
/// NOTE: the converse arm ("an archetype-HRID node must carry
/// `archetype_details`") is NOT enforced — `locatable.adoc` §Functions gives
/// `is_archetype_root ()` no postcondition or derivation, so the arm is
/// underivable from the released text (a tautology under the
/// reference-object reading) and is reported rather than gated; the
/// COMPOSITION root arm stays separately enforced
/// (`composition_impl.rs` `Is_archetype_root`).
///
/// The second arm is the root node-id **identity** rule: a node that DOES
/// carry `archetype_details` is an archetype root, and
/// `docs/specs/openehr/RM/docs/UML/classes/org.openehr.rm.common.locatable.adoc`
/// §Attributes (`archetype_node_id`) fixes its value verbatim — "At an
/// archetype root point, the value of this attribute is always the
/// stringified form of the `archetype_id` found in the `archetype_details`
/// object" (restated in
/// `docs/specs/openehr/RM/docs/common/master03-archetyped_package.adoc`
/// §The LOCATABLE Class: "the only exception is at archetype root points in
/// data, where `archetype_node_id` carries the archetype identifier in
/// string form"). A root whose two archetype identities disagree names no
/// resolvable generating archetype.
///
/// Both violations are reported on the node itself (an empty
/// [`InvariantViolation::path`]); the caller prefixes the absolute RM path.
/// The `CLUSTER.items` PRESENCE duty.
///
/// It is split out of
/// [`check_data_structure_shapes`] because it is a mandatory-presence rule and
/// therefore the one shape duty the `553|incomplete|` state relaxes (RM common
/// `master06-change_control_package.adoc` §Incomplete Content: "container
/// attributes may be empty, even though they may have minimum existence and
/// cardinality respectively of one").
///
/// `CLUSTER.items` is 1..1 (RM `data_structures` `cluster.adoc`; the ITS-JSON
/// CLUSTER schema lists `items` as required) — after deserialize an absent
/// list collapses into an empty `Vec`, so presence is only checkable here.
/// Reported on the node itself; the caller prefixes the absolute RM path.
/// JSON-level data-structure shape duties the typed model cannot express:
///
/// - one `HISTORY`'s events all carry the SAME `ITEM_STRUCTURE` subtype
/// in `data` — "A History of type `HISTORY<ITEM_LIST>` … constrains the
/// type of the data at each Event to be of type `ITEM_LIST` and nothing
/// else" (`docs/specs/openehr/RM/docs/data_structures/master06-history_package.adoc`;
/// `history.adoc` generic parameter) — the monomorphized runtime type
/// cannot see `T`.
///
/// The `CLUSTER.items` presence duty is [`check_cluster_items_present`]'s (it
/// is a mandatory-presence rule, so the incomplete-commit path relaxes it while
/// keeping every duty here). A HISTORY violation carries the `events[i]`
/// sub-path of the offending event; the caller prefixes the absolute RM path.
/// Build a class-invariant violation in this workspace's uniform message
/// shape: `"Invariant <name> failed on type <RM_TYPE>"`, where `<name>` is the
/// invariant's own BMM name, so a failure is identifiable by that name alone.
///
/// NOTE: no openEHR spec governs the wording of a violation message — the
/// `Invariant <Name> failed on type <RM_TYPE>` shape is our own design, and only
/// `<Name>`/`<RM_TYPE>` are the spec's. The path is left empty (the value
/// itself); the composition validator prefixes the absolute RM path.
pub
/// `true` when a floating value denotes a whole number — the integrality
/// probe the DV_PROPORTION `Precision_validity` / `Is_integral_validity` /
/// `Fraction_validity` invariants need, which state integrality as
/// `numerator.floor = numerator and denominator.floor = denominator`
/// (`docs/specs/openehr/RM/docs/UML/classes/org.openehr.rm.data_types.dv_proportion.adoc`
/// §Invariants).
pub
// ── named runtime realizations of the BMM assertion-dialect predicates ────────
//
// These are the callable runtime helpers the assertion-dialect emitter maps its
// leaf predicates onto (the `plan::overrides` dialect table names each). They
// were previously inlined into the invariant cores below; extracting them under
// the BMM predicate spelling makes the emitter's future generated cores call one
// named runtime function per dialect predicate. Behaviour is identical to the
// former inline forms.
/// BASE/RM `valid_magnitude_status (s)`: `s` is one of `= < > <= >= ~` — the
/// DV_QUANTIFIED `Magnitude_status_valid` predicate
/// (`docs/specs/openehr/RM/docs/UML/classes/org.openehr.rm.data_types.dv_quantified.adoc`).
pub
/// RM `valid_percentage (v)`: `0 <= v <= 100` — the DV_AMOUNT `Accuracy_validity`
/// predicate for a percent-recorded accuracy
/// (`docs/specs/openehr/RM/docs/UML/classes/org.openehr.rm.data_types.dv_amount.adoc`).
pub
/// RM `valid_proportion_kind (k)`: `k` is one of the PROPORTION_KIND codes —
/// the DV_PROPORTION `Type_validity` predicate
/// (`docs/specs/openehr/RM/docs/UML/classes/org.openehr.rm.data_types.proportion_kind.adoc`).
///
/// The value space is read from the emitted constant set, whose
/// `from_value` maps anything the `BMM_ENUMERATION` does not declare to
/// `Other`, so a re-vendoring that adds a constant widens this predicate with
/// it rather than leaving a hand-written range behind.
pub
// ── ISO-8601 value validation ────────────────────────────────────────────────
// The DV_DATE / DV_TIME / DV_DATE_TIME / DV_DURATION `Value_valid` invariants
// are `valid_iso8601_date` / `_time` / `_date_time` / `_duration` (each class
// page's §Invariants), and those are BASE `Time_Definitions` functions — so
// this crate calls them rather than re-deriving the grammar. It used to carry
// its own reader; the two drifted twice and both drifts shipped (#2273), each
// time letting a value pass validation, commit, and then behave as invalid.
pub use ;