openehr-rails 0.7.0 → 0.7.1

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: 065a9ba44f893adaea9e6c0ccf6f3c5f8a0af8c8c3897379cd008e5642e99a7a
4
- data.tar.gz: 98f81ad0baf2b086e2193102c25804b3affcd2fa1ffb4c0730952d6a916e3a58
3
+ metadata.gz: cf0122af26f620b3082c006c5d469fdf3710c5dd1647b856c9024c8e4e3f7c66
4
+ data.tar.gz: b54d925956180cd9baffab027d42564ecd55864f839698952ed2bfbfdfb9356d
5
5
  SHA512:
6
- metadata.gz: 42353942db8a02b901e3727a85446fe1995f3c953fd33f3da2dfea68d7018b533de95b97494db856593df77ccbacd8d57f0f4bb4cdcedee086da6c4e44035a04
7
- data.tar.gz: 4fd7e793d2d77230e585dcf06204147926d3b36a86a576525614d19395930f80b4fe38bc0b534de5a0f6b26e98e0859d5c8275ce865a1efedf3aa942dc1d272e
6
+ metadata.gz: 90fadabcf5f3215e6c24f4c75f5bef98e38b831286e0f0da33d6d67ae6743937d7958d286384c85042830452e47fe08e22eca6dce72541ab7bfb5395cc029ef2
7
+ data.tar.gz: 700d4afc6e60805cc76c478035131d0aa62b5f0decc09d44c67f0ac1bb4e8181500de65ba56b40f2974a0e24259945fb26285a6b3eac0ebe3ca394656cdc1c56
data/CHANGELOG.md CHANGED
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.1] - 2026-09-25
11
+
12
+ > **Upgrade note.** AQL now skips a stored composition that holds an RM type
13
+ > `RmObjectBuilder` cannot rebuild, with a warning
14
+ > (`openehr-rails AQL: skipping composition uid=...`), instead of failing every
15
+ > query in the store. No host-app change is needed; if that line appears in
16
+ > your log, the composition is a #45 case (SECTION / INSTRUCTION / ACTIVITY
17
+ > read-back, next minor release).
18
+
19
+ ### Fixed
20
+ - A single-leaf entry mapped to a resource other than `Observation` and without a
21
+ `TypeMap::ENTRY_ELEMENT_MAPS` row no longer produces `<Resource>.value[x]`, which
22
+ `Condition`/`ServiceRequest`/`Procedure`/`Encounter` do not have; it is skipped and
23
+ reported through `#skipped` like the multi-leaf case since 0.7.0 (#38). The
24
+ `UnsupportedProfileError` message now names both missing elements.
25
+ - AQL no longer fails for the whole store when one stored composition cannot be
26
+ rebuilt as RM objects (a node type outside `RmObjectBuilder::TYPE_CLASSES`,
27
+ such as `SECTION`/`INSTRUCTION`, or an RM constructor rejecting stored data).
28
+ `DatasetAdapter` now warns (`Rails.logger` when present, else stderr), naming
29
+ the composition uid and the cause, and skips that composition; queries on the
30
+ rest of the store return their rows. `RmObjectBuilder` raises
31
+ `OpenehrRails::Rm::UnsupportedRmTypeError` (composition uid, node path,
32
+ rm_type) instead of an accidental `NoMethodError` (#44).
33
+
10
34
  ## [0.7.0] - 2026-09-10
11
35
 
12
36
  > **Upgrade note.** Regenerate `app/fhir/profiles/*.json`. `Condition.component`
data/docs/backlog.md CHANGED
@@ -74,6 +74,15 @@ that verification is deferred to the next real release (>= 0.5.0), not simulated
74
74
 
75
75
  ## Release automation
76
76
 
77
+ - **`release.yml` records no sha256 of the built `.gem`** (found at the 0.7.0
78
+ release, 2026-09-10, `docs/reports/fsh-generator-log.md` R14). The only digest in
79
+ the run log is `actions/upload-artifact`'s "SHA256 digest of uploaded artifact",
80
+ which hashes the uploaded archive, not the gem file, so `CLAUDE.md`'s "compare
81
+ against the value recorded for that run" cannot be followed literally; the
82
+ tag-rebuild cross-check is what pins the bytes today. Add a "Record sha256" step
83
+ after `rake build` (`sha256sum pkg/*.gem`), as openehr-ruby's `release.yml` has.
84
+ CI-only change; needs a PR, no Issue.
85
+
77
86
  **Done (2026-08-23, PR #28)** — kept below for the original rationale; see the CI
78
87
  status entry above for verification details.
79
88
 
@@ -122,6 +131,14 @@ status entry above for verification details.
122
131
 
123
132
  ## Fixture conventions
124
133
 
134
+ - **`spec/templates/sample_blood_pressure.opt` does not parse** -- `OpenehrRails::Opt.parse`
135
+ raises `ArgumentError: invalid archetype id form` from
136
+ `OpenEHR::RM::Support::Identification::ArchetypeID` under openehr 2.4.2 *and* 2.4.3
137
+ (measured 2026-09-10 while checking fixtures for #38), and no spec, script or
138
+ generator references the file. Orphan fixture: either find which archetype id in
139
+ it is malformed and decide whether that is an openehr-ruby parser gap worth an
140
+ upstream Issue, or delete the file. Not touched in 0.7.0.
141
+
125
142
  - **Licensed terminology-code literals in spec expectations: keep minimal, cite the
126
143
  source fixture's file:line.** Adopted from Anlage's C2 firewall precedent (SNOMED
127
144
  CT is a licensed terminology; reproducing its codes verbatim in more places than
@@ -365,3 +365,34 @@ Still **minor**, on top of §8.5: `#skipped` and `UnsupportedProfileError` are
365
365
  new public API; the `category` anchor and the `comment` change the JSON facade's
366
366
  shape again for `EVALUATION` entries. `CHANGELOG.md` `[Unreleased]` updated in
367
367
  the same change.
368
+
369
+ ## 9.7 #38 (2026-09-10 ruling): the decision point widened to any leaf count
370
+
371
+ The ruling on 9.5 allowed #38 to ride in 0.7.0 if it stayed on the same
372
+ decision point, small, and green. It does: `TypeMap.assert_supported!` drops
373
+ its `fields.size <= 1` early return, so *any* entry whose base resource is not
374
+ `Observation` and which has no `ENTRY_ELEMENT_MAPS` row is skipped and
375
+ reported -- a single leaf would have produced `<Resource>.value[x]`, which
376
+ `Condition` / `ServiceRequest` / `Procedure` / `Encounter` lack just as they
377
+ lack `component` (measured: 1 sushi Error on the 9.5 probe). The error message
378
+ now names both missing elements and the leaf count in the singular where it is
379
+ one. Resolution shape (a) bug.
380
+
381
+ - **Fixture check before widening**: every OPT under `spec/templates/`,
382
+ `spec/generators/templates/` and `demo_assets/templates/` was listed with
383
+ `FieldExtractor#entries` -- all single-leaf entries are `OBSERVATION`
384
+ (`height.v2`, `body_weight.v2`, `heart_rate-pulse.v1`); the only
385
+ non-`Observation` entry is `problem_diagnosis.v1` (5 leaves, mapped). So no
386
+ fixture's output changes. (`spec/templates/sample_blood_pressure.opt` does not
387
+ parse at all -- `ArgumentError: invalid archetype id form` -- under openehr
388
+ 2.4.2 *and* 2.4.3, and no spec references it; pre-existing, noted in
389
+ `docs/backlog.md`, not touched here.)
390
+ - **Red**: 4 new examples (two per generator, synthetic single-leaf
391
+ `EVALUATION` entry `openEHR-EHR-EVALUATION.synthetic_single_leaf_test.v1`):
392
+ **40 examples, 4 failures** across the two spec files.
393
+ - **Green**: the same files 40/0; `spec/openehr_rails/fhir/` 52/0; full suite
394
+ 308/0. Regenerated FSH for `problem_list.opt` + `bmi_calculation.opt`
395
+ byte-identical to before and **0 Errors** under `sushi` 3.16.0 -- the pin.
396
+ - **Semver**: 0.7.1 (0.7.0 shipped before PR #43 was merged; see log R13); a bug fix (patch) that changes no
397
+ fixture output, only what an unmapped single-leaf non-`Observation` entry
398
+ yields (invalid FHIR before, a `#skipped` entry now).
@@ -0,0 +1,134 @@
1
+ # Plan: `RmObjectBuilder` reads back SECTION / INSTRUCTION / ACTIVITY
2
+
3
+ - Status: **explore + plan, awaiting approval** (2026-09-25). No code written.
4
+ - Issue: [#45](https://github.com/skoba/openehr-rails/issues/45) (anlage upstream candidate 15)
5
+ - Log: `docs/reports/referral-upstream-log.md` R1
6
+ - Prerequisite landed as its own fix: #44 / PR #50 (a failing `to_rm` no longer
7
+ fails the whole store; unknown types raise `UnsupportedRmTypeError`).
8
+
9
+ ## 1. Current behaviour (measured, `file:line` on `master` after PR #50)
10
+
11
+ **Write side accepts the types.** `Rm::TypeMap::NODE_TYPES`
12
+ (`lib/openehr_rails/rm/type_map.rb:9-26`) lists SECTION, INSTRUCTION, ACTION,
13
+ ACTIVITY (and ITEM_SINGLE, ITEM_TABLE); the STI classes exist
14
+ (`lib/openehr_rails/rm/nodes.rb:10,14,15,18`). `GraphBuilder#infer_type`
15
+ (`lib/openehr_rails/rm/graph_builder.rb:111-122`) maps `activities` -> ACTIVITY and
16
+ `protocol` / `description` -> ITEM_TREE for `_type`-less hashes. `build_children`
17
+ (`:64-79`) persists Hash-valued attributes as child nodes or data-value rows and
18
+ Array-valued ones as ordered children; **String-valued attributes are dropped** --
19
+ so `ACTIVITY.action_archetype_id` never reaches the graph. `INSTRUCTION.narrative`
20
+ (DV_TEXT), `ACTIVITY.timing` (DV_PARSABLE: `text_value` + `formalism`, `:180-181`)
21
+ and `INSTRUCTION.expiry_time` (DV_DATE_TIME) are stored as data-value rows keyed by
22
+ `rm_attribute_name`.
23
+
24
+ **Read side does not.** `RmObjectBuilder::TYPE_CLASSES`
25
+ (`lib/openehr_rails/rm/rm_object_builder.rb`, 10 entries) has none of them;
26
+ `build_node` sets entry defaults only for Observation / Evaluation / AdminEntry,
27
+ `data` only for `EntryNode`, and never `protocol`, `narrative`, `activities`,
28
+ `description`. After PR #50 such a node raises `UnsupportedRmTypeError` and the
29
+ composition is skipped from AQL with a warning; before it, the whole store failed
30
+ (anlage referral-intake-log R6).
31
+
32
+ **What the openehr gem (2.4.3) demands of the objects** (`openehr-ruby`
33
+ `lib/openehr/rm/composition/content/`):
34
+
35
+ | class | mandatory | optional / notes |
36
+ |---|---|---|
37
+ | `Navigation::Section` (`navigation.rb:12-27`) | -- | `items` must be nil or non-empty; `path_attribute :items` |
38
+ | `Entry::Instruction` (`entry.rb:112-138`) | Entry: `language`, `encoding`, `subject`; `narrative` | `activities` nil or non-empty; `expiry_time`, `wf_definition`; CareEntry `protocol`, `guideline_id` (`entry.rb:69-78`); `path_attribute :activities, :protocol` |
39
+ | `Entry::Activity` (`entry.rb:140-171`) | `description`, `action_archetype_id` (non-empty String) | `timing` optional since RM 1.1.0; `path_attribute :description` |
40
+ | `Entry::Action` (`entry.rb:173-204`) | Entry attrs; `time`, `description`, `ism_transition` (with `current_state` validated against the openEHR terminology, `:236-275`) | out of scope here, see section 5 |
41
+
42
+ `OpenEHR::RM::Composition::Composition` has `path_attribute :content, :context`,
43
+ so objects built with the attributes above are navigable by the AQL engine's
44
+ path evaluator without engine changes.
45
+
46
+ ## 2. Design -- phase A, no schema change
47
+
48
+ 1. `TYPE_CLASSES` gains `SECTION`, `INSTRUCTION`, `ACTIVITY`.
49
+ 2. `build_node` gains, per type (each in its own small private helper, because
50
+ `build_node` already sits at rubocop's AbcSize/complexity limits -- PR #50 had to
51
+ extract `node_class` for a one-line change):
52
+ - **Section**: `items` = children built in position order; `nil` when there
53
+ are none (the constructor rejects `[]`).
54
+ - **Instruction**: the same entry defaults as the other entries
55
+ (`language` / `encoding` / `subject`; extend the `is_a?` list);
56
+ `narrative` = the `narrative` data-value row as DvText, **defaulting to the
57
+ node's name** when the row is absent (an injected default, documented like
58
+ `language`); `activities` = children with `rm_attribute_name == 'activities'`,
59
+ `nil` when none; `expiry_time` when its row exists.
60
+ - **Activity**: `description` = the `description` child (ITEM_TREE) -- no
61
+ default: a missing description is an RM-conformance failure and surfaces
62
+ through #44's warning; `timing` = the `timing` row as
63
+ `OpenEHR::RM::DataTypes::Encapsulated::DvParsable` when present;
64
+ `action_archetype_id` = the injected default `'/.*/'` (the RM's "any ACTION
65
+ archetype" pattern) because the graph cannot carry it yet (section 5).
66
+ - **`protocol` for every CareEntry** (Observation, Evaluation, Instruction):
67
+ `attrs[:protocol] = build_node(protocol child)` when present. Same
68
+ mechanism, and it is where `jp_referral` keeps 紹介先/紹介元
69
+ (`service_request` `protocol[at0008]`, anlage memory of 2026-09-25) -- without
70
+ it the INSTRUCTION would be readable but its most-queried data not.
71
+ 3. `UnsupportedRmTypeError` keeps covering ACTION, ITEM_SINGLE, ITEM_TABLE.
72
+
73
+ Injected defaults are listed in the class comment next to the existing ones.
74
+
75
+ ## 3. Spec plan (t-wada: red before green), resolution shape (b) enhancement
76
+
77
+ - **Fixture**: a spec-level synthetic canonical hash (invented ids, stated in the
78
+ comment with this section as design authority; no real canonical JSON with
79
+ SECTION/INSTRUCTION exists in this repo -- anlage's is hand-mapped and outside):
80
+ COMPOSITION whose `content` is `[SECTION { items: [EVALUATION { data: ITEM_TREE {
81
+ items: [ELEMENT DV_TEXT] } }] }, INSTRUCTION { narrative, protocol: ITEM_TREE {
82
+ items: [ELEMENT DV_TEXT] }, activities: [ACTIVITY { description: ITEM_TREE {
83
+ items: [ELEMENT DV_TEXT] }, timing: DV_PARSABLE }] }]`, committed with
84
+ `CompositionCommitter`.
85
+ - `rm_object_builder_spec.rb`: **red** today = `UnsupportedRmTypeError`; green =
86
+ `Section` with one `Evaluation` item; `Instruction` with the stored narrative,
87
+ one `Activity` whose `description.items.first.value` is the DvText,
88
+ `action_archetype_id == '/.*/'`, `protocol.items` present; entry defaults on the
89
+ Instruction.
90
+ - `executor_spec.rb`: AQL through the new objects --
91
+ `SELECT i/activities[at0001]/description[at0009]/items[at0121]/value/value ...
92
+ CONTAINS INSTRUCTION i[<synthetic id>]` and
93
+ `SELECT i/protocol[at0008]/items[at0010]/value/value`, plus
94
+ `... CONTAINS SECTION s[<id>] CONTAINS EVALUATION ev[<id>]`. Whether the
95
+ engine's CONTAINS chain accepts SECTION/ACTIVITY is **unknown until measured**;
96
+ if it does not, that becomes an openehr-ruby item and the spec pins the `i/...`
97
+ path form only.
98
+ - `dataset_adapter_spec.rb` / `executor_spec.rb` (#44): switch the "to_rm fails"
99
+ fixture from SECTION to ITEM_TABLE, which stays unsupported.
100
+ - Regression pins: `bmi_calculation` / `problem_list` specs unchanged; full suite
101
+ green; `CanonicalSerializer` round-trip of the new fixture is checked and, if it
102
+ does not survive (`narrative` / `timing` rows), recorded as its own issue rather
103
+ than fixed here.
104
+
105
+ ## 4. Size and schedule
106
+
107
+ Estimate: +3 map entries; ~50 lines of new helpers plus splitting `build_node`'s
108
+ existing per-type `if` blocks into helpers (~80 lines moved, behaviour-neutral,
109
+ needed to stay inside the rubocop limits); ~130 lines of specs across three files;
110
+ no migration, no generator template change, no dependency change. **Small** by the
111
+ ruling's criterion (same builder, one file of runtime code, existing test
112
+ infrastructure). Recommendation: **implement before the freeze**, in one PR,
113
+ `Fixes #45`, after this plan is approved.
114
+
115
+ ## 5. Out of scope (phase B, December unless pulled forward)
116
+
117
+ - **Persisting `action_archetype_id`**, `narrative`, `timing` as columns instead
118
+ of injected defaults / data-value rows: needs a migration in
119
+ `lib/generators/**/templates/` (shipped product, host apps migrate) -- its own
120
+ issue and plan.
121
+ - **ACTION**: `ism_transition` is persisted today as a generic CLUSTER node
122
+ (`infer_type` falls through), and rebuilding it needs codes valid against the
123
+ terminology; separate design.
124
+ - **ITEM_SINGLE / ITEM_TABLE**: no consumer yet.
125
+
126
+ ## 6. Semver
127
+
128
+ New read-side capability, observable through AQL and `to_rm`: **minor**. If it
129
+ lands with #38 (PR #43) and #44 (PR #50), the next release is **0.8.0**, not 0.7.1
130
+ -- to be decided at the inventory.
131
+
132
+ ## 7. Stop point
133
+
134
+ Explore + plan only. Implementation starts on approval.
@@ -923,3 +923,79 @@ retitled `[0.7.0] - 2026-09-10` with the Upgrade note, fresh empty
923
923
  `[Unreleased]`) -> tag `v0.7.0` -> `release.yml` -> artifact sha256 (R14) ->
924
924
  human `gem push` of that artifact -> confirmation by published checksum, the
925
925
  first application of the rule PR #42 added.
926
+
927
+ ## R14 -- v0.7.0 tagged, release.yml green, artifact sha256 recorded; gem push is the human's
928
+
929
+ - Release prep on `master` after `acb1e3f`: `b1bfaa3` (lint), `2212955` (R13),
930
+ `aafbd2c` (Release: bump version to 0.7.0). Before the release commit: full
931
+ suite **304 examples, 0 failures**, `rubocop` **109 files, no offenses**,
932
+ `release:check OK` (no tag yet, so the tag check was silent by design).
933
+ - Tag `v0.7.0` -> `aafbd2c`, pushed 2026-09-10. **Deviation noted**: this is a
934
+ lightweight tag; `v0.6.0` is an annotated tag object. Harmless for
935
+ `release.yml` (tag push) and `release:check` (`v0.7.0^{commit}`), but the
936
+ next release should use `git tag -a` to keep the series uniform.
937
+ - `release.yml` run **34438238454**: all 12 jobs success (9 spec matrix, demo
938
+ smoke, application template smoke, Build gem artifact). `release:check OK`
939
+ inside the run, i.e. HEAD was the tag commit.
940
+ - **Artifact**: downloaded with `gh run download 34438238454 -n gem -D pkg`:
941
+ `pkg/openehr-rails-0.7.0.gem`, 242176 bytes,
942
+ sha256 **`922ec940f15c8793ef6ee08568e4d7ea16f8a158da0d45936ff898e73cd1171a`**.
943
+ - **Cross-check**: a fresh `gem build` in a scratch worktree at `v0.7.0`
944
+ (local ruby 4.0.6, not `pkg/`) produced the **same sha256** -- CI artifact and
945
+ tag rebuild are byte-identical, as for 0.5.0 and 0.6.0.
946
+ - **Gap found in the comparison rule**: `CLAUDE.md` says to compare the
947
+ downloaded artifact "against the value recorded for that run", but
948
+ `release.yml` records no gem sha256 -- the only digest in the run log is
949
+ `actions/upload-artifact`'s `SHA256 digest of uploaded artifact`
950
+ (`671c8de1…238b`), which is the digest of the uploaded archive, **not of the
951
+ .gem**. The two are not comparable. The tag-rebuild cross-check above is what
952
+ actually pins the bytes today. Filed in `docs/backlog.md` "Release
953
+ automation": add a "Record sha256" step (as openehr-ruby's `release.yml`
954
+ has) so the rule can be followed literally.
955
+ - **Handed to the human**: `gem push pkg/openehr-rails-0.7.0.gem` (the
956
+ downloaded CI artifact, never a local build), then confirm by checksum per
957
+ the rule PR #42 added: `https://rubygems.org/api/v1/gems/openehr-rails.json`
958
+ `sha` (or the compact index `checksum:`) must equal `922ec940…171a`; use
959
+ `versions/openehr-rails/latest.json` rather than `versions/openehr-rails.json`
960
+ while the latter lags. Then delete `pkg/openehr-rails-0.7.0.gem`. Result goes
961
+ in R15.
962
+ - `master` is now past the tag by the docs commits that record this entry, so
963
+ `release:check` on `master` fails again by design until 0.7.1.
964
+
965
+ ## R15 -- 0.7.0 published; confirmed by checksum (first application of the PR #42 rule)
966
+
967
+ Human `gem push` of the CI artifact `pkg/openehr-rails-0.7.0.gem` (R14). Confirmation
968
+ 2026-09-10, every source compared against the CI artifact's sha256
969
+ `922ec940f15c8793ef6ee08568e4d7ea16f8a158da0d45936ff898e73cd1171a`:
970
+
971
+ | source | value |
972
+ |---|---|
973
+ | `/api/v1/gems/openehr-rails.json` `sha` | `922ec940…171a`, version 0.7.0 |
974
+ | compact index `https://index.rubygems.org/info/openehr-rails` | `checksum:922ec940…171a`, `created_at:2026-09-10T11:47:16Z` |
975
+ | `/api/v1/versions/openehr-rails/latest.json` | `{"version":"0.7.0"}` |
976
+ | `/api/v1/versions/openehr-rails.json` | 0.7.0, `sha` `922ec940…171a` (no lag observed this time) |
977
+ | `gem list -r -e openehr-rails` | 0.7.0 |
978
+
979
+ CI artifact, tag rebuild and published gem are one and the same bytes. The
980
+ `pkg/` copy was deleted after the confirmation, leaving `pkg/` empty.
981
+
982
+ ### Tooling note (why two commits on the #43 branch fix single characters)
983
+
984
+ The PowerShell -> `wsl` wrapper used to run this session's scripts carried a
985
+ `sed -i "s/\r$//"`; the `\r` reached WSL as a plain `r`, so every script line
986
+ ending in "r" lost that letter. Effects found and handled: `git checkout master`
987
+ silently became `maste` (harmless, caught by the error), a backlog line on the
988
+ #43 branch was committed as "script o" (restored in `566cddb`), and the
989
+ `5fcc0ec` commit message reads "unde" for "under" (left as is). `master`'s
990
+ tracked files were grepped for the same pattern: clean. The wrapper no longer
991
+ runs sed.
992
+
993
+ ### State after 0.7.0
994
+
995
+ - `master` = tag `v0.7.0` + docs (R14, R15). `release:check` fails on `master`
996
+ by design until the next release.
997
+ - PR #43 (`fix/38-single-leaf-non-observation`, Fixes #38): rebased for 0.7.1,
998
+ CHANGELOG line under the new `[Unreleased]`, awaiting the human's merge.
999
+ - Open follow-ups: `release.yml` "Record sha256" step (backlog, CI-only PR);
1000
+ next tag annotated (`git tag -a`); `json < 3` pins until Rails ships the
1001
+ `JSON.decode` fix (#40, backlog); reserves #35, #36, #37.
@@ -0,0 +1,64 @@
1
+ # Referral-v2 upstream batch log (anlage candidates 15-17 and siblings)
2
+
3
+ Work on `openehr-rails` driven by `skoba/anlage`'s `jp_referral` v0.1 findings
4
+ (`docs/upstream-candidates.md` items 15-17, 2026-09-25). R1〜.
5
+
6
+ ---
7
+
8
+ ## R1 -- filing, #44 fixed, #45 planned, #46-#49 filed (2026-09-25)
9
+
10
+ ### State on waking
11
+
12
+ `master` = `6b0e357` (v0.7.0 + R14/R15 docs). PR #43 (#38, 0.7.1) still open and
13
+ unmerged since 2026-09-10, CI green. Open issues before this batch: #35-#38.
14
+
15
+ ### 1. #44 -- AQL store-wide failure isolated per composition (PR #50)
16
+
17
+ - Measured cause: `DatasetAdapter` materialises every head composition with
18
+ `to_rm` inside the lazy chain (`dataset_adapter.rb:35,37`); `RmObjectBuilder#build_node`
19
+ did `TYPE_CLASSES[rm_type]` -> nil -> `NoMethodError` for SECTION / INSTRUCTION /
20
+ ACTIVITY / ACTION / ITEM_SINGLE / ITEM_TABLE, all of which `GraphBuilder`
21
+ persists (`Rm::TypeMap::NODE_TYPES`).
22
+ - Resolution shape (a) bug. **Red**: 23 examples, 3 failures (DatasetAdapter walk,
23
+ Executor end to end, `to_rm` directly) against a synthetic SECTION composition
24
+ committed beside a BmiCalculation record. **Green**: 23/0, full suite **307/0**
25
+ (304 + 3), rubocop clean (one extraction, `node_class`, to stay under
26
+ `build_node`'s AbcSize/complexity limits).
27
+ - Behaviour: `DatasetAdapter#materialize` rescues, reports through `Rails.logger`
28
+ when present else `Kernel#warn` (spec env has no `Rails.logger`, so the specs
29
+ pin stderr), skips the composition. `OpenehrRails::Rm::UnsupportedRmTypeError`
30
+ (composition_uid, rm_type, path) replaces the accidental `NoMethodError`.
31
+ - PR #50 (`dd157b6`, Fixes #44), CI run 36115865528. Semver patch; CHANGELOG
32
+ `[Unreleased]` Fixed.
33
+
34
+ ### 2. #45 -- SECTION / INSTRUCTION / ACTIVITY on the read side: explore + plan
35
+
36
+ `docs/design/rm-object-builder-section-instruction-plan.md`. Verdict: **small**
37
+ (phase A: 3 map entries, helper methods in one file, ~130 spec lines, no schema
38
+ change) -> recommended before the freeze, awaiting approval. Phase B (persist
39
+ `action_archetype_id`, ACTION, ITEM_SINGLE/TABLE) December. Note: minor, so a
40
+ release carrying it is 0.8.0.
41
+
42
+ ### 3. Filed only
43
+
44
+ - **#46** context (EVENT_CONTEXT) not persisted from canonical JSON (upstream 16).
45
+ - **#47** FieldExtractor does not descend `activities` / `description` / `protocol`
46
+ (upstream 17; `DESCENDABLE_ATTRIBUTES = %w[data events items value]`,
47
+ `field_extractor.rb:47`).
48
+ - **#48** DV_TEXT leaf on a CodeableConcept target -> `code only string` (invalid);
49
+ mapping row should carry the FHIR type.
50
+ - **#49** 0-leaf entry gets an empty component-header profile instead of
51
+ skip-and-report (#38 sibling; #43 covers non-Observation 0-leaf once merged,
52
+ Observation 0-leaf not).
53
+
54
+ ### 4. 0.7.1 inventory (candidates, not yet a gate)
55
+
56
+ | item | state | class |
57
+ |---|---|---|
58
+ | PR #43 (#38 single-leaf skip) | open, CI green, rebased on 0.7.0 | patch |
59
+ | PR #50 (#44 isolation) | open, CI running | patch |
60
+ | #45 phase A | plan awaiting approval | minor -> 0.8.0 |
61
+ | #46 / #47 / #48 / #49 | filed | -- |
62
+
63
+ Gate when the human has merged what should ride; version number decided from the
64
+ merged content (0.7.1 if patch-only, 0.8.0 if #45 rides).
@@ -32,14 +32,32 @@ module OpenehrRails
32
32
 
33
33
  def ehr_records
34
34
  linked = @ehr_scope.find_each.lazy.map do |ehr|
35
- { ehr_id: ehr.ehr_id, compositions: compositions_for(ehr_id: ehr.id).map(&:to_rm) }
35
+ { ehr_id: ehr.ehr_id, compositions: rm_compositions_for(ehr_id: ehr.id) }
36
36
  end
37
- unlinked = [{ ehr_id: nil, compositions: compositions_for(ehr_id: nil).map(&:to_rm) }]
37
+ unlinked = [{ ehr_id: nil, compositions: rm_compositions_for(ehr_id: nil) }]
38
38
  linked + unlinked.lazy
39
39
  end
40
40
 
41
- def compositions_for(ehr_id:)
42
- @composition_scope.where(ehr_id: ehr_id).find_each.lazy
41
+ def rm_compositions_for(ehr_id:)
42
+ @composition_scope.where(ehr_id: ehr_id).find_each.lazy.filter_map { |composition| materialize(composition) }
43
+ end
44
+
45
+ # One composition whose graph cannot be rebuilt (a node type outside
46
+ # RmObjectBuilder::TYPE_CLASSES, or an RM constructor rejecting stored
47
+ # data) must not take every query in the store down with it (#44):
48
+ # warn, naming the composition and the cause, and leave it out. The
49
+ # rest of the EHR stays queryable; the warning is the report.
50
+ def materialize(composition)
51
+ composition.to_rm
52
+ rescue StandardError => e
53
+ report_skip("openehr-rails AQL: skipping composition uid=#{composition.uid} (id=#{composition.id}): " \
54
+ "#{e.class}: #{e.message}")
55
+ nil
56
+ end
57
+
58
+ def report_skip(message)
59
+ logger = defined?(::Rails) && ::Rails.respond_to?(:logger) ? ::Rails.logger : nil
60
+ logger ? logger.warn(message) : warn(message)
43
61
  end
44
62
  end
45
63
  end
@@ -10,7 +10,7 @@ module OpenehrRails
10
10
  # the entry's RM type selects the base FHIR resource, and the leaf
11
11
  # ELEMENTs constrain value[x] (single leaf), component slices (many, on
12
12
  # Observation) or the real elements named by TypeMap::ENTRY_ELEMENT_MAPS.
13
- # Multi-leaf entries with neither are skipped and reported (#skipped).
13
+ # Entries with neither are skipped and reported (#skipped).
14
14
  class ProfileGenerator
15
15
  FHIR_VERSION = '5.0.0'
16
16
  ARCHETYPE_SYSTEM = 'http://openehr.org/ckm/archetypes'
@@ -91,13 +91,13 @@ module OpenehrRails
91
91
  end
92
92
 
93
93
  # The one shared decision point for both generators (#33 ruling, plan
94
- # section 9.3): an entry with more than one leaf whose base resource has no
95
- # `component` element and which has no row in ENTRY_ELEMENT_MAPS cannot be
96
- # profiled. Raises UnsupportedProfileError so the caller can skip and
97
- # report it instead of emitting constraints on a path the resource lacks.
94
+ # section 9.3; widened to any leaf count by #38, section 9.7): an entry
95
+ # whose base resource is not Observation -- so it has neither `value[x]`
96
+ # for a single leaf nor `component` for several -- and which has no row in
97
+ # ENTRY_ELEMENT_MAPS cannot be profiled. Raises UnsupportedProfileError so
98
+ # the caller can skip and report it instead of emitting constraints on a
99
+ # path the resource lacks.
98
100
  def assert_supported!(entry)
99
- return if entry[:fields].size <= 1
100
-
101
101
  resource_type = resource_for_entry(entry[:rm_type])
102
102
  return if resource_type == 'Observation' || element_map_for(entry[:archetype_id])
103
103
 
@@ -2,13 +2,14 @@
2
2
 
3
3
  module OpenehrRails
4
4
  module Fhir
5
- # Raised for an ENTRY that has more than one leaf, maps to a FHIR resource
6
- # without a `component` element (anything but Observation), and has no row
7
- # in TypeMap::ENTRY_ELEMENT_MAPS -- nothing valid can be generated for it
8
- # (skoba/openehr-rails#33 ruling, docs/design/multi-leaf-non-observation-plan.md
9
- # section 9.3). ProfileGenerator and FshGenerator rescue this per entry,
10
- # skip the entry and expose the error through #skipped; a caller that wants
11
- # a hard failure re-raises from there.
5
+ # Raised for an ENTRY that maps to a FHIR resource other than Observation --
6
+ # which has neither `value[x]` (single leaf) nor `component` (several) --
7
+ # and has no row in TypeMap::ENTRY_ELEMENT_MAPS: nothing valid can be
8
+ # generated for it (skoba/openehr-rails#33 ruling and #38,
9
+ # docs/design/multi-leaf-non-observation-plan.md sections 9.3 and 9.7).
10
+ # ProfileGenerator and FshGenerator rescue this per entry, skip the entry
11
+ # and expose the error through #skipped; a caller that wants a hard failure
12
+ # re-raises from there.
12
13
  class UnsupportedProfileError < StandardError
13
14
  attr_reader :archetype_id, :resource_type, :leaf_count
14
15
 
@@ -17,9 +18,10 @@ module OpenehrRails
17
18
  @resource_type = resource_type
18
19
  @leaf_count = leaf_count
19
20
  super(
20
- "#{archetype_id}: #{leaf_count} leaves map to #{resource_type}, which has no " \
21
- 'component element, and TypeMap::ENTRY_ELEMENT_MAPS has no row for this ' \
22
- 'archetype -- skipped (skoba/openehr-rails#33, #35)'
21
+ "#{archetype_id}: #{leaf_count} #{leaf_count == 1 ? 'leaf maps' : 'leaves map'} to " \
22
+ "#{resource_type}, which has neither value[x] nor component, and " \
23
+ 'TypeMap::ENTRY_ELEMENT_MAPS has no row for this archetype -- skipped ' \
24
+ '(skoba/openehr-rails#33, #38; mapping rows: #35, #37)'
23
25
  )
24
26
  end
25
27
  end
@@ -4,6 +4,26 @@ require 'openehr'
4
4
 
5
5
  module OpenehrRails
6
6
  module Rm
7
+ # Raised by RmObjectBuilder for a stored node whose rm_type the graph
8
+ # accepts (Rm::TypeMap::NODE_TYPES) but the read side cannot rebuild
9
+ # (RmObjectBuilder::TYPE_CLASSES) -- SECTION, INSTRUCTION, ACTIVITY,
10
+ # ACTION, ITEM_SINGLE, ITEM_TABLE today (#44; #45 narrows the gap).
11
+ # Names the composition and the node so a warning built from it is
12
+ # actionable.
13
+ class UnsupportedRmTypeError < StandardError
14
+ attr_reader :composition_uid, :rm_type, :path
15
+
16
+ def initialize(composition, node)
17
+ @composition_uid = composition.uid
18
+ @rm_type = node.rm_type
19
+ @path = node.path
20
+ super(
21
+ "composition uid=#{composition.uid}: node #{node.path} has rm_type #{node.rm_type}, " \
22
+ 'which RmObjectBuilder::TYPE_CLASSES cannot rebuild as an OpenEHR::RM object'
23
+ )
24
+ end
25
+ end
26
+
7
27
  # Builds a full OpenEHR::RM::Composition object from the stored
8
28
  # graph, injecting config defaults for mandatory attributes not
9
29
  # carried in OPT/stored data. The gem's own constructor validations
@@ -66,8 +86,12 @@ module OpenehrRails
66
86
  )
67
87
  end
68
88
 
89
+ def node_class(node)
90
+ TYPE_CLASSES.fetch(node.rm_type) { raise UnsupportedRmTypeError.new(@composition, node) }
91
+ end
92
+
69
93
  def build_node(node)
70
- klass = TYPE_CLASSES[node.rm_type]
94
+ klass = node_class(node)
71
95
  attrs = {
72
96
  archetype_node_id: node.archetype_node_id,
73
97
  name: dv_text(node.name_value || node.archetype_node_id)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenehrRails
4
- VERSION = '0.7.0'
4
+ VERSION = '0.7.1'
5
5
  end
@@ -54,4 +54,51 @@ describe OpenehrRails::Aql::DatasetAdapter do
54
54
  ensure
55
55
  ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
56
56
  end
57
+
58
+ # skoba/openehr-rails#44, resolution shape (a) bug: one composition whose
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).
65
+ describe 'a composition whose to_rm fails' do
66
+ let(:section_hash) do
67
+ {
68
+ '_type' => 'COMPOSITION',
69
+ 'archetype_node_id' => 'openEHR-EHR-COMPOSITION.synthetic_section_test.v1',
70
+ 'archetype_details' => {
71
+ '_type' => 'ARCHETYPED',
72
+ 'archetype_id' => { 'value' => 'openEHR-EHR-COMPOSITION.synthetic_section_test.v1' },
73
+ 'template_id' => { 'value' => 'synthetic_section_test' },
74
+ 'rm_version' => '1.0.4'
75
+ },
76
+ 'content' => [
77
+ {
78
+ '_type' => 'SECTION',
79
+ 'archetype_node_id' => 'openEHR-EHR-SECTION.synthetic_test.v1',
80
+ 'name' => { '_type' => 'DV_TEXT', 'value' => 'Synthetic section' },
81
+ 'items' => []
82
+ }
83
+ ]
84
+ }
85
+ end
86
+
87
+ before do
88
+ BmiCalculation.create!(height: 170.0, ehr_id: 'ehr-mixed')
89
+ ehr = OpenehrRails::Rm::Ehr.find_by!(ehr_id: 'ehr-mixed')
90
+ OpenehrRails::Rm::CompositionCommitter.commit(section_hash, uid: 'uid-section', ehr: ehr)
91
+ end
92
+
93
+ it 'warns, skips that composition, and keeps the rest of the EHR queryable' do
94
+ records = nil
95
+
96
+ expect { records = described_class.build.each_ehr.to_a }
97
+ .to output(/uid-section.*UnsupportedRmTypeError.*SECTION/).to_stderr
98
+
99
+ matching = records.find { |r| r.ehr_id == 'ehr-mixed' }
100
+ expect(matching.compositions.size).to eq(1)
101
+ expect(matching.compositions.map { |c| c.uid.value }).not_to include('uid-section')
102
+ end
103
+ end
57
104
  end
@@ -96,4 +96,32 @@ describe OpenehrRails::Aql::Executor do
96
96
  expect(OpenehrRails::Aql.execute(height_query).rows).to eq([[170.0]])
97
97
  end
98
98
  end
99
+
100
+ # skoba/openehr-rails#44 (a) bug, end to end: the store also holds a
101
+ # composition whose to_rm fails (synthetic SECTION graph, see
102
+ # dataset_adapter_spec.rb); a query on an unrelated template must still
103
+ # return its rows.
104
+ describe 'with a composition whose to_rm fails in the store' do
105
+ before do
106
+ BmiCalculation.create!(height: 170.0)
107
+ OpenehrRails::Rm::CompositionCommitter.commit(
108
+ {
109
+ '_type' => 'COMPOSITION',
110
+ 'archetype_node_id' => 'openEHR-EHR-COMPOSITION.synthetic_section_test.v1',
111
+ 'content' => [
112
+ { '_type' => 'SECTION', 'archetype_node_id' => 'openEHR-EHR-SECTION.synthetic_test.v1', 'items' => [] }
113
+ ]
114
+ },
115
+ uid: 'uid-section-exec'
116
+ )
117
+ end
118
+
119
+ it 'still answers a query on another template, skipping the broken composition' do
120
+ rows = nil
121
+
122
+ expect { rows = described_class.execute(height_query).rows }.to output(/uid-section-exec/).to_stderr
123
+
124
+ expect(rows).to eq([[170.0]])
125
+ end
126
+ end
99
127
  end
@@ -225,6 +225,49 @@ describe OpenehrRails::Fhir::FshGenerator do
225
225
  end
226
226
  end
227
227
 
228
+ # skoba/openehr-rails#38, resolution shape (a) bug: a single-leaf entry whose
229
+ # base resource is not Observation took the legacy `code` + `value[x]` path,
230
+ # and Condition has no value[x] either (sushi: 1 Error on a probe, plan
231
+ # section 9.5). Same decision point as the multi-leaf case, now regardless of
232
+ # leaf count. Synthetic entry for the same reason as above: no real
233
+ # single-leaf non-Observation fixture exists in this repo.
234
+ describe 'an unmapped single-leaf non-Observation entry' do
235
+ subject(:files) { generator.to_fsh_files }
236
+
237
+ let(:opt_file) { File.expand_path('../../templates/problem_list.opt', __dir__) }
238
+ let(:real_entries) { OpenehrRails::Opt::FieldExtractor.new(template).entries }
239
+ let(:synthetic_id) { 'openEHR-EHR-EVALUATION.synthetic_single_leaf_test.v1' }
240
+ let(:synthetic_entry) do
241
+ real = real_entries.first
242
+ real.merge(
243
+ archetype_id: synthetic_id,
244
+ concept: 'synthetic_single_leaf_test',
245
+ fields: real[:fields].first(1).map { |field| field.merge(archetype_id: synthetic_id) }
246
+ )
247
+ end
248
+
249
+ before do
250
+ extractor = instance_double(OpenehrRails::Opt::FieldExtractor, entries: [synthetic_entry, *real_entries])
251
+ allow(OpenehrRails::Opt::FieldExtractor).to receive(:new).with(template).and_return(extractor)
252
+ end
253
+
254
+ it 'skips the entry instead of constraining value[x] on Condition' do
255
+ expect(files.keys).to eq(['openehr-evaluation-problem-diagnosis-v1'])
256
+ expect(files.values.join).not_to include('value[x]')
257
+ end
258
+
259
+ it 'reports it through #skipped with the resource named' do
260
+ files
261
+ error = generator.skipped.first
262
+
263
+ expect(generator.skipped.size).to eq(1)
264
+ expect(error.archetype_id).to eq(synthetic_id)
265
+ expect(error.resource_type).to eq('Condition')
266
+ expect(error.leaf_count).to eq(1)
267
+ expect(error.message).to include('value[x]')
268
+ end
269
+ end
270
+
228
271
  describe '#skipped' do
229
272
  it 'is empty for an all-Observation template' do
230
273
  generator.to_fsh_files
@@ -241,6 +241,47 @@ describe OpenehrRails::Fhir::ProfileGenerator do
241
241
  end
242
242
  end
243
243
 
244
+ # skoba/openehr-rails#38, resolution shape (a) bug, JSON facade half: a
245
+ # single-leaf non-Observation entry produced `<Resource>.value[x]`, which
246
+ # Condition does not have. Synthetic entry as above.
247
+ describe 'an unmapped single-leaf non-Observation entry' do
248
+ subject(:profiles) { generator.profiles }
249
+
250
+ let(:opt_file) { File.expand_path('../../templates/problem_list.opt', __dir__) }
251
+ let(:real_entries) { OpenehrRails::Opt::FieldExtractor.new(template).entries }
252
+ let(:synthetic_id) { 'openEHR-EHR-EVALUATION.synthetic_single_leaf_test.v1' }
253
+ let(:synthetic_entry) do
254
+ real = real_entries.first
255
+ real.merge(
256
+ archetype_id: synthetic_id,
257
+ concept: 'synthetic_single_leaf_test',
258
+ fields: real[:fields].first(1).map { |field| field.merge(archetype_id: synthetic_id) }
259
+ )
260
+ end
261
+
262
+ before do
263
+ extractor = instance_double(OpenehrRails::Opt::FieldExtractor, entries: [synthetic_entry, *real_entries])
264
+ allow(OpenehrRails::Opt::FieldExtractor).to receive(:new).with(template).and_return(extractor)
265
+ end
266
+
267
+ it 'skips the entry instead of emitting Condition.value[x]' do
268
+ expect(profiles.map { |p| p[:id] }).to eq(['openehr-evaluation-problem-diagnosis-v1'])
269
+ expect(profiles.flat_map { |p| p[:differential][:element] }.map { |e| e[:path] })
270
+ .not_to include('Condition.value[x]')
271
+ end
272
+
273
+ it 'reports it through #skipped with the resource named' do
274
+ profiles
275
+ error = generator.skipped.first
276
+
277
+ expect(generator.skipped.size).to eq(1)
278
+ expect(error.archetype_id).to eq(synthetic_id)
279
+ expect(error.resource_type).to eq('Condition')
280
+ expect(error.leaf_count).to eq(1)
281
+ expect(error.message).to include('value[x]')
282
+ end
283
+ end
284
+
244
285
  describe '#skipped' do
245
286
  it 'is empty for an all-Observation template' do
246
287
  generator.profiles
@@ -73,4 +73,25 @@ describe OpenehrRails::Rm::RmObjectBuilder do
73
73
  expect(result).to be_a(OpenEHR::RM::Composition::Composition)
74
74
  expect(result.language.code_string).to eq(OpenehrRails.default_language)
75
75
  end
76
+
77
+ # skoba/openehr-rails#44 (a) bug: a node type the graph stores but
78
+ # TYPE_CLASSES lacks used to surface as `NoMethodError: undefined method
79
+ # 'new' for nil`. The typed error names what the caller needs to act on.
80
+ it 'raises UnsupportedRmTypeError, naming composition and node, for an rm_type outside TYPE_CLASSES' do
81
+ composition = OpenehrRails::Rm::CompositionCommitter.commit(
82
+ {
83
+ '_type' => 'COMPOSITION',
84
+ 'archetype_node_id' => 'openEHR-EHR-COMPOSITION.synthetic_section_test.v1',
85
+ 'content' => [
86
+ { '_type' => 'SECTION', 'archetype_node_id' => 'openEHR-EHR-SECTION.synthetic_test.v1', 'items' => [] }
87
+ ]
88
+ },
89
+ uid: 'uid-section-builder'
90
+ )
91
+
92
+ 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')
95
+ }
96
+ end
76
97
  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.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shinji KOBAYASHI
@@ -260,9 +260,11 @@ files:
260
260
  - docs/design/fix-terminology-scope-plan.md
261
261
  - docs/design/fsh-generator-plan.md
262
262
  - docs/design/multi-leaf-non-observation-plan.md
263
+ - docs/design/rm-object-builder-section-instruction-plan.md
263
264
  - docs/reports/binding-extraction-log.md
264
265
  - docs/reports/fsh-generator-log.md
265
266
  - docs/reports/institutionalization-log.md
267
+ - docs/reports/referral-upstream-log.md
266
268
  - gemfiles/rails_7_2.gemfile
267
269
  - gemfiles/rails_8_0.gemfile
268
270
  - gemfiles/rails_8_1.gemfile
@@ -410,7 +412,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
410
412
  - !ruby/object:Gem::Version
411
413
  version: '0'
412
414
  requirements: []
413
- rubygems_version: 4.0.16
415
+ rubygems_version: 4.0.20
414
416
  specification_version: 4
415
417
  summary: Rails extension for the openEHR archetypes
416
418
  test_files: []