openehr-rails 0.5.0 → 0.6.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 +4 -4
- data/.github/ISSUE_TEMPLATE/bug_report.md +3 -0
- data/CHANGELOG.md +16 -0
- data/CLAUDE.md +19 -0
- data/docs/backlog.md +17 -0
- data/docs/design/fsh-generator-plan.md +113 -0
- data/docs/design/multi-leaf-non-observation-plan.md +180 -0
- data/docs/reports/binding-extraction-log.md +66 -0
- data/docs/reports/fsh-generator-log.md +317 -0
- data/lib/openehr_rails/fhir/fsh_generator.rb +166 -0
- data/lib/openehr_rails/version.rb +1 -1
- data/lib/openehr_rails.rb +1 -0
- data/spec/openehr_rails/fhir/fsh_generator_spec.rb +102 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69d9f917d3eb32a92e7d47aeb3f43bf8992a6a44f8ceb49320a1e5a6130ecebf
|
|
4
|
+
data.tar.gz: 1f39015dbd51241ea4dca624bd89e988a82bf62ef3a02964e54a343b6e8ccbf1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5acec88b6d3dee1ab32167080639a81052911eee90043ae09474e083fa0c92e081da69dade909848da04743cffc96603e887765a8efaff29d6172f873e93a074
|
|
7
|
+
data.tar.gz: 1c134c5a1147f54ee8628b62efdcfcb867f257f634a5094a4f0aacb2af460ac28cfcaa011457017610262686437041ec85045186f9c05acb034ed4080e6ec793
|
|
@@ -16,6 +16,9 @@ assignees: ''
|
|
|
16
16
|
- `openehr` version: <!-- e.g. ~> 2.3 -->
|
|
17
17
|
- Rails version: <!-- e.g. 8.1 -->
|
|
18
18
|
- Ruby version: <!-- e.g. 3.3, 3.4, 4.0 -->
|
|
19
|
+
- If the report involves an OPT/ADL file: the generating tool and its
|
|
20
|
+
version (e.g. Better Archetype Designer / Ocean Template Designer 2.6 /
|
|
21
|
+
ADL Workbench / LinkEHR / HMC).
|
|
19
22
|
|
|
20
23
|
## Reproduction
|
|
21
24
|
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] - 2026-08-26
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `OpenehrRails::Fhir::FshGenerator` renders one FHIR Shorthand profile per
|
|
14
|
+
openEHR entry, including metadata, archetype codes, value constraints,
|
|
15
|
+
component slicing, quantity units, fixed terminology codes, and required
|
|
16
|
+
external value-set bindings. Observation-mapped output is Sushi-verified;
|
|
17
|
+
multi-leaf non-Observation entries have a known structural gap tracked as #33.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- `FshGenerator` no longer depends on `active_support/core_ext/string`; the
|
|
21
|
+
three String helpers it used are now small plain-Ruby private methods,
|
|
22
|
+
verified byte-identical output. Restores the Rails-independence this class
|
|
23
|
+
was designed for (see `anlage`'s `docs/design/fsh-plan.md`), so it can move
|
|
24
|
+
wholesale to a future satellite gem without a Rails dependency riding along.
|
|
25
|
+
|
|
10
26
|
## [0.5.0] - 2026-08-25
|
|
11
27
|
|
|
12
28
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -102,6 +102,25 @@ lasting effect, but the near-miss is what prompted this generalization. See
|
|
|
102
102
|
logging the underlying structural fix under consideration - one
|
|
103
103
|
worktree/session per repo instead of per-command vigilance.)
|
|
104
104
|
|
|
105
|
+
## Cross-repository implementation work needs its own authorization gate
|
|
106
|
+
|
|
107
|
+
The moment it becomes clear that the actual implementation target for a piece of
|
|
108
|
+
work is a repository other than this one, stop before starting that work and insert
|
|
109
|
+
a gate report confirming the implementation target, rather than sliding from
|
|
110
|
+
read-only cross-repo reference (already permitted for design/evidence-gathering)
|
|
111
|
+
into cross-repo write work on the strength of that same permission. Correctly
|
|
112
|
+
targeting a command once you've decided to cross the boundary (the previous
|
|
113
|
+
section's `cd`/`-C`/`-R` discipline) is a different concern from being authorized to
|
|
114
|
+
cross it at all - following the operational convention precisely does not retroactively
|
|
115
|
+
authorize the crossing.
|
|
116
|
+
|
|
117
|
+
(Added 2026-08-26, alongside the `#32`/`#33` FSH-generator work continuation, to
|
|
118
|
+
name explicitly a distinction that the existing repository-context rule left
|
|
119
|
+
implicit: it governs *how* to target a cross-repo command correctly, not *whether*
|
|
120
|
+
crossing into implementation work on another repository has actually been
|
|
121
|
+
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.)
|
|
123
|
+
|
|
105
124
|
## Project Overview
|
|
106
125
|
|
|
107
126
|
This is `openehr-rails`, a Rails engine gem that turns an openEHR Operational Template (`.opt`, ADL2/XML) into a working Rails resource in one command: `rails generate openehr:scaffold path/to/template.opt --fhir` emits a model, migration, controller, views, i18n locale, and (with `--fhir`) HL7 FHIR R5 `StructureDefinition` profiles. Generated models persist both as typed columns (for forms/search) and as full openEHR RM data (canonical JSON + a typed node graph with immutable-append versioning), and are queryable via a growing AQL surface. A mountable admin engine (`/openehr`) provides template upload/management, runtime scaffolding, and a FHIR R5 facade. Legacy ADL-archetype-only generators (model/controller/migration/helper/assets/i18n/template/template_model, based on `Openehr::Generators::ArchetypedBase`) have been removed — OPT is the only supported input format for scaffolding.
|
data/docs/backlog.md
CHANGED
|
@@ -34,6 +34,23 @@ actually running, not just present:
|
|
|
34
34
|
push will reproduce the same red run. See "Release automation" below.
|
|
35
35
|
- Most recent `master` push at the time of this record (`4055ec3`): CI run
|
|
36
36
|
`32569774700`, success.
|
|
37
|
+
- **Tag `v0.5.0` (2026-08-25, #30): `release.yml` run
|
|
38
|
+
[32831263811](https://github.com/skoba/openehr-rails/actions/runs/32831263811) --
|
|
39
|
+
overall conclusion **success**, all 12 jobs green (9 reused `ci.yml` spec-matrix
|
|
40
|
+
jobs, `demo smoke`, `application template smoke test`, `Build gem artifact`).**
|
|
41
|
+
First fully-green tag-push run on record -- confirms the "Release automation fix
|
|
42
|
+
landed (2026-08-23, PR #28)" note below actually fixed the structural red
|
|
43
|
+
described above, not just that the workflow file changed (`v0.4.1`/`v0.4.0`/`v0.3.0`
|
|
44
|
+
were all red at the now-removed RubyGems-publish step; `v0.5.0` has no such step to
|
|
45
|
+
fail at). Downloaded the run's `gem` artifact and independently built
|
|
46
|
+
`pkg/openehr-rails-0.5.0.gem` locally at the same tagged commit (`69db63f`): both
|
|
47
|
+
197632 bytes, identical sha256
|
|
48
|
+
`e07815bd1c86736403cbb558fec869fbe04666f695e6cc12a41dad9be77230e6`. RubyGems
|
|
49
|
+
publish (human `gem push`, per "Release automation" below) confirmed complete via
|
|
50
|
+
`https://rubygems.org/api/v1/versions/openehr-rails.json`: version `0.5.0`,
|
|
51
|
+
`created_at: 2026-08-25T09:26:15.342Z`, published `sha` field matches the same
|
|
52
|
+
sha256 above exactly -- local build, CI artifact, and the published gem are all
|
|
53
|
+
byte-identical.
|
|
37
54
|
|
|
38
55
|
Recorded here after this file previously claimed "this repo currently has no CI
|
|
39
56
|
workflow gating pull requests" — wrong on both existence and execution. This file is
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Enhancement: FshGenerator (FSH output alongside ProfileGenerator)
|
|
2
|
+
|
|
3
|
+
- Status: draft, approved-by-citation (design decisions made upstream in
|
|
4
|
+
`skoba/anlage#17`, not re-litigated here)
|
|
5
|
+
- Target: `openehr-rails` (this repo)
|
|
6
|
+
- Issue: [#32](https://github.com/skoba/openehr-rails/issues/32)
|
|
7
|
+
- Log: `docs/reports/fsh-generator-log.md`
|
|
8
|
+
- Design authority: `skoba/anlage`'s `docs/design/fsh-plan.md` (2026-08-26
|
|
9
|
+
ruling, all 4 judgments approved) and `docs/reports/fsh-log.md` R1-R4.
|
|
10
|
+
This doc translates that already-approved plan into this repo's
|
|
11
|
+
file:line reality; it does not re-decide source/scope/output-form.
|
|
12
|
+
|
|
13
|
+
## 1. Summary
|
|
14
|
+
|
|
15
|
+
Add `OpenehrRails::Fhir::FshGenerator`, a sibling to
|
|
16
|
+
`OpenehrRails::Fhir::ProfileGenerator` that renders FSH (FHIR Shorthand)
|
|
17
|
+
text instead of a JSON StructureDefinition Hash, from the exact same
|
|
18
|
+
input: `OpenehrRails::Opt::FieldExtractor#entries`. No independent OPT
|
|
19
|
+
walk — this is the "share the intermediate representation" design
|
|
20
|
+
anlage's plan settled on (rejecting a pathcards-based or
|
|
21
|
+
independently-walking alternative as violating the no-double-derivation
|
|
22
|
+
constraint).
|
|
23
|
+
|
|
24
|
+
## 2. Input contract (already shipped, `skoba/openehr-rails#30` / 0.5.0)
|
|
25
|
+
|
|
26
|
+
`FieldExtractor#entries[].fields[]` now always carries (verified live
|
|
27
|
+
in `field_extractor.rb:160-177`, doc comment `field_extractor.rb:1-23`):
|
|
28
|
+
|
|
29
|
+
- `value_set_uri`: `C_CODE_REFERENCE#reference_set_uri`, else `nil`
|
|
30
|
+
- `code_bindings`: `[{system_uri:, code:}]`, else `[]` — not limited to
|
|
31
|
+
`DV_CODED_TEXT` (BMI's LOINC binding sits on a `DV_QUANTITY` element,
|
|
32
|
+
`CHANGELOG.md` [0.5.0] Added note)
|
|
33
|
+
|
|
34
|
+
No further gem work is a prerequisite for this Issue.
|
|
35
|
+
|
|
36
|
+
## 3. v1 scope (per the approved anlage plan, §"v1サブセットの範囲確定")
|
|
37
|
+
|
|
38
|
+
Mirror exactly what `ProfileGenerator` already emits, in FSH syntax,
|
|
39
|
+
plus binding writes (now unblocked):
|
|
40
|
+
|
|
41
|
+
| JSON (`profile_generator.rb`) | FSH form |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `build_profile` metadata (`:33-50`) | `Profile:`/`Parent:`/`Id:`/`Title:` header lines |
|
|
44
|
+
| `code_element` (`:62-69`) | `* code.coding.system = ...` / `* code.coding.code = ...` (or a `patternCodeableConcept`-equivalent assignment — pick whichever Sushi compiles identically to the JSON shape; verify by comparing compiled output, not by assumption) |
|
|
45
|
+
| `value_elements` (`:71-80`) | `* value[x] <min>..<max>` + `* value[x] only <Type>` |
|
|
46
|
+
| `component_elements`/`component_slice` (`:82-113`) | slicing declaration (`^slicing.discriminator...`) + `* component contains <slice> <card>` + per-slice `code`/`value[x]` assignment (see anlage's `docs/reports/fsh-log.md` R1 for a working Sushi-verified example of this exact shape) |
|
|
47
|
+
| `DV_QUANTITY` unit (`apply_value_constraints`, `:117-124`) | `* value[x].unit = "..."` (or equivalent fixed-value rule) |
|
|
48
|
+
| `DV_CODED_TEXT` binding (`:125-131`, now includes `valueSet`) | `field[:value_set_uri]` present → `* value[x] from <uri> (required)`. `field[:code_bindings]` present (any rm_type) → fixed `patternCodeableConcept`-equivalent assignment per binding entry |
|
|
49
|
+
|
|
50
|
+
Explicitly out of scope for v1 (matches the JSON facade's own gaps, not
|
|
51
|
+
new ones introduced here): `magnitude_range` (extracted but never
|
|
52
|
+
emitted by `ProfileGenerator` either — a separate, pre-existing gap).
|
|
53
|
+
`mml_referral`-scale templates are out of v1's verification scope per
|
|
54
|
+
the approved plan (size cap); this Issue's fixtures stay to the 3 small
|
|
55
|
+
ones listed below.
|
|
56
|
+
|
|
57
|
+
## 4. Fixtures (no new SNOMED literal budget — reuse what's already here)
|
|
58
|
+
|
|
59
|
+
- `spec/generators/templates/bmi_calculation.opt` — single-leaf,
|
|
60
|
+
`DV_QUANTITY`, real SNOMED-CT (`60621009`) + LOINC `code_binding` on
|
|
61
|
+
`at0004` (`spec/templates/bmi_calculation_without_uid.opt:1683-1697`
|
|
62
|
+
for the byte-identical sibling with visible XML)
|
|
63
|
+
- `spec/templates/problem_list.opt` — `C_CODE_REFERENCE`/ICD-11
|
|
64
|
+
`value_set_binding` on `at0002`, local `code_list` contrast on
|
|
65
|
+
`at0073` (byte-identical copy of anlage's `ProblemList.opt`,
|
|
66
|
+
provenance comment already in the file per `0bbbc47`)
|
|
67
|
+
|
|
68
|
+
Both are already indexed by existing specs
|
|
69
|
+
(`spec/openehr_rails/fhir/profile_generator_spec.rb`,
|
|
70
|
+
`spec/openehr_rails/opt/field_extractor_binding_spec.rb`) — reuse the
|
|
71
|
+
same `template` `let` pattern.
|
|
72
|
+
|
|
73
|
+
## 5. TDD (t-wada, per this repo's CLAUDE.md)
|
|
74
|
+
|
|
75
|
+
1. Red/Green: `FshGenerator` non-binding v1 subset against
|
|
76
|
+
`bmi_calculation.opt` — metadata, single-leaf `value[x]`, unit. Fix
|
|
77
|
+
the exact FSH text with a golden spec (string equality or a small
|
|
78
|
+
set of `include` assertions on stable substrings — match whichever
|
|
79
|
+
style `profile_generator_spec.rb` already uses for the JSON side)
|
|
80
|
+
2. Red/Green: `code_binding` write against `bmi_calculation.opt`'s
|
|
81
|
+
`at0004` (SNOMED-CT `60621009` and/or the LOINC binding — pick
|
|
82
|
+
whichever demonstrates the fixed-value FSH form most clearly)
|
|
83
|
+
3. Red/Green: `value_set_binding` write against `problem_list.opt`'s
|
|
84
|
+
`at0002` (ICD-11) — fix the `from <uri> (required)` line
|
|
85
|
+
4. Red/Green: multi-leaf `component` slicing — `bmi_calculation.opt`'s
|
|
86
|
+
`openEHR-EHR-OBSERVATION.body_mass_index.v2` entry already has 2 leaf
|
|
87
|
+
fields (verified: `bundle exec ruby` with `FieldExtractor#entries`,
|
|
88
|
+
2026-08-26), and `at0004`'s SNOMED/LOINC `code_binding` sits inside
|
|
89
|
+
this same multi-leaf entry — one fixture covers both slicing and
|
|
90
|
+
binding for this case. No new fixture needed (`problem_list.opt` also
|
|
91
|
+
has 5 leaf fields, `lab_result_report_reduced.opt` has 3, for
|
|
92
|
+
reference)
|
|
93
|
+
5. Manual verification (not part of this Issue's automated suite,
|
|
94
|
+
done once locally before closing): pipe generated FSH through
|
|
95
|
+
`fsh-sushi` and confirm 0 Errors, matching the shapes anlage already
|
|
96
|
+
verified by hand (`docs/reports/fsh-log.md` R1 in `skoba/anlage`)
|
|
97
|
+
|
|
98
|
+
## 6. Structural note
|
|
99
|
+
|
|
100
|
+
`FshGenerator` takes only `FieldExtractor#entries` (or a `template` it
|
|
101
|
+
extracts from, same as `ProfileGenerator.new(template)`) — no
|
|
102
|
+
`ActiveRecord::Base`/`Rails` dependency. This keeps it relocatable if a
|
|
103
|
+
future `openehr-fhirbridge` satellite gem is split out (anlage
|
|
104
|
+
`docs/backlog.md` item 5, not a decision made in this Issue).
|
|
105
|
+
|
|
106
|
+
## 7. Out of scope here
|
|
107
|
+
|
|
108
|
+
- CI wiring for Sushi verification (anlage-side decision, ruled out for
|
|
109
|
+
v1 — `docs/design/fsh-plan.md`'s 判断3 in `skoba/anlage`)
|
|
110
|
+
- The `rake fsh:export` output path (anlage-side, `skoba/anlage`'s own
|
|
111
|
+
Issue #17 scope)
|
|
112
|
+
- Any RubyGems release/publish step (human-gated per this repo's
|
|
113
|
+
`docs/backlog.md` "Release automation" / `skoba/openehr-rails#27`)
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Fix: multi-leaf non-Observation entries constrain a nonexistent `component`
|
|
2
|
+
|
|
3
|
+
- Status: draft, awaiting approval
|
|
4
|
+
- Target: `openehr-rails` (this repo). No cross-repo work.
|
|
5
|
+
- Issue: [#33](https://github.com/skoba/openehr-rails/issues/33)
|
|
6
|
+
- Log: `docs/reports/fsh-generator-log.md` (continuing R1-R5)
|
|
7
|
+
|
|
8
|
+
## 1. Bug summary (from the issue, re-confirmed directly)
|
|
9
|
+
|
|
10
|
+
`TypeMap::ENTRY_RESOURCES` (`type_map.rb:12-18`) maps openEHR ENTRY types to five
|
|
11
|
+
different FHIR R5 resources: `OBSERVATION`→`Observation`, `EVALUATION`→`Condition`,
|
|
12
|
+
`INSTRUCTION`→`ServiceRequest`, `ACTION`→`Procedure`, `ADMIN_ENTRY`→`Encounter`.
|
|
13
|
+
Only `Observation` has a `component` element in FHIR R5 — confirmed against the
|
|
14
|
+
[FHIR R5 Observation resource definition](http://hl7.org/fhir/R5/observation.html);
|
|
15
|
+
`Condition`/`ServiceRequest`/`Procedure`/`Encounter` have no equivalent repeatable
|
|
16
|
+
slot for arbitrary additional codings/values. `ProfileGenerator#differential_elements`
|
|
17
|
+
(`profile_generator.rb:52-60`) and `FshGenerator#build_profile`
|
|
18
|
+
(`fsh_generator.rb:26-42`) both choose between a single-leaf `value[x]` path and a
|
|
19
|
+
multi-leaf `component` path purely on `entry[:fields].size`, with no check for
|
|
20
|
+
whether `resource_type` actually supports `component`. Confirmed via `sushi`:
|
|
21
|
+
`problem_list.opt`'s 5-leaf `EVALUATION`→`Condition` entry produces 29
|
|
22
|
+
`"No element found at path component..."` errors (matches the issue's own
|
|
23
|
+
citation).
|
|
24
|
+
|
|
25
|
+
This is **not EVALUATION-specific** — it affects any multi-leaf entry mapped to any
|
|
26
|
+
of the four non-`Observation` resources. No existing fixture currently has a
|
|
27
|
+
multi-leaf `INSTRUCTION`/`ACTION`/`ADMIN_ENTRY` entry, so those paths are untested
|
|
28
|
+
today, but the same defect applies to them by construction.
|
|
29
|
+
|
|
30
|
+
### Related latent dead code, found during this investigation
|
|
31
|
+
|
|
32
|
+
`TypeMap.value_element(resource_type)` (`type_map.rb:53-58`):
|
|
33
|
+
```ruby
|
|
34
|
+
def value_element(resource_type)
|
|
35
|
+
resource_type == 'Observation' ? 'value[x]' : 'value[x]'
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
Its own comment says "Single-leaf Observations use `value[x]`; everything else hangs
|
|
39
|
+
off a component" — but the ternary returns the identical string on both branches, so
|
|
40
|
+
it implements none of that. Confirmed via `grep`: **called nowhere in `lib/`** (the
|
|
41
|
+
only other hits are unrelated local-variable names in `profile_generator_spec.rb`).
|
|
42
|
+
This is exactly the resource-type branch this issue needs, stubbed and never wired
|
|
43
|
+
up. Recommend removing it as part of this fix rather than leaving dead, misleading
|
|
44
|
+
code that describes behavior it doesn't implement — see §3.
|
|
45
|
+
|
|
46
|
+
## 2. Options considered
|
|
47
|
+
|
|
48
|
+
**(a) FHIR Extension-based encoding for non-Observation multi-leaf entries.**
|
|
49
|
+
Define a custom complex extension (its own `StructureDefinition`) to carry
|
|
50
|
+
additional leaf values on `Condition`/`ServiceRequest`/`Procedure`/`Encounter`,
|
|
51
|
+
sliced under `resource_type.extension`. This is the FHIR-canonical way to add
|
|
52
|
+
structured data beyond a base resource's element set, and is the only option that
|
|
53
|
+
actually *represents* the extra leaves rather than dropping or refusing them.
|
|
54
|
+
**Rejected for this issue**: it requires designing and shipping a second
|
|
55
|
+
`StructureDefinition` (the extension itself) with its own cardinality/binding/typing
|
|
56
|
+
decisions that no prior art in this repo or `anlage`'s design docs addresses (checked
|
|
57
|
+
directly: `anlage/docs/design/fsh-plan.md` only says "multi-leaf `component`
|
|
58
|
+
slicing" as v1 scope, never anticipates the non-Observation case). Getting an
|
|
59
|
+
extension's shape right is a real FHIR-modeling decision with downstream
|
|
60
|
+
consequences for anyone consuming these profiles — not something to decide
|
|
61
|
+
unilaterally inside a bug-fix issue. Worth a future issue of its own if a real
|
|
62
|
+
`EVALUATION`/`INSTRUCTION`/`ACTION`/`ADMIN_ENTRY` multi-leaf template is actually
|
|
63
|
+
needed by a consumer (none is, today — see §5).
|
|
64
|
+
|
|
65
|
+
**(b) Split into multiple linked FHIR resources** (e.g. a primary `Condition` plus
|
|
66
|
+
separate `Observation` resources per extra leaf, linked via `Observation.focus`).
|
|
67
|
+
**Rejected**: changes the "one profile per ENTRY" architecture
|
|
68
|
+
(`profile_generator.rb:8-11`'s own doc comment) into "one-to-many," a much larger
|
|
69
|
+
structural change than this issue's scope, and duplicates concerns `#33`'s own
|
|
70
|
+
"needs its own explore/plan" framing already flags as too large for this pass.
|
|
71
|
+
|
|
72
|
+
**(c) Restrict multi-leaf profile generation to `Observation`-mapped entries; raise
|
|
73
|
+
a clear, documented error for multi-leaf entries mapped elsewhere.** No data is
|
|
74
|
+
silently dropped (an error beats silent corruption) and no unvetted FHIR-modeling
|
|
75
|
+
decision is made. The limitation is explicit and testable end-to-end today.
|
|
76
|
+
**Recommended** — see §3.
|
|
77
|
+
|
|
78
|
+
## 3. Recommended fix: (c), restrict + explicit error
|
|
79
|
+
|
|
80
|
+
- `TypeMap` gains a way to answer "does this resource type support `component`?" —
|
|
81
|
+
replace the dead `value_element` method (§1) with what it should have been:
|
|
82
|
+
```ruby
|
|
83
|
+
COMPONENT_CAPABLE_RESOURCES = %w[Observation].freeze
|
|
84
|
+
|
|
85
|
+
def component_capable?(resource_type)
|
|
86
|
+
COMPONENT_CAPABLE_RESOURCES.include?(resource_type)
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
(A one-element array reads oddly today, but names the actual FHIR fact this repo
|
|
90
|
+
has verified — `Observation` is the only R5 resource among the five in
|
|
91
|
+
`ENTRY_RESOURCES` with a `component` element — and gives future resource-type
|
|
92
|
+
additions one place to declare the same fact, rather than re-deriving it.)
|
|
93
|
+
- `ProfileGenerator#differential_elements` (`profile_generator.rb:52-60`): when
|
|
94
|
+
`entry[:fields].size > 1` **and** `!TypeMap.component_capable?(resource_type)`,
|
|
95
|
+
raise a new `OpenehrRails::Fhir::UnsupportedProfileError` (or similar; exact class
|
|
96
|
+
name/placement to be finalized in implementation, not a design-doc-blocking
|
|
97
|
+
detail) with a message naming the entry's archetype id, resource type, and leaf
|
|
98
|
+
count — not a silent skip, not a partial/wrong profile.
|
|
99
|
+
- `FshGenerator#build_profile` (`fsh_generator.rb:26-42`): same branch, same error
|
|
100
|
+
class — one shared decision point, not two independently-drifting ones. Consider
|
|
101
|
+
whether the check belongs in a shared location both classes call (e.g. a small
|
|
102
|
+
module method) rather than duplicated inline logic, to avoid the exact kind of
|
|
103
|
+
two-copies drift `#33` itself was born from (`ProfileGenerator` and
|
|
104
|
+
`FshGenerator` independently duplicating the same `size == 1` branch that neither
|
|
105
|
+
originally checked `resource_type` for).
|
|
106
|
+
- **Where the error surfaces to a caller**: `ProfileGenerator#profiles` and
|
|
107
|
+
`FshGenerator#to_fsh_files` currently map over `@entries` unconditionally
|
|
108
|
+
(`profile_generator.rb:23-24`, `fsh_generator.rb:17-22`) — a raised error on one
|
|
109
|
+
entry would currently abort the whole batch, silently dropping profiles for
|
|
110
|
+
*other*, unaffected entries in the same template. Decide during implementation
|
|
111
|
+
whether that's acceptable (single-entry templates are today's only real fixtures)
|
|
112
|
+
or whether `profiles`/`to_fsh_files` should skip-and-report per-entry instead of
|
|
113
|
+
raising through the whole batch — flagging this as a design question for
|
|
114
|
+
approval, not deciding it here, since it changes both public methods' contracts
|
|
115
|
+
either way.
|
|
116
|
+
|
|
117
|
+
## 4. Spec plan (t-wada: red before green)
|
|
118
|
+
|
|
119
|
+
- New spec (both `ProfileGenerator` and `FshGenerator`, or a shared example group if
|
|
120
|
+
the check ends up in one shared place per §3): parsing `problem_list.opt`
|
|
121
|
+
(5-leaf `EVALUATION`→`Condition`) and calling `.profiles`/`.to_fsh_files` raises
|
|
122
|
+
the new error, with a message that names the archetype id. **Red** today: no
|
|
123
|
+
error is raised; instead a differential/FSH referencing a nonexistent `component`
|
|
124
|
+
path is silently produced. **Enhancement** resolution kind (new documented
|
|
125
|
+
behavior, not a pre-existing property being pinned).
|
|
126
|
+
- Regression pin: `bmi_calculation.opt`'s multi-leaf `Observation`-mapped entry
|
|
127
|
+
(`body_mass_index.v2`, 2 leaves) is unaffected — existing
|
|
128
|
+
`profile_generator_spec.rb`/`fsh_generator_spec.rb` coverage of this fixture
|
|
129
|
+
already exercises it; confirm those specs still pass unchanged (they should, this
|
|
130
|
+
fix only adds a new branch for the non-capable-resource case).
|
|
131
|
+
- FSH-side confirmation per the issue's own acceptance criteria: after the fix,
|
|
132
|
+
regenerate FSH for `bmi_calculation.opt` and pipe through `sushi` again — expect
|
|
133
|
+
the same 0 Errors/0 Warnings as R2/R3 already established, unchanged.
|
|
134
|
+
`problem_list.opt` no longer produces any FSH to compile (it raises instead), so
|
|
135
|
+
there's nothing left to feed `sushi` for that fixture — satisfies the acceptance
|
|
136
|
+
criterion's spirit (no invalid FSH exists for a case that can't be represented
|
|
137
|
+
correctly yet) without a false claim of validity.
|
|
138
|
+
|
|
139
|
+
## 5. Compatibility, scope, and semver
|
|
140
|
+
|
|
141
|
+
- **No existing fixture regresses**: no repo fixture has a multi-leaf
|
|
142
|
+
`INSTRUCTION`/`ACTION`/`ADMIN_ENTRY` entry today, and `problem_list.opt`'s
|
|
143
|
+
`EVALUATION` entry going from "silently wrong" to "clear error" is a bug fix, not
|
|
144
|
+
a behavior anyone could have been relying on (the prior output was invalid FHIR).
|
|
145
|
+
- **Host-app impact**: any host app that scaffolded `--fhir` output from a
|
|
146
|
+
multi-leaf `EVALUATION`/`INSTRUCTION`/`ACTION`/`ADMIN_ENTRY` template today has a
|
|
147
|
+
silently-broken generated profile already (confirmed for `EVALUATION` via
|
|
148
|
+
`sushi`; the other three are the same code path). This fix surfaces that as a
|
|
149
|
+
loud error on regeneration rather than continuing to ship invalid output.
|
|
150
|
+
`CHANGELOG.md` should say this plainly.
|
|
151
|
+
- **Semver**: raising a new, previously-unraised exception for input that already
|
|
152
|
+
produced semantically-invalid output is a bug fix — **patch**, not minor (no new
|
|
153
|
+
public API surface is added for callers to use; `UnsupportedProfileError`, if
|
|
154
|
+
it becomes part of the public interface at all, is something callers only see
|
|
155
|
+
when hitting the previously-broken case, not a new capability to opt into).
|
|
156
|
+
- **`docs/reports/fsh-generator-log.md`** continues as this fix's progress log
|
|
157
|
+
(already tracking `#32`/`#33` as one continuing thread).
|
|
158
|
+
|
|
159
|
+
## 6. Open questions for approval
|
|
160
|
+
|
|
161
|
+
1. Confirm option (c) (restrict + explicit error) over (a)/(b) — recommended, but
|
|
162
|
+
this is the actual FHIR-modeling judgment call this design doc exists to get
|
|
163
|
+
signed off on.
|
|
164
|
+
2. Per-entry error handling in `profiles`/`to_fsh_files` (§3's last bullet): raise
|
|
165
|
+
through the whole batch, or skip-and-report per entry? No existing fixture has
|
|
166
|
+
more than one entry per template that would surface this distinction today, so
|
|
167
|
+
either choice is currently unobservable in this repo's own fixtures — pick the
|
|
168
|
+
simpler one (raise-through) unless there's a reason to prefer graceful
|
|
169
|
+
degradation now.
|
|
170
|
+
3. Exact error class name/namespace (`OpenehrRails::Fhir::UnsupportedProfileError`
|
|
171
|
+
suggested, not fixed).
|
|
172
|
+
|
|
173
|
+
## 7. Stop point
|
|
174
|
+
|
|
175
|
+
Explore + design only, per this repo's ticket-driven workflow. Do not implement
|
|
176
|
+
until this document is approved. Next steps after approval: implement (Codex per
|
|
177
|
+
this repo's division of labor, or directly if the change is judged small enough to
|
|
178
|
+
skip that split — decide at approval time), Claude Code review, full
|
|
179
|
+
`bundle exec rspec` + full-repo `rubocop` + `sushi` re-verification for
|
|
180
|
+
`bmi_calculation.opt`, commit(s), `docs/reports/fsh-generator-log.md` entry.
|
|
@@ -343,3 +343,69 @@ section). So tagging `v0.5.0` and pushing the tag is lower-risk than it might so
|
|
|
343
343
|
publishing to RubyGems is a distinct, later, human-executed step. This would also be
|
|
344
344
|
the **first real tag to exercise PR #28's fixed release path end-to-end** -- its own
|
|
345
345
|
body deferred that verification to "the next real release (>= 0.5.0)," which this is.
|
|
346
|
+
|
|
347
|
+
## R8 -- v0.5.0 tagged, release.yml verified green, artifact reproducibility confirmed
|
|
348
|
+
|
|
349
|
+
User approved tagging. Executed:
|
|
350
|
+
|
|
351
|
+
1. `lib/openehr_rails/version.rb` bumped `0.4.1` -> `0.5.0`; `CHANGELOG.md`'s
|
|
352
|
+
`[Unreleased]` retitled to `## [0.5.0] - 2026-08-25` (fresh empty `[Unreleased]`
|
|
353
|
+
left above it), matching the exact pattern the `0.4.1` release-bump commit used
|
|
354
|
+
(`2cddb0d`, verified by reading its diff directly rather than guessing the
|
|
355
|
+
convention). Full suite re-run after the bump: 275 examples, 0 failures.
|
|
356
|
+
`bundle exec rake release:check` failed first (dirty tree, expected -- the check
|
|
357
|
+
requires a clean tree), then passed clean after committing (`69db63f`, pushed).
|
|
358
|
+
`git tag -a v0.5.0` (annotated, full CHANGELOG-derived message) and
|
|
359
|
+
`git push origin v0.5.0`.
|
|
360
|
+
|
|
361
|
+
2. **`release.yml` run verified green end-to-end** -- GitHub Actions run
|
|
362
|
+
[32831263811](https://github.com/skoba/openehr-rails/actions/runs/32831263811),
|
|
363
|
+
triggered by the `v0.5.0` tag push. Overall conclusion: **success** (confirmed via
|
|
364
|
+
the run's own `conclusion` field, not inferred from individual step icons). All 12
|
|
365
|
+
jobs green: the 9 reused `ci.yml` spec-matrix jobs, `demo smoke`, `application
|
|
366
|
+
template smoke test`, and `Build gem artifact` (its `release:check`, `Build gem`,
|
|
367
|
+
and `Upload gem artifact` steps all succeeded). This is the **first tag-push run
|
|
368
|
+
with a fully green overall conclusion** -- `v0.4.1`
|
|
369
|
+
(`32550344344`) and `v0.4.0` (`31660884406`) were both structurally red at the
|
|
370
|
+
now-removed RubyGems-publish step (`docs/backlog.md` "CI status" section); that
|
|
371
|
+
step no longer exists in `release.yml` (PR #28), and this run empirically confirms
|
|
372
|
+
its removal actually fixed the red -- not just that the workflow file changed.
|
|
373
|
+
|
|
374
|
+
3. **Artifact/local-build reproducibility confirmed** -- downloaded the run's `gem`
|
|
375
|
+
artifact (`gh run download 32831263811 -n gem`):
|
|
376
|
+
`openehr-rails-0.5.0.gem`, 197632 bytes, sha256
|
|
377
|
+
`e07815bd1c86736403cbb558fec869fbe04666f695e6cc12a41dad9be77230e6`. Ran
|
|
378
|
+
`bundle exec rake build` locally at the same tagged commit (`69db63f`, clean
|
|
379
|
+
tree): `pkg/openehr-rails-0.5.0.gem`, same size, **identical sha256**. CI's
|
|
380
|
+
built artifact and a local build from the same commit are byte-identical --
|
|
381
|
+
confirms build reproducibility, not just "both builds succeeded."
|
|
382
|
+
|
|
383
|
+
RubyGems publish is next: per condition, that remains a deliberate human `gem push`
|
|
384
|
+
step (established operating model, `docs/backlog.md` "Release automation"), not
|
|
385
|
+
something this session executes -- the sha256 above is the value to check the
|
|
386
|
+
locally-published gem against before/after `gem push`, and after publish confirms.
|
|
387
|
+
Awaiting that confirmation before the final backlog/CHANGELOG follow-up (condition 5)
|
|
388
|
+
and returning to dormancy.
|
|
389
|
+
|
|
390
|
+
## R9 -- RubyGems publish confirmed, backlog updated, dormant
|
|
391
|
+
|
|
392
|
+
User reported publish complete. Verified directly rather than trusting the report at
|
|
393
|
+
face value: queried `https://rubygems.org/api/v1/versions/openehr-rails.json`.
|
|
394
|
+
`openehr-rails` `0.5.0` is listed, `created_at: 2026-08-25T09:26:15.342Z`, and its
|
|
395
|
+
published `sha` field (`e07815bd1c86736403cbb558fec869fbe04666f695e6cc12a41dad9be77230e6`)
|
|
396
|
+
matches R8's CI-artifact and local-build sha256 exactly -- full chain confirmed
|
|
397
|
+
byte-identical: local build == CI artifact == published gem.
|
|
398
|
+
|
|
399
|
+
Added a bullet to `docs/backlog.md`'s "CI status" section recording the `v0.5.0`
|
|
400
|
+
tag run (first fully-green tag-push run, confirming PR #28's release-path fix)
|
|
401
|
+
and the three-way sha256 match including the now-confirmed RubyGems listing.
|
|
402
|
+
Verified `CHANGELOG.md`'s `[0.5.0] - 2026-08-25` section is correctly finalized
|
|
403
|
+
with a fresh empty `[Unreleased]` above it -- no further edit needed. Full suite
|
|
404
|
+
re-confirmed green (275 examples, 0 failures) before this commit.
|
|
405
|
+
|
|
406
|
+
**Issue #30 fully closed out**: filed, designed, implemented (Codex + independent
|
|
407
|
+
review), merged, released as `0.5.0`, published to RubyGems, and the upstream
|
|
408
|
+
knowledge shared back to `skoba/openehr-ruby#31`. `anlage`'s FSH export
|
|
409
|
+
(`skoba/anlage#17`) can now pick up `field[:value_set_uri]`/`field[:code_bindings]`
|
|
410
|
+
via a normal `bundle update` -- the consumption prerequisite flagged in R7 is
|
|
411
|
+
satisfied. Returning to dormancy.
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# FshGenerator progress log
|
|
2
|
+
|
|
3
|
+
R1〜.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## R1 -- Filing and design doc (Step 0-1)
|
|
8
|
+
|
|
9
|
+
Filed `skoba/openehr-rails#32` (goal Issue). Design authority is
|
|
10
|
+
`skoba/anlage`'s already-approved `docs/design/fsh-plan.md`
|
|
11
|
+
(2026-08-26 ruling) -- this Issue implements that plan's gem-level
|
|
12
|
+
commit split, not a fresh design.
|
|
13
|
+
|
|
14
|
+
Confirmed the input contract this Issue depends on is already live
|
|
15
|
+
(`skoba/openehr-rails#30`, shipped 0.5.0): `FieldExtractor#entries[].
|
|
16
|
+
fields[]` always carries `value_set_uri` and `code_bindings`
|
|
17
|
+
(`field_extractor.rb:160-177`).
|
|
18
|
+
|
|
19
|
+
Checked fixture leaf-field counts directly (`bundle exec ruby` with
|
|
20
|
+
`require 'active_record'; require 'openehr_rails'` -- `openehr_rails`
|
|
21
|
+
needs ActiveRecord loaded first, this repo has no `bin/rails`):
|
|
22
|
+
|
|
23
|
+
- `bmi_calculation.opt` / `bmi_calculation_without_uid.opt`: `height.v2`
|
|
24
|
+
(1 leaf), `body_weight.v2` (1 leaf), `body_mass_index.v2` (**2
|
|
25
|
+
leaves** -- and `at0004` already carries real SNOMED-CT (`60621009`)
|
|
26
|
+
+ LOINC `code_binding`, so this one entry covers both multi-leaf
|
|
27
|
+
`component` slicing and code_binding in a single fixture)
|
|
28
|
+
- `problem_list.opt`: `problem_diagnosis.v1`, 5 leaves;
|
|
29
|
+
`value_set_binding` (ICD-11) on `at0002`
|
|
30
|
+
- `lab_result_report_reduced.opt`: `laboratory_test_result.v1`, 3 leaves
|
|
31
|
+
- `sample_blood_pressure.opt`: crashes on parse (`ArgumentError:
|
|
32
|
+
invalid archetype id form`, pre-existing and unrelated to this
|
|
33
|
+
Issue -- not investigated further, not needed for this fixture set)
|
|
34
|
+
|
|
35
|
+
No new SNOMED literal budget needed -- all binding demonstrations reuse
|
|
36
|
+
codes already checked into this repo's fixtures.
|
|
37
|
+
|
|
38
|
+
Design doc written: `docs/design/fsh-generator-plan.md`. Proceeding to
|
|
39
|
+
implementation (Codex, reviewed and committed by Claude Code per this
|
|
40
|
+
repo's division of labor).
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## R2 -- Implementation, two correction rounds, commit `5f669af` (2026-08-26)
|
|
45
|
+
|
|
46
|
+
### Round 0: Codex's first delivery
|
|
47
|
+
|
|
48
|
+
Codex implemented `FshGenerator` covering the full v1 scope table.
|
|
49
|
+
Self-reported: "SUSHI 3.16.0 was present, but could not finish loading
|
|
50
|
+
its R5 package under restricted network access" -- Codex's own sandbox
|
|
51
|
+
could not actually verify the generated FSH compiles. Claude Code has
|
|
52
|
+
unrestricted network access in this session and re-verified directly.
|
|
53
|
+
|
|
54
|
+
### Round 1 finding: conflicting CodeableConcept assignments
|
|
55
|
+
|
|
56
|
+
Regenerated FSH from Codex's actual committed code (not a
|
|
57
|
+
paraphrase) and piped it through `sushi` (v3.16.0, real local install)
|
|
58
|
+
myself. `bmi_calculation.opt`'s `body_mass_index.v2` (2 code_bindings
|
|
59
|
+
on `at0004`: SNOMED-CT + LOINC) produced:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
error Cannot assign http://snomed.info/sct to this element; a different
|
|
63
|
+
uri is already assigned: "http://openehr.org/ckm/archetypes".
|
|
64
|
+
error Cannot assign http://loinc.org to this element; a different uri
|
|
65
|
+
is already assigned: "http://openehr.org/ckm/archetypes".
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Root cause: the original `binding_rules` wrote one `* path =
|
|
69
|
+
SYSTEM#code` fixed-value assignment per `code_bindings` entry, stacked
|
|
70
|
+
after the archetype_id anchor's `.coding.system`/`.coding.code` pair.
|
|
71
|
+
`* path = SYSTEM#code` is FSH shorthand for fixing an *entire*
|
|
72
|
+
CodeableConcept to one coding -- writing it twice (or after an
|
|
73
|
+
existing `.coding.system`/`.coding.code` pair) is a conflicting
|
|
74
|
+
reassignment of the same slot, not an append.
|
|
75
|
+
|
|
76
|
+
Explored the fix space empirically (all runs actually compiled with
|
|
77
|
+
`sushi`, not assumed):
|
|
78
|
+
- `code.coding[0]`/`code.coding[1]` explicit indexing without a prior
|
|
79
|
+
slicing declaration: **fails** ("No element found at path
|
|
80
|
+
code.coding[0]") -- FHIR profile differentials don't support
|
|
81
|
+
indexing into an un-sliced repeating element this way.
|
|
82
|
+
- `code.coding[+]`/`[=]` append syntax, same issue: **fails**,
|
|
83
|
+
identical error.
|
|
84
|
+
- Unindexed `code.coding.system = X` / `code.coding.code = Y` alone
|
|
85
|
+
(no second coding attempted): **compiles** (0 Errors) -- but this
|
|
86
|
+
form sets a *pattern* on the coding sub-elements
|
|
87
|
+
(`patternUri`/`patternCode` in the resulting differential), not an
|
|
88
|
+
indexed instance value, which is why indexing on top of it doesn't
|
|
89
|
+
work.
|
|
90
|
+
- **Slicing `code.coding` by the `system` value discriminator, one
|
|
91
|
+
named slice per source** (`ckm` for the archetype_id anchor, one per
|
|
92
|
+
`code_bindings` entry, using the terminology alias downcased as the
|
|
93
|
+
slice name): **compiles, 0 Errors**, both standalone (`code.coding`)
|
|
94
|
+
and nested inside a `component[slice].code.coding` path. This is the
|
|
95
|
+
fix that shipped.
|
|
96
|
+
|
|
97
|
+
Sent Codex the exact verified-working FSH pattern (both the standalone
|
|
98
|
+
and nested-in-component forms, copy-pasteable) plus the failing
|
|
99
|
+
pattern and its error, and asked for a rewrite -- not a vague "fix the
|
|
100
|
+
conflict" instruction. Codex's fix matches: `code_rules` now branches
|
|
101
|
+
on whether `bindings` is empty (`simple_code_rules`, unchanged
|
|
102
|
+
unindexed form) vs non-empty (new sliced form via `binding_slices`).
|
|
103
|
+
|
|
104
|
+
### Round 2 finding: `component` doesn't exist on `Condition`
|
|
105
|
+
|
|
106
|
+
Re-verified again after the round-1 fix -- `bmi_calculation.opt`
|
|
107
|
+
(all-`Observation` entries) now compiles with **0 Errors**, confirmed
|
|
108
|
+
independently (regenerated from the committed code, not from Codex's
|
|
109
|
+
pasted example). `problem_list.opt` (single `EVALUATION` entry, 5
|
|
110
|
+
leaves) does not: **29 errors**, all `"No element found at path
|
|
111
|
+
component..."` for `Condition`.
|
|
112
|
+
|
|
113
|
+
Root cause: `TypeMap::ENTRY_RESOURCES` maps `EVALUATION` → `Condition`
|
|
114
|
+
(`type_map.rb:14`). `Condition` has no `component` element -- that's
|
|
115
|
+
`Observation`-specific in FHIR R5. `FshGenerator`'s multi-leaf branch
|
|
116
|
+
(mirroring `ProfileGenerator#component_elements`) always emits
|
|
117
|
+
`component`-path rules whenever `entry[:fields].size > 1`, with no
|
|
118
|
+
check for whether the target `resource_type` actually has a
|
|
119
|
+
`component` slot. This is a **pre-existing gap**, not something this
|
|
120
|
+
Issue introduced -- `ProfileGenerator`'s JSON output for the same
|
|
121
|
+
fixture almost certainly has the identical semantic defect, just never
|
|
122
|
+
caught (JSON isn't schema-validated the way FSH is by Sushi, and
|
|
123
|
+
`profile_generator_spec.rb` only exercises all-`Observation` fixtures).
|
|
124
|
+
|
|
125
|
+
Filed separately: `skoba/openehr-rails#33`. Out of scope for `#32` --
|
|
126
|
+
needs its own explore/plan (candidate directions: an extension,
|
|
127
|
+
restricting multi-leaf support to `Observation`-mapped entries with an
|
|
128
|
+
explicit documented limitation, or something else not yet decided).
|
|
129
|
+
|
|
130
|
+
Narrowed `#32`'s scope to match reality rather than over-claim: added
|
|
131
|
+
a 2-line code comment on the multi-leaf branch citing `#33`, and a
|
|
132
|
+
CHANGELOG note distinguishing "Observation-mapped output is
|
|
133
|
+
Sushi-verified" from "multi-leaf non-Observation entries have a known
|
|
134
|
+
gap tracked as #33". The `value_set_binding` spec assertion for
|
|
135
|
+
`problem_list.opt` stays as a text-level substring check (the `from
|
|
136
|
+
<uri> (required)` write itself is correct FSH, verified in isolation
|
|
137
|
+
in `docs/reports/fsh-log.md` R1 in `skoba/anlage`) -- it does not claim
|
|
138
|
+
whole-document Sushi compilation, which would be false for this
|
|
139
|
+
fixture.
|
|
140
|
+
|
|
141
|
+
### Final verification (Claude Code, independent of Codex's own reports)
|
|
142
|
+
|
|
143
|
+
- Regenerated FSH from the final committed `fsh_generator.rb` for both
|
|
144
|
+
`bmi_calculation.opt` and `problem_list.opt`, piped each through
|
|
145
|
+
`sushi` directly: **0 Errors** for `bmi_calculation.opt` (3
|
|
146
|
+
profiles), **29 errors** for `problem_list.opt` (expected, tracked
|
|
147
|
+
as `#33`, not a regression from anything this Issue claims)
|
|
148
|
+
- `bundle exec rspec spec/openehr_rails/fhir/`: 28 examples, 0 failures
|
|
149
|
+
- `bundle exec rspec` (full suite, this session's own environment, not
|
|
150
|
+
Codex's more restricted sandbox): **281 examples, 0 failures** --
|
|
151
|
+
the 18 failures Codex reported (`Errno::EPERM`/`getifaddrs` in
|
|
152
|
+
remote-fetch specs) did not reproduce here, consistent with this
|
|
153
|
+
project's prior observation that this class of sandbox network
|
|
154
|
+
restriction doesn't reproduce on GitHub Actions runners either
|
|
155
|
+
- `bundle exec rubocop`: 108 files, no offenses
|
|
156
|
+
- Committed `5f669af`, pushed, CI run verified green (see bundle
|
|
157
|
+
report for run ID)
|
|
158
|
+
|
|
159
|
+
### Closure
|
|
160
|
+
|
|
161
|
+
`#32`'s acceptance criteria are met for the scope that's actually
|
|
162
|
+
Sushi-clean (`Observation`-mapped entries). `#33` tracks the
|
|
163
|
+
`Condition`/`component` gap as separate follow-up work, not a blocker
|
|
164
|
+
for closing `#32`.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## R3 -- Remove active_support from FshGenerator (0.6.0 task, condition 1)
|
|
169
|
+
|
|
170
|
+
Codex's `#32` delivery required `active_support/core_ext/string` for three
|
|
171
|
+
`String` methods (`camelize`, `humanize`, `parameterize.dasherize`),
|
|
172
|
+
violating the structural clause `anlage`'s `docs/design/fsh-plan.md`
|
|
173
|
+
already committed to (追記1, 2026-08-26): FshGenerator must depend on
|
|
174
|
+
`openehr` only (Rails-independent), so it can move wholesale to the
|
|
175
|
+
future `openehr-fhirbridge` satellite gem -- explicitly named review
|
|
176
|
+
criterion: no dependency on `ActiveRecord::Base` or the `Rails`
|
|
177
|
+
namespace. `require 'active_support/core_ext/string'` doesn't touch
|
|
178
|
+
either directly, but ActiveSupport is Rails-family tooling, not
|
|
179
|
+
`openehr`-only; the clause's spirit is what's being restored here, not a
|
|
180
|
+
literal `ActiveRecord::Base`/`Rails` grep hit.
|
|
181
|
+
|
|
182
|
+
Measured exact behavior before replacing anything (not guessed): ran the
|
|
183
|
+
three ActiveSupport methods against representative inputs directly, then
|
|
184
|
+
cross-checked plain-Ruby candidate replacements against every entry from
|
|
185
|
+
every fixture this generator is exercised against (`bmi_calculation.opt`,
|
|
186
|
+
`bmi_calculation_without_uid.opt`, `problem_list.opt`,
|
|
187
|
+
`lab_result_report_reduced.opt`) -- all matched exactly, including the
|
|
188
|
+
one non-obvious case: `.parameterize` alone leaves internal underscores
|
|
189
|
+
intact (`"OBSERVATION.body_mass_index.v2".parameterize` =>
|
|
190
|
+
`"observation-body_mass_index-v2"`); `.dasherize` afterward is doing real
|
|
191
|
+
work (not redundant), converting those to `"observation-body-mass-index-v2"`.
|
|
192
|
+
|
|
193
|
+
Replaced with three small private helpers (`camelize`, `humanize`,
|
|
194
|
+
`parameterize`) matching the measured behavior. Re-verified with `sushi`
|
|
195
|
+
after the change, not just `rspec`: `bmi_calculation.opt` still 0
|
|
196
|
+
Errors/0 Warnings (3 profiles), `problem_list.opt` still exactly 29
|
|
197
|
+
errors (unchanged `#33` gap). Full suite: 281 examples, 0 failures.
|
|
198
|
+
RuboCop clean. Committed directly to `master` (`0181f7f`, tiny/self-
|
|
199
|
+
contained per the task's own "極小コミット" framing -- no separate
|
|
200
|
+
issue/branch/PR, matching this repo's docs-and-tooling-adjacent-fix
|
|
201
|
+
precedent for changes that don't touch public API or behavior).
|
|
202
|
+
|
|
203
|
+
`ProfileGenerator` still requires `active_support/core_ext/string` --
|
|
204
|
+
explicitly out of scope: it isn't planned for extraction to
|
|
205
|
+
`openehr-fhirbridge` on its own, so the structural clause doesn't bind it.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## R4 -- Release inventory (v0.5.0..master) and 0.6.0 proposal (condition 3)
|
|
210
|
+
|
|
211
|
+
Classified every non-merge commit since `v0.5.0` (8 commits; per-commit
|
|
212
|
+
file lists pulled via `git log --name-only`, not asserted from memory):
|
|
213
|
+
|
|
214
|
+
- **Neutral (6 commits)**: `19c905f`, `0d2a8ce` (docs, #30 closeout),
|
|
215
|
+
`ec019b4` (`.github/ISSUE_TEMPLATE/bug_report.md` only -- dev-tooling
|
|
216
|
+
config), `f7cc0eb` (design doc + report log), `a996ef4` (report log),
|
|
217
|
+
`7e8014f` (`CLAUDE.md` + report log).
|
|
218
|
+
- **`5f669af`** (adds `FshGenerator`): new file, new public class
|
|
219
|
+
(`OpenehrRails::Fhir::FshGenerator`, public `to_fsh_files`), touches
|
|
220
|
+
`lib/openehr_rails.rb` to require it. New backward-compatible public
|
|
221
|
+
API surface -- **minor**.
|
|
222
|
+
- **`0181f7f`** (drops `active_support` from `FshGenerator`): touches
|
|
223
|
+
`lib/` directly but verified byte-identical output (R3 above) -- no
|
|
224
|
+
new capability, no behavior change, no public API change, no gemspec
|
|
225
|
+
dependency change. Per this repo's own release convention ("a commit
|
|
226
|
+
touching shipped runtime code is at minimum patch, even when its
|
|
227
|
+
observable behavior is unchanged"), classified **patch**.
|
|
228
|
+
|
|
229
|
+
Overall: the range's highest-level commit dominates -- **minor**, i.e.
|
|
230
|
+
`v0.5.0 -> v0.6.0`. No breaking changes; `FshGenerator` is net-new so
|
|
231
|
+
there's no existing consumer to break. `CHANGELOG.md`'s `[Unreleased]`
|
|
232
|
+
content matches (Added: FshGenerator; Changed: the active_support
|
|
233
|
+
removal -- the latter's entry was missing and added in `4b595ca` while
|
|
234
|
+
preparing this inventory, since this repo's convention writes CHANGELOG
|
|
235
|
+
entries at merge/commit time and it had been skipped when `0181f7f`
|
|
236
|
+
landed).
|
|
237
|
+
|
|
238
|
+
No downstream-consumption blocker analogous to R7's anlage-FSH note is
|
|
239
|
+
outstanding for this release -- `anlage`'s FSH work is what's consuming
|
|
240
|
+
`FshGenerator` itself, and per condition 4 of this task, the immediate
|
|
241
|
+
next step after this release is `#33`'s own explore/plan, not a
|
|
242
|
+
release-gated anlage dependency.
|
|
243
|
+
|
|
244
|
+
Proceeding to version bump, CHANGELOG finalization, and tag per the
|
|
245
|
+
task's condition 3 -- same mechanical process as R8/R9 for `v0.5.0`,
|
|
246
|
+
already reused rather than re-derived.
|
|
247
|
+
|
|
248
|
+
## R5 -- v0.6.0 tagged, release.yml verified green, artifact reproducibility confirmed
|
|
249
|
+
|
|
250
|
+
Caught and fixed a missing `CHANGELOG.md` entry for `0181f7f` (the
|
|
251
|
+
active_support removal) while preparing the version bump -- added a
|
|
252
|
+
`### Changed` note (`4b595ca`), full suite re-confirmed green first.
|
|
253
|
+
|
|
254
|
+
`lib/openehr_rails/version.rb` bumped `0.5.0` -> `0.6.0`;
|
|
255
|
+
`CHANGELOG.md`'s `[Unreleased]` retitled `## [0.6.0] - 2026-08-26` (fresh
|
|
256
|
+
empty `[Unreleased]` left above). Full suite: 281 examples, 0 failures.
|
|
257
|
+
Full-repo RuboCop (not just the touched files): 108 files, no offenses.
|
|
258
|
+
`bundle exec rake release:check`: OK on the first try (tree was already
|
|
259
|
+
clean from the prior commit). Committed (`4080053`), pushed, annotated
|
|
260
|
+
tag `v0.6.0` pushed.
|
|
261
|
+
|
|
262
|
+
`release.yml` run [32915373663](https://github.com/skoba/openehr-rails/actions/runs/32915373663):
|
|
263
|
+
overall conclusion **success** (confirmed via the run's own `conclusion`
|
|
264
|
+
field), all 12 jobs green -- second consecutive fully-green tag-push run,
|
|
265
|
+
same as `v0.5.0`'s (R8), confirming the release-path fix (PR #28) holds
|
|
266
|
+
across releases, not just as a one-off.
|
|
267
|
+
|
|
268
|
+
Artifact/local-build reproducibility, same method as R8: downloaded the
|
|
269
|
+
run's `gem` artifact (`gh run download 32915373663 -n gem`):
|
|
270
|
+
`openehr-rails-0.6.0.gem`, sha256
|
|
271
|
+
`08cdd14ab1f3890b0c6b5f0ae0d5ca55615f0b4874ed5efb0cd4d7bda9e573ca`. Built
|
|
272
|
+
`pkg/openehr-rails-0.6.0.gem` locally at the same tagged commit
|
|
273
|
+
(`4080053`, clean tree): **identical sha256**. CI artifact and local
|
|
274
|
+
build byte-identical, second release in a row.
|
|
275
|
+
|
|
276
|
+
RubyGems publish is the human's step next (established operating model,
|
|
277
|
+
unchanged from `v0.5.0`) -- the sha256 above is what to check the
|
|
278
|
+
published gem against. Awaiting that confirmation, then per condition 4
|
|
279
|
+
of the task moving on to `#33`'s explore/plan (already directed, not a
|
|
280
|
+
new decision to make here) and returning to dormancy in the meantime.
|
|
281
|
+
|
|
282
|
+
## R6 -- #33 explore + design doc (condition 4)
|
|
283
|
+
|
|
284
|
+
Read the issue directly (`gh issue view 33 --json ...`, not summarized
|
|
285
|
+
from memory) and the current `type_map.rb`/`profile_generator.rb`/
|
|
286
|
+
`fsh_generator.rb` source. Confirmed independently against the FHIR R5
|
|
287
|
+
spec: only `Observation` has a `component` element among the five
|
|
288
|
+
resources `TypeMap::ENTRY_RESOURCES` maps to -- the bug is not
|
|
289
|
+
`EVALUATION`-specific, it affects any multi-leaf entry mapped to
|
|
290
|
+
`Condition`/`ServiceRequest`/`Procedure`/`Encounter` (no existing
|
|
291
|
+
fixture exercises the latter three multi-leaf, so only `EVALUATION` is
|
|
292
|
+
empirically confirmed today, but the defect applies by construction).
|
|
293
|
+
|
|
294
|
+
Found a related dead-code smell while reading `type_map.rb`:
|
|
295
|
+
`TypeMap.value_element(resource_type)` has a comment describing exactly
|
|
296
|
+
the resource-type branch `#33` needs, but its ternary returns the same
|
|
297
|
+
string on both branches and the method is called nowhere in `lib/`
|
|
298
|
+
(confirmed via `grep`) -- a stub for this exact check that was never
|
|
299
|
+
wired up. Recommending its removal/replacement as part of this fix
|
|
300
|
+
rather than leaving misleading dead code.
|
|
301
|
+
|
|
302
|
+
Checked `anlage`'s `docs/design/fsh-plan.md` directly for prior art on
|
|
303
|
+
this exact problem before designing from scratch -- none exists; the v1
|
|
304
|
+
scope only anticipated `component` slicing, never the non-Observation
|
|
305
|
+
resource-type mismatch.
|
|
306
|
+
|
|
307
|
+
Design doc written: `docs/design/multi-leaf-non-observation-plan.md`.
|
|
308
|
+
Three options considered (FHIR extension encoding / split into linked
|
|
309
|
+
resources / restrict-to-Observation with an explicit error);
|
|
310
|
+
**recommending the third** -- no unvetted FHIR-modeling decision made
|
|
311
|
+
unilaterally, no silent data loss, verifiable end-to-end today with the
|
|
312
|
+
tools already in hand (`sushi`). Two genuine judgment calls flagged for
|
|
313
|
+
approval rather than decided here: the option choice itself, and whether
|
|
314
|
+
`profiles`/`to_fsh_files` should raise through the whole batch or
|
|
315
|
+
skip-and-report per entry when one entry hits the restriction.
|
|
316
|
+
|
|
317
|
+
Gate: reporting to the user for approval before implementation.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenehrRails
|
|
4
|
+
module Fhir
|
|
5
|
+
# Generates one FHIR Shorthand profile per openEHR ENTRY.
|
|
6
|
+
class FshGenerator
|
|
7
|
+
ARCHETYPE_SYSTEM = 'http://openehr.org/ckm/archetypes'
|
|
8
|
+
TERMINOLOGY_ALIASES = {
|
|
9
|
+
'SNOMED-CT' => ['SNOMEDCT', 'http://snomed.info/sct'],
|
|
10
|
+
'LOINC' => ['LOINC', 'http://loinc.org']
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
def initialize(template)
|
|
14
|
+
@entries = OpenehrRails::Opt::FieldExtractor.new(template).entries
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_fsh_files
|
|
18
|
+
@entries.to_h do |entry|
|
|
19
|
+
id = profile_id(entry[:archetype_id])
|
|
20
|
+
[id, build_profile(entry, id)]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def build_profile(entry, id)
|
|
27
|
+
resource_type = TypeMap.resource_for_entry(entry[:rm_type])
|
|
28
|
+
lines = alias_rules(entry[:fields])
|
|
29
|
+
lines << '' unless lines.empty?
|
|
30
|
+
lines.concat(metadata(entry, id, resource_type))
|
|
31
|
+
if entry[:fields].one?
|
|
32
|
+
field = entry[:fields].first
|
|
33
|
+
lines.concat(code_rules(entry[:archetype_id], 'code', field[:code_bindings]))
|
|
34
|
+
lines.concat(value_rules('value[x]', field))
|
|
35
|
+
else
|
|
36
|
+
# Multi-leaf non-Observation resources may not expose component;
|
|
37
|
+
# this pre-existing ProfileGenerator structural gap is tracked in #33.
|
|
38
|
+
lines.concat(code_rules(entry[:archetype_id]))
|
|
39
|
+
lines.concat(component_rules(entry[:fields]))
|
|
40
|
+
end
|
|
41
|
+
"#{lines.join("\n")}\n"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def alias_rules(fields)
|
|
45
|
+
bindings = fields.flat_map { |field| field[:code_bindings] }
|
|
46
|
+
return [] if bindings.empty?
|
|
47
|
+
|
|
48
|
+
aliases = [['CKM', ARCHETYPE_SYSTEM]]
|
|
49
|
+
aliases.concat(bindings.filter_map { |binding| TERMINOLOGY_ALIASES[binding[:system_uri]] })
|
|
50
|
+
aliases.uniq.map { |name, uri| "Alias: #{name} = #{uri}" }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def metadata(entry, id, resource_type)
|
|
54
|
+
[
|
|
55
|
+
"Profile: #{camelize(id.tr('-', '_'))}",
|
|
56
|
+
"Parent: #{resource_type}",
|
|
57
|
+
"Id: #{id}",
|
|
58
|
+
"Title: \"openEHR #{humanize(entry[:concept])} (#{entry[:archetype_id]})\"",
|
|
59
|
+
''
|
|
60
|
+
]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Plain-Ruby equivalent of ActiveSupport's String#camelize on an
|
|
64
|
+
# underscore-separated input: "foo_bar" -> "FooBar".
|
|
65
|
+
def camelize(str)
|
|
66
|
+
str.split('_').map { |word| word[0].upcase + word[1..].to_s }.join
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Plain-Ruby equivalent of ActiveSupport's String#humanize on an
|
|
70
|
+
# already-lowercase, underscore-separated input: "foo_bar" -> "Foo bar".
|
|
71
|
+
def humanize(str)
|
|
72
|
+
spaced = str.tr('_', ' ')
|
|
73
|
+
spaced[0] ? spaced[0].upcase + spaced[1..].to_s.downcase : spaced
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def code_rules(archetype_id, path = 'code', bindings = [])
|
|
77
|
+
return simple_code_rules(archetype_id, path) if bindings.empty?
|
|
78
|
+
|
|
79
|
+
coding_path = "#{path}.coding"
|
|
80
|
+
slices = ['ckm 1..1', *binding_slices(bindings).map { |slice, _| "#{slice} 0..1" }]
|
|
81
|
+
[
|
|
82
|
+
"* #{coding_path} ^slicing.discriminator.type = #value",
|
|
83
|
+
"* #{coding_path} ^slicing.discriminator.path = \"system\"",
|
|
84
|
+
"* #{coding_path} ^slicing.rules = #open",
|
|
85
|
+
"* #{coding_path} contains #{slices.join(' and ')}",
|
|
86
|
+
"* #{coding_path}[ckm] = CKM##{archetype_id}",
|
|
87
|
+
*binding_slices(bindings).map do |slice, binding|
|
|
88
|
+
alias_name = TERMINOLOGY_ALIASES.fetch(binding[:system_uri]).first
|
|
89
|
+
"* #{coding_path}[#{slice}] = #{alias_name}##{bare_code(binding[:code])}"
|
|
90
|
+
end
|
|
91
|
+
]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def simple_code_rules(archetype_id, path)
|
|
95
|
+
[
|
|
96
|
+
"* #{path}.coding.system = \"#{ARCHETYPE_SYSTEM}\"",
|
|
97
|
+
"* #{path}.coding.code = ##{archetype_id}"
|
|
98
|
+
]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def component_rules(fields)
|
|
102
|
+
rules = [
|
|
103
|
+
'* component ^slicing.discriminator.type = #pattern',
|
|
104
|
+
'* component ^slicing.discriminator.path = "code"',
|
|
105
|
+
'* component ^slicing.rules = #open'
|
|
106
|
+
]
|
|
107
|
+
fields.each do |field|
|
|
108
|
+
slice_name = field[:name].downcase.gsub(/[^a-z0-9]/, '')
|
|
109
|
+
path = "component[#{slice_name}]"
|
|
110
|
+
rules << "* component contains #{slice_name} #{field[:required] ? 1 : 0}..1"
|
|
111
|
+
rules.concat(
|
|
112
|
+
code_rules(
|
|
113
|
+
"#{field[:archetype_id]}##{field[:node_id]}",
|
|
114
|
+
"#{path}.code",
|
|
115
|
+
field[:code_bindings]
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
rules.concat(value_rules("#{path}.value[x]", field))
|
|
119
|
+
end
|
|
120
|
+
rules
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def binding_slices(bindings)
|
|
124
|
+
bindings.filter_map do |binding|
|
|
125
|
+
terminology = TERMINOLOGY_ALIASES[binding[:system_uri]]
|
|
126
|
+
next unless terminology
|
|
127
|
+
|
|
128
|
+
[terminology.first.downcase, binding]
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# OPT term bindings wrap codes as `[TERMINOLOGY(version)::code]` (the
|
|
133
|
+
# version is optional). FSH needs only the code after the final `::`.
|
|
134
|
+
def bare_code(raw_code)
|
|
135
|
+
raw_code.to_s.sub(/\A\[.*::/, '').delete_suffix(']')
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def value_rules(path, field)
|
|
139
|
+
rules = [
|
|
140
|
+
"* #{path} only #{TypeMap.datatype_for(field[:rm_type])}"
|
|
141
|
+
]
|
|
142
|
+
rules.unshift("* #{path} #{field[:required] ? 1 : 0}..1") if path == 'value[x]'
|
|
143
|
+
rules << "* #{path}.unit = \"#{field[:units]}\"" if field[:rm_type] == 'DV_QUANTITY' && field[:units]
|
|
144
|
+
rules << "* #{path} from #{value_set_uri(field[:value_set_uri])} (required)" if field[:value_set_uri]
|
|
145
|
+
rules
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# C_CODE_REFERENCE uses `terminology:<canonical>` in OPT; FSH binding
|
|
149
|
+
# targets use the canonical itself.
|
|
150
|
+
def value_set_uri(uri)
|
|
151
|
+
uri.delete_prefix('terminology:')
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def profile_id(archetype_id)
|
|
155
|
+
"openehr-#{parameterize(archetype_id.delete_prefix('openEHR-EHR-'))}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Plain-Ruby equivalent of ActiveSupport's String#parameterize.dasherize:
|
|
159
|
+
# downcase, collapse every run of non-alphanumeric characters to a
|
|
160
|
+
# single "-", and strip any leading/trailing "-".
|
|
161
|
+
def parameterize(str)
|
|
162
|
+
str.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/\A-+|-+\z/, '')
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
data/lib/openehr_rails.rb
CHANGED
|
@@ -32,6 +32,7 @@ require 'openehr_rails/template_importer'
|
|
|
32
32
|
require 'openehr_rails/runtime_scaffolder'
|
|
33
33
|
require 'openehr_rails/fhir/type_map'
|
|
34
34
|
require 'openehr_rails/fhir/profile_generator'
|
|
35
|
+
require 'openehr_rails/fhir/fsh_generator'
|
|
35
36
|
require 'openehr_rails/fhir/resource_registry'
|
|
36
37
|
require 'openehr_rails/fhir/serializer'
|
|
37
38
|
require 'openehr_rails/fhir/deserializer'
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'openehr_rails'
|
|
5
|
+
|
|
6
|
+
describe OpenehrRails::Fhir::FshGenerator do
|
|
7
|
+
let(:opt_file) do
|
|
8
|
+
File.expand_path('../../generators/templates/bmi_calculation.opt', __dir__)
|
|
9
|
+
end
|
|
10
|
+
let(:template) { OpenehrRails::Opt.parse(opt_file) }
|
|
11
|
+
let(:generator) { described_class.new(template) }
|
|
12
|
+
|
|
13
|
+
describe '#to_fsh_files' do
|
|
14
|
+
subject(:files) { generator.to_fsh_files }
|
|
15
|
+
|
|
16
|
+
it 'builds one FSH profile per OBSERVATION entry' do
|
|
17
|
+
expect(files.keys).to contain_exactly(
|
|
18
|
+
'openehr-observation-height-v2',
|
|
19
|
+
'openehr-observation-body-weight-v2',
|
|
20
|
+
'openehr-observation-body-mass-index-v2'
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe 'the height profile' do
|
|
25
|
+
subject(:fsh) { files.fetch('openehr-observation-height-v2') }
|
|
26
|
+
|
|
27
|
+
it 'emits metadata and slices codings by system' do
|
|
28
|
+
expect(fsh).to include(
|
|
29
|
+
"Alias: CKM = http://openehr.org/ckm/archetypes\n",
|
|
30
|
+
"Alias: LOINC = http://loinc.org\n",
|
|
31
|
+
"Profile: OpenehrObservationHeightV2\n",
|
|
32
|
+
"Parent: Observation\n",
|
|
33
|
+
"Id: openehr-observation-height-v2\n",
|
|
34
|
+
"Title: \"openEHR Height (openEHR-EHR-OBSERVATION.height.v2)\"\n",
|
|
35
|
+
"* code.coding ^slicing.discriminator.type = #value\n",
|
|
36
|
+
"* code.coding ^slicing.discriminator.path = \"system\"\n",
|
|
37
|
+
"* code.coding ^slicing.rules = #open\n",
|
|
38
|
+
"* code.coding contains ckm 1..1 and loinc 0..1\n",
|
|
39
|
+
"* code.coding[ckm] = CKM#openEHR-EHR-OBSERVATION.height.v2\n",
|
|
40
|
+
"* code.coding[loinc] = LOINC#8302-2\n"
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'constrains value[x] to an optional Quantity with a fixed unit' do
|
|
45
|
+
expect(fsh).to include(
|
|
46
|
+
"* value[x] 0..1\n",
|
|
47
|
+
"* value[x] only Quantity\n",
|
|
48
|
+
'* value[x].unit = "cm"'
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe 'a multi-element entry (body_mass_index)' do
|
|
54
|
+
subject(:fsh) { files.fetch('openehr-observation-body-mass-index-v2') }
|
|
55
|
+
|
|
56
|
+
it 'slices component by code and constrains each slice value' do
|
|
57
|
+
expect(fsh).to include(
|
|
58
|
+
"* component ^slicing.discriminator.type = #pattern\n",
|
|
59
|
+
"* component ^slicing.discriminator.path = \"code\"\n",
|
|
60
|
+
"* component ^slicing.rules = #open\n",
|
|
61
|
+
"* component contains bodymassindex 0..1\n",
|
|
62
|
+
'* component[bodymassindex].code.coding ' \
|
|
63
|
+
"^slicing.discriminator.type = #value\n",
|
|
64
|
+
'* component[bodymassindex].code.coding ' \
|
|
65
|
+
"^slicing.discriminator.path = \"system\"\n",
|
|
66
|
+
"* component[bodymassindex].code.coding ^slicing.rules = #open\n",
|
|
67
|
+
'* component[bodymassindex].code.coding contains ' \
|
|
68
|
+
"ckm 1..1 and snomedct 0..1 and loinc 0..1\n",
|
|
69
|
+
'* component[bodymassindex].code.coding[ckm] = ' \
|
|
70
|
+
"CKM#openEHR-EHR-OBSERVATION.body_mass_index.v2#at0004\n",
|
|
71
|
+
"* component[bodymassindex].value[x] only Quantity\n",
|
|
72
|
+
'* component[bodymassindex].value[x].unit = "kg/m2"'
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'writes each ontology code binding into its coding slice' do
|
|
77
|
+
expect(fsh).to include(
|
|
78
|
+
"Alias: CKM = http://openehr.org/ckm/archetypes\n",
|
|
79
|
+
"Alias: SNOMEDCT = http://snomed.info/sct\n",
|
|
80
|
+
"Alias: LOINC = http://loinc.org\n",
|
|
81
|
+
'* component[bodymassindex].code.coding[snomedct] = ' \
|
|
82
|
+
"SNOMEDCT#60621009\n",
|
|
83
|
+
'* component[bodymassindex].code.coding[loinc] = ' \
|
|
84
|
+
"LOINC#39156-5\n"
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe 'value-set bindings' do
|
|
91
|
+
let(:opt_file) { File.expand_path('../../templates/problem_list.opt', __dir__) }
|
|
92
|
+
|
|
93
|
+
it 'emits a required binding for a C_CODE_REFERENCE constraint' do
|
|
94
|
+
fsh = generator.to_fsh_files.fetch('openehr-evaluation-problem-diagnosis-v1')
|
|
95
|
+
|
|
96
|
+
expect(fsh).to include(
|
|
97
|
+
'* component[problemdiagnosisat0002].value[x] from ' \
|
|
98
|
+
'http://id.who.int/icd/release/11/mms (required)'
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
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.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shinji KOBAYASHI
|
|
@@ -258,7 +258,10 @@ files:
|
|
|
258
258
|
- docs/backlog.md
|
|
259
259
|
- docs/design/binding-extraction-plan.md
|
|
260
260
|
- docs/design/fix-terminology-scope-plan.md
|
|
261
|
+
- docs/design/fsh-generator-plan.md
|
|
262
|
+
- docs/design/multi-leaf-non-observation-plan.md
|
|
261
263
|
- docs/reports/binding-extraction-log.md
|
|
264
|
+
- docs/reports/fsh-generator-log.md
|
|
262
265
|
- docs/reports/institutionalization-log.md
|
|
263
266
|
- gemfiles/rails_7_2.gemfile
|
|
264
267
|
- gemfiles/rails_8_0.gemfile
|
|
@@ -292,6 +295,7 @@ files:
|
|
|
292
295
|
- lib/openehr_rails/engine.rb
|
|
293
296
|
- lib/openehr_rails/fhir/capability_statement.rb
|
|
294
297
|
- lib/openehr_rails/fhir/deserializer.rb
|
|
298
|
+
- lib/openehr_rails/fhir/fsh_generator.rb
|
|
295
299
|
- lib/openehr_rails/fhir/profile_generator.rb
|
|
296
300
|
- lib/openehr_rails/fhir/profile_repository.rb
|
|
297
301
|
- lib/openehr_rails/fhir/resource_registry.rb
|
|
@@ -342,6 +346,7 @@ files:
|
|
|
342
346
|
- spec/openehr_rails/aql/model_api_spec.rb
|
|
343
347
|
- spec/openehr_rails/aql/query_validator_spec.rb
|
|
344
348
|
- spec/openehr_rails/authentication_spec.rb
|
|
349
|
+
- spec/openehr_rails/fhir/fsh_generator_spec.rb
|
|
345
350
|
- spec/openehr_rails/fhir/profile_generator_spec.rb
|
|
346
351
|
- spec/openehr_rails/fhir/serializer_spec.rb
|
|
347
352
|
- spec/openehr_rails/naming_spec.rb
|