fhirsmith 0.12.0 → 0.13.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 (57) hide show
  1. package/CHANGELOG.md +323 -4
  2. package/README.md +1 -1
  3. package/config-template.json +13 -0
  4. package/library/languages.js +191 -34
  5. package/library/path-safety.js +51 -0
  6. package/library/request-token.js +56 -0
  7. package/package.json +2 -2
  8. package/packages/packages.js +17 -118
  9. package/publisher/publisher.js +60 -30
  10. package/publisher/validation.js +15 -3
  11. package/server.js +12 -3
  12. package/translations/Messages.properties +1 -0
  13. package/tx/README.md +21 -0
  14. package/tx/cs/cs-api.js +30 -1
  15. package/tx/cs/cs-cs.js +14 -1
  16. package/tx/cs/cs-icd11.js +844 -0
  17. package/tx/cs/cs-lang.js +331 -85
  18. package/tx/cs/cs-loinc.js +45 -4
  19. package/tx/cs/cs-mimetypes.js +492 -21
  20. package/tx/cs/cs-snomed.js +38 -4
  21. package/tx/cs/cs-ucum.js +33 -3
  22. package/tx/cs/icd11-expressions.js +243 -0
  23. package/tx/importers/export-icd11-codesystem.js +823 -0
  24. package/tx/importers/icd11-schema.md +285 -0
  25. package/tx/importers/import-icd11.js +861 -0
  26. package/tx/importers/import-loinc.module.js +14 -7
  27. package/tx/importers/import-sct.module.js +10 -1
  28. package/tx/library/codesystem.js +21 -27
  29. package/tx/library/nesting-limit.js +79 -0
  30. package/tx/library/operation-outcome.js +97 -3
  31. package/tx/library/valueset.js +16 -20
  32. package/tx/library.js +23 -1
  33. package/tx/ocl/cache/cache-utils.cjs +5 -3
  34. package/tx/ocl/cs-ocl.cjs +73 -17
  35. package/tx/ocl/jobs/background-queue.cjs +6 -3
  36. package/tx/ocl/mappers/concept-mapper.cjs +9 -1
  37. package/tx/ocl/shared/patches.cjs +48 -1
  38. package/tx/ocl/vs-ocl.cjs +15 -13
  39. package/tx/provider.js +1 -1
  40. package/tx/sct/expressions.js +194 -17
  41. package/tx/tx.fhir.org.yml +2 -1
  42. package/tx/tx.js +15 -42
  43. package/tx/workers/batch-validate.js +6 -16
  44. package/tx/workers/batch.js +16 -28
  45. package/tx/workers/cache-control.js +6 -12
  46. package/tx/workers/closure.js +3 -8
  47. package/tx/workers/compare.js +6 -34
  48. package/tx/workers/expand.js +36 -45
  49. package/tx/workers/lookup.js +21 -39
  50. package/tx/workers/read.js +6 -40
  51. package/tx/workers/search.js +2 -8
  52. package/tx/workers/subsumes.js +58 -41
  53. package/tx/workers/translate.js +11 -17
  54. package/tx/workers/validate.js +76 -77
  55. package/tx/workers/worker.js +70 -1
  56. package/tx/xml/xml-base.js +243 -76
  57. package/tx/xversion/xv-valueset.js +39 -1
@@ -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.