openehr-rails 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf0122af26f620b3082c006c5d469fdf3710c5dd1647b856c9024c8e4e3f7c66
4
- data.tar.gz: b54d925956180cd9baffab027d42564ecd55864f839698952ed2bfbfdfb9356d
3
+ metadata.gz: a30bd4a561f8193c0603c1e43e1873e1c8835d2e7c418b95c429ac6e2b397e83
4
+ data.tar.gz: 180145cf2e294cc68ae84ddc35b147e07e6fb52f99062d14f8df3cb0e306108e
5
5
  SHA512:
6
- metadata.gz: 90fadabcf5f3215e6c24f4c75f5bef98e38b831286e0f0da33d6d67ae6743937d7958d286384c85042830452e47fe08e22eca6dce72541ab7bfb5395cc029ef2
7
- data.tar.gz: 700d4afc6e60805cc76c478035131d0aa62b5f0decc09d44c67f0ac1bb4e8181500de65ba56b40f2974a0e24259945fb26285a6b3eac0ebe3ca394656cdc1c56
6
+ metadata.gz: 46b84468c263139208e522562831d103cd43d8d3c865aa274fe7cf6db7ebe486fa67329b4e175564a9fc0f94d0a0a4454d679f3d42735ac4c7214b739fa7ba3a
7
+ data.tar.gz: dc189063ef16d43253287e9fed472bdc7d69b0b0c72fb5f393f772cf9adbe596ebd24b6dbea57514435c3a6afe8cdba048fd4b1c3c2e73e39b4a17315ad03b6d
@@ -26,6 +26,8 @@ jobs:
26
26
  run: bundle exec rake release:check
27
27
  - name: Build gem
28
28
  run: bundle exec rake build
29
+ - name: Record sha256
30
+ run: sha256sum pkg/*.gem | tee pkg/SHA256SUMS
29
31
  - name: Upload gem artifact
30
32
  uses: actions/upload-artifact@v7
31
33
  with:
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 4.0.6
1
+ 4.0.7
data/CHANGELOG.md CHANGED
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.0] - 2026-10-12
11
+
12
+ > **Upgrade note.** A stored Composition holding a `SECTION` or an `INSTRUCTION`
13
+ > (with its `ACTIVITY`s) now reads back through `to_rm` and AQL; the ones 0.7.1
14
+ > skipped with `openehr-rails AQL: skipping composition uid=...` are exactly the
15
+ > ones that become visible, so query row counts can grow. Two injected defaults
16
+ > are approximations until a later release persists them as columns: an
17
+ > INSTRUCTION without a stored narrative reads back with its node name as
18
+ > `narrative`, and every ACTIVITY with `action_archetype_id` `'/.*/'`.
19
+
20
+ ### Added
21
+ - `RmObjectBuilder` rebuilds `SECTION`, `INSTRUCTION` and `ACTIVITY` nodes (and
22
+ every CARE_ENTRY's `protocol`), so compositions holding them -- a referral's
23
+ `service_request` with its requester/receiver under `protocol[...]`, entries
24
+ nested in a `SECTION` -- become readable through `to_rm` and queryable by AQL
25
+ instead of being skipped with the 0.7.1 warning (#45). Two values are
26
+ **approximations** because the graph does not carry them yet: an INSTRUCTION
27
+ without a stored narrative gets its node name as `narrative`, and every
28
+ ACTIVITY gets `action_archetype_id` `'/.*/'` (any ACTION archetype). Both will
29
+ be persisted as columns in a later release. `ACTION`, `ITEM_SINGLE` and
30
+ `ITEM_TABLE` are still unsupported (skipped with the warning).
31
+ - `FieldExtractor` fields carry `rm_type_alternatives`: every value type the OPT
32
+ allows for the ELEMENT, in OPT order (e.g. `["DV_TEXT", "DV_CODED_TEXT"]` for
33
+ `problem_diagnosis` at0002). Additive; the chosen `rm_type` is unchanged. Lets a
34
+ host app tell an OR-constrained coded text (free text allowed) from a coded-only
35
+ one instead of rendering an empty select (#52, anlage #33).
36
+
37
+ ### Fixed
38
+ - An entry with no extracted leaves (today every `INSTRUCTION`, see #47) no
39
+ longer gets a profile consisting of the archetype `code` rules and an empty
40
+ `component` slicing header; it is skipped and reported through `#skipped`
41
+ with a message saying the entry has no leaves, whatever its resource (#49).
42
+
10
43
  ## [0.7.1] - 2026-09-25
11
44
 
12
45
  > **Upgrade note.** AQL now skips a stored composition that holds an RM type
data/CLAUDE.md CHANGED
@@ -61,6 +61,13 @@ documentation files and `lib/` was byte-identical - but nothing in the release
61
61
  path caught it. Measurements in `docs/reports/fsh-generator-log.md` R8; the
62
62
  matching `release:check` guard is `skoba/openehr-rails#34`.)
63
63
 
64
+ **Local dev Ruby tracks the patch release CI builds with.** `.ruby-version`
65
+ moved 4.0.6 -> 4.0.7 (2026-10-12) so a local rebuild at the tag stamps the same
66
+ `rubygems_version` (4.0.20) as `ruby/setup-ruby`'s ruby 4.0 image did for 0.7.1,
67
+ whose `metadata.gz` differed from the local rebuild by that one line
68
+ (`docs/reports/referral-upstream-log.md` R2). The cross-check still judges by
69
+ `data.tar.gz`, not the whole-gem sha256: the two toolchains can drift apart again.
70
+
64
71
  **Confirm publication by checksum, not by version number.** RubyGems publishes
65
72
  the sha256 it recorded at push time (`/api/v1/gems/openehr-rails.json` `sha`, and
66
73
  the `checksum:` field in `https://index.rubygems.org/info/openehr-rails`). The
@@ -225,7 +232,7 @@ This is `openehr-rails`, a Rails engine gem that turns an openEHR Operational Te
225
232
 
226
233
  ## Development Notes
227
234
 
228
- - Ruby 3.3+ required (raised from 3.0 in 2026-08 to track the `openehr` gem dependency dropping Ruby 3.2 support at EOL); CI matrix covers 3.3/3.4/4.0, local dev pinned via `.ruby-version` (currently 4.0.6)
235
+ - Ruby 3.3+ required (raised from 3.0 in 2026-08 to track the `openehr` gem dependency dropping Ruby 3.2 support at EOL); CI matrix covers 3.3/3.4/4.0, local dev pinned via `.ruby-version` (currently 4.0.7)
229
236
  - Rails 7.0+ and Rails 8.x supported (CI matrix: `gemfiles/rails_{7_2,8_0,8_1}.gemfile`; demo app runs on Rails 8.1)
230
237
  - URI compatibility workaround implemented for Ruby 3.4
231
238
  - `ostruct` is an explicit development dependency — it left Ruby's default gems as of 4.0, and a couple of specs use `OpenStruct` as a lightweight test double
data/docs/backlog.md CHANGED
@@ -81,7 +81,18 @@ that verification is deferred to the next real release (>= 0.5.0), not simulated
81
81
  against the value recorded for that run" cannot be followed literally; the
82
82
  tag-rebuild cross-check is what pins the bytes today. Add a "Record sha256" step
83
83
  after `rake build` (`sha256sum pkg/*.gem`), as openehr-ruby's `release.yml` has.
84
- CI-only change; needs a PR, no Issue.
84
+ CI-only change; needs a PR, no Issue. **In PR #51 as `1410274` (2026-10-12,
85
+ `docs/reports/referral-upstream-log.md` R5)**; first run that prints it will be
86
+ the `v0.8.0` tag's.
87
+ - **The tag-rebuild cross-check compares `data.tar.gz`, not the whole `.gem`**
88
+ (0.7.1, 2026-09-25, `docs/reports/referral-upstream-log.md` R2). A `.gem` embeds
89
+ `rubygems_version` in `metadata.gz`, so a rebuild on a different ruby/rubygems
90
+ (local 4.0.6/4.0.16 vs CI 4.0.7/4.0.20) yields a different whole-gem sha256 with
91
+ byte-identical shipped files. Procedure: `tar xf` both gems, compare
92
+ `sha256sum data.tar.gz` (must match) and `diff` the gunzipped `metadata.gz`
93
+ (only `rubygems_version` may differ). 0.7.0's whole-gem match was a coincidence
94
+ of equal toolchains. Fold this into `CLAUDE.md`'s Release convention together
95
+ with the "Record sha256" step above.
85
96
 
86
97
  **Done (2026-08-23, PR #28)** — kept below for the original rationale; see the CI
87
98
  status entry above for verification details.
@@ -62,3 +62,242 @@ release carrying it is 0.8.0.
62
62
 
63
63
  Gate when the human has merged what should ride; version number decided from the
64
64
  merged content (0.7.1 if patch-only, 0.8.0 if #45 rides).
65
+
66
+ ## R2 -- 0.7.1 gate approved and tagged; artifact sha256 differs from the tag rebuild by the rubygems stamp only (2026-09-25)
67
+
68
+ ### Gate (approved by the 統括, 2026-09-25)
69
+
70
+ `master` `df93c71` (= #43 + #50 merged): `lib/` touched by `5fcc0ec` (#38) and
71
+ `dd157b6` (#44), both bug fixes -> **patch**; gemspec / version / dependencies
72
+ unchanged; everything else docs. **0.7.1**, #45 deliberately excluded (minor ->
73
+ 0.8.0 later, its own inventory). CHANGELOG plan adopted with one addition asked
74
+ for by the ruling: a one-line Upgrade note that AQL now warns and skips instead
75
+ of failing every query, quoting the log line
76
+ `openehr-rails AQL: skipping composition uid=...` so an operator can find it.
77
+
78
+ ### Release
79
+
80
+ - `ceb4c5b` Release: bump version to 0.7.1 (`version.rb`, `[0.7.1] - 2026-09-25`
81
+ with the Upgrade note, fresh empty `[Unreleased]`). Before it: full suite
82
+ **311 examples, 0 failures**, rubocop **no offenses**, `release:check OK`;
83
+ `master` CI run 36128248178 success.
84
+ - Tag `v0.7.1` -> `ceb4c5b`, **annotated** this time (`git tag -a`, the R14
85
+ correction applied).
86
+ - `release.yml` run **36128715938**: 12 jobs success, `release:check OK` at the
87
+ tag. Artifact `pkg/openehr-rails-0.7.1.gem`, 251904 bytes,
88
+ sha256 **`29e89993894fe974a1f7eb1cbd51b1809c67aadd1cd5917f4b01af8142159784`**.
89
+ upload-artifact archive digest (not the gem): `216aff34…7c9a`.
90
+
91
+ ### The rebuild cross-check did not match, and why that is fine
92
+
93
+ A `gem build` in a scratch worktree at `v0.7.1` (local ruby 4.0.6, rubygems
94
+ 4.0.16) gave sha256 `fd06e784…4f8a`, **not** the artifact's. Taken apart
95
+ (`tar xf`; the .gem is a tar of `metadata.gz`, `data.tar.gz`, `checksums.yaml.gz`):
96
+
97
+ | part | CI artifact | local rebuild |
98
+ |---|---|---|
99
+ | `data.tar.gz` (the shipped files) | `b54d9259…` | `b54d9259…` **identical** |
100
+ | `metadata.gz` | differs on one line: `rubygems_version: 4.0.20` (CI ruby 4.0.7) | `rubygems_version: 4.0.16` |
101
+ | `checksums.yaml.gz` | differs (it hashes metadata) | -- |
102
+
103
+ So the bytes host apps load are identical; only rubygems' own version stamp
104
+ moved, because `ruby/setup-ruby`'s ruby 4.0 image advanced from 4.0.6 to 4.0.7
105
+ since 0.7.0 (when both sides happened to run rubygems 4.0.16 and the whole-gem
106
+ sha256 matched). **Lesson, recorded in `docs/backlog.md`**: the rebuild
107
+ cross-check must compare `data.tar.gz` (and `metadata.gz` minus
108
+ `rubygems_version`), not the whole `.gem`; a whole-gem match is a coincidence of
109
+ equal toolchains. The "Record sha256" CI step in the backlog stays the real pin
110
+ for the published bytes.
111
+
112
+ ### Handed to the human
113
+
114
+ `gem push pkg/openehr-rails-0.7.1.gem` (the downloaded CI artifact), then confirm
115
+ `https://rubygems.org/api/v1/gems/openehr-rails.json` `sha` == `29e89993…9784`
116
+ (compact index `checksum:` likewise), then delete the file from `pkg/`. Result
117
+ -> R3. `master` is past the tag by these docs commits (`release:check` on
118
+ `master` fails by design).
119
+
120
+ ### Pending
121
+
122
+ #45: the ruling's "conditions a-c" (pre-freeze implementation approved, 0.8.0)
123
+ have not reached this session as text; implementation starts once they do.
124
+
125
+ ## R3 -- 0.7.1 published; confirmed by checksum (2026-09-25)
126
+
127
+ Human `gem push` of the CI artifact (R2). Confirmation against the artifact's
128
+ sha256 `29e89993894fe974a1f7eb1cbd51b1809c67aadd1cd5917f4b01af8142159784`:
129
+
130
+ | source | value |
131
+ |---|---|
132
+ | `/api/v1/gems/openehr-rails.json` | version 0.7.1, `sha` `29e89993…9784` |
133
+ | compact index `info/openehr-rails` | `0.7.1 … checksum:29e89993…9784, created_at:2026-09-25T11:43:12Z` |
134
+ | `/api/v1/versions/openehr-rails/latest.json` | `{"version":"0.7.1"}` |
135
+ | `/api/v1/versions/openehr-rails.json` | **still 0.7.0 first** at check time -- the propagation lag CLAUDE.md warns about, observed for real this time |
136
+
137
+ Published gem == CI artifact. `pkg/` emptied. The judgement rule from R2 (tag
138
+ rebuild: `data.tar.gz` identical, metadata differs by `rubygems_version` only)
139
+ stands as the cross-check for future releases.
140
+
141
+ Next: #45 phase A implementation per the ruling (conditions a-c), PR `Fixes #45`,
142
+ then the 0.8.0 inventory.
143
+
144
+ ## R4 -- #45 phase A implemented (PR #51), ride-alongs #52 (PR #53) and #49 (PR #54) (2026-09-25)
145
+
146
+ Ruling: #45 phase A approved for pre-freeze implementation with conditions (a)
147
+ injected defaults stated as approximations, (b) regression pin on the existing
148
+ fixtures and the anlage demo queries, (c) #44's SECTION fixture -> ITEM_TABLE;
149
+ ride-alongs to weigh: anlage upstream 19 (`rm_type_alternatives`) and #49.
150
+
151
+ ### PR #51 -- `feat/45-rm-object-builder-section-instruction` (Fixes #45)
152
+
153
+ - (b) enhancement. **Red**: 8 new examples on a synthetic canonical hash
154
+ (`spec/support/synthetic_referral_canonical_hash.rb`: SECTION > EVALUATION;
155
+ INSTRUCTION with narrative, protocol ITEM_TREE, one ACTIVITY with description
156
+ and DV_PARSABLE timing) -> `UnsupportedRmTypeError` / empty AQL rows.
157
+ **Green**: 36/0 in the four touched spec files, full suite **324 examples,
158
+ 0 failures**, rubocop clean (after one lint follow-up commit for the spec
159
+ file name).
160
+ - `TYPE_CLASSES` += SECTION, INSTRUCTION, ACTIVITY; `build_node` split into
161
+ one helper per stored type (`entry_attributes`, `section_attributes`,
162
+ `instruction_attributes`, `activity_attributes`, `history_attributes`,
163
+ `event_attributes`, `element_attributes`); every CARE_ENTRY gets `protocol`
164
+ when stored. Constructor probes against openehr 2.4.3 before coding:
165
+ `Section.new(items: nil)` ok, `Activity.new(description:, action_archetype_id:)`
166
+ ok with `timing` nil, `DvParsable.new(value:, formalism:)` ok.
167
+ - AQL measured through the new objects: `i/activities[at0001]/description[at0009]/items[at0121]/value/value`,
168
+ `i/protocol[at0008]/items[at0010]/value/value`, and an EVALUATION nested in a
169
+ SECTION via `CONTAINS EVALUATION ev[...]` -- all return the stored values.
170
+ (`CONTAINS SECTION s[...]` as a hop was not needed and not pinned.)
171
+ - (a): `narrative` <- node name when no row; `action_archetype_id` <- `'/.*/'`
172
+ -- stated in the class comment, at the injection sites, in the specs and in
173
+ CHANGELOG `[Unreleased]` Added, as approximations pending phase B.
174
+ - (b): `spec/openehr_rails/aql/executor_execute_demo_queries_spec.rb` pins the
175
+ four anlage demo queries (bmi_calculation graph + a synthetic
176
+ problem_diagnosis composition with the real at-codes). Measured on the way:
177
+ a naive `DV_DATE_TIME` value committed as `2026-02-01T00:00:00` comes back
178
+ as `2026-01-31T15:00:00Z` under JST -- the pin compares instants, not
179
+ strings (pre-existing behaviour, not touched). `protocol` nil pinned on an
180
+ OBSERVATION without one.
181
+ - (c): the #44 fixtures in `dataset_adapter_spec` / `executor_spec` /
182
+ `rm_object_builder_spec` now use an ITEM_TABLE as an EVALUATION's data.
183
+ - CI runs 36131797849 (`ca1af81`) and 36132042465 (`368da91`, lint follow-up):
184
+ success. Semver minor -> 0.8.0.
185
+
186
+ ### PR #53 -- `feat/52-rm-type-alternatives` (Fixes #52, filed for upstream 19)
187
+
188
+ Small, so it rides. (b) enhancement: **red** 9/3 (key absent) -> **green** 9/0,
189
+ full suite 314/0. `FieldExtractor` fields gain `rm_type_alternatives` (the
190
+ value's child constraint types in OPT order), `rm_type` selection untouched.
191
+ **Measured before the spec**: at0002 `[DV_TEXT, DV_CODED_TEXT]`, at0073
192
+ `[DV_CODED_TEXT, DV_TEXT]` -- anlage's issue33-plan says at0073 is coded-only;
193
+ the fixture disagrees, and the spec pins the fixture. CI 36132700126 success.
194
+
195
+ ### PR #54 -- `fix/49-zero-leaf-skip` (Fixes #49)
196
+
197
+ Small, same decision point as #38, so it rides. (a) bug: **red** 44/4 (a
198
+ 0-leaf OBSERVATION and a 0-leaf INSTRUCTION each got a profile with an empty
199
+ `component` slicing header) -> **green** 44/0, full suite 315/0.
200
+ `TypeMap.assert_supported!` raises first for `fields.empty?`;
201
+ `UnsupportedProfileError` says "the entry has no leaves" (pointing at #47).
202
+ Built in a git worktree beside the main checkout (removed afterwards) so #52
203
+ and #49 could proceed in parallel. CI 36132843280 success.
204
+
205
+ ### Merge notes for the human
206
+
207
+ All three PRs are CI-green and `MERGEABLE CLEAN` against `560647a`, but each
208
+ adds to the same `[Unreleased]` section of `CHANGELOG.md`, so after the first
209
+ merge the other two will show as conflicting; this session resolves each by
210
+ taking `master`'s CHANGELOG and re-adding the PR's bullet (as done for #43/#50),
211
+ then re-runs CI. Any order works. Then the 0.8.0 inventory (R5).
212
+
213
+ ## R5 -- #53 and #54 merged into #51's branch; `release.yml` "Record sha256" rides along; CI green (2026-10-12)
214
+
215
+ Instruction: fold the two ride-alongs into #51 the way #41 was folded into #39
216
+ / #42 (branch-into-branch merge, identical commit objects), so the human merges
217
+ one PR; then the 0.8.0 inventory.
218
+
219
+ ### Branch `feat/45-rm-object-builder-section-instruction` (PR #51), now
220
+
221
+ | commit | what |
222
+ |---|---|
223
+ | `ae71034` | Merge branch `feat/52-rm-type-alternatives` (#53, `d574358`) |
224
+ | `e180eef` | Merge branch `fix/49-zero-leaf-skip` (#54, `7d4d203`) |
225
+ | `1410274` | CI: `release.yml` gains `Record sha256` (`sha256sum pkg/*.gem \| tee pkg/SHA256SUMS`) after `rake build`, ported back from openehr-ruby's copy; `docs/backlog.md` "Release automation" item. CI-only, no Issue (the backlog's own call); rides here so one merge lands it ahead of the `v0.8.0` tag run, which is the first run that will print it. |
226
+
227
+ - Code files were disjoint; the only conflict in both merges was
228
+ `CHANGELOG.md` `[Unreleased]` (as R4 predicted). Resolution: #45's and #52's
229
+ bullets kept in that order under one `### Added`; #49's bullet under a
230
+ `### Fixed` below. `git diff origin/master...HEAD`: 14 files, the union of
231
+ the three PRs' diffs plus `release.yml`.
232
+ - Local, after the merges (bundle had to be installed first -- this checkout
233
+ had no gems): **331 examples, 0 failures**; rubocop **no offenses**
234
+ (111 files).
235
+ - CI: run **38216664617** (`e180eef`, the two merges) success; run
236
+ **38216762262** (`1410274`, final head) **success, 11/11 jobs**. PR #51
237
+ `MERGEABLE CLEAN` against `master` `0434e06`.
238
+ - PR #51's body now names the ride-alongs with `Fixes #52` / `Fixes #49`, so
239
+ the one merge closes #45, #52 and #49.
240
+
241
+ ### Handed to the human
242
+
243
+ Merge #51 with a **merge commit** (not squash): then `d574358` and `7d4d203`
244
+ become reachable from `master` as the same objects #53 / #54 point at, exactly
245
+ the #41 situation. After the merge this session comments on #53 and #54
246
+ ("taken in via #51") and closes them if GitHub has not already, then runs the
247
+ 0.8.0 inventory (`v0.7.1..master`) as a gate report (R6).
248
+
249
+ Inventory preview, measured on the branch (the gate itself waits for the
250
+ merge): `lib/` touched by `rm_object_builder.rb` (#45, enhancement),
251
+ `opt/field_extractor.rb` (#52, additive output), `fhir/type_map.rb` +
252
+ `fhir/unsupported_profile_error.rb` (#49, bug); gemspec / version /
253
+ dependencies untouched; `release.yml` and the docs commits neutral. Expected
254
+ call: **minor -> 0.8.0**, as #45's plan said.
255
+
256
+ ## R6 -- #51 merged (with #53/#54 inside); 0.8.0 inventory and gate (2026-10-12)
257
+
258
+ ### Step 0: measured on `master` `1185c07` (the #51 merge commit)
259
+
260
+ - `lib/openehr_rails/rm/rm_object_builder.rb:48-53` `TYPE_CLASSES` has
261
+ `SECTION`, `INSTRUCTION`, `ACTIVITY` (#45);
262
+ `lib/openehr_rails/opt/field_extractor.rb:167` emits `rm_type_alternatives`
263
+ (#52); `lib/openehr_rails/fhir/type_map.rb:29` raises first on
264
+ `fields.empty?` (#49); `.github/workflows/release.yml:29-30` has the
265
+ `Record sha256` step.
266
+ - `CHANGELOG.md` `[Unreleased]`: `### Added` with the #45 and #52 bullets,
267
+ `### Fixed` with the #49 bullet -- the R5 resolution survived the merge.
268
+ - #51 merged 13:37 UTC as a merge commit; GitHub marked #53 (`ae71034`) and
269
+ #54 (`e180eef`) merged by reachability, #45/#52/#49 closed. Both PRs got a
270
+ "taken in via #51" comment.
271
+ - `master` CI run **38256998466** (`1185c07`) success. Local under ruby
272
+ 4.0.7: **331 examples, 0 failures**, rubocop no offenses (111 files).
273
+ `release:check` on `master` fails by design (HEAD is past the `v0.7.1` tag).
274
+ - #55 (FHIR profile id collision, anlage ledger 23): open issue, no PR,
275
+ nothing in `v0.7.1..master` references it -- stays out, December.
276
+
277
+ ### Step 1: inventory `v0.7.1..master`
278
+
279
+ | commit | touches | class |
280
+ |---|---|---|
281
+ | `b403226` `560647a` `0434e06` `84244cd` | docs/reports, docs/backlog | neutral |
282
+ | `32a79e7` `29a1444` | `.ruby-version`, CLAUDE.md | neutral (dev env / docs) |
283
+ | `1185c07` = #51: `ca1af81` | `rm/rm_object_builder.rb` + specs (#45, enhancement) | **minor** |
284
+ | -- `368da91` | spec file rename | neutral |
285
+ | -- `ae71034` <- `d574358` | `opt/field_extractor.rb` (#52, additive output) | minor |
286
+ | -- `e180eef` <- `7d4d203` | `fhir/type_map.rb`, `fhir/unsupported_profile_error.rb` (#49, bug) | patch |
287
+ | -- `1410274` | `release.yml` Record sha256 | neutral (CI) |
288
+
289
+ Gemspec, `version.rb`, `Gemfile`, `gemfiles/*` untouched. Highest class is
290
+ minor -> **0.8.0**, matching the instructed number.
291
+
292
+ ### Prepared, uncommitted, for the release commit
293
+
294
+ `CHANGELOG.md`: `[Unreleased]` -> `[0.8.0] - 2026-10-12` with an Upgrade note
295
+ (two points: SECTION/INSTRUCTION compositions now read back through `to_rm`
296
+ and AQL -- the ones 0.7.1 skipped with the warning are the ones that become
297
+ visible; `narrative` / `action_archetype_id` injected defaults are
298
+ approximations until phase B persists them), the three bullets unchanged, a
299
+ fresh empty `[Unreleased]` above. `version.rb` 0.7.1 -> 0.8.0 goes in the
300
+ same commit (`ceb4c5b`'s shape). Tag `v0.8.0` annotated; the Release run
301
+ prints the gem's sha256 in `Record sha256`, which is the value the human's
302
+ publish and the post-publish checksum are compared against; the tag-rebuild
303
+ cross-check judges by `data.tar.gz` (R2 rule).
@@ -99,6 +99,8 @@ module OpenehrRails
99
99
  # path the resource lacks.
100
100
  def assert_supported!(entry)
101
101
  resource_type = resource_for_entry(entry[:rm_type])
102
+ # No leaves at all: nothing to profile, whatever the resource (#49).
103
+ raise UnsupportedProfileError.new(entry[:archetype_id], resource_type, 0) if entry[:fields].empty?
102
104
  return if resource_type == 'Observation' || element_map_for(entry[:archetype_id])
103
105
 
104
106
  raise UnsupportedProfileError.new(entry[:archetype_id], resource_type, entry[:fields].size)
@@ -17,12 +17,23 @@ module OpenehrRails
17
17
  @archetype_id = archetype_id
18
18
  @resource_type = resource_type
19
19
  @leaf_count = leaf_count
20
- super(
21
- "#{archetype_id}: #{leaf_count} #{leaf_count == 1 ? 'leaf maps' : 'leaves map'} to " \
20
+ super(leaf_count.zero? ? no_leaves_message : unmapped_message)
21
+ end
22
+
23
+ private
24
+
25
+ # 0 leaves: FieldExtractor found no ELEMENT under the entry (today that
26
+ # includes every INSTRUCTION, #47), so there is nothing to profile.
27
+ def no_leaves_message
28
+ "#{archetype_id} (#{resource_type}): the entry has no leaves, nothing to profile -- skipped " \
29
+ '(skoba/openehr-rails#49; INSTRUCTION leaves: #47)'
30
+ end
31
+
32
+ def unmapped_message
33
+ "#{archetype_id}: #{leaf_count} #{leaf_count == 1 ? 'leaf maps' : 'leaves map'} to " \
22
34
  "#{resource_type}, which has neither value[x] nor component, and " \
23
35
  'TypeMap::ENTRY_ELEMENT_MAPS has no row for this archetype -- skipped ' \
24
36
  '(skoba/openehr-rails#33, #38; mapping rows: #35, #37)'
25
- )
26
37
  end
27
38
  end
28
39
  end
@@ -10,6 +10,8 @@ module OpenehrRails
10
10
  # label: display text from the template terminology (any language)
11
11
  # path: RM path from the COMPOSITION content
12
12
  # rm_type: openEHR data value type (e.g. 'DV_QUANTITY')
13
+ # rm_type_alternatives: every alternative value type the OPT allows, in OPT order
14
+ # (e.g. ['DV_TEXT', 'DV_CODED_TEXT'] for an OR constraint; rm_type is the chosen one)
13
15
  # node_id: at-code of the ELEMENT
14
16
  # archetype_id: id of the nearest enclosing archetype root (entry or embedded C_ARCHETYPE_ROOT)
15
17
  # column_type: ActiveRecord column type symbol
@@ -162,6 +164,7 @@ module OpenehrRails
162
164
  label: label || element.node_id,
163
165
  path: "#{path}/value",
164
166
  rm_type: rm_type,
167
+ rm_type_alternatives: value_children(element).map(&:rm_type_name),
165
168
  node_id: element.node_id,
166
169
  archetype_id: archetype_id,
167
170
  entry_rm_type: entry[:rm_type],
@@ -176,10 +179,15 @@ module OpenehrRails
176
179
  field
177
180
  end
178
181
 
179
- def value_constraint(element)
182
+ # All alternative constraints on the ELEMENT's value, in OPT order.
183
+ def value_children(element)
180
184
  attrs = element.attributes || []
181
185
  value = attrs.find { |a| a.rm_attribute_name == 'value' }
182
- children = value&.children || []
186
+ value&.children || []
187
+ end
188
+
189
+ def value_constraint(element)
190
+ children = value_children(element)
183
191
  return children.first if children.size <= 1
184
192
 
185
193
  code_reference_class = OpenEHR::AM::OpenEHRProfile::DataTypes::Text::CCodeReference
@@ -29,11 +29,28 @@ module OpenehrRails
29
29
  # carried in OPT/stored data. The gem's own constructor validations
30
30
  # (ArgumentError on missing mandatory attrs) become the RM-conformance
31
31
  # check.
32
+ #
33
+ # Injected values, all documented approximations (#45 ruling condition
34
+ # (a); the graph does not carry them yet, phase B adds columns):
35
+ # - ENTRY language / encoding / subject: OpenehrRails config defaults
36
+ # - HISTORY.origin, EVENT.time: composition context time or now
37
+ # - INSTRUCTION.narrative: the node's name when no narrative row exists
38
+ # - ACTIVITY.action_archetype_id: '/.*/' (any ACTION archetype)
39
+ # rubocop:disable-next Metrics/ClassLength
32
40
  class RmObjectBuilder
33
- # Map rm_type to the corresponding OpenEHR::RM class.
41
+ # The RM's "any ACTION archetype" pattern, injected because the graph
42
+ # cannot carry ACTIVITY.action_archetype_id yet (see class comment).
43
+ DEFAULT_ACTION_ARCHETYPE_ID = '/.*/'
44
+
45
+ # Map rm_type to the corresponding OpenEHR::RM class. ACTION,
46
+ # ITEM_SINGLE and ITEM_TABLE are persisted by GraphBuilder but not
47
+ # rebuilt here yet (UnsupportedRmTypeError; #44 skips them in AQL).
34
48
  TYPE_CLASSES = {
49
+ 'SECTION' => OpenEHR::RM::Composition::Content::Navigation::Section,
35
50
  'OBSERVATION' => OpenEHR::RM::Composition::Content::Entry::Observation,
36
51
  'EVALUATION' => OpenEHR::RM::Composition::Content::Entry::Evaluation,
52
+ 'INSTRUCTION' => OpenEHR::RM::Composition::Content::Entry::Instruction,
53
+ 'ACTIVITY' => OpenEHR::RM::Composition::Content::Entry::Activity,
37
54
  'ADMIN_ENTRY' => OpenEHR::RM::Composition::Content::Entry::AdminEntry,
38
55
  'HISTORY' => OpenEHR::RM::DataStructures::History::History,
39
56
  'POINT_EVENT' => OpenEHR::RM::DataStructures::History::PointEvent,
@@ -90,68 +107,125 @@ module OpenehrRails
90
107
  TYPE_CLASSES.fetch(node.rm_type) { raise UnsupportedRmTypeError.new(@composition, node) }
91
108
  end
92
109
 
110
+ # One helper per stored node type; each adds the attributes the
111
+ # corresponding RM constructor needs, injecting the documented defaults
112
+ # where the graph carries nothing.
93
113
  def build_node(node)
94
114
  klass = node_class(node)
95
115
  attrs = {
96
116
  archetype_node_id: node.archetype_node_id,
97
117
  name: dv_text(node.name_value || node.archetype_node_id)
98
118
  }
119
+ attrs.merge!(entry_attributes(node)) if node.is_a?(OpenehrRails::Rm::EntryNode)
120
+ attrs.merge!(section_attributes(node)) if node.is_a?(OpenehrRails::Rm::Section)
121
+ attrs.merge!(instruction_attributes(node)) if node.is_a?(OpenehrRails::Rm::Instruction)
122
+ attrs.merge!(activity_attributes(node)) if node.is_a?(OpenehrRails::Rm::Activity)
123
+ attrs.merge!(history_attributes(node)) if node.is_a?(OpenehrRails::Rm::History)
124
+ attrs.merge!(event_attributes(node)) if event?(node)
125
+ attrs[:items] = child_nodes(node) if item_container?(node)
126
+ attrs.merge!(element_attributes(node)) if node.is_a?(OpenehrRails::Rm::Element)
127
+ klass.new(attrs)
128
+ end
99
129
 
100
- # Entry-specific mandatory attributes
101
- if [OpenehrRails::Rm::Observation, OpenehrRails::Rm::Evaluation,
102
- OpenehrRails::Rm::AdminEntry].any? { |c| node.is_a?(c) }
103
- attrs.merge!(
104
- language: code_phrase(OpenehrRails.default_language, 'ISO_639-1'),
105
- encoding: code_phrase(OpenehrRails.default_encoding, 'IANA_character-sets'),
106
- subject: party_self
107
- )
108
- end
130
+ # ENTRY: language / encoding / subject are mandatory on every RM ENTRY
131
+ # and not stored, so the config defaults are injected; `data` for the
132
+ # entries that have one; `protocol` for every CARE_ENTRY that stored one
133
+ # (#45 -- INSTRUCTION keeps its requester/receiver there).
134
+ def entry_attributes(node)
135
+ attrs = {
136
+ language: code_phrase(OpenehrRails.default_language, 'ISO_639-1'),
137
+ encoding: code_phrase(OpenehrRails.default_encoding, 'IANA_character-sets'),
138
+ subject: party_self
139
+ }
140
+ data = child_node(node, 'data')
141
+ attrs[:data] = build_node(data) if data
142
+ protocol = child_node(node, 'protocol')
143
+ attrs[:protocol] = build_node(protocol) if protocol
144
+ attrs
145
+ end
109
146
 
110
- # Entry-specific: add data from first child
111
- if node.is_a?(OpenehrRails::Rm::EntryNode)
112
- data_child = node.children.find { |c| c.rm_attribute_name == 'data' }
113
- attrs[:data] = build_node(data_child) if data_child
114
- end
147
+ # SECTION.items must be nil or non-empty for the RM constructor.
148
+ def section_attributes(node)
149
+ items = child_nodes(node)
150
+ items.empty? ? {} : { items: items }
151
+ end
115
152
 
116
- # History-specific: origin is mandatory
117
- if node.is_a?(OpenehrRails::Rm::History)
118
- attrs[:origin] = dv_date_time(
119
- node.history_origin || node.composition.context_start_time || Time.current
120
- )
121
- attrs[:events] = node.children.sort_by(&:position).map { |c| build_node(c) }
122
- end
153
+ # INSTRUCTION.narrative is mandatory; the graph stores it as a
154
+ # `narrative` data-value row. When that row is absent the node's name
155
+ # stands in -- an approximation (#45 ruling condition (a)); phase B
156
+ # persists narrative as a column.
157
+ def instruction_attributes(node)
158
+ narrative = data_value_row(node, 'narrative')
159
+ attrs = { narrative: narrative ? dv_text(narrative.text_value) : dv_text(node.name_value || node.archetype_node_id) }
160
+ activities = child_nodes(node, 'activities')
161
+ attrs[:activities] = activities unless activities.empty?
162
+ expiry = data_value_row(node, 'expiry_time')
163
+ attrs[:expiry_time] = dv_date_time(expiry.datetime_value) if expiry&.datetime_value
164
+ attrs
165
+ end
123
166
 
124
- # Event-specific: time is mandatory
125
- if [OpenehrRails::Rm::PointEvent, OpenehrRails::Rm::IntervalEvent]
126
- .any? { |c| node.is_a?(c) }
127
- attrs[:time] = dv_date_time(node.event_time || Time.current)
128
- attrs[:data] = build_node(
129
- node.children.find { |c| c.rm_attribute_name == 'data' }
130
- )
131
- if node.is_a?(OpenehrRails::Rm::IntervalEvent)
132
- attrs[:width] = parse_duration(node.width || 'PT0S')
133
- attrs[:math_function] = code_phrase(node.math_function_code || '144', 'openehr')
134
- end
135
- end
167
+ # ACTIVITY.description is mandatory and never defaulted: a graph without
168
+ # one fails RM conformance (the gem raises, #44 reports and skips).
169
+ # action_archetype_id is mandatory too but the graph cannot carry it
170
+ # (GraphBuilder drops String-valued attributes), so the RM's "any ACTION
171
+ # archetype" pattern is injected -- an approximation (#45 ruling
172
+ # condition (a)); phase B persists the real value.
173
+ def activity_attributes(node)
174
+ attrs = { action_archetype_id: DEFAULT_ACTION_ARCHETYPE_ID }
175
+ description = child_node(node, 'description')
176
+ attrs[:description] = build_node(description) if description
177
+ timing = data_value_row(node, 'timing')
178
+ attrs[:timing] = dv_parsable(timing.text_value, timing.formalism) if timing
179
+ attrs
180
+ end
136
181
 
137
- # ItemStructure: items child list
138
- if [OpenehrRails::Rm::ItemTree, OpenehrRails::Rm::ItemList]
139
- .any? { |c| node.is_a?(c) }
140
- attrs[:items] = node.children.sort_by(&:position).map { |c| build_node(c) }
141
- end
182
+ # HISTORY.origin is mandatory.
183
+ def history_attributes(node)
184
+ {
185
+ origin: dv_date_time(node.history_origin || node.composition.context_start_time || Time.current),
186
+ events: child_nodes(node)
187
+ }
188
+ end
142
189
 
143
- # Cluster: items child list
144
- if node.is_a?(OpenehrRails::Rm::Cluster)
145
- attrs[:items] = node.children.sort_by(&:position).map { |c| build_node(c) }
146
- end
190
+ def event?(node)
191
+ node.is_a?(OpenehrRails::Rm::PointEvent) || node.is_a?(OpenehrRails::Rm::IntervalEvent)
192
+ end
147
193
 
148
- # Element: value leaf
149
- if node.is_a?(OpenehrRails::Rm::Element)
150
- dv = node.data_values.first
151
- attrs[:value] = build_data_value(dv) if dv
194
+ # EVENT.time and .data are mandatory; INTERVAL_EVENT adds width and
195
+ # math_function.
196
+ def event_attributes(node)
197
+ attrs = { time: dv_date_time(node.event_time || Time.current), data: build_node(child_node(node, 'data')) }
198
+ if node.is_a?(OpenehrRails::Rm::IntervalEvent)
199
+ attrs[:width] = parse_duration(node.width || 'PT0S')
200
+ attrs[:math_function] = code_phrase(node.math_function_code || '144', 'openehr')
152
201
  end
202
+ attrs
203
+ end
153
204
 
154
- klass.new(attrs)
205
+ def item_container?(node)
206
+ node.is_a?(OpenehrRails::Rm::ItemTree) || node.is_a?(OpenehrRails::Rm::ItemList) ||
207
+ node.is_a?(OpenehrRails::Rm::Cluster)
208
+ end
209
+
210
+ def element_attributes(node)
211
+ dv = node.data_values.first
212
+ dv ? { value: build_data_value(dv) } : {}
213
+ end
214
+
215
+ def child_node(node, attribute)
216
+ node.children.find { |c| c.rm_attribute_name == attribute }
217
+ end
218
+
219
+ # Children in position order, optionally restricted to one RM attribute
220
+ # (INSTRUCTION has `activities` and `protocol` children side by side).
221
+ def child_nodes(node, attribute = nil)
222
+ children = node.children.sort_by(&:position)
223
+ children = children.select { |c| c.rm_attribute_name == attribute } if attribute
224
+ children.map { |c| build_node(c) }
225
+ end
226
+
227
+ def data_value_row(node, attribute)
228
+ node.data_values.find { |dv| dv.rm_attribute_name == attribute }
155
229
  end
156
230
 
157
231
  def build_data_value(dv)
@@ -202,6 +276,10 @@ module OpenehrRails
202
276
  OpenEHR::RM::DataTypes::Text::DvText.new(value: value.to_s)
203
277
  end
204
278
 
279
+ def dv_parsable(value, formalism)
280
+ OpenEHR::RM::DataTypes::Encapsulated::DvParsable.new(value: value.to_s, formalism: formalism)
281
+ end
282
+
205
283
  def dv_coded_text(value, code, terminology)
206
284
  OpenEHR::RM::DataTypes::Text::DvCodedText.new(
207
285
  value: value,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenehrRails
4
- VERSION = '0.7.1'
4
+ VERSION = '0.8.0'
5
5
  end
@@ -57,28 +57,28 @@ describe OpenehrRails::Aql::DatasetAdapter do
57
57
 
58
58
  # skoba/openehr-rails#44, resolution shape (a) bug: one composition whose
59
59
  # graph cannot be rebuilt as RM objects used to fail every query in the
60
- # store. Synthetic canonical hash (invented ids): a SECTION is accepted by
61
- # GraphBuilder (Rm::TypeMap::NODE_TYPES) but not by RmObjectBuilder::
62
- # TYPE_CLASSES, so it is the smallest graph whose to_rm fails today; #45
63
- # makes SECTION buildable later, at which point this spec needs a different
64
- # unsupported type (ITEM_TABLE is the next candidate).
60
+ # store. Synthetic canonical hash (invented ids): an ITEM_TABLE as an
61
+ # EVALUATION's data is accepted by GraphBuilder (Rm::TypeMap::NODE_TYPES)
62
+ # but not by RmObjectBuilder::TYPE_CLASSES. (It was a SECTION until #45
63
+ # made SECTION buildable -- ruling condition (c).)
65
64
  describe 'a composition whose to_rm fails' do
66
- let(:section_hash) do
65
+ let(:table_hash) do
67
66
  {
68
67
  '_type' => 'COMPOSITION',
69
- 'archetype_node_id' => 'openEHR-EHR-COMPOSITION.synthetic_section_test.v1',
68
+ 'archetype_node_id' => 'openEHR-EHR-COMPOSITION.synthetic_table_test.v1',
70
69
  'archetype_details' => {
71
70
  '_type' => 'ARCHETYPED',
72
- 'archetype_id' => { 'value' => 'openEHR-EHR-COMPOSITION.synthetic_section_test.v1' },
73
- 'template_id' => { 'value' => 'synthetic_section_test' },
71
+ 'archetype_id' => { 'value' => 'openEHR-EHR-COMPOSITION.synthetic_table_test.v1' },
72
+ 'template_id' => { 'value' => 'synthetic_table_test' },
74
73
  'rm_version' => '1.0.4'
75
74
  },
76
75
  'content' => [
77
76
  {
78
- '_type' => 'SECTION',
79
- 'archetype_node_id' => 'openEHR-EHR-SECTION.synthetic_test.v1',
80
- 'name' => { '_type' => 'DV_TEXT', 'value' => 'Synthetic section' },
81
- 'items' => []
77
+ '_type' => 'EVALUATION',
78
+ 'archetype_node_id' => 'openEHR-EHR-EVALUATION.synthetic_table_test.v1',
79
+ 'archetype_details' => { 'archetype_id' => { 'value' => 'openEHR-EHR-EVALUATION.synthetic_table_test.v1' } },
80
+ 'name' => { '_type' => 'DV_TEXT', 'value' => 'Synthetic table' },
81
+ 'data' => { '_type' => 'ITEM_TABLE', 'archetype_node_id' => 'at0001', 'rows' => [] }
82
82
  }
83
83
  ]
84
84
  }
@@ -87,18 +87,18 @@ describe OpenehrRails::Aql::DatasetAdapter do
87
87
  before do
88
88
  BmiCalculation.create!(height: 170.0, ehr_id: 'ehr-mixed')
89
89
  ehr = OpenehrRails::Rm::Ehr.find_by!(ehr_id: 'ehr-mixed')
90
- OpenehrRails::Rm::CompositionCommitter.commit(section_hash, uid: 'uid-section', ehr: ehr)
90
+ OpenehrRails::Rm::CompositionCommitter.commit(table_hash, uid: 'uid-table', ehr: ehr)
91
91
  end
92
92
 
93
93
  it 'warns, skips that composition, and keeps the rest of the EHR queryable' do
94
94
  records = nil
95
95
 
96
96
  expect { records = described_class.build.each_ehr.to_a }
97
- .to output(/uid-section.*UnsupportedRmTypeError.*SECTION/).to_stderr
97
+ .to output(/uid-table.*UnsupportedRmTypeError.*ITEM_TABLE/).to_stderr
98
98
 
99
99
  matching = records.find { |r| r.ehr_id == 'ehr-mixed' }
100
100
  expect(matching.compositions.size).to eq(1)
101
- expect(matching.compositions.map { |c| c.uid.value }).not_to include('uid-section')
101
+ expect(matching.compositions.map { |c| c.uid.value }).not_to include('uid-table')
102
102
  end
103
103
  end
104
104
  end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'openehr'
5
+ require 'openehr/aql'
6
+ require 'openehr_rails'
7
+ require_relative '../storable_spec_model'
8
+
9
+ # Regression pin (skoba/openehr-rails#45, ruling condition (b)): the four AQL
10
+ # queries the anlage demo runs (anlage docs/demo/aql-queries.md 1-4) must keep
11
+ # returning the same rows after RmObjectBuilder started rebuilding `protocol`,
12
+ # SECTION, INSTRUCTION and ACTIVITY. Resolution shape (c): these results were
13
+ # already true before #45 and red is not achievable -- this file pins them.
14
+ #
15
+ # Data: query 1 and 3 run on the BmiCalculation model (real bmi_calculation.opt
16
+ # graph). Queries 2 and 4 need a problem_diagnosis EVALUATION, for which this
17
+ # repo has no model, so a **synthetic** canonical hash is committed directly:
18
+ # hand-authored, but its archetype id and at-codes are the real ones from
19
+ # spec/templates/problem_list.opt (at0002 diagnosis, at0073 certainty with the
20
+ # local code at0076, at0003 date/time recognised) so the demo queries run
21
+ # verbatim. Design authority: docs/design/rm-object-builder-section-instruction-plan.md
22
+ # section 3.
23
+ describe OpenehrRails::Aql::Executor, '.execute' do
24
+ # anlage demo AQL queries (regression pin) -- see the file comment above.
25
+ let(:problem_diagnosis_hash) do
26
+ {
27
+ '_type' => 'COMPOSITION',
28
+ 'archetype_node_id' => 'openEHR-EHR-COMPOSITION.problem_list.v1',
29
+ 'archetype_details' => {
30
+ '_type' => 'ARCHETYPED',
31
+ 'archetype_id' => { 'value' => 'openEHR-EHR-COMPOSITION.problem_list.v1' },
32
+ 'template_id' => { 'value' => 'problem_list' }, 'rm_version' => '1.0.4'
33
+ },
34
+ 'name' => { '_type' => 'DV_TEXT', 'value' => 'Problem list' },
35
+ 'content' => [
36
+ {
37
+ '_type' => 'EVALUATION', 'archetype_node_id' => 'openEHR-EHR-EVALUATION.problem_diagnosis.v1',
38
+ 'archetype_details' => { 'archetype_id' => { 'value' => 'openEHR-EHR-EVALUATION.problem_diagnosis.v1' } },
39
+ 'name' => { '_type' => 'DV_TEXT', 'value' => 'Problem/Diagnosis' },
40
+ 'data' => {
41
+ '_type' => 'ITEM_TREE', 'archetype_node_id' => 'at0001',
42
+ 'items' => [
43
+ { '_type' => 'ELEMENT', 'archetype_node_id' => 'at0002',
44
+ 'value' => { '_type' => 'DV_TEXT', 'value' => 'Hypertension' } },
45
+ { '_type' => 'ELEMENT', 'archetype_node_id' => 'at0073',
46
+ 'value' => { '_type' => 'DV_CODED_TEXT', 'value' => 'Confirmed',
47
+ 'defining_code' => { 'terminology_id' => { 'value' => 'local' }, 'code_string' => 'at0076' } } },
48
+ { '_type' => 'ELEMENT', 'archetype_node_id' => 'at0003',
49
+ 'value' => { '_type' => 'DV_DATE_TIME', 'value' => '2026-02-01T00:00:00' } }
50
+ ]
51
+ }
52
+ }
53
+ ]
54
+ }
55
+ end
56
+
57
+ before do
58
+ BmiCalculation.create!(height: 170.0)
59
+ BmiCalculation.create!(height: 180.0)
60
+ OpenehrRails::Rm::CompositionCommitter.commit(problem_diagnosis_hash, uid: 'uid-demo-problem')
61
+ end
62
+
63
+ it '1. height inequality (alias in SELECT, path in WHERE)' do
64
+ query = 'SELECT o/data[at0001]/events[at0002]/data[at0003]/items[at0004]/value/magnitude AS height ' \
65
+ 'FROM EHR e CONTAINS COMPOSITION c CONTAINS OBSERVATION o[openEHR-EHR-OBSERVATION.height.v2] ' \
66
+ 'WHERE o/data[at0001]/events[at0002]/data[at0003]/items[at0004]/value/magnitude > 170'
67
+
68
+ expect(OpenehrRails::Aql.execute(query).rows).to eq([[180.0]])
69
+ end
70
+
71
+ it '2. MATCHES against a literal value list on the certainty label' do
72
+ query = 'SELECT c/name/value AS composition_name, o/data[at0001]/items[at0073]/value/value AS certainty ' \
73
+ 'FROM EHR e CONTAINS COMPOSITION c CONTAINS EVALUATION o[openEHR-EHR-EVALUATION.problem_diagnosis.v1] ' \
74
+ "WHERE o/data[at0001]/items[at0073]/value/value MATCHES {'Confirmed', 'Suspected'}"
75
+
76
+ expect(OpenehrRails::Aql.execute(query).rows).to eq([['Problem list', 'Confirmed']])
77
+ end
78
+
79
+ it '3. CONTAINS nodePredicate with WHERE EXISTS' do
80
+ query = 'SELECT c/name/value AS composition_name ' \
81
+ 'FROM EHR e CONTAINS COMPOSITION c CONTAINS OBSERVATION o[openEHR-EHR-OBSERVATION.height.v2] ' \
82
+ 'CONTAINS ELEMENT el[at0004] WHERE EXISTS el/value/magnitude'
83
+
84
+ expect(OpenehrRails::Aql.execute(query).rows)
85
+ .to eq([['openEHR-EHR-COMPOSITION.report-result.v1'], ['openEHR-EHR-COMPOSITION.report-result.v1']])
86
+ end
87
+
88
+ it '4. date range WHERE on an ELEMENT-held date/time' do
89
+ query = 'SELECT o/data[at0001]/items[at0003]/value/value AS recognized_at ' \
90
+ 'FROM EHR e CONTAINS COMPOSITION c CONTAINS EVALUATION o[openEHR-EHR-EVALUATION.problem_diagnosis.v1] ' \
91
+ 'WHERE o/data[at0001]/items[at0003]/value/value >= "2026-01-01T00:00:00"'
92
+
93
+ rows = OpenehrRails::Aql.execute(query).rows
94
+
95
+ # Measured: the naive input is parsed in the process's local zone and
96
+ # comes back as UTC (`2026-01-31T15:00:00Z` under JST) -- pinned as the
97
+ # same instant, not as the literal string.
98
+ expect(rows.size).to eq(1)
99
+ expect(Time.iso8601(rows.first.first)).to eq(Time.parse('2026-02-01T00:00:00').utc)
100
+ end
101
+ end
@@ -104,24 +104,64 @@ describe OpenehrRails::Aql::Executor do
104
104
  describe 'with a composition whose to_rm fails in the store' do
105
105
  before do
106
106
  BmiCalculation.create!(height: 170.0)
107
+ # ITEM_TABLE stays outside RmObjectBuilder::TYPE_CLASSES after #45
108
+ # made SECTION buildable (ruling condition (c)).
107
109
  OpenehrRails::Rm::CompositionCommitter.commit(
108
110
  {
109
111
  '_type' => 'COMPOSITION',
110
- 'archetype_node_id' => 'openEHR-EHR-COMPOSITION.synthetic_section_test.v1',
112
+ 'archetype_node_id' => 'openEHR-EHR-COMPOSITION.synthetic_table_test.v1',
111
113
  'content' => [
112
- { '_type' => 'SECTION', 'archetype_node_id' => 'openEHR-EHR-SECTION.synthetic_test.v1', 'items' => [] }
114
+ {
115
+ '_type' => 'EVALUATION', 'archetype_node_id' => 'openEHR-EHR-EVALUATION.synthetic_table_test.v1',
116
+ 'archetype_details' => { 'archetype_id' => { 'value' => 'openEHR-EHR-EVALUATION.synthetic_table_test.v1' } },
117
+ 'data' => { '_type' => 'ITEM_TABLE', 'archetype_node_id' => 'at0001', 'rows' => [] }
118
+ }
113
119
  ]
114
120
  },
115
- uid: 'uid-section-exec'
121
+ uid: 'uid-table-exec'
116
122
  )
117
123
  end
118
124
 
119
125
  it 'still answers a query on another template, skipping the broken composition' do
120
126
  rows = nil
121
127
 
122
- expect { rows = described_class.execute(height_query).rows }.to output(/uid-section-exec/).to_stderr
128
+ expect { rows = described_class.execute(height_query).rows }.to output(/uid-table-exec/).to_stderr
123
129
 
124
130
  expect(rows).to eq([[170.0]])
125
131
  end
126
132
  end
133
+
134
+ # skoba/openehr-rails#45 (b) enhancement, end to end: values under
135
+ # INSTRUCTION.protocol / activities and inside a SECTION are reachable by
136
+ # AQL once the builder rebuilds those nodes. Same synthetic fixture as
137
+ # rm_object_builder_spec.rb.
138
+ describe 'INSTRUCTION and SECTION paths (#45)' do
139
+ before do
140
+ OpenehrRails::Rm::CompositionCommitter.commit(SyntheticReferralCanonicalHash.composition, uid: 'uid-referral-aql')
141
+ end
142
+
143
+ it 'reaches an ACTIVITY description leaf through i/activities[...]/description[...]' do
144
+ query = 'SELECT i/activities[at0001]/description[at0009]/items[at0121]/value/value ' \
145
+ 'FROM EHR e CONTAINS COMPOSITION c ' \
146
+ "CONTAINS INSTRUCTION i[#{SyntheticReferralCanonicalHash::INSTRUCTION_ID}]"
147
+
148
+ expect(described_class.execute(query).rows).to eq([['Chest pain work-up']])
149
+ end
150
+
151
+ it 'reaches a protocol leaf through i/protocol[...]' do
152
+ query = 'SELECT i/protocol[at0008]/items[at0010]/value/value ' \
153
+ 'FROM EHR e CONTAINS COMPOSITION c ' \
154
+ "CONTAINS INSTRUCTION i[#{SyntheticReferralCanonicalHash::INSTRUCTION_ID}]"
155
+
156
+ expect(described_class.execute(query).rows).to eq([['Synthetic Hospital']])
157
+ end
158
+
159
+ it 'reaches an EVALUATION nested in a SECTION' do
160
+ query = 'SELECT ev/data[at0001]/items[at0002]/value/value ' \
161
+ 'FROM EHR e CONTAINS COMPOSITION c ' \
162
+ "CONTAINS EVALUATION ev[#{SyntheticReferralCanonicalHash::EVALUATION_ID}]"
163
+
164
+ expect(described_class.execute(query).rows).to eq([['stable']])
165
+ end
166
+ end
127
167
  end
@@ -268,6 +268,45 @@ describe OpenehrRails::Fhir::FshGenerator do
268
268
  end
269
269
  end
270
270
 
271
+ # skoba/openehr-rails#49, resolution shape (a) bug: an entry with no leaves
272
+ # at all produced an archetype `code` rule plus an empty `component` slicing
273
+ # header (and, for a non-Observation resource, invalid FHIR). Any 0-leaf
274
+ # entry is now skipped and reported, whatever its resource. Synthetic
275
+ # entries (stubbed FieldExtractor, invented ids), as for #33/#38.
276
+ describe '0-leaf entries' do
277
+ let(:real_entries) { OpenehrRails::Opt::FieldExtractor.new(template).entries }
278
+ let(:empty_observation) do
279
+ real_entries.first.merge(archetype_id: 'openEHR-EHR-OBSERVATION.synthetic_empty_test.v1',
280
+ concept: 'synthetic_empty_test', fields: [])
281
+ end
282
+ let(:empty_instruction) do
283
+ real_entries.first.merge(archetype_id: 'openEHR-EHR-INSTRUCTION.synthetic_empty_test.v1',
284
+ rm_type: 'INSTRUCTION', concept: 'synthetic_empty_test', fields: [])
285
+ end
286
+
287
+ before do
288
+ extractor = instance_double(OpenehrRails::Opt::FieldExtractor,
289
+ entries: [empty_observation, empty_instruction, *real_entries])
290
+ allow(OpenehrRails::Opt::FieldExtractor).to receive(:new).with(template).and_return(extractor)
291
+ end
292
+
293
+ it 'emits nothing for them and keeps the real entries' do
294
+ expect(generator.to_fsh_files.keys).to eq(
295
+ %w[openehr-observation-height-v2 openehr-observation-body-weight-v2 openehr-observation-body-mass-index-v2]
296
+ )
297
+ end
298
+
299
+ it 'reports each as an UnsupportedProfileError that says the entry has no leaves' do
300
+ generator.to_fsh_files
301
+
302
+ expect(generator.skipped.map(&:archetype_id)).to eq(
303
+ %w[openEHR-EHR-OBSERVATION.synthetic_empty_test.v1 openEHR-EHR-INSTRUCTION.synthetic_empty_test.v1]
304
+ )
305
+ expect(generator.skipped.map(&:leaf_count)).to eq([0, 0])
306
+ expect(generator.skipped.map(&:message)).to all(include('no leaves'))
307
+ end
308
+ end
309
+
271
310
  describe '#skipped' do
272
311
  it 'is empty for an all-Observation template' do
273
312
  generator.to_fsh_files
@@ -282,6 +282,45 @@ describe OpenehrRails::Fhir::ProfileGenerator do
282
282
  end
283
283
  end
284
284
 
285
+ # skoba/openehr-rails#49, resolution shape (a) bug: an entry with no leaves
286
+ # at all produced an archetype `code` rule plus an empty `component` slicing
287
+ # header (and, for a non-Observation resource, invalid FHIR). Any 0-leaf
288
+ # entry is now skipped and reported, whatever its resource. Synthetic
289
+ # entries (stubbed FieldExtractor, invented ids), as for #33/#38. JSON facade half.
290
+ describe '0-leaf entries' do
291
+ let(:real_entries) { OpenehrRails::Opt::FieldExtractor.new(template).entries }
292
+ let(:empty_observation) do
293
+ real_entries.first.merge(archetype_id: 'openEHR-EHR-OBSERVATION.synthetic_empty_test.v1',
294
+ concept: 'synthetic_empty_test', fields: [])
295
+ end
296
+ let(:empty_instruction) do
297
+ real_entries.first.merge(archetype_id: 'openEHR-EHR-INSTRUCTION.synthetic_empty_test.v1',
298
+ rm_type: 'INSTRUCTION', concept: 'synthetic_empty_test', fields: [])
299
+ end
300
+
301
+ before do
302
+ extractor = instance_double(OpenehrRails::Opt::FieldExtractor,
303
+ entries: [empty_observation, empty_instruction, *real_entries])
304
+ allow(OpenehrRails::Opt::FieldExtractor).to receive(:new).with(template).and_return(extractor)
305
+ end
306
+
307
+ it 'emits nothing for them and keeps the real entries' do
308
+ expect(generator.profiles.map { |p| p[:id] }).to eq(
309
+ %w[openehr-observation-height-v2 openehr-observation-body-weight-v2 openehr-observation-body-mass-index-v2]
310
+ )
311
+ end
312
+
313
+ it 'reports each as an UnsupportedProfileError that says the entry has no leaves' do
314
+ generator.profiles
315
+
316
+ expect(generator.skipped.map(&:archetype_id)).to eq(
317
+ %w[openEHR-EHR-OBSERVATION.synthetic_empty_test.v1 openEHR-EHR-INSTRUCTION.synthetic_empty_test.v1]
318
+ )
319
+ expect(generator.skipped.map(&:leaf_count)).to eq([0, 0])
320
+ expect(generator.skipped.map(&:message)).to all(include('no leaves'))
321
+ end
322
+ end
323
+
285
324
  describe '#skipped' do
286
325
  it 'is empty for an all-Observation template' do
287
326
  generator.profiles
@@ -65,6 +65,26 @@ describe OpenehrRails::Opt::FieldExtractor do
65
65
  expect(diagnostic_status[:value_set_uri]).to be_nil
66
66
  expect(diagnostic_status[:code_list]).not_to be_empty
67
67
  end
68
+
69
+ # skoba/openehr-rails#52, resolution shape (b) enhancement: the value
70
+ # constraint's alternative RM types travel with the field, in OPT order,
71
+ # so a host app can tell an OR-constrained coded text (DV_TEXT allowed)
72
+ # from a coded-only one. rm_type selection is unchanged. Values measured
73
+ # on the fixture before the spec was written.
74
+ describe 'rm_type_alternatives' do
75
+ it 'lists every alternative of an OR-constrained value in OPT order' do
76
+ expect(field_for(fields, archetype_id, 'at0002')[:rm_type_alternatives]).to eq(%w[DV_TEXT DV_CODED_TEXT])
77
+ expect(field_for(fields, archetype_id, 'at0073')[:rm_type_alternatives]).to eq(%w[DV_CODED_TEXT DV_TEXT])
78
+ end
79
+
80
+ it 'lists the single type of a plain value' do
81
+ expect(field_for(fields, archetype_id, 'at0077')[:rm_type_alternatives]).to eq(%w[DV_DATE_TIME])
82
+ end
83
+
84
+ it 'leaves the chosen rm_type as before' do
85
+ expect(field_for(fields, archetype_id, 'at0002')[:rm_type]).to eq('DV_CODED_TEXT')
86
+ end
87
+ end
68
88
  end
69
89
 
70
90
  describe 'a hand-built template without terminology objects' do
@@ -93,7 +113,7 @@ describe OpenehrRails::Opt::FieldExtractor do
93
113
 
94
114
  # enhancement: additive binding keys remain safe for lightweight template doubles.
95
115
  expect(described_class.new(template).fields.first)
96
- .to include(value_set_uri: nil, code_bindings: [])
116
+ .to include(value_set_uri: nil, code_bindings: [], rm_type_alternatives: %w[DV_TEXT])
97
117
  end
98
118
  end
99
119
  end
@@ -77,21 +77,100 @@ describe OpenehrRails::Rm::RmObjectBuilder do
77
77
  # skoba/openehr-rails#44 (a) bug: a node type the graph stores but
78
78
  # TYPE_CLASSES lacks used to surface as `NoMethodError: undefined method
79
79
  # 'new' for nil`. The typed error names what the caller needs to act on.
80
+ # Since #45 SECTION is buildable, so the unsupported type here is an
81
+ # ITEM_TABLE as an EVALUATION's data (ruling condition (c)).
80
82
  it 'raises UnsupportedRmTypeError, naming composition and node, for an rm_type outside TYPE_CLASSES' do
81
83
  composition = OpenehrRails::Rm::CompositionCommitter.commit(
82
84
  {
83
85
  '_type' => 'COMPOSITION',
84
- 'archetype_node_id' => 'openEHR-EHR-COMPOSITION.synthetic_section_test.v1',
86
+ 'archetype_node_id' => 'openEHR-EHR-COMPOSITION.synthetic_table_test.v1',
85
87
  'content' => [
86
- { '_type' => 'SECTION', 'archetype_node_id' => 'openEHR-EHR-SECTION.synthetic_test.v1', 'items' => [] }
88
+ {
89
+ '_type' => 'EVALUATION', 'archetype_node_id' => 'openEHR-EHR-EVALUATION.synthetic_table_test.v1',
90
+ 'archetype_details' => { 'archetype_id' => { 'value' => 'openEHR-EHR-EVALUATION.synthetic_table_test.v1' } },
91
+ 'data' => { '_type' => 'ITEM_TABLE', 'archetype_node_id' => 'at0001', 'rows' => [] }
92
+ }
87
93
  ]
88
94
  },
89
- uid: 'uid-section-builder'
95
+ uid: 'uid-table-builder'
90
96
  )
91
97
 
92
98
  expect { composition.to_rm }.to raise_error(OpenehrRails::Rm::UnsupportedRmTypeError) { |error|
93
- expect(error.message).to include('uid-section-builder', 'SECTION', 'openEHR-EHR-SECTION.synthetic_test.v1')
94
- expect(error.rm_type).to eq('SECTION')
99
+ expect(error.message).to include('uid-table-builder', 'ITEM_TABLE', 'data[at0001]')
100
+ expect(error.rm_type).to eq('ITEM_TABLE')
95
101
  }
96
102
  end
103
+
104
+ # skoba/openehr-rails#45, resolution shape (b) enhancement: SECTION,
105
+ # INSTRUCTION and ACTIVITY nodes -- persisted by GraphBuilder all along --
106
+ # are rebuilt as RM objects. Fixture: spec/support/synthetic_referral_canonical_hash.rb
107
+ # (synthetic; design authority docs/design/rm-object-builder-section-instruction-plan.md
108
+ # section 3). Red before #45: UnsupportedRmTypeError on the SECTION.
109
+ describe 'SECTION, INSTRUCTION and ACTIVITY (#45)' do
110
+ let(:fixture) { SyntheticReferralCanonicalHash }
111
+ let(:rm) do
112
+ OpenehrRails::Rm::CompositionCommitter.commit(fixture.composition, uid: 'uid-referral').to_rm
113
+ end
114
+
115
+ it 'rebuilds a SECTION with its items' do
116
+ section = rm.content.first
117
+
118
+ expect(section).to be_a(OpenEHR::RM::Composition::Content::Navigation::Section)
119
+ expect(section.archetype_node_id).to eq(fixture::SECTION_ID)
120
+ expect(section.items.size).to eq(1)
121
+ expect(section.items.first).to be_a(OpenEHR::RM::Composition::Content::Entry::Evaluation)
122
+ expect(section.items.first.data.items.first.value.value).to eq('stable')
123
+ end
124
+
125
+ it 'rebuilds an INSTRUCTION with entry defaults, narrative, protocol and activities' do
126
+ instruction = rm.content.last
127
+
128
+ expect(instruction).to be_a(OpenEHR::RM::Composition::Content::Entry::Instruction)
129
+ expect(instruction.language.code_string).to eq(OpenehrRails.default_language)
130
+ expect(instruction.narrative.value).to eq('Refer to cardiology')
131
+ expect(instruction.protocol.items.first.value.value).to eq('Synthetic Hospital')
132
+ expect(instruction.activities.size).to eq(1)
133
+ end
134
+
135
+ it 'rebuilds an ACTIVITY with description, timing and the injected action_archetype_id' do
136
+ activity = rm.content.last.activities.first
137
+
138
+ expect(activity).to be_a(OpenEHR::RM::Composition::Content::Entry::Activity)
139
+ expect(activity.archetype_node_id).to eq('at0001')
140
+ expect(activity.description.items.first.value.value).to eq('Chest pain work-up')
141
+ expect(activity.timing).to be_a(OpenEHR::RM::DataTypes::Encapsulated::DvParsable)
142
+ expect(activity.timing.value).to eq('R1')
143
+ expect(activity.timing.formalism).to eq('timing')
144
+ # Approximation (ruling condition (a)): the graph does not carry
145
+ # action_archetype_id yet, so the RM's "any ACTION archetype" pattern is
146
+ # injected; phase B persists the real value.
147
+ expect(activity.action_archetype_id).to eq('/.*/')
148
+ end
149
+
150
+ # Approximation (ruling condition (a)): narrative is mandatory on the RM
151
+ # INSTRUCTION; when the graph has no narrative row, the node's name stands
152
+ # in. Phase B persists it as a column.
153
+ it 'injects the node name as narrative when the graph has none' do
154
+ composition = OpenehrRails::Rm::CompositionCommitter.commit(
155
+ fixture.composition(content: [fixture.instruction(narrative: false)]), uid: 'uid-referral-no-narrative'
156
+ )
157
+
158
+ expect(composition.to_rm.content.first.narrative.value).to eq('Request')
159
+ end
160
+
161
+ it 'builds a SECTION without items as items nil (the RM rejects an empty list)' do
162
+ composition = OpenehrRails::Rm::CompositionCommitter.commit(
163
+ fixture.composition(content: [fixture.section([])]), uid: 'uid-referral-empty-section'
164
+ )
165
+
166
+ expect(composition.to_rm.content.first.items).to be_nil
167
+ end
168
+
169
+ # Ruling condition (b): restoring `protocol` for every CARE_ENTRY must not
170
+ # change what an entry without a protocol node rebuilds to.
171
+ # Regression pin -- this was already true before #45.
172
+ it 'leaves protocol nil on an OBSERVATION that has no protocol node' do
173
+ expect(BmiCalculation.create!(height: 170.0).rm_graph.to_rm.content.first.protocol).to be_nil
174
+ end
175
+ end
97
176
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Fixture kind: **synthetic** (hand-authored, not derived from any real
4
+ # artifact). Design authority: docs/design/rm-object-builder-section-instruction-plan.md
5
+ # section 3 (skoba/openehr-rails#45). Every archetype id is self-evidently
6
+ # invented (`synthetic_*_test`); the at-codes mirror the *shape* of
7
+ # jp_referral's service_request (activities[at0001]/description[at0009],
8
+ # protocol[at0008]) so the AQL paths in the specs read like the real ones,
9
+ # but no real canonical JSON with SECTION/INSTRUCTION exists in this repo --
10
+ # anlage's hand-mapped composition lives outside it.
11
+ #
12
+ # Shape: COMPOSITION whose content is a SECTION holding one EVALUATION, and
13
+ # an INSTRUCTION with a narrative, a protocol ITEM_TREE and one ACTIVITY
14
+ # (description ITEM_TREE + DV_PARSABLE timing). Everything GraphBuilder
15
+ # accepts today; before #45 RmObjectBuilder could rebuild none of it.
16
+ module SyntheticReferralCanonicalHash
17
+ COMPOSITION_ID = 'openEHR-EHR-COMPOSITION.synthetic_referral_test.v1'
18
+ SECTION_ID = 'openEHR-EHR-SECTION.synthetic_details_test.v1'
19
+ EVALUATION_ID = 'openEHR-EHR-EVALUATION.synthetic_summary_test.v1'
20
+ INSTRUCTION_ID = 'openEHR-EHR-INSTRUCTION.synthetic_request_test.v1'
21
+
22
+ module_function
23
+
24
+ def dv_text(value)
25
+ { '_type' => 'DV_TEXT', 'value' => value }
26
+ end
27
+
28
+ def element(node_id, value)
29
+ { '_type' => 'ELEMENT', 'archetype_node_id' => node_id, 'name' => dv_text(node_id), 'value' => value }
30
+ end
31
+
32
+ def item_tree(node_id, items)
33
+ { '_type' => 'ITEM_TREE', 'archetype_node_id' => node_id, 'name' => dv_text(node_id), 'items' => items }
34
+ end
35
+
36
+ def archetyped(id)
37
+ { '_type' => 'ARCHETYPED', 'archetype_id' => { 'value' => id }, 'rm_version' => '1.0.4' }
38
+ end
39
+
40
+ def evaluation
41
+ {
42
+ '_type' => 'EVALUATION', 'archetype_node_id' => EVALUATION_ID,
43
+ 'archetype_details' => archetyped(EVALUATION_ID), 'name' => dv_text('Summary'),
44
+ 'data' => item_tree('at0001', [element('at0002', dv_text('stable'))])
45
+ }
46
+ end
47
+
48
+ def activity
49
+ {
50
+ '_type' => 'ACTIVITY', 'archetype_node_id' => 'at0001', 'name' => dv_text('Request'),
51
+ 'description' => item_tree('at0009', [element('at0121', dv_text('Chest pain work-up'))]),
52
+ 'timing' => { '_type' => 'DV_PARSABLE', 'value' => 'R1', 'formalism' => 'timing' }
53
+ }
54
+ end
55
+
56
+ # narrative: pass false to omit the narrative (the injected-default case).
57
+ def instruction(narrative: 'Refer to cardiology')
58
+ hash = {
59
+ '_type' => 'INSTRUCTION', 'archetype_node_id' => INSTRUCTION_ID,
60
+ 'archetype_details' => archetyped(INSTRUCTION_ID), 'name' => dv_text('Request'),
61
+ 'protocol' => item_tree('at0008', [element('at0010', dv_text('Synthetic Hospital'))]),
62
+ 'activities' => [activity]
63
+ }
64
+ hash['narrative'] = dv_text(narrative) if narrative
65
+ hash
66
+ end
67
+
68
+ def section(items = [evaluation])
69
+ { '_type' => 'SECTION', 'archetype_node_id' => SECTION_ID, 'name' => dv_text('Details'), 'items' => items }
70
+ end
71
+
72
+ def composition(content: [section, instruction])
73
+ {
74
+ '_type' => 'COMPOSITION', 'archetype_node_id' => COMPOSITION_ID,
75
+ 'archetype_details' => archetyped(COMPOSITION_ID).merge('template_id' => { 'value' => 'synthetic_referral_test' }),
76
+ 'name' => dv_text('Synthetic referral'),
77
+ 'content' => content
78
+ }
79
+ end
80
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openehr-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shinji KOBAYASHI
@@ -346,6 +346,7 @@ files:
346
346
  - spec/generators/templates/openEHR-EHR-OBSERVATION.blood_pressure.v1.adl
347
347
  - spec/models/openehr_template_spec.rb
348
348
  - spec/openehr_rails/aql/dataset_adapter_spec.rb
349
+ - spec/openehr_rails/aql/executor_execute_demo_queries_spec.rb
349
350
  - spec/openehr_rails/aql/executor_spec.rb
350
351
  - spec/openehr_rails/aql/model_api_spec.rb
351
352
  - spec/openehr_rails/aql/query_validator_spec.rb
@@ -383,6 +384,7 @@ files:
383
384
  - spec/openehr_rails/template_uploader_spec.rb
384
385
  - spec/spec_helper.rb
385
386
  - spec/support/active_record.rb
387
+ - spec/support/synthetic_referral_canonical_hash.rb
386
388
  - spec/templates/bmi_calculation_without_uid.opt
387
389
  - spec/templates/lab_result_report_reduced.opt
388
390
  - spec/templates/problem_list.opt