fhirsmith 0.11.2 → 0.13.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.
- package/CHANGELOG.md +377 -2
- package/README.md +1 -1
- package/config-template.json +31 -1
- package/library/languages.js +191 -34
- package/library/path-safety.js +51 -0
- package/library/request-token.js +56 -0
- package/package.json +2 -2
- package/packages/packages.js +17 -118
- package/publisher/publisher.js +208 -30
- package/publisher/validation.js +210 -0
- package/server.js +24 -15
- package/stats-db.js +127 -0
- package/stats.js +172 -6
- package/translations/Messages.properties +5 -1
- package/tx/README.md +21 -0
- package/tx/cs/cs-api.js +30 -1
- package/tx/cs/cs-api.md +1 -1
- package/tx/cs/cs-cs.js +1 -1
- package/tx/cs/cs-icd11.js +805 -0
- package/tx/cs/cs-lang.js +331 -85
- package/tx/cs/cs-loinc.js +45 -4
- package/tx/cs/cs-mimetypes.js +492 -21
- package/tx/cs/cs-snomed.js +141 -58
- package/tx/cs/cs-ucum.js +33 -3
- package/tx/cs/icd11-expressions.js +243 -0
- package/tx/importers/export-icd11-codesystem.js +823 -0
- package/tx/importers/icd11-schema.md +285 -0
- package/tx/importers/import-icd11.js +861 -0
- package/tx/importers/import-loinc.module.js +14 -7
- package/tx/importers/import-sct.module.js +10 -1
- package/tx/library/codesystem.js +21 -27
- package/tx/library/conceptmap.js +39 -0
- package/tx/library/nesting-limit.js +79 -0
- package/tx/library/operation-outcome.js +97 -3
- package/tx/library/parameter-check.js +139 -0
- package/tx/library/valueset.js +16 -20
- package/tx/library.js +23 -1
- package/tx/ocl/cache/cache-utils.cjs +5 -3
- package/tx/ocl/cs-ocl.cjs +73 -17
- package/tx/ocl/jobs/background-queue.cjs +6 -3
- package/tx/ocl/mappers/concept-mapper.cjs +9 -1
- package/tx/ocl/shared/patches.cjs +48 -1
- package/tx/ocl/vs-ocl.cjs +15 -13
- package/tx/operation-context.js +220 -10
- package/tx/params.js +226 -175
- package/tx/sct/expressions.js +677 -15
- package/tx/tx.fhir.org.yml +3 -2
- package/tx/tx.js +72 -92
- package/tx/workers/batch-validate.js +11 -18
- package/tx/workers/batch.js +16 -28
- package/tx/workers/cache-control.js +87 -17
- package/tx/workers/closure.js +3 -8
- package/tx/workers/compare.js +6 -34
- package/tx/workers/expand.js +108 -60
- package/tx/workers/lookup.js +35 -40
- package/tx/workers/read.js +6 -40
- package/tx/workers/search.js +2 -8
- package/tx/workers/subsumes.js +58 -41
- package/tx/workers/translate.js +472 -47
- package/tx/workers/validate.js +83 -78
- package/tx/workers/worker.js +92 -11
- package/tx/xml/xml-base.js +264 -77
- package/tx/xversion/xv-conceptmap.js +81 -0
- package/tx/xversion/xv-parameters.js +8 -0
- package/tx/xversion/xv-valueset.js +39 -1
- package/utilities/merge-audit.py +178 -0
- package/utilities/why-merge-audit.md +39 -0
- package/tx/data/snomed-subset.txt +0 -84
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# ICD-11 import: database schema
|
|
2
|
+
|
|
3
|
+
Built by `tx/importers/import-icd11.js`. One database holds all three WHO code systems.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
node tx/importers/import-icd11.js \
|
|
7
|
+
--native https://id.who.int \
|
|
8
|
+
--fhir https://.../fhir \
|
|
9
|
+
--dest data/icd11-2026-01.db
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Useful options: `--systems mms,icf,foundation`, `--languages all` (or `en,fr`),
|
|
13
|
+
`--release 2026-01`, `--concurrency 16`, `--fhir-sample 500`, `--overwrite`.
|
|
14
|
+
|
|
15
|
+
The crawl is resumable. Re-running against an existing database skips concepts already
|
|
16
|
+
recorded for a language, so it can be interrupted, or topped up later with
|
|
17
|
+
`--languages es` without refetching structure.
|
|
18
|
+
|
|
19
|
+
## Where the content comes from
|
|
20
|
+
|
|
21
|
+
The native API is the only endpoint that can enumerate the classification: GET the
|
|
22
|
+
linearization root for its top-level children, then `?include=descendant` on each child
|
|
23
|
+
for its entire subtree. That is 1 + n calls instead of one per concept, and it is what
|
|
24
|
+
makes a complete copy provable rather than hopeful. Each concept is then fetched
|
|
25
|
+
individually (with `?include=diagnosticCriteria`, which costs nothing extra) once per
|
|
26
|
+
language.
|
|
27
|
+
|
|
28
|
+
The FHIR endpoint contributes only its TerminologyCapabilities and a random sample of
|
|
29
|
+
`$lookup` responses, kept in `fhir_sample` so a provider's output can be diffed against
|
|
30
|
+
WHO's own server.
|
|
31
|
+
|
|
32
|
+
## Design rule
|
|
33
|
+
|
|
34
|
+
Anything with structural consequence is a column or a row: hierarchy, codes, class kinds,
|
|
35
|
+
selectability, postcoordination axes and their permitted values, cross references. Free
|
|
36
|
+
text lives in `designation`. The untouched native JSON for each concept is kept in
|
|
37
|
+
`concept.blob` so nothing is lost, but no query needs to open it.
|
|
38
|
+
|
|
39
|
+
## Tables
|
|
40
|
+
|
|
41
|
+
| table | what it holds |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `meta` | key/value: release, languages, endpoints, tool version, request count |
|
|
44
|
+
| `system` | one row per code system — `mms`, `icf`, `foundation` — with its canonical url, release and `is_tree` |
|
|
45
|
+
| `concept` | one row per entity |
|
|
46
|
+
| `concept_parent` | parent/child edges, `seq` preserving the API's order |
|
|
47
|
+
| `concept_closure` | transitive closure including the reflexive row, so is-a never needs recursion |
|
|
48
|
+
| `designation` | all text, per language and kind |
|
|
49
|
+
| `concept_reference` | exclusions and the other non-hierarchical pointers |
|
|
50
|
+
| `pc_scale` | the postcoordination axes declared on a stem |
|
|
51
|
+
| `pc_scale_entity` | the roots of each axis; permitted values are these plus their descendants |
|
|
52
|
+
| `fhir_sample` | sampled `$lookup` responses |
|
|
53
|
+
| `crawl_lang` | resumability bookkeeping |
|
|
54
|
+
| `problem` | anything that failed, kept rather than discarded |
|
|
55
|
+
|
|
56
|
+
### concept
|
|
57
|
+
|
|
58
|
+
`uri` is the unversioned canonical form (`http://id.who.int/icd/release/11/mms/257068234`),
|
|
59
|
+
which is what the FHIR API accepts as a `code`. `entity_id` is the tail — `257068234`, or
|
|
60
|
+
`1363559646/other` for a residual. `code` is the short code, NULL where the entity has none.
|
|
61
|
+
|
|
62
|
+
`selectable` is 1 when the concept has a short code and `class_kind = 'category'`. Blocks
|
|
63
|
+
and windows have an empty code in the API and are stored as NULL; they are groupers, not
|
|
64
|
+
codes you may use. `residual` is `other`, `unspecified` or NULL.
|
|
65
|
+
|
|
66
|
+
`lft`/`rgt` are nested-set intervals, populated only where the hierarchy is genuinely a
|
|
67
|
+
tree — MMS and ICF. The Foundation is a DAG (around 9% of its entities have more than one
|
|
68
|
+
parent) so its intervals stay NULL and `concept_closure` is the only correct route there.
|
|
69
|
+
`depth` is the shortest distance from a root.
|
|
70
|
+
|
|
71
|
+
### Two ways to ask for descendants
|
|
72
|
+
|
|
73
|
+
Nested set, for MMS and ICF — a single range scan:
|
|
74
|
+
|
|
75
|
+
```sql
|
|
76
|
+
SELECT d.code, d.entity_id
|
|
77
|
+
FROM concept a JOIN concept d ON d.system = a.system
|
|
78
|
+
WHERE a.code = '1A00' AND d.lft > a.lft AND d.rgt < a.rgt;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Closure, correct for every system including the Foundation:
|
|
82
|
+
|
|
83
|
+
```sql
|
|
84
|
+
SELECT d.code FROM concept_closure cl
|
|
85
|
+
JOIN concept a ON a.id = cl.ancestor
|
|
86
|
+
JOIN concept d ON d.id = cl.descendant
|
|
87
|
+
WHERE a.code = '1A00' AND cl.depth > 0;
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`is-a` including self is the same query with `cl.depth >= 0`. Subsumption between two
|
|
91
|
+
concepts is a single primary-key probe on `concept_closure`.
|
|
92
|
+
|
|
93
|
+
### Postcoordination
|
|
94
|
+
|
|
95
|
+
`pc_scale` carries one row per axis on a stem: the axis URI, its short name, whether it is
|
|
96
|
+
required, `allow_multiple` (`AllowAlways` / `NotAllowed` / `AllowedExceptFromSameBlock`),
|
|
97
|
+
and the FHIR ValueSet canonical for that axis. `pc_scale_entity` holds the is-a roots of
|
|
98
|
+
the axis, so the permitted values are those roots plus their descendants.
|
|
99
|
+
|
|
100
|
+
Validating `d5409.qp3` — is `qp3` allowed on the `performance` axis of `d5409`? — needs no
|
|
101
|
+
JSON at all:
|
|
102
|
+
|
|
103
|
+
```sql
|
|
104
|
+
SELECT v.code
|
|
105
|
+
FROM pc_scale ps
|
|
106
|
+
JOIN concept s ON s.id = ps.concept
|
|
107
|
+
JOIN pc_scale_entity e ON e.scale = ps.id
|
|
108
|
+
JOIN concept_closure cl ON cl.ancestor = e.target
|
|
109
|
+
JOIN concept v ON v.id = cl.descendant
|
|
110
|
+
WHERE s.code = 'd5409' AND ps.axis_name = 'performance';
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The same shape answers the questions a provider has to answer: which axes exist on a stem,
|
|
114
|
+
which are required, whether a second value on one axis is legal, and whether a given
|
|
115
|
+
extension code belongs to a given axis.
|
|
116
|
+
|
|
117
|
+
### designation
|
|
118
|
+
|
|
119
|
+
`kind` is one of `title`, `definition`, `longDefinition`, `fullySpecifiedName`,
|
|
120
|
+
`codingNote`, `diagnosticCriteria`, `indexTerm`, `inclusion`. The display for a concept is
|
|
121
|
+
its `title` in the requested language:
|
|
122
|
+
|
|
123
|
+
```sql
|
|
124
|
+
SELECT value FROM designation
|
|
125
|
+
WHERE concept = ? AND kind = 'title' AND lang = ?;
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Index terms and inclusions are the search surface — they are what the ICD Browser matches
|
|
129
|
+
on — and they are held as ordinary rows so a `$expand` filter can be a LIKE or an FTS
|
|
130
|
+
query rather than a scan through JSON.
|
|
131
|
+
|
|
132
|
+
### concept_reference
|
|
133
|
+
|
|
134
|
+
`kind` is `exclusion`, `foundationChildElsewhere`, `relatedMaternal` or `relatedPerinatal`.
|
|
135
|
+
`target` is resolved to a concept id where the target is in the same system; `target_uri`
|
|
136
|
+
and `foundation_uri` are always kept so an unresolved reference is still visible. Labels
|
|
137
|
+
are language-tagged, so this table has one row per reference per language.
|
|
138
|
+
|
|
139
|
+
## Notes for the provider
|
|
140
|
+
|
|
141
|
+
- Both URI forms work as a `code` against the WHO FHIR server: versioned
|
|
142
|
+
(`.../release/11/2026-01/mms/{id}`) and unversioned (`.../release/11/mms/{id}`). The
|
|
143
|
+
database stores the unversioned form. Bare numeric ids are rejected by WHO, which is a
|
|
144
|
+
live question in the community — see the Geneva straw poll.
|
|
145
|
+
- The MMS and ICF linearizations are trees; the Foundation is not. Any code that assumes a
|
|
146
|
+
single parent must check `system.is_tree` first.
|
|
147
|
+
- A concept can have an entity id and no short code. Those are `selectable = 0` and should
|
|
148
|
+
not appear as selectable in an expansion.
|
|
149
|
+
- Chapters are the roots: 28 in MMS (01–26, V, X), 2 each in ICF and the Foundation. They
|
|
150
|
+
report no parent, which is why the FHIR API alone cannot enumerate the code system.
|
|
151
|
+
- `system.version` is the release the crawl ran against. Nothing in the schema is
|
|
152
|
+
release-aware beyond that, so a new release means a new database.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
# Exporting FHIR CodeSystem resources
|
|
157
|
+
|
|
158
|
+
`tx/importers/export-icd11-codesystem.js` writes CodeSystem resources out of the database.
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
node tx/importers/export-icd11-codesystem.js \
|
|
162
|
+
--src data/icd11-2026-01.db --out out/ --gzip
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Nine files for the 2026-01 release, plus the schema vocabulary:
|
|
166
|
+
|
|
167
|
+
| file | concepts | raw | gzipped |
|
|
168
|
+
|---|---:|---:|---:|
|
|
169
|
+
| CodeSystem-icd11-mms-2026-01.json | 37,211 | 43.5 MB | 3.9 MB |
|
|
170
|
+
| CodeSystem-icd11-mms-2026-01-es.json | 37,211 | 28.5 MB | 2.5 MB |
|
|
171
|
+
| CodeSystem-icd11-mms-2026-01-fr.json | 37,211 | 24.8 MB | 2.4 MB |
|
|
172
|
+
| CodeSystem-icd11-icf-2026-01.json | 1,665 | 1.6 MB | 0.1 MB |
|
|
173
|
+
| CodeSystem-icd11-icf-2026-01-{es,fr}.json | 1,665 | 0.7 MB each | 0.1 MB each |
|
|
174
|
+
| CodeSystem-icd11-foundation-2026-01.json | 71,565 | 46.1 MB | 5.1 MB |
|
|
175
|
+
| CodeSystem-icd11-foundation-2026-01-{es,fr}.json | 71,565 | ~18.7 MB each | ~3.1 MB each |
|
|
176
|
+
| CodeSystem-icd11-schema.json | 12 | 3 KB | — |
|
|
177
|
+
|
|
178
|
+
184 MB raw, 21 MB gzipped, produced in about 7 seconds. Everything is streamed, so no
|
|
179
|
+
resource is ever held in memory and `JSON.stringify` only ever sees one concept at a time.
|
|
180
|
+
|
|
181
|
+
## Shape
|
|
182
|
+
|
|
183
|
+
**Hierarchy.** MMS and ICF are emitted nested (`concept.concept`), which is natural for a
|
|
184
|
+
tree and avoids repeating parent codes; the Foundation is a DAG so it is emitted flat with
|
|
185
|
+
`parent` and `child` properties. `--hierarchy flat` forces flat everywhere.
|
|
186
|
+
|
|
187
|
+
**Languages.** A base CodeSystem in the first language, plus one CodeSystem supplement
|
|
188
|
+
(`content = supplement`, `supplements = <url>|<version>`) per additional language. This
|
|
189
|
+
keeps the base file usable on its own and matches WHO treating translations as separately
|
|
190
|
+
governed. `--languages en,fr` picks a subset.
|
|
191
|
+
|
|
192
|
+
**Codes.** `--code-form` decides what goes in `concept.code`, because the community has not:
|
|
193
|
+
|
|
194
|
+
| form | `concept.code` | notes |
|
|
195
|
+
|---|---|---|
|
|
196
|
+
| `hybrid` (default) | short code where there is one, entity uri otherwise | what the WHO FHIR server does today |
|
|
197
|
+
| `short` | the short code | drops the 1,519 MMS concepts that have none |
|
|
198
|
+
| `uri` | the unversioned entity uri, always | uniform, always present |
|
|
199
|
+
| `id` | the bare entity id, always | Geneva straw poll option 4, which won 7-1 |
|
|
200
|
+
|
|
201
|
+
Whichever is chosen, the others are still available as `entityId`, `uri` and `shortCode`
|
|
202
|
+
properties, so nothing is lost and the four can be generated and compared.
|
|
203
|
+
|
|
204
|
+
**Declarations.** The exported resources declare `filter` (concept is-a/descendent-of,
|
|
205
|
+
classKind, notSelectable), thirteen `property` definitions, `count`, `hierarchyMeaning`,
|
|
206
|
+
`compositional`, `versionNeeded`, and a `CodeSystem.valueSet` naming an all-codes value
|
|
207
|
+
set — all of which WHO's own CodeSystem resource omits.
|
|
208
|
+
|
|
209
|
+
## Two things worth knowing
|
|
210
|
+
|
|
211
|
+
`CodeSystem.property.type` is bound to a required value set that does **not** include
|
|
212
|
+
`uri`, and `concept.property.value[x]` has no `valueUri` choice — the allowed types are
|
|
213
|
+
code, Coding, string, integer, boolean, dateTime, decimal. Uri-valued properties are
|
|
214
|
+
therefore declared and emitted as strings. This is the same mistake the WHO server makes
|
|
215
|
+
on `ValueSet.expansion.contains.property.value[x]`, where it returns `valueUri` and no
|
|
216
|
+
conformant client can parse the expansion at all.
|
|
217
|
+
|
|
218
|
+
`classKind` is emitted as a `Coding`, not a `code`. `CodeSystem.property` gives no way to
|
|
219
|
+
say which code system a code-typed property draws its values from, so a validator assumes
|
|
220
|
+
the code system being defined — and `category` is not a code in ICD-11. A Coding names
|
|
221
|
+
`http://id.who.int/icd/schema` outright, which is why the exporter also writes
|
|
222
|
+
`CodeSystem-icd11-schema.json` defining the designation-use codes and class kinds.
|
|
223
|
+
|
|
224
|
+
## Validation
|
|
225
|
+
|
|
226
|
+
Validated with the HL7 validator at R5. **0 errors** on the nested ICF resource, the flat
|
|
227
|
+
ICF resource, an ICF supplement, and an MMS extract of 1,094 concepts covering blocks,
|
|
228
|
+
windows, residuals and postcoordination. The full 43 MB MMS and 46 MB Foundation files were
|
|
229
|
+
not validated end to end — the validator did not finish them in the memory available — but
|
|
230
|
+
they come off the same code paths.
|
|
231
|
+
|
|
232
|
+
The remaining warnings are all one thing: `designation.use` codes are not in
|
|
233
|
+
`http://hl7.org/fhir/ValueSet/designation-use`. That binding is extensible and ICD's index
|
|
234
|
+
terms have no standard equivalent, so this is expected rather than a defect.
|
|
235
|
+
|
|
236
|
+
## Postcoordination ValueSets
|
|
237
|
+
|
|
238
|
+
`--value-sets` (on by default) also writes one ValueSet per postcoordination axis per stem
|
|
239
|
+
— 25,354 of them for 2026-01, across 46 distinct axes.
|
|
240
|
+
|
|
241
|
+
They are **compose-only**. Pre-expanding the set would be 14.6 million expansion rows, with
|
|
242
|
+
1,743 axes carrying over a thousand members each, so membership is expressed the way WHO
|
|
243
|
+
expresses it: one `concept is-a` include per scale root. Resolving them needs the
|
|
244
|
+
CodeSystem, which is the point of shipping both together.
|
|
245
|
+
|
|
246
|
+
Filter values follow `--code-form`, so they are consistent with the CodeSystem in the same
|
|
247
|
+
package — 113,785 come out as short codes and 45,145 as entity uris, the latter being scale
|
|
248
|
+
roots that are groupers with no short code. (WHO's own value sets mix the two forms
|
|
249
|
+
arbitrarily.)
|
|
250
|
+
|
|
251
|
+
The axis rules have nowhere to live in a ValueSet, so four extensions carry them:
|
|
252
|
+
|
|
253
|
+
| extension | type | what it says |
|
|
254
|
+
|---|---|---|
|
|
255
|
+
| `.../schema/postcoordinationAxis` | uri | which axis this value set enumerates |
|
|
256
|
+
| `.../schema/stem` | uri | the concept the axis belongs to |
|
|
257
|
+
| `.../schema/requiredPostcoordination` | boolean | whether WHO marks the axis required |
|
|
258
|
+
| `.../schema/allowMultipleValues` | code | AllowAlways \| NotAllowed \| AllowedExceptFromSameBlock |
|
|
259
|
+
|
|
260
|
+
An undefined extension is an **error** to the validator, not a warning, so the exporter also
|
|
261
|
+
writes a StructureDefinition for each of the four. Without them the package will not load.
|
|
262
|
+
|
|
263
|
+
ValueSet ids are `icd11-{system}-{entityId}-{axis}`. FHIR caps ids at 64 characters; 35 of
|
|
264
|
+
them exceed that and keep a deterministic 7-character hash of the full name instead of being
|
|
265
|
+
blindly truncated, so two long axis names on one stem cannot collide. The exporter asserts
|
|
266
|
+
uniqueness as it goes.
|
|
267
|
+
|
|
268
|
+
## FHIR package
|
|
269
|
+
|
|
270
|
+
`--package` assembles everything into a FHIR NPM package — `package/` with one file per
|
|
271
|
+
resource, a `package.json` and a `.index.json` — and tars it with a small built-in ustar
|
|
272
|
+
writer, so no external `tar` is needed.
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
node tx/importers/export-icd11-codesystem.js \
|
|
276
|
+
--src data/icd11-2026-01.db --out out/ --package
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`who.icd11-2026.1.0.tgz`: 25,368 resources — 10 CodeSystems, 25,354 ValueSets, 4
|
|
280
|
+
StructureDefinitions — **23.0 MB**, built in 22 seconds. English only it is 11.7 MB.
|
|
281
|
+
`--package-id` and `--package-version` override the defaults; the version is derived from
|
|
282
|
+
the release (`2026-01` becomes `2026.1.0`) since package versions must be semver.
|
|
283
|
+
|
|
284
|
+
Verified by pointing the validator at the `.tgz` as an `-ig` and validating a ValueSet
|
|
285
|
+
against it: it loads and resolves. 12 ICF ValueSets validate at 0 errors, 0 warnings.
|