@cosyte/terminology 0.1.2 → 0.1.3
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 +6 -0
- package/README.md +143 -97
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8d1e983: The README follows the house layout, and the package description gains two serial commas. The README now opens with why the engine exists and its status, then install and usage with a link to the guides on docs.cosyte.com, and ends with support, security, contributing, and license. It names the Node.js lines CI tests (22, 24, and 26); its conformance statements and its list of bundled material are unchanged. No code changed.
|
|
8
|
+
|
|
3
9
|
## 0.1.2
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -7,8 +7,22 @@
|
|
|
7
7
|
|
|
8
8
|
# @cosyte/terminology
|
|
9
9
|
|
|
10
|
-
> A
|
|
11
|
-
|
|
10
|
+
> A zero-dependency, FHIR-shaped terminology engine for US healthcare code systems that never fabricates a code.
|
|
11
|
+
|
|
12
|
+
[](https://www.npmjs.com/package/@cosyte/terminology)
|
|
13
|
+
[](https://github.com/cosyte/terminology/actions/workflows/ci.yml)
|
|
14
|
+
[](https://github.com/cosyte/terminology/blob/main/LICENSE)
|
|
15
|
+
[](https://nodejs.org)
|
|
16
|
+
|
|
17
|
+
Terminology engine for US healthcare code systems in Node.js and TypeScript: FHIR `$translate`,
|
|
18
|
+
`$lookup`, `$validate-code`, and `$expand` over code systems, value sets, and maps you supply.
|
|
19
|
+
|
|
20
|
+
## Why this exists
|
|
21
|
+
|
|
22
|
+
A wrong code, map, or value-set binding does not fail loudly: it becomes a different clinical
|
|
23
|
+
meaning in whatever consumes the answer, and the output still looks valid. So this engine never
|
|
24
|
+
fabricates and never inverts. An unknown or unmapped code comes back as a typed result rather than a
|
|
25
|
+
guess, and a directional map is read only in its authored direction.
|
|
12
26
|
|
|
13
27
|
`@cosyte/terminology` is **not a wire parser**. It mirrors the FHIR **Terminology Module**
|
|
14
28
|
(`$translate`, `$lookup`, `$validate-code`, `$expand`, …), operating over **consumer-supplied** FHIR
|
|
@@ -19,51 +33,56 @@ unit table, the code-system _identities_ (OID ↔ canonical URI), the SNOMED CT
|
|
|
19
33
|
resolver names, and RxNorm's relationship and term-type names, is listed with its copyright under
|
|
20
34
|
[What is bundled](#what-is-bundled).
|
|
21
35
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
36
|
+
## Status
|
|
37
|
+
|
|
38
|
+
**The 0.1 line, published on npm.** The public API is settled: below `1.0.0` a breaking change ships
|
|
39
|
+
in a new minor version, never a patch, and the changelog says what broke. The **engine is
|
|
40
|
+
complete**: every operation below ships today. The surface:
|
|
41
|
+
|
|
42
|
+
- the code-system **identity resolver** (`resolveSystem`) and the ConceptMap **`$translate`** engine
|
|
43
|
+
(`loadConceptMap` / `translate`);
|
|
44
|
+
- the **CodeSystem load layer** (RRF / CSV / fixed-width / FHIR JSON) with **`$lookup`** and
|
|
45
|
+
**`$validate-code`**;
|
|
46
|
+
- **ValueSet** binding (`compose` / **`$expand`** / **`$validate-code`**), subsumption read from the
|
|
47
|
+
release's own hierarchy, measured against HL7's own terminology ecosystem test cases
|
|
48
|
+
([terminology ecosystem conformance](#terminology-ecosystem-conformance));
|
|
49
|
+
- **UCUM** unit validation and canonicalization (`validateUcum` / `ucumEqual`, recognition only, **no
|
|
50
|
+
magnitude conversion**), gated on the official UCUM functional tests: the **validation** and
|
|
51
|
+
**conversion** case kinds, against the suite dated 3-Feb 2021, and no other kind
|
|
52
|
+
([UCUM conformance](#ucum-conformance));
|
|
53
|
+
- the **crosswalk resolvers**: the CMS **ICD-9↔ICD-10 GEMs** (`loadGems` / `applyGem`,
|
|
54
|
+
public-domain) and the NLM **SNOMED CT → ICD-10-CM complex map** (`loadComplexMap` /
|
|
55
|
+
`applyComplexMap`, BYO: no SNOMED CT refset bundled), never inverted;
|
|
56
|
+
- the **RxNorm drug graph** (`loadRxNormGraph` + `ingredientsOf` / `genericFor` / `brandsFor` /
|
|
57
|
+
`doseFormsOf` / `consistsOf` / `resolveNdc` / `approximateMatch`) over a **BYO** RxNorm RRF release,
|
|
58
|
+
edges read in RxNorm's documented direction and never inverted, an absent `RXCUI`/NDC typed, never
|
|
59
|
+
fabricated.
|
|
60
|
+
|
|
61
|
+
**Bring your own data.** The engine is whole; **no code-system release is bundled.** It runs over the
|
|
62
|
+
FHIR resources and standard releases _you_ supply: SNOMED CT, CPT, LOINC, UMLS/RxNorm, the CMS GEM
|
|
63
|
+
files, the NLM complex-map refset, and VSAC value sets are all bring-your-own, under whatever terms
|
|
64
|
+
their steward sets. The content packs that would change that (RxNorm Prescribable, ICD-10-CM) are not
|
|
65
|
+
bundled either. For the things that **are** bundled, and under whose copyright, see
|
|
66
|
+
[What is bundled](#what-is-bundled).
|
|
51
67
|
|
|
52
68
|
## Install
|
|
53
69
|
|
|
54
|
-
> **On npm.** `@cosyte/terminology` is **published**, on the 0.1 line. The command below
|
|
55
|
-
> installs the real package; the registry, not this page, is the authority on the exact version.
|
|
56
|
-
|
|
57
70
|
```bash
|
|
58
71
|
npm install @cosyte/terminology
|
|
59
72
|
```
|
|
60
73
|
|
|
61
|
-
Node.js 22 or later, tested on 22 and
|
|
74
|
+
Node.js 22 or later, tested on 22, 24, and 26.
|
|
75
|
+
|
|
76
|
+
The package is published on the 0.1 line, and the command above installs the real package; the
|
|
77
|
+
registry, not this page, is the authority on the exact version.
|
|
78
|
+
|
|
79
|
+
## Usage
|
|
62
80
|
|
|
63
|
-
|
|
64
|
-
[
|
|
81
|
+
The guides and the full API reference are at
|
|
82
|
+
[docs.cosyte.com/terminology](https://docs.cosyte.com/terminology). Runnable programs for the main
|
|
83
|
+
jobs are in [`examples/`](https://github.com/cosyte/terminology/tree/main/examples).
|
|
65
84
|
|
|
66
|
-
|
|
85
|
+
### Resolve a code system to its canonical URI
|
|
67
86
|
|
|
68
87
|
```ts
|
|
69
88
|
import { resolveSystem, isUnknownSystem } from "@cosyte/terminology";
|
|
@@ -72,7 +91,7 @@ const r = resolveSystem("2.16.840.1.113883.6.1"); // OID, mnemonic, or URI
|
|
|
72
91
|
console.log(isUnknownSystem(r) ? "unknown" : r.url); // http://loinc.org
|
|
73
92
|
```
|
|
74
93
|
|
|
75
|
-
|
|
94
|
+
### Translate a code through a ConceptMap
|
|
76
95
|
|
|
77
96
|
```ts
|
|
78
97
|
import { loadConceptMap, translate } from "@cosyte/terminology";
|
|
@@ -114,7 +133,7 @@ if (result.unmapped) {
|
|
|
114
133
|
When at least one target does assert a relationship, the source is translated exactly as before and
|
|
115
134
|
any `disjoint` row rides along in `result.matches`, in its declared position.
|
|
116
135
|
|
|
117
|
-
|
|
136
|
+
### Look up a code in a code system
|
|
118
137
|
|
|
119
138
|
```ts
|
|
120
139
|
import { loadCodeSystem, lookup, validateCode } from "@cosyte/terminology";
|
|
@@ -134,7 +153,7 @@ if (hit.found) {
|
|
|
134
153
|
validateCode(cs, "2160-0").valid; // $validate-code: true iff present; never a guessed true
|
|
135
154
|
```
|
|
136
155
|
|
|
137
|
-
|
|
156
|
+
### Validate and compare UCUM units
|
|
138
157
|
|
|
139
158
|
Recognition and canonicalization only: **no magnitude conversion** (`5 mg/dL` → `mmol/L` needs the
|
|
140
159
|
analyte's molar mass, a clinical computation this engine refuses).
|
|
@@ -166,6 +185,65 @@ functional-test cases it claims, and claims no more than it runs: see
|
|
|
166
185
|
> `TERM_UCUM_INVALID` (fail-safe, never silently reinterpreted). Magnitude conversion is likewise a
|
|
167
186
|
> deliberate non-goal (recognition and canonicalization only).
|
|
168
187
|
|
|
188
|
+
### Navigate the RxNorm drug graph
|
|
189
|
+
|
|
190
|
+
Load a **bring-your-own** RxNorm RRF release and walk the ingredient / brand / clinical-drug /
|
|
191
|
+
dose-form graph. Relationships are read in RxNorm's documented direction (`RELA` is the relationship
|
|
192
|
+
the **second** `RXCUI` has to the **first**); navigation follows only authored edges: the engine
|
|
193
|
+
never synthesizes an inverse, and never fabricates a concept, edge, or NDC.
|
|
194
|
+
|
|
195
|
+
Authored edges only means the graph you get is RxNorm's, including where its shape surprises you.
|
|
196
|
+
`has_ingredient` is authored from the **clinical** side (`SCDC`/`SCDF`/`SCDG`) to the ingredient
|
|
197
|
+
`IN`, and from the **branded** side (`SBD`/`SBDC`/`SBDF`/`SBDG`) to the **brand name** `BN` rather
|
|
198
|
+
than to the active ingredient. RxNorm authors no ingredient edge on a clinical drug (`SCD`) at all,
|
|
199
|
+
so that query answers with an honest empty set rather than a guess. To reach an active ingredient,
|
|
200
|
+
walk to the **clinical** component and take its edge: `consistsOf` then `ingredientsOf`. From an
|
|
201
|
+
`SBD`, `consistsOf` returns both its branded component (`SBDC`, whose ingredient edge is the brand
|
|
202
|
+
name again) and the clinical `SCDC`; it is the `SCDC` that leads to the `IN`. The **precise** forms
|
|
203
|
+
(`SCDFP`, `SBDFP`, `SCDGP`) carry no ingredient edge at all: an `SCDFP` instead reaches its
|
|
204
|
+
basis-of-strength substances by `has_boss`, a one-to-many relation with a different meaning whose
|
|
205
|
+
targets can be a `PIN` or a plain `IN`, and which `ingredientsOf` does not follow. Read the `tty` of
|
|
206
|
+
what comes back rather than assuming it.
|
|
207
|
+
|
|
208
|
+
A concept is typed by its **defining** atom, whichever line of `RXNCONSO` that sits on. `PSN`, `SY`,
|
|
209
|
+
and `TMSY` type a _name_ rather than a concept, so they never establish an `RXCUI`'s term type. An
|
|
210
|
+
`RXCUI` no defining atom could type is left out of the graph and reported as
|
|
211
|
+
`TERM_RXNORM_UNTYPED_CONCEPT` on `graph.warnings`, rather than loaded under a synonym's term type:
|
|
212
|
+
a concept that answers with a term type it does not have is a fabricated claim about a drug, and
|
|
213
|
+
this engine refuses instead.
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import {
|
|
217
|
+
loadRxNormGraph,
|
|
218
|
+
ingredientsOf,
|
|
219
|
+
consistsOf,
|
|
220
|
+
genericFor,
|
|
221
|
+
resolveNdc,
|
|
222
|
+
} from "@cosyte/terminology";
|
|
223
|
+
|
|
224
|
+
const graph = loadRxNormGraph({
|
|
225
|
+
conso: rxnconsoRrf,
|
|
226
|
+
rel: rxnrelRrf,
|
|
227
|
+
sat: rxnsatRrf,
|
|
228
|
+
version: "RXNORM_2026AA",
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
ingredientsOf(graph, "316151"); // an SCDC → { found: true, targets: [ { rxcui: "29046", tty: "IN", name: "lisinopril", … } ] }
|
|
232
|
+
ingredientsOf(graph, "314076"); // an SCD → { found: true, targets: [] }, because no such edge is authored
|
|
233
|
+
consistsOf(graph, "314076"); // → the SCDC, the first of the two hops to the ingredient
|
|
234
|
+
genericFor(graph, "104377"); // an SBD → its SCD, via the authored tradename_of edge
|
|
235
|
+
resolveNdc(graph, "00000000001"); // { resolved: true, rxcui: "314076", status: "active", asOf: "RXNORM_2026AA" }
|
|
236
|
+
ingredientsOf(graph, "99999999"); // { found: false, code: "TERM_RXNORM_UNKNOWN_RXCUI" }, never a guess
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
The engine ships **no** RxNorm release; you supply it (the public-domain Current Prescribable
|
|
240
|
+
Content subset, or the full BYO release). What it does ship is RxNorm's relationship and term-type
|
|
241
|
+
_names_, listed under [What is bundled](#what-is-bundled).
|
|
242
|
+
|
|
243
|
+
## Conformance
|
|
244
|
+
|
|
245
|
+
Each claim below is stated in full and held to a fresh run by the package's own test suite.
|
|
246
|
+
|
|
169
247
|
### UCUM conformance
|
|
170
248
|
|
|
171
249
|
The claim in full, so a reader can tell what was run from what was not. UCUM permits a qualified
|
|
@@ -189,7 +267,7 @@ here, if the suite's most recent history date stops matching the date named here
|
|
|
189
267
|
here is not one the tests execute, if a named kind's section carries no case, or if this page and
|
|
190
268
|
`vendor/ucum/NOTICE.md` state different suite dates.
|
|
191
269
|
|
|
192
|
-
|
|
270
|
+
### Terminology ecosystem conformance
|
|
193
271
|
|
|
194
272
|
The ValueSet layer is measured against **HL7's own FHIR terminology ecosystem test cases**
|
|
195
273
|
(`HL7/fhir-tx-ecosystem-ig`) as well as against tests written here. The claim in full, so a reader
|
|
@@ -228,61 +306,6 @@ stale in silence.
|
|
|
228
306
|
its own process. Nothing here is an approval, a certification, or an endorsement of this package by
|
|
229
307
|
HL7 or by anyone else.
|
|
230
308
|
|
|
231
|
-
## Navigate the RxNorm drug graph
|
|
232
|
-
|
|
233
|
-
Load a **bring-your-own** RxNorm RRF release and walk the ingredient / brand / clinical-drug /
|
|
234
|
-
dose-form graph. Relationships are read in RxNorm's documented direction (`RELA` is the relationship
|
|
235
|
-
the **second** `RXCUI` has to the **first**); navigation follows only authored edges: the engine
|
|
236
|
-
never synthesizes an inverse, and never fabricates a concept, edge, or NDC.
|
|
237
|
-
|
|
238
|
-
Authored edges only means the graph you get is RxNorm's, including where its shape surprises you.
|
|
239
|
-
`has_ingredient` is authored from the **clinical** side (`SCDC`/`SCDF`/`SCDG`) to the ingredient
|
|
240
|
-
`IN`, and from the **branded** side (`SBD`/`SBDC`/`SBDF`/`SBDG`) to the **brand name** `BN` rather
|
|
241
|
-
than to the active ingredient. RxNorm authors no ingredient edge on a clinical drug (`SCD`) at all,
|
|
242
|
-
so that query answers with an honest empty set rather than a guess. To reach an active ingredient,
|
|
243
|
-
walk to the **clinical** component and take its edge: `consistsOf` then `ingredientsOf`. From an
|
|
244
|
-
`SBD`, `consistsOf` returns both its branded component (`SBDC`, whose ingredient edge is the brand
|
|
245
|
-
name again) and the clinical `SCDC`; it is the `SCDC` that leads to the `IN`. The **precise** forms
|
|
246
|
-
(`SCDFP`, `SBDFP`, `SCDGP`) carry no ingredient edge at all: an `SCDFP` instead reaches its
|
|
247
|
-
basis-of-strength substances by `has_boss`, a one-to-many relation with a different meaning whose
|
|
248
|
-
targets can be a `PIN` or a plain `IN`, and which `ingredientsOf` does not follow. Read the `tty` of
|
|
249
|
-
what comes back rather than assuming it.
|
|
250
|
-
|
|
251
|
-
A concept is typed by its **defining** atom, whichever line of `RXNCONSO` that sits on. `PSN`, `SY`,
|
|
252
|
-
and `TMSY` type a _name_ rather than a concept, so they never establish an `RXCUI`'s term type. An
|
|
253
|
-
`RXCUI` no defining atom could type is left out of the graph and reported as
|
|
254
|
-
`TERM_RXNORM_UNTYPED_CONCEPT` on `graph.warnings`, rather than loaded under a synonym's term type:
|
|
255
|
-
a concept that answers with a term type it does not have is a fabricated claim about a drug, and
|
|
256
|
-
this engine refuses instead.
|
|
257
|
-
|
|
258
|
-
```ts
|
|
259
|
-
import {
|
|
260
|
-
loadRxNormGraph,
|
|
261
|
-
ingredientsOf,
|
|
262
|
-
consistsOf,
|
|
263
|
-
genericFor,
|
|
264
|
-
resolveNdc,
|
|
265
|
-
} from "@cosyte/terminology";
|
|
266
|
-
|
|
267
|
-
const graph = loadRxNormGraph({
|
|
268
|
-
conso: rxnconsoRrf,
|
|
269
|
-
rel: rxnrelRrf,
|
|
270
|
-
sat: rxnsatRrf,
|
|
271
|
-
version: "RXNORM_2026AA",
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
ingredientsOf(graph, "316151"); // an SCDC → { found: true, targets: [ { rxcui: "29046", tty: "IN", name: "lisinopril", … } ] }
|
|
275
|
-
ingredientsOf(graph, "314076"); // an SCD → { found: true, targets: [] }, because no such edge is authored
|
|
276
|
-
consistsOf(graph, "314076"); // → the SCDC, the first of the two hops to the ingredient
|
|
277
|
-
genericFor(graph, "104377"); // an SBD → its SCD, via the authored tradename_of edge
|
|
278
|
-
resolveNdc(graph, "00000000001"); // { resolved: true, rxcui: "314076", status: "active", asOf: "RXNORM_2026AA" }
|
|
279
|
-
ingredientsOf(graph, "99999999"); // { found: false, code: "TERM_RXNORM_UNKNOWN_RXCUI" }, never a guess
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
The engine ships **no** RxNorm release; you supply it (the public-domain Current Prescribable
|
|
283
|
-
Content subset, or the full BYO release). What it does ship is RxNorm's relationship and term-type
|
|
284
|
-
_names_, listed under [What is bundled](#what-is-bundled).
|
|
285
|
-
|
|
286
309
|
## The invariants this engine is built on
|
|
287
310
|
|
|
288
311
|
- **Never fabricate.** An unmapped source is a typed `unmapped`, never a guessed target. Every target
|
|
@@ -349,6 +372,29 @@ with its copyright:
|
|
|
349
372
|
This section states what is distributed and under whose copyright. It is not legal advice, and makes
|
|
350
373
|
no claim about whether any particular use of these materials is permitted to you.
|
|
351
374
|
|
|
375
|
+
## Support
|
|
376
|
+
|
|
377
|
+
Questions, bug reports, and an answer the engine got wrong go in
|
|
378
|
+
[GitHub issues](https://github.com/cosyte/terminology/issues), with the smallest synthetic code
|
|
379
|
+
system, map, or value set that reproduces it. Use synthetic data only: a code in patient context can
|
|
380
|
+
be PHI. [SUPPORT.md](https://github.com/cosyte/.github/blob/main/SUPPORT.md) says where to ask what.
|
|
381
|
+
|
|
382
|
+
## Security
|
|
383
|
+
|
|
384
|
+
Report a vulnerability privately, never in a public issue, and with no real patient data in it:
|
|
385
|
+
through GitHub private vulnerability reporting on this repository, or by email to
|
|
386
|
+
[security@cosyte.com](mailto:security@cosyte.com). Our
|
|
387
|
+
[security policy](https://github.com/cosyte/.github/blob/main/SECURITY.md) says how.
|
|
388
|
+
|
|
389
|
+
## Contributing
|
|
390
|
+
|
|
391
|
+
For anything that changes what the engine answers, open an issue first, so the behavior can be agreed
|
|
392
|
+
before it is written; typo, link, and test fixes can go straight to a pull request. A contribution
|
|
393
|
+
clears the gate CI runs, listed in
|
|
394
|
+
[AGENTS.md](https://github.com/cosyte/terminology/blob/main/AGENTS.md), and a change a consumer would
|
|
395
|
+
notice adds a changeset (`pnpm changeset`), whose summary becomes the changelog entry. See
|
|
396
|
+
[CONTRIBUTING](https://github.com/cosyte/.github/blob/main/CONTRIBUTING.md).
|
|
397
|
+
|
|
352
398
|
## License
|
|
353
399
|
|
|
354
400
|
[MIT](https://github.com/cosyte/terminology/blob/main/LICENSE) © Cosyte LLC: this package's own code.
|
package/dist/index.cjs
CHANGED
|
@@ -4979,7 +4979,7 @@ function approximateMatch(graph, query, options = {}) {
|
|
|
4979
4979
|
}
|
|
4980
4980
|
|
|
4981
4981
|
// src/index.ts
|
|
4982
|
-
var VERSION = "0.1.
|
|
4982
|
+
var VERSION = "0.1.3";
|
|
4983
4983
|
|
|
4984
4984
|
exports.COMPLEX_MAP_SOURCE_SYSTEM = COMPLEX_MAP_SOURCE_SYSTEM;
|
|
4985
4985
|
exports.DIAGNOSTIC_CODES = DIAGNOSTIC_CODES;
|