openehr-rails 0.6.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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +99 -0
  3. data/CLAUDE.md +34 -2
  4. data/Gemfile +7 -0
  5. data/docs/backlog.md +39 -0
  6. data/docs/design/multi-leaf-non-observation-plan.md +219 -1
  7. data/docs/design/rm-object-builder-section-instruction-plan.md +134 -0
  8. data/docs/reports/fsh-generator-log.md +684 -0
  9. data/docs/reports/referral-upstream-log.md +64 -0
  10. data/lib/generators/openehr/fhir_profile/fhir_profile_generator.rb +5 -1
  11. data/lib/generators/openehr/scaffold/scaffold_generator.rb +5 -3
  12. data/lib/openehr_rails/aql/dataset_adapter.rb +22 -4
  13. data/lib/openehr_rails/fhir/fsh_generator.rb +56 -7
  14. data/lib/openehr_rails/fhir/profile_generator.rb +71 -2
  15. data/lib/openehr_rails/fhir/type_map.rb +72 -7
  16. data/lib/openehr_rails/fhir/unsupported_profile_error.rb +29 -0
  17. data/lib/openehr_rails/opt/field_extractor.rb +1 -2
  18. data/lib/openehr_rails/opt/parser.rb +4 -0
  19. data/lib/openehr_rails/release_check.rb +47 -4
  20. data/lib/openehr_rails/rm/rm_object_builder.rb +25 -1
  21. data/lib/openehr_rails/version.rb +1 -1
  22. data/lib/openehr_rails.rb +1 -0
  23. data/script/build_demo.sh +6 -0
  24. data/spec/fixtures/fsh/openehr-evaluation-problem-diagnosis-v1.fsh +22 -0
  25. data/spec/openehr_rails/aql/dataset_adapter_spec.rb +47 -0
  26. data/spec/openehr_rails/aql/executor_spec.rb +28 -0
  27. data/spec/openehr_rails/fhir/fsh_generator_spec.rb +178 -2
  28. data/spec/openehr_rails/fhir/profile_generator_spec.rb +188 -9
  29. data/spec/openehr_rails/opt/field_extractor_binding_spec.rb +1 -2
  30. data/spec/openehr_rails/release_check_spec.rb +39 -0
  31. data/spec/openehr_rails/rm/rm_object_builder_spec.rb +21 -0
  32. data/templates/openehr_template.rb +6 -0
  33. metadata +6 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69d9f917d3eb32a92e7d47aeb3f43bf8992a6a44f8ceb49320a1e5a6130ecebf
4
- data.tar.gz: 1f39015dbd51241ea4dca624bd89e988a82bf62ef3a02964e54a343b6e8ccbf1
3
+ metadata.gz: cf0122af26f620b3082c006c5d469fdf3710c5dd1647b856c9024c8e4e3f7c66
4
+ data.tar.gz: b54d925956180cd9baffab027d42564ecd55864f839698952ed2bfbfdfb9356d
5
5
  SHA512:
6
- metadata.gz: 5acec88b6d3dee1ab32167080639a81052911eee90043ae09474e083fa0c92e081da69dade909848da04743cffc96603e887765a8efaff29d6172f873e93a074
7
- data.tar.gz: 1c134c5a1147f54ee8628b62efdcfcb867f257f634a5094a4f0aacb2af460ac28cfcaa011457017610262686437041ec85045186f9c05acb034ed4080e6ec793
6
+ metadata.gz: 90fadabcf5f3215e6c24f4c75f5bef98e38b831286e0f0da33d6d67ae6743937d7958d286384c85042830452e47fe08e22eca6dce72541ab7bfb5395cc029ef2
7
+ data.tar.gz: 700d4afc6e60805cc76c478035131d0aa62b5f0decc09d44c67f0ac1bb4e8181500de65ba56b40f2974a0e24259945fb26285a6b3eac0ebe3ca394656cdc1c56
data/CHANGELOG.md CHANGED
@@ -7,8 +7,107 @@ 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
+
34
+ ## [0.7.0] - 2026-09-10
35
+
36
+ > **Upgrade note.** Regenerate `app/fhir/profiles/*.json`. `Condition.component`
37
+ > slices are gone; anything reading them must move to the mapped elements
38
+ > (`category` slice `ckm`, `code`, `onsetDateTime`, `recordedDate`,
39
+ > `abatementDateTime`, `verificationStatus`), and an entry with no mapping onto
40
+ > a non-`Observation` resource now appears in the generator's `#skipped` list
41
+ > instead of producing a profile.
42
+
43
+ ### Changed
44
+ - **Breaking for consumers of generated FHIR profiles.** `EVALUATION` entries no
45
+ longer produce `Condition.component` slices, which FHIR R5 `Condition` does not
46
+ have. Each leaf now maps onto the real `Condition` element that means the same
47
+ thing — `code` (bound to the leaf's value set), `onsetDateTime`,
48
+ `recordedDate`, `abatementDateTime`, `verificationStatus` — and the archetype
49
+ anchor moves from `Condition.code` to a `Condition.category` slice (`ckm`,
50
+ `1..1`, pattern-discriminated on `$this`, so an instance keeps room for its
51
+ other categories), since `code` is now the diagnosis itself. `recordedDate`
52
+ carries an `ElementDefinition.comment` stating that it approximates at0003
53
+ (date/time clinically recognised). `ProfileGenerator` (JSON) and
54
+ `FshGenerator` generate from one table in `TypeMap`, so the two outputs cannot
55
+ disagree. Generated FSH for `problem_list.opt` goes from 29 Sushi errors to 0.
56
+ Host apps must regenerate cached `app/fhir/profiles/*.json`; anything reading
57
+ the old `Condition.component` slices must be updated (#33). Currently mapped:
58
+ `openEHR-EHR-EVALUATION.problem_diagnosis.v1`. A multi-leaf entry mapped to
59
+ `ServiceRequest`/`Procedure`/`Encounter` without a mapping-table row is now
60
+ skipped and reported (see Added) instead of producing `component` constraints
61
+ those resources do not have; its mapping table remains reserved as #35.
62
+ - Value-set canonicals on mapped leaves no longer carry OPT's `terminology:`
63
+ prefix, in either output.
64
+
65
+ ### Added
66
+ - `ProfileGenerator#skipped` and `FshGenerator#skipped`: the entries left out of
67
+ `#profiles` / `#to_fsh_files`, each as an
68
+ `OpenehrRails::Fhir::UnsupportedProfileError` (`archetype_id`,
69
+ `resource_type`, `leaf_count`) in template order. An entry is skipped when it
70
+ has more than one leaf, maps to a resource without `component`, and has no row
71
+ in `TypeMap::ENTRY_ELEMENT_MAPS`; the rest of the template still generates.
72
+ `rails g openehr:fhir_profile` and `openehr:scaffold --fhir` print each skip
73
+ (#33).
74
+
75
+ ### Removed
76
+ - `TypeMap.value_element`, dead since it was added — its ternary returned the
77
+ same string on both branches and nothing called it. Its intended
78
+ resource-type branch is now the mapping table (#33).
79
+
80
+ ### Fixed
81
+ - `templates/openehr_template.rb` pins `json < 3` in the generated app's
82
+ `Gemfile`. `json` 3.0 (2026-09-08) changed `JSON.parse`'s arity and
83
+ `ActiveSupport::JSON.decode` as of activesupport 8.1.3.1 still passes options
84
+ positionally, so a freshly generated host app failed at `db:seed` on its first
85
+ json-column write (`ArgumentError: wrong number of arguments (given 2, expected
86
+ 1)`). Temporary: remove once a Rails patch release carries the fix. This
87
+ repo's own `Gemfile` and `script/build_demo.sh` carry the same dev-only pin.
88
+ - `release:check` now fails when a tag matching the gemspec version exists and
89
+ `HEAD` is not that tag's commit. `gem.files` comes from `git ls-files`, so a
90
+ gem built at a later commit ships different bytes under the same version
91
+ number — the defect that let 0.6.0 be published as a `master`-HEAD build
92
+ (#34). The check stays silent when the version has no tag yet, so ordinary
93
+ pre-release development is unaffected.
94
+
10
95
  ## [0.6.0] - 2026-08-26
11
96
 
97
+ > **Published artifact note.** The gem published to RubyGems for this version
98
+ > was built from `master` HEAD rather than the `v0.6.0` tag, so it contains the
99
+ > tagged tree plus two documentation files added after the tag
100
+ > (`docs/design/multi-leaf-non-observation-plan.md`, and this project's
101
+ > `docs/reports/fsh-generator-log.md` carrying its R6 entry). `lib/` is
102
+ > byte-identical to the tag artifact and version, runtime dependencies and
103
+ > `required_ruby_version` are unchanged, so the published gem is functionally
104
+ > identical to what CI built and verified at the tag; it was accepted rather
105
+ > than yanked. Published sha256
106
+ > `223e3b3897f85c38eaffe7ea39bd7c2acf4c5de9cab7d50fe38f754ff9d65db6`; tag-build
107
+ > sha256 `08cdd14ab1f3890b0c6b5f0ae0d5ca55615f0b4874ed5efb0cd4d7bda9e573ca`.
108
+ > Full measurements: `docs/reports/fsh-generator-log.md` R8. Prevention:
109
+ > `skoba/openehr-rails#34`.
110
+
12
111
  ### Added
13
112
  - `OpenehrRails::Fhir::FshGenerator` renders one FHIR Shorthand profile per
14
113
  openEHR entry, including metadata, archetype codes, value constraints,
data/CLAUDE.md CHANGED
@@ -46,6 +46,37 @@ Before tagging, make the final semver determination from the actual content of
46
46
  `[Unreleased]`, not from a pre-assigned version number. If the instructed version number
47
47
  contradicts the actual content, stop instead of tagging and ask for re-arbitration.
48
48
 
49
+ **Publish only the CI artifact itself.** `gem push` takes the `.gem` downloaded
50
+ from the tag's Release run (`gh run download <run-id> -n gem`), after its sha256
51
+ has been compared against the value recorded for that run - never a locally built
52
+ `pkg/*.gem`. `gem.files` comes from `git ls-files`, so a build made at any commit
53
+ other than the tag ships different bytes under the same version number, and a
54
+ stale `pkg/` artifact from unrelated local work is indistinguishable by filename
55
+ from a release build.
56
+
57
+ (Added 2026-08-27, from the 0.6.0 publish: the gem that reached RubyGems was a
58
+ `master`-HEAD build left in `pkg/` by an unrelated verification run, not the
59
+ CI-verified `v0.6.0` artifact. Accepted rather than yanked - the delta was two
60
+ documentation files and `lib/` was byte-identical - but nothing in the release
61
+ path caught it. Measurements in `docs/reports/fsh-generator-log.md` R8; the
62
+ matching `release:check` guard is `skoba/openehr-rails#34`.)
63
+
64
+ **Confirm publication by checksum, not by version number.** RubyGems publishes
65
+ the sha256 it recorded at push time (`/api/v1/gems/openehr-rails.json` `sha`, and
66
+ the `checksum:` field in `https://index.rubygems.org/info/openehr-rails`). The
67
+ post-push confirmation compares that value against the sha256 the tag's Release
68
+ run recorded for its artifact - the same value already compared before the push.
69
+ Seeing the version number appear is not the check; it only shows that *a* gem
70
+ landed under that number. (Ported 2026-09-10 from openehr-ruby's 2.4.3 release,
71
+ where CI run, downloaded artifact and published gem all agreed on one sha256.)
72
+
73
+ **RubyGems API propagation lag.** `/api/v1/versions/openehr-rails.json` can lag
74
+ the push by minutes; do not read the new version's absence there as a failed
75
+ publish, and do not re-push. Confirm with
76
+ `/api/v1/versions/openehr-rails/latest.json`, `/api/v1/gems/openehr-rails.json`,
77
+ or the compact index `https://index.rubygems.org/info/openehr-rails`, which
78
+ updated first in that same 2.4.3 run.
79
+
49
80
  ## Verification
50
81
 
51
82
  - **Verify against the repo before recording a fact in it**, even when a prompt or an
@@ -119,7 +150,8 @@ name explicitly a distinction that the existing repository-context rule left
119
150
  implicit: it governs *how* to target a cross-repo command correctly, not *whether*
120
151
  crossing into implementation work on another repository has actually been
121
152
  authorized for the task at hand. A matching line is planned for `anlage`'s own
122
- `CLAUDE.md` in a later batch - not yet added there as of this entry.)
153
+ `CLAUDE.md` in a later batch - not yet added there as of this entry. Update
154
+ 2026-09-04: added there as 絶対規律 item 9.)
123
155
 
124
156
  ## Project Overview
125
157
 
@@ -200,4 +232,4 @@ This is `openehr-rails`, a Rails engine gem that turns an openEHR Operational Te
200
232
  - Guard setup available for TDD workflow
201
233
  - RuboCop Rails linting configured
202
234
  - SimpleCov for test coverage
203
- - `openehr` gem dependency; see the gem's own README/CHANGELOG for OPT-parser and AQL-engine capabilities and known gaps before relying on either.
235
+ - `openehr` gem dependency; see the gem's own README/CHANGELOG for OPT-parser and AQL-engine capabilities and known gaps before relying on either.
data/Gemfile CHANGED
@@ -1,3 +1,10 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
+
5
+ # json 3.0 (released 2026-09-09) changed JSON.parse's arity; ActiveSupport::JSON.decode
6
+ # as of activesupport 8.1.3.1 still passes its options positionally, so reading any
7
+ # json column raises ArgumentError (wrong number of arguments (given 2, expected 1)).
8
+ # Dev/test-only pin -- the gemspec is untouched, host apps resolve their own json.
9
+ # Drop once a Rails patch release carries the fix (docs/backlog.md, "Dependencies").
10
+ gem 'json', '< 3'
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
 
@@ -90,6 +99,13 @@ status entry above for verification details.
90
99
  docs-only pass — implementing the workflow change goes through an Issue (once
91
100
  ticket-driven work applies to it) plus the normal explore → plan → approval gate,
92
101
  not a direct docs commit.
102
+ - A point in Trusted Publishing's favour, measured 2026-08-27: the gem build is
103
+ byte-identical across machines and Ruby installs for a given commit — `4080053`
104
+ (the `v0.6.0` tag) produced sha256 `08cdd14a…` from CI, from the old machine
105
+ (R5), and from a rebuild on the replacement machine under local ruby 4.0.6 vs
106
+ CI's `ruby/setup-ruby@v1` ruby 4.0. So a CI- or Trusted-Publishing-published
107
+ artifact stays independently verifiable against a local rebuild
108
+ (`docs/reports/fsh-generator-log.md` R8).
93
109
 
94
110
  ## From #25 / PR #26 (FieldExtractor terminology scope fix, 2026-08-22)
95
111
 
@@ -115,6 +131,14 @@ status entry above for verification details.
115
131
 
116
132
  ## Fixture conventions
117
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
+
118
142
  - **Licensed terminology-code literals in spec expectations: keep minimal, cite the
119
143
  source fixture's file:line.** Adopted from Anlage's C2 firewall precedent (SNOMED
120
144
  CT is a licensed terminology; reproducing its codes verbatim in more places than
@@ -155,3 +179,18 @@ status entry above for verification details.
155
179
  duplicate-method inventory being produced first.
156
180
  - **#2** (constraint -> HTML attribute mapping extraction): gated on a generality decision
157
181
  after Anlage Slice 4; whether to even start is undecided.
182
+
183
+ ## Dependencies
184
+
185
+ - **Remove the `json < 3` pins once Rails ships the fix.** Added 2026-09-10 in
186
+ three places: `Gemfile` (dev/test), `script/build_demo.sh` (the demo app's
187
+ Gemfile), `templates/openehr_template.rb` (the generated host app's Gemfile).
188
+ Cause: `json` 3.0 (3.0.1 on 2026-09-08, 3.0.2 on 2026-09-09) changed
189
+ `JSON.parse`'s arity and `ActiveSupport::JSON.decode` as of activesupport
190
+ 8.1.3.1 still passes options positionally, so every json-column read raises
191
+ `ArgumentError: wrong number of arguments (given 2, expected 1)`. Measured on
192
+ PR #39's CI run `34423912397` (62 failures, all RM-graph/AQL specs) and
193
+ reproduced locally with `bundle update` on `master`; with the pin and a fresh
194
+ resolve, 295 examples / 0 failures. Removal condition: an activesupport release
195
+ whose `JSON.decode` accepts json 3 (check the Rails CHANGELOG), then delete the
196
+ three pins in one docs+tooling commit and let CI resolve fresh.
@@ -1,6 +1,6 @@
1
1
  # Fix: multi-leaf non-Observation entries constrain a nonexistent `component`
2
2
 
3
- - Status: draft, awaiting approval
3
+ - Status: **ruled 2026-08-27 — option (d), proper mapping, adopted. §§2-6 below are superseded; §8 is the normative spec, as corrected by §9 (the ruling's four corrections, applied 2026-09-10).**
4
4
  - Target: `openehr-rails` (this repo). No cross-repo work.
5
5
  - Issue: [#33](https://github.com/skoba/openehr-rails/issues/33)
6
6
  - Log: `docs/reports/fsh-generator-log.md` (continuing R1-R5)
@@ -178,3 +178,221 @@ this repo's division of labor, or directly if the change is judged small enough
178
178
  skip that split — decide at approval time), Claude Code review, full
179
179
  `bundle exec rspec` + full-repo `rubocop` + `sushi` re-verification for
180
180
  `bmi_calculation.opt`, commit(s), `docs/reports/fsh-generator-log.md` entry.
181
+
182
+
183
+ ---
184
+
185
+ # 8. RULING (2026-08-27): option (d), proper mapping to `Condition`
186
+
187
+ The recommendation in §3 — option (c), restrict multi-leaf non-`Observation`
188
+ entries and raise — **was not adopted**. The ruling directs a *proper mapping*:
189
+ `problem_diagnosis`'s leaves land on the real `Condition` elements that mean the
190
+ same thing. §§2-6 are kept for the record but are superseded by this section.
191
+
192
+ **This table is the single specification for both outputs.** `ProfileGenerator`
193
+ (the JSON facade) and `FshGenerator` both generate from it; neither may carry a
194
+ mapping decision the other doesn't.
195
+
196
+ ## 8.1 Mapping table — `openEHR-EHR-EVALUATION.problem_diagnosis.v1` → `Condition` (FHIR R5)
197
+
198
+ Measured, not assumed: leaves are `FieldExtractor#entries` output for
199
+ `spec/templates/problem_list.opt`; every target element was compiled against
200
+ `hl7.fhir.r5.core#5.0.0` with `sushi` 3.16.0 (**0 Errors**) before this table was
201
+ written.
202
+
203
+ | openEHR leaf | Label (fixture, ja) | RM type | → `Condition` element | Rationale |
204
+ |---|---|---|---|---|
205
+ | *(archetype anchor)* | — | — | `category` — fixed coding `CKM#openEHR-EHR-EVALUATION.problem_diagnosis.v1` | The anchor cannot stay on `code`: under a proper mapping `code` is claimed by at0002, the diagnosis itself. `category` is R5's 0..* CodeableConcept for "what kind of Condition record is this", with an *example* binding, so a fixed archetype coding is legal there. |
206
+ | `at0002` | プロブレム・診断名 | `DV_CODED_TEXT`, value set `http://id.who.int/icd/release/11/mms` | `code` 0..1, `only CodeableConcept`, `from <ICD-11 MMS> (required)` | `Condition.code` is "identification of the condition, problem or diagnosis" — the direct counterpart. Its base binding is *example*, so a profile may tighten it to *required*. |
207
+ | `at0077` | 発症日時 | `DV_DATE_TIME` | `onsetDateTime` 0..1, `only dateTime` | `onset[x]` is the date/time the condition began; the `dateTime` choice matches `DV_DATE_TIME` exactly. (`sushi` normalises the path to `Condition.onset[x]` with `type: [dateTime]` — that is the shape the JSON facade emits.) |
208
+ | `at0003` | 臨床的に認識された日時 | `DV_DATE_TIME` | `recordedDate` 0..1, `only dateTime` | Nearest R5 element. **Approximation, recorded as such**: `recordedDate` is "when this Condition record was created in the system", which is not a synonym for "clinically recognised". No closer element exists in R5; the gap is written down here rather than implied by the mapping. |
209
+ | `at0030` | 治癒日時 | `DV_DATE_TIME` | `abatementDateTime` 0..1, `only dateTime` | `abatement[x]` is "the date the condition resolved or went into remission" — the direct counterpart. |
210
+ | `at0073` | 診断確度 (`at0074` 疑い / `at0075` 推定 / `at0076` 確定) | `DV_CODED_TEXT`, `terminology_id = "local"` | `verificationStatus` 0..1, `only CodeableConcept`, **no value-set binding emitted** | `verificationStatus` (unconfirmed \| provisional \| differential \| confirmed \| refuted \| entered-in-error) is the semantic counterpart of 診断確度. See 8.2 for why the local codes are deliberately *not* bound. |
211
+
212
+ **Nothing in this archetype is unmappable** — all five leaves land. What is
213
+ deliberately *not* emitted is in 8.2.
214
+
215
+ ## 8.2 Deliberate omissions
216
+
217
+ - **`at0073`'s local code list (`at0074`/`at0075`/`at0076`) is not bound.**
218
+ `Condition.verificationStatus` has a **required** binding to
219
+ `http://hl7.org/fhir/ValueSet/condition-ver-status`; binding an archetype's
220
+ local at-codes there would be invalid. Translating 疑い/推定/確定 into
221
+ `provisional`/`confirmed`/etc. is a `ConceptMap` concern, outside what a
222
+ `StructureDefinition` can express. The profile therefore constrains the
223
+ element's cardinality and type only. This means the current
224
+ `apply_value_constraints` behaviour — emitting
225
+ `binding: { strength: 'required' }` for any `DV_CODED_TEXT` carrying a local
226
+ `code_list` — must **not** apply to a mapped leaf.
227
+ - **Multi-leaf non-`Observation` entries with no mapping table entry keep their
228
+ current behaviour.** The ruling scopes this fix to `problem_diagnosis`; the
229
+ `INSTRUCTION`→`ServiceRequest` case (`request-referral`, arriving with
230
+ referral v2) is reserved as its own Issue rather than generalised here.
231
+
232
+ ## 8.3 Where the table lives
233
+
234
+ In `TypeMap`, next to `ENTRY_RESOURCES`, keyed by archetype id — the existing
235
+ RM-type→FHIR-resource mechanism, not a new conditional scattered across the two
236
+ generators. Both generators ask `TypeMap` the same question. The dead
237
+ `TypeMap.value_element` (§1) is removed as part of this: it was a stub for
238
+ exactly this resource-type branch and never implemented it.
239
+
240
+ ## 8.4 TDD
241
+
242
+ - **Red**: `problem_list.opt`'s generated FSH under `sushi` 3.16.0 — measured
243
+ **29 Errors** today (all `No element found at path component…`). Pinned as the
244
+ starting measurement.
245
+ - **Green**: the same fixture compiles with **0 Errors**. The exact rule set the
246
+ implementation must emit was pre-verified against
247
+ `hl7.fhir.r5.core#5.0.0` before implementation began.
248
+ - **JSON facade**: `profile_generator_spec.rb` gains expectations for the mapped
249
+ `Condition` elements, and asserts no `Condition.component` element is produced
250
+ — the original complaint in #33.
251
+ - **Regression pin**: `bmi_calculation.opt` (multi-leaf `Observation`) is
252
+ untouched by the new branch and must stay green, `component` slicing intact.
253
+
254
+ ## 8.5 Semver
255
+
256
+ **Minor.** The JSON facade's output shape changes for `EVALUATION` entries
257
+ (`Condition.component` slices disappear, real `Condition` elements appear), which
258
+ is observable to any host app consuming `app/fhir/profiles/*.json`. Ships with
259
+ `#34`'s `release:check` change; version finalised at release inventory, 0.7.0
260
+ expected.
261
+
262
+ # 9. RULING FOLLOW-UP (2026-09-10): the four corrections to §8, applied
263
+
264
+ The 2026-08-27 ruling approved §8 **with four corrections**: (1) the archetype
265
+ anchor is a *slice* of `category`, (2) `at0003`'s approximation is stated on the
266
+ element as a `^comment`, (3) unmapped multi-leaf non-`Observation` entries are
267
+ *skipped and reported* per entry, (4) through a named exception class. The
268
+ implementation commit `01f31f3` (2026-08-27 12:38 JST) landed 21 minutes after §8
269
+ itself (`07767cc`, 12:17 JST) and carries none of them; this repository held no
270
+ record of the four points until this section. Applied under the reopened #33.
271
+ Everything below was measured against `hl7.fhir.r5.core#5.0.0` with `sushi`
272
+ 3.16.0 before the code was written, as §8 was.
273
+
274
+ ## 9.1 Correction 1 — the anchor is a slice of `category`
275
+
276
+ - **Before**: `* category.coding.system = "…"` / `* category.coding.code = #…`
277
+ (JSON: one `Condition.category` element with `patternCodeableConcept`).
278
+ - **Why that was wrong**: `Condition.category` is `0..*`. Fixing the coding on the
279
+ element constrains *every* repetition, so a conforming instance could not also
280
+ carry e.g. `problem-list-item` beside the archetype coding.
281
+ - **After**: pattern slicing on `$this`, `rules = #open`, `contains ckm 1..1`,
282
+ `category[ckm] = http://openehr.org/ckm/archetypes#<archetype id>` (FSH
283
+ assignment to a `CodeableConcept` is a `patternCodeableConcept`, the same shape
284
+ the JSON facade emits). JSON: two `Condition.category` elements — the slicing
285
+ root (`discriminator: [{type: pattern, path: $this}]`, `rules: open`) and the
286
+ `ckm` slice (`min 1`, `max "1"`, `patternCodeableConcept`). The slice name
287
+ reuses the name the FSH output already gives the CKM coding slice on
288
+ `code.coding`; it is `TypeMap::ANCHOR_SLICE` so both generators share it.
289
+ - **Measured**: the candidate rule set compiled to **0 Errors** before
290
+ implementation; the generator's own output afterwards byte-matches the golden
291
+ and compiles to **0 Errors** together with `bmi_calculation.opt`.
292
+
293
+ ## 9.2 Correction 2 — `at0003` carries a `^comment`
294
+
295
+ §8.1 records `at0003` → `recordedDate` as an approximation in this document only;
296
+ the profile now says so itself. `ENTRY_ELEMENT_MAPS` gains a `:comment` key on
297
+ the leaf, emitted as `* recordedDate ^comment = "…"` (FSH) and
298
+ `ElementDefinition.comment` (JSON) — table-driven, so the two outputs cannot
299
+ differ on the wording. Text: *Approximation: openEHR at0003 (Date/time clinically
300
+ recognised) has no exact counterpart in FHIR R5 Condition; recordedDate is when
301
+ this Condition record was created in the system. See
302
+ docs/design/multi-leaf-non-observation-plan.md section 8.1.*
303
+
304
+ ## 9.3 Corrections 3 and 4 — skip-and-report, `UnsupportedProfileError`
305
+
306
+ These answer §6's open questions 2 and 3.
307
+
308
+ - **Condition**: an entry with **more than one leaf**, whose base resource is
309
+ **not `Observation`**, and which has **no row** in `ENTRY_ELEMENT_MAPS`. Before
310
+ this section such an entry silently took the `component` path — the original
311
+ #33 defect, still live for `ServiceRequest`/`Procedure`/`Encounter` (the
312
+ `[Unreleased]` CHANGELOG said as much).
313
+ - **One decision point** (§3): `TypeMap.assert_supported!(entry)` raises
314
+ `OpenehrRails::Fhir::UnsupportedProfileError` (`archetype_id`, `resource_type`,
315
+ `leaf_count`, and a message naming all three plus #33/#35). Both generators
316
+ call it; neither carries its own conditional.
317
+ - **Skip-and-report per entry, not raise-through** (Q2): `ProfileGenerator` and
318
+ `FshGenerator` partition entries at construction, generate for the supported
319
+ ones, and expose the errors in template order through `#skipped`. The batch
320
+ never aborts on one bad entry. The Rails generators (`openehr:fhir_profile`,
321
+ `openehr:scaffold --fhir`) print each skip as `say_status :skip, …, :yellow` —
322
+ the report reaches the person running the generator, and a library caller that
323
+ wants a hard failure re-raises from `#skipped`.
324
+ - **Class** (Q3): `OpenehrRails::Fhir::UnsupportedProfileError < StandardError`,
325
+ in `lib/openehr_rails/fhir/unsupported_profile_error.rb`, required before
326
+ `type_map`.
327
+ - **Not covered, deliberately**: single-leaf entries. The ruling's wording is
328
+ 多葉 (multi-leaf); the single-leaf non-`Observation` path has its own defect,
329
+ recorded in 9.5 rather than folded in here.
330
+
331
+ ## 9.4 TDD (resolution shape (b) enhancement for all four)
332
+
333
+ - **Red**: specs written first in `fsh_generator_spec.rb` and
334
+ `profile_generator_spec.rb` (slice, `^comment`/`comment`, skip-and-report,
335
+ `#skipped` empty for an all-`Observation` template) plus the regenerated
336
+ golden: **36 examples, 12 failures** on the pre-correction code.
337
+ - **Green**: `spec/openehr_rails/fhir/`: **48 examples, 0 failures**; full suite
338
+ **304 examples, 0 failures** (295 before).
339
+ - **Synthetic entry, spec-level, not a fixture file**: no real multi-leaf
340
+ `INSTRUCTION` OPT exists in this repo (#35 is blocked on exactly that), so the
341
+ skip-and-report specs stub `FieldExtractor` to return `problem_list.opt`'s real
342
+ entry plus one invented `INSTRUCTION` entry
343
+ (`openEHR-EHR-INSTRUCTION.synthetic_unmapped_test.v1`, two of the real leaves
344
+ relabelled). The id is self-evidently invented; the spec comment says so and
345
+ points here.
346
+ - **Golden**: regenerated from the corrected generator, diffed against the
347
+ pre-verified candidate (identical), then compiled again: **0 Errors**.
348
+
349
+ ## 9.5 Residual found while measuring 9.1 — not fixed here
350
+
351
+ A single-leaf non-`Observation` entry still takes the legacy path and emits
352
+ `* value[x] 0..1` (FSH) / `<Resource>.value[x]` (JSON). `Condition` has no
353
+ `value[x]` either: a hand-written probe (`Parent: Condition`, `* value[x] 0..1`)
354
+ compiled under the same `sushi` run to **1 Error**, `No element found at path
355
+ value[x] for CardRule`. No fixture in this repository reaches that path
356
+ (`problem_list.opt` is 5-leaf; every single-leaf fixture entry is
357
+ `OBSERVATION`), so nothing observable regressed. Outside the ruling's scope
358
+ (多葉); filed as its own bug Issue rather than widened into #33 — the natural fix
359
+ is to extend `assert_supported!` to it, but that is a second contract change and
360
+ gets its own red spec.
361
+
362
+ ## 9.6 Semver
363
+
364
+ Still **minor**, on top of §8.5: `#skipped` and `UnsupportedProfileError` are
365
+ new public API; the `category` anchor and the `comment` change the JSON facade's
366
+ shape again for `EVALUATION` entries. `CHANGELOG.md` `[Unreleased]` updated in
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.