@cosyte/terminology 0.0.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.
- package/CHANGELOG.md +249 -0
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/dist/index.cjs +4739 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3466 -0
- package/dist/index.d.ts +3466 -0
- package/dist/index.mjs +4669 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +99 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
Versions and publishing are managed with [Changesets](https://github.com/changesets/changesets);
|
|
8
|
+
this file is maintained by hand (Changesets handles the version bump and publish only).
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
The first pre-alpha release (`0.0.1`) will ship the initial public API surface. The package begins
|
|
13
|
+
its public history at `0.0.x`, per the cosyte version ladder (`0.0.x` until first alpha).
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
- **Dev-dependency advisory remediation (no runtime impact — `@cosyte/terminology`
|
|
18
|
+
ships zero runtime dependencies, so the published artifact is unchanged).**
|
|
19
|
+
Added the shared canonical scoped `pnpm.overrides` block pinning two transitive
|
|
20
|
+
**dev/build-time** packages to their patched releases: `esbuild`
|
|
21
|
+
(`>=0.27.3 <0.28.1` → `0.28.1`; GHSA dev-server path-traversal — not reachable
|
|
22
|
+
here: the library builds via `tsup`/`vitest` and never runs `esbuild serve`) and
|
|
23
|
+
the `@changesets/parse` copy of `js-yaml` (`>=4.0.0 <4.2.0` → `4.2.0`;
|
|
24
|
+
GHSA-h67p-54hq-rp68 merge-key DoS). The `js-yaml@3.x` pulled by `read-yaml-file`
|
|
25
|
+
(via `@manypkg/get-packages` → `@changesets/cli`) is **intentionally left**: it
|
|
26
|
+
calls `yaml.safeLoad`, removed/throwing in js-yaml 4, so it cannot be
|
|
27
|
+
force-upgraded without breaking the release tooling, and it only parses trusted
|
|
28
|
+
local repo YAML at release time. This mirrors `@cosyte/hl7` and is the shared
|
|
29
|
+
override block enforced suite-wide by the `@cosyte/config` drift check.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **Phase 6 — RxNorm drug relationship graph** (`TERMINOLOGY-6`). Ingredient / brand / clinical-drug /
|
|
34
|
+
dose-form graph navigation over a **caller-supplied** RxNorm RRF release — the never-fabricate
|
|
35
|
+
invariant applied to the drug graph (roadmap §4.2). Ships the graph **mechanism** over the real RRF
|
|
36
|
+
wire format; ships **no** RxNorm content.
|
|
37
|
+
- **`loadRxNormGraph(source)`** — load `RXNCONSO` (concepts, typed by `TTY`: `IN`/`PIN`/`BN`/`SCD`/
|
|
38
|
+
`SBD`/`SCDC`/`DF`/…), `RXNREL` (directed `RELA` edges), and optionally `RXNSAT` (`ATN=NDC`
|
|
39
|
+
attributes) into an immutable graph. Reuses the shared zero-dep RRF reader. The **column layouts and
|
|
40
|
+
the direction convention are grounded firsthand** on the NLM RxNorm Technical Documentation (§ file
|
|
41
|
+
descriptions; §12.7) and the UMLS Reference Manual: `RELA` is _"the relationship which the second
|
|
42
|
+
concept (`RXCUI2`) has to the first (`RXCUI1`)"_, so every row is read `RXCUI2 ⟶RELA⟶ RXCUI1` and
|
|
43
|
+
normalized to `subject = RXCUI2`, `object = RXCUI1` — the documented medication-safety trap
|
|
44
|
+
(roadmap §10 Q5), **pinned by fixtures** built in the real wire format. Liberal on load: a
|
|
45
|
+
structurally unusable row is a skipped, surfaced `TERM_RXNORM_MALFORMED_ROW`; rows not of interest
|
|
46
|
+
(non-`RXNORM` atoms, atom-level relationships, non-`NDC` attributes) are skipped silently.
|
|
47
|
+
- **`ingredientsOf` / `genericFor` / `brandsFor` / `doseFormsOf` / `consistsOf` / `relatedByRela`** —
|
|
48
|
+
graph navigation following **authored edges only**. RxNorm ships both directions of an asymmetric
|
|
49
|
+
relationship as separate rows, so `genericFor` follows the authored `tradename_of` and `brandsFor`
|
|
50
|
+
the authored `has_tradename` — the engine **never synthesizes an inverse**. An absent `RXCUI` is a
|
|
51
|
+
typed `TERM_RXNORM_UNKNOWN_RXCUI`; a present concept with no such edge is a found result with an
|
|
52
|
+
**empty** target set (an honest "no such edge", distinct from "unknown concept").
|
|
53
|
+
- **`resolveNdc(graph, ndc)`** — NDC → `RXCUI` carrying the temporal status and the **as-of release**
|
|
54
|
+
(NDC↔RXCUI is many:1 and temporal). An NDC in the loaded release is `active` as of that release;
|
|
55
|
+
obsolete/alien statuses come from RxNav NDC-history data (a differential/BYO source, not the base
|
|
56
|
+
RRF), never fabricated. An absent NDC is a typed `TERM_RXNORM_NDC_UNMAPPED`, never a guessed `RXCUI`.
|
|
57
|
+
- **`approximateMatch(graph, query, options?)`** — the **opt-in, explicitly labeled** similarity path
|
|
58
|
+
(never the default, never an exact code assertion): every candidate carries `approximate: true` and
|
|
59
|
+
a derived token-overlap score; a no-match is an empty array, never a nearest guess.
|
|
60
|
+
- Also exports `getConcept`, the `RELA` / `RELA_INVERSE` / `TERM_TYPES` / `asTermType` /
|
|
61
|
+
`RXNORM_SYSTEM` identity facts, and the graph types. New stable diagnostic codes:
|
|
62
|
+
`TERM_RXNORM_MALFORMED_ROW`, `TERM_RXNORM_UNKNOWN_RXCUI`, `TERM_RXNORM_NDC_UNMAPPED`.
|
|
63
|
+
- **Content posture (honest).** This phase does **not** bundle RxNorm content. RxNorm's Current
|
|
64
|
+
Prescribable Content is public-domain and _bundleable_ per the licensing matrix, but a genuine
|
|
65
|
+
verbatim release could not be obtained in the build sandbox (no build-time network for the release
|
|
66
|
+
archive), and fabricating "real RxNorm content" would breach the never-fabricate / grounding
|
|
67
|
+
discipline. So Phase 6 ships the **BYO** graph mechanism grounded firsthand on the RxNorm technical
|
|
68
|
+
documentation, and **actual content-bundling is deferred to the content-packs phase (Phase 7)**.
|
|
69
|
+
All fixtures are synthetic rows in the real RRF wire format. Zero runtime dependencies.
|
|
70
|
+
|
|
71
|
+
- **Phase 5 — Crosswalk resolvers: ICD-9↔ICD-10 GEMs + SNOMED CT → ICD-10-CM complex map**
|
|
72
|
+
(`TERMINOLOGY-5`). The never-fabricate / never-invert invariant applied to the published,
|
|
73
|
+
**directional** reference maps — the safety crown (roadmap §4.1). Both stewards say these are _not_
|
|
74
|
+
crosswalks (CMS: _"GEMs are not crosswalks… reference mappings"_; NLM: _"semi-automated"_), so a hit
|
|
75
|
+
is a **candidate carrying the steward's flags**, never an equivalence and never a fabricated code.
|
|
76
|
+
- **`loadGems(source)` / `applyGem(map, code)`** — the CMS **public-domain** GEMs, loaded in their
|
|
77
|
+
authored `direction` (`"9-to-10"` / `"10-to-9"`) and applied only that way. Decodes the 5-position
|
|
78
|
+
flag field (**approximate | no-map | combination | scenario | choice-list**, grounded firsthand on
|
|
79
|
+
the CMS Dx GEM User's Guide / Technical Documentation) and carries it through verbatim. A **1:many**
|
|
80
|
+
source returns the **full** candidate set (never collapsed to one); a **combination** source
|
|
81
|
+
surfaces its scenario→choice-list structure so a caller can build valid clusters (one target per
|
|
82
|
+
choice list); a **`NoDx`** No-Map source is the typed `TERM_CROSSWALK_NO_MAP`; a source **absent**
|
|
83
|
+
from the file is the distinct `TERM_CROSSWALK_UNMAPPED`. Liberal on load (a malformed line is a
|
|
84
|
+
skipped, surfaced `TERM_GEM_MALFORMED_ROW`, never partial).
|
|
85
|
+
- **`loadComplexMap(input)` / `applyComplexMap(map, source, context)`** — the NLM SNOMED CT →
|
|
86
|
+
ICD-10-CM complex map, **BYO** (SNOMED is licensed — **zero** SNOMED content is bundled; the engine
|
|
87
|
+
ships only the rule machinery, roadmap §5). Accepts structured rows or a raw RF2 extended-map
|
|
88
|
+
refset. A source's **map groups are an AND** (a manifestation code _and_ an etiology code → two
|
|
89
|
+
groups, one resolution each); within a group, **priorities are an if-then-else** chain of `IFA`
|
|
90
|
+
rules evaluated against caller-supplied `PatientContext` (age band, gender). A group whose decision
|
|
91
|
+
needs context the caller did **not** supply is the typed `TERM_CROSSWALK_CONTEXT_REQUIRED` — the
|
|
92
|
+
candidate rules + Map Advice ride through, and the engine **refuses to pick a branch** it lacks the
|
|
93
|
+
data for. Map Categories (`447638001` cannot-classify → No-Map, `447639009` context-dependent,
|
|
94
|
+
`447640006` ambiguous) and Map Advice are carried verbatim.
|
|
95
|
+
- **`invertGem(map)`** — the never-invert refusal made a first-class, thrown contract: the forward
|
|
96
|
+
(9→10) and backward (10→9) GEM files are **separate, non-inverse** artifacts, so an inversion
|
|
97
|
+
request always throws `TERM_MAP_NOT_INVERTIBLE` rather than fabricate a transpose.
|
|
98
|
+
- **New stable codes** (additions-only): diagnostics `TERM_CROSSWALK_NO_MAP`,
|
|
99
|
+
`TERM_CROSSWALK_UNMAPPED`, `TERM_CROSSWALK_CONTEXT_REQUIRED`, `TERM_GEM_MALFORMED_ROW`,
|
|
100
|
+
`TERM_COMPLEX_MAP_MALFORMED_ROW`; fatals `TERM_CROSSWALK_MALFORMED`, `TERM_MAP_NOT_INVERTIBLE`.
|
|
101
|
+
- **No map content bundled.** The GEMs are CMS public-domain, so a caller may supply the file today
|
|
102
|
+
(BYO); bundling a public-domain GEM pack is deferred to the content-packs phase. SNOMED/CPT/UMLS
|
|
103
|
+
stay BYO permanently (a licensing wall — roadmap §5). Property + unit tests lock the
|
|
104
|
+
never-fabricate / never-invert / No-Map / context-required invariants. Zero runtime dependencies.
|
|
105
|
+
- **Phase 4 — UCUM: grammar parser + `validateUcum` + `ucumEqual`** (`TERMINOLOGY-4`). Unit
|
|
106
|
+
recognition, validation, and **representation canonicalization only** — **no** magnitude conversion
|
|
107
|
+
(`mg/dL` → `mmol/L` needs an analyte's molar mass, a clinical computation the engine refuses;
|
|
108
|
+
roadmap §2/§4.3). Engine only; the UCUM table is **vendored verbatim**, never a derivative.
|
|
109
|
+
- **`validateUcum(unit)`** — a hand-rolled, zero-dep UCUM grammar parser (base units, metric
|
|
110
|
+
prefixes attached with no delimiter and resolved by **longest-match**, `.` multiply / `/` divide,
|
|
111
|
+
signed integer exponents, `{…}` inert annotations, the `10*` / `10^` powers-of-ten, **case
|
|
112
|
+
sensitive**) over the known atom table. Returns `{ valid: true, canonical }` for a well-formed
|
|
113
|
+
unit, or the typed `{ valid: false, code: "TERM_UCUM_INVALID", reason }` — an unparseable or
|
|
114
|
+
unknown unit is **never** a guessed "nearest" unit (the never-fabricate invariant, applied to
|
|
115
|
+
units; roadmap §4.3). Total and fail-safe: any string in, a typed result out, never a throw.
|
|
116
|
+
- **`ucumEqual(a, b)`** — reduces both expressions to base dimensions and reports whether they
|
|
117
|
+
denote the **same unit** (`N` ≡ `kg.m/s2`, `Pa` ≡ `N/m2`, `mmol/L` ≡ `mmol.L-1`, annotations
|
|
118
|
+
inert). This is **not** magnitude conversion — `mg` ≠ `g`, `km` ≠ `m`. A **special** (non-linear)
|
|
119
|
+
unit (`Cel`, `[pH]`, `B`) is never equated with a linear one, and distinct **arbitrary** units
|
|
120
|
+
(`[IU]` vs `[iU]`) never compare equal (the never-fabricate posture: no unproven equivalence).
|
|
121
|
+
- **`parseUcum` / `reduce` / `loadUcumEssence`** — the underlying AST parser, dimensional reducer,
|
|
122
|
+
and the in-memory model of the vendored UCUM table, exported for advanced use.
|
|
123
|
+
- **Vendored, verbatim UCUM data.** `vendor/ucum/ucum-essence.xml` (official v2.2) is checked in
|
|
124
|
+
byte-for-byte and embedded verbatim as a string (`src/ucum/essence-data.generated.ts`, via
|
|
125
|
+
`pnpm gen:ucum`); the engine parses it at runtime into a lookup table (a permitted UCUM use, not a
|
|
126
|
+
modification/derivative — see `vendor/ucum/NOTICE.md`; UCUM © Regenstrief Institute). No file is
|
|
127
|
+
read at runtime; a drift test proves the embedded copy is byte-identical to the vendored source.
|
|
128
|
+
- **Conformance gate.** The official `UcumFunctionalTests.xml` suite (EPL) is vendored and run: all
|
|
129
|
+
**530 validation cases** pass, and the **31 conversion pairs** are verified commensurable
|
|
130
|
+
(same reduced dimension). Magnitude-conversion sub-tests are a documented non-goal.
|
|
131
|
+
- **Scope, flagged.** Case-**sensitive** (c/s) mode only — the normative FHIR/HL7 interchange mode;
|
|
132
|
+
the case-insensitive (c/i) variant is **not** implemented (a c/i-only string is a typed
|
|
133
|
+
`TERM_UCUM_INVALID`, never silently reinterpreted). Deeply-nested/pathological input degrades to a
|
|
134
|
+
typed invalid (nesting is capped), never a crash. No magnitude conversion (documented non-goal).
|
|
135
|
+
- New value-free stable diagnostic code: **`TERM_UCUM_INVALID`**. Zero runtime dependencies.
|
|
136
|
+
|
|
137
|
+
- **Phase 3 — ValueSet binding: `compose` / `$expand` / `$validate-code` against a value set**
|
|
138
|
+
(`TERMINOLOGY-3`). Loads a **consumer-supplied** FHIR `ValueSet` and answers the FHIR R4 ValueSet
|
|
139
|
+
terminology operations over the CodeSystem loaders — engine only, **zero bundled content** (VSAC
|
|
140
|
+
value sets and their embedded SNOMED/CPT/RxNorm/LOINC remain BYO by license).
|
|
141
|
+
- **`loadValueSet(json)`** — validates an untrusted FHIR R4 `ValueSet` into an immutable model
|
|
142
|
+
(intensional `compose` of `include`/`exclude` over `system`/`concept`/`filter`/`valueSet`, and/or
|
|
143
|
+
a **pre-computed** `expansion` whose `total`/`valueset-toocostly` extension derive a `truncated`
|
|
144
|
+
flag). Conservative on load: a structurally unusable resource is the typed fatal
|
|
145
|
+
`TERM_VALUESET_MALFORMED`; an unimplemented `filter` operator is **not** fatal (it surfaces at
|
|
146
|
+
expansion time).
|
|
147
|
+
- **`expand(vs, ctx)`** — FHIR `$expand`: the union of `include` minus `exclude`, over the
|
|
148
|
+
**consumer-supplied** `CodeSystem`s (and referenced `ValueSet`s) in the `ExpansionContext`.
|
|
149
|
+
Supports explicit `concept` lists, whole-`system` includes, referenced value sets (intersection,
|
|
150
|
+
cycle-safe), and `filter`s — `is-a` / `descendent-of` / `is-not-a` subsumption over the release's
|
|
151
|
+
hierarchy plus `=` / `in` / `not-in` / `exists` property predicates. Returns an honest `complete`
|
|
152
|
+
flag: a part it could not compute (missing code system, unresolved value set, unimplemented op) is
|
|
153
|
+
a typed `TERM_VALUESET_CANNOT_EXPAND` and the `contains` set is an explicit **lower bound**, never
|
|
154
|
+
a silently-empty or fabricated membership.
|
|
155
|
+
- **`validateCodeInValueSet(coding, vs, ctx)`** — FHIR ValueSet `$validate-code` (binding): a
|
|
156
|
+
**decided** `result: true`/`false` only when membership is proven (found in a computable include
|
|
157
|
+
and not removed by any computable exclude / absent from a fully-evaluated value set), otherwise a
|
|
158
|
+
typed `undetermined` (`TERM_VALUESET_CANNOT_EXPAND`). A **truncated** pre-computed expansion never
|
|
159
|
+
reads as complete membership — a code absent from it is `undetermined`, never a fabricated "not a
|
|
160
|
+
member" (roadmap §4.4: a false negative on a binding is a clinical error).
|
|
161
|
+
- **Subsumption from the release's own hierarchy** — the FHIR `CodeSystem` loader now synthesizes a
|
|
162
|
+
standard `parent` concept-property from a **nested `concept`** hierarchy (default `is-a`
|
|
163
|
+
meaning), so `is-a`/`descendent-of` work over nested and explicit-`parent` code systems alike;
|
|
164
|
+
`buildSubsumption` / `isA` are exposed. Cycle-safe.
|
|
165
|
+
- **The never-fabricate invariant extends to value-set binding** — locked by property-based tests
|
|
166
|
+
(every expanded code is one the value set selects; excludes are honored; membership is
|
|
167
|
+
order-independent; a decided membership agrees with the full expansion; a truncated expansion is
|
|
168
|
+
never read as complete). Grounded firsthand on the FHIR R4 Terminology Module
|
|
169
|
+
(`valueset`, `valueset-operation-expand`, `valueset-operation-validate-code`, `valueset-filter-operator`).
|
|
170
|
+
- New value-free stable codes: `TERM_VALUESET_CANNOT_EXPAND`, `TERM_VALUESET_EXPANSION_TRUNCATED`
|
|
171
|
+
(diagnostics) and `TERM_VALUESET_MALFORMED` (fatal).
|
|
172
|
+
- **Zero runtime dependencies** retained.
|
|
173
|
+
|
|
174
|
+
- **Phase 2 — the CodeSystem load layer + FHIR `$lookup` / `$validate-code`** (`TERMINOLOGY-2`).
|
|
175
|
+
Loads a **consumer-supplied** code-system release into an immutable, queryable model and answers the
|
|
176
|
+
two FHIR R4 CodeSystem identity operations. Ships the engine only — **zero bundled copyrighted
|
|
177
|
+
content** (RxNorm/LOINC/ICD-10-CM/SNOMED/CPT releases are BYO).
|
|
178
|
+
- **`loadCodeSystem(source)`** — one entry point over four hand-rolled, zero-dep readers, dispatched
|
|
179
|
+
on `source.format`: **RRF** (pipe-delimited, single trailing pipe; one parameterized reader for
|
|
180
|
+
RxNorm `RXNCONSO` and UMLS `MRCONSO`), **RFC-4180 CSV** (real quote handling — LOINC `Loinc.csv`),
|
|
181
|
+
**fixed-width** (slice-by-column order files; `ICD10CM_ORDER_FILE_FIELDS` preset with the
|
|
182
|
+
position-15 header/billable flag), and native **FHIR R4 `CodeSystem` JSON** (nested-concept
|
|
183
|
+
flattening, standard `inactive`/`deprecated`/`status` properties). Liberal on load: a malformed
|
|
184
|
+
row is skipped and surfaced in `warnings` (`TERM_RRF_MALFORMED_ROW` / `TERM_CSV_MALFORMED` /
|
|
185
|
+
`TERM_FIXED_WIDTH_MALFORMED` / `TERM_FHIR_CONCEPT_MALFORMED`); a structurally unusable _source_ is
|
|
186
|
+
the typed fatal `TERM_CODESYSTEM_MALFORMED`.
|
|
187
|
+
- **`lookup(cs, code)`** — FHIR `$lookup`: code → display + definition + properties, carrying a
|
|
188
|
+
`ConceptStatus` (deprecated / header-not-billable / obsolete / suppressed, with `active` and, for
|
|
189
|
+
ICD-10-CM, `billable`). An unknown code is a typed `{ found: false }` — never a fabricated display.
|
|
190
|
+
- **`validateCode(cs, code)`** — FHIR `$validate-code`: `valid: true` for a present code **carrying
|
|
191
|
+
its status** (a deprecated or header code validates as present but is flagged, never presented
|
|
192
|
+
clean); an absent code is `valid: false` with `TERM_CODE_UNKNOWN` — never a guessed `true`.
|
|
193
|
+
- **The never-fabricate invariant extends to code identity** — locked by property-based +
|
|
194
|
+
fuzz tests (the RRF/CSV/fixed-width readers never crash on hostile input; a lookup display always
|
|
195
|
+
comes verbatim from the loaded release). Grounded firsthand on FHIR R4
|
|
196
|
+
(`codesystem-operation-lookup`/`-validate-code`, concept-properties).
|
|
197
|
+
- New value-free stable codes: `TERM_CODE_UNKNOWN`, `TERM_CONCEPT_DEPRECATED`,
|
|
198
|
+
`TERM_CONCEPT_HEADER_NOT_BILLABLE`, `TERM_RRF_MALFORMED_ROW`, `TERM_CSV_MALFORMED`,
|
|
199
|
+
`TERM_FIXED_WIDTH_MALFORMED`, `TERM_FHIR_CONCEPT_MALFORMED` (diagnostics) and
|
|
200
|
+
`TERM_CODESYSTEM_MALFORMED` (fatal). New JSON helpers `getBoolean` / `getNumber`.
|
|
201
|
+
- **Zero runtime dependencies** retained.
|
|
202
|
+
|
|
203
|
+
- **Phase 1 — the ConceptMap `$translate` engine + code-system identity/canonical-URI resolver**
|
|
204
|
+
(`TERMINOLOGY-1`). This replaces the parser-shaped scaffold stubs: `@cosyte/terminology` is a
|
|
205
|
+
**terminology engine** modeled on the FHIR Terminology Module, not a wire parser.
|
|
206
|
+
- **Code-system identity resolver** — `resolveSystem(id)` maps a canonical URI, OID (bare or
|
|
207
|
+
`urn:oid:`), or HL7 v2 mnemonic to a single `SystemIdentity` (canonical URI + OID + mnemonics), or
|
|
208
|
+
a typed `{ unknown }` — never a guessed URI. Seeded with identities grounded **firsthand** against
|
|
209
|
+
the FHIR R4 terminology-systems registry and HL7 Table 0396: LOINC, SNOMED CT, ICD-10-CM, RxNorm,
|
|
210
|
+
UCUM, CPT, NDC, CVX, and the HL7 v2 table family (`v2-XXXX` URI ↔ `…12.[table]` OID, resolved
|
|
211
|
+
structurally). Exposed as `SYSTEM_IDENTITIES`, `HL7_V2_OID_ROOT`, `HL7_V2_URL_PREFIX`.
|
|
212
|
+
- **ConceptMap `$translate` engine** — `loadConceptMap(json)` validates an untrusted FHIR R4
|
|
213
|
+
`ConceptMap` into an immutable model (malformed → typed fatal `TERM_CONCEPTMAP_MALFORMED`);
|
|
214
|
+
`translate(coding, map)` returns the declared target(s) with their FHIR relationship (an internal
|
|
215
|
+
R5-vocabulary `Relationship` plus the verbatim R4 `equivalence`), steward comments, and the map's
|
|
216
|
+
provenance — or a typed `unmapped` result carrying any `group.unmapped` fallback mode.
|
|
217
|
+
- **The never-fabricate / never-invert invariants** (the safety crown for every later phase): an
|
|
218
|
+
unmapped source is surfaced, never a guessed target; a directional map is read source→target only
|
|
219
|
+
and never inverted. Locked by property-based tests.
|
|
220
|
+
- **Value types + diagnostics**: immutable `coding()` / `codeableConcept()`; untrusted-JSON
|
|
221
|
+
navigation helpers (`isJsonObject`/`getString`/`getArray`); the stable, value-free
|
|
222
|
+
`DIAGNOSTIC_CODES` (`TERM_TRANSLATE_UNMAPPED`, `TERM_SYSTEM_UNRECOGNIZED`) + `FATAL_CODES`
|
|
223
|
+
(`TERM_CONCEPTMAP_MALFORMED`) registries and the typed `TerminologyError`.
|
|
224
|
+
- **Zero runtime dependencies** retained; **zero bundled copyrighted terminology content**
|
|
225
|
+
(SNOMED/CPT/full-LOINC/UMLS/VSAC are BYO by license).
|
|
226
|
+
|
|
227
|
+
### Changed
|
|
228
|
+
|
|
229
|
+
- **Docs: README scope corrected to the complete engine.** The status/scope block led with "Ships
|
|
230
|
+
**Phase 1**" and read as a phase-by-phase build ladder, understating the shipped surface. Rewritten to
|
|
231
|
+
state plainly that the **engine is complete** (identity resolver + `$translate`, CodeSystem load +
|
|
232
|
+
`$lookup`/`$validate-code`, ValueSet `compose`/`$expand`/binding, UCUM, the GEMs + SNOMED→ICD-10-CM
|
|
233
|
+
crosswalks, and the RxNorm drug graph) while keeping the two accurate caveats intact: **pre-alpha
|
|
234
|
+
`0.0.x`, not yet published to npm**, and **no code-system content is bundled** (BYO data; the
|
|
235
|
+
public-domain content packs are still to come). Added a "not on npm yet" note beside the `npm install`
|
|
236
|
+
line so the aspirational command is not mistaken for a live one. Docs-only — no API, version, or
|
|
237
|
+
runtime change.
|
|
238
|
+
|
|
239
|
+
### Removed
|
|
240
|
+
|
|
241
|
+
- The parser-shaped scaffold stubs (`parseTerminology`, `ParsedTerminology`, the placeholder
|
|
242
|
+
`WARNING_CODES` entry, and the parse-oriented option/warning types). This package is an engine, not
|
|
243
|
+
a parser; that placeholder surface never shipped.
|
|
244
|
+
|
|
245
|
+
### Fixed
|
|
246
|
+
|
|
247
|
+
### Security
|
|
248
|
+
|
|
249
|
+
[Unreleased]: https://github.com/cosyte/terminology/commits/main
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cosyte
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# @cosyte/terminology
|
|
2
|
+
|
|
3
|
+
> A **zero-dependency terminology engine** for US healthcare code systems — FHIR-shaped, BYO-data,
|
|
4
|
+
> and it **never fabricates a code**.
|
|
5
|
+
|
|
6
|
+
`@cosyte/terminology` is **not a wire parser**. It mirrors the FHIR **Terminology Module**
|
|
7
|
+
(`$translate`, `$lookup`, `$validate-code`, `$expand`, …), operating over **consumer-supplied** FHIR
|
|
8
|
+
resources. It is the sibling engine `@cosyte/transform` and, later, the parsers' code-system
|
|
9
|
+
recognition consume. It ships the **engine, never copyrighted terminology content** — SNOMED CT, CPT,
|
|
10
|
+
full LOINC/UMLS, and VSAC value sets are strictly bring-your-own; code-system _identities_ (OID ↔
|
|
11
|
+
canonical URI) are published facts, grounded firsthand and encoded.
|
|
12
|
+
|
|
13
|
+
> **Status:** pre-alpha (`0.0.x`), **not yet published to npm.** The **engine is complete** — every
|
|
14
|
+
> operation below ships today. What is still to come is bundled _content_, not capability. The surface:
|
|
15
|
+
>
|
|
16
|
+
> - the code-system **identity resolver** (`resolveSystem`) and the ConceptMap **`$translate`** engine
|
|
17
|
+
> (`loadConceptMap` / `translate`);
|
|
18
|
+
> - the **CodeSystem load layer** (RRF / CSV / fixed-width / FHIR JSON) with **`$lookup`** and
|
|
19
|
+
> **`$validate-code`**;
|
|
20
|
+
> - **ValueSet** binding (`compose` / **`$expand`** / **`$validate-code`**), subsumption read from the
|
|
21
|
+
> release's own hierarchy;
|
|
22
|
+
> - **UCUM** unit validation and canonicalization (`validateUcum` / `ucumEqual`, recognition only, **no
|
|
23
|
+
> magnitude conversion**) — the official UCUM functional-test suite is the conformance gate;
|
|
24
|
+
> - the **crosswalk resolvers** — the CMS **ICD-9↔ICD-10 GEMs** (`loadGems` / `applyGem`,
|
|
25
|
+
> public-domain) and the NLM **SNOMED CT → ICD-10-CM complex map** (`loadComplexMap` /
|
|
26
|
+
> `applyComplexMap`, BYO — zero SNOMED content bundled), never inverted;
|
|
27
|
+
> - the **RxNorm drug graph** (`loadRxNormGraph` + `ingredientsOf` / `genericFor` / `brandsFor` /
|
|
28
|
+
> `doseFormsOf` / `consistsOf` / `resolveNdc` / `approximateMatch`) over a **BYO** RxNorm RRF release,
|
|
29
|
+
> edges read in RxNorm's documented direction and never inverted, an absent `RXCUI`/NDC typed, never
|
|
30
|
+
> fabricated.
|
|
31
|
+
>
|
|
32
|
+
> **Bring your own data.** The engine is whole; **no code-system _content_ is bundled yet.** It runs
|
|
33
|
+
> over the FHIR resources and standard releases _you_ supply. The bundleable public-domain content packs
|
|
34
|
+
> (RxNorm Prescribable, ICD-10-CM, UCUM, LOINC) are still to come — they are blocked on getting a genuine
|
|
35
|
+
> verbatim public-domain release into the build, not on engine work — and the copyrighted content
|
|
36
|
+
> (SNOMED CT / CPT / full LOINC / UMLS / VSAC) is bring-your-own permanently by license.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
> **Not on npm yet.** `@cosyte/terminology` is pre-alpha and **unpublished** — the command below is how
|
|
41
|
+
> you will install it once the first `0.0.x` release lands. Until then the package is source-only.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install @cosyte/terminology
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Resolve a code system to its canonical URI
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { resolveSystem, isUnknownSystem } from "@cosyte/terminology";
|
|
51
|
+
|
|
52
|
+
const r = resolveSystem("2.16.840.1.113883.6.1"); // OID, mnemonic, or URI
|
|
53
|
+
isUnknownSystem(r) ? "unknown" : r.url; // "http://loinc.org"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Translate a code through a ConceptMap
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { loadConceptMap, translate } from "@cosyte/terminology";
|
|
60
|
+
|
|
61
|
+
const map = loadConceptMap(myFhirConceptMapJson); // a standard FHIR R4 ConceptMap
|
|
62
|
+
const result = translate({ system: "http://loinc.org", code: "2160-0" }, map);
|
|
63
|
+
|
|
64
|
+
if (result.unmapped) {
|
|
65
|
+
// Typed, surfaced outcome — carries any group.unmapped fallback mode. Never a guessed target.
|
|
66
|
+
} else {
|
|
67
|
+
for (const m of result.matches) {
|
|
68
|
+
m.target; // a frozen Coding, drawn verbatim from the map
|
|
69
|
+
m.relationship; // "equivalent" | "source-is-narrower-than-target" | … (R5 vocabulary)
|
|
70
|
+
m.equivalence; // the verbatim FHIR R4 equivalence token
|
|
71
|
+
m.comment; // steward map advice, carried through verbatim
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Look up a code in a code system
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { loadCodeSystem, lookup, validateCode } from "@cosyte/terminology";
|
|
80
|
+
|
|
81
|
+
// Load a consumer-supplied release: RRF (RxNorm/UMLS), CSV (LOINC), fixed-width
|
|
82
|
+
// (ICD-10-CM order file — see ICD10CM_ORDER_FILE_FIELDS), or a FHIR CodeSystem JSON.
|
|
83
|
+
const cs = loadCodeSystem({ format: "fhir", resource: myFhirCodeSystemJson });
|
|
84
|
+
|
|
85
|
+
const hit = lookup(cs, "2160-0"); // $lookup
|
|
86
|
+
if (hit.found) {
|
|
87
|
+
hit.display; // the preferred display, verbatim from the release
|
|
88
|
+
hit.status; // deprecated / header-not-billable / obsolete / … carried, never presented clean
|
|
89
|
+
} else {
|
|
90
|
+
hit.code; // "TERM_CODE_UNKNOWN" — never a fabricated display
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
validateCode(cs, "2160-0").valid; // $validate-code — true iff present; never a guessed true
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Validate and compare UCUM units
|
|
97
|
+
|
|
98
|
+
Recognition and canonicalization only — **no magnitude conversion** (`5 mg/dL` → `mmol/L` needs the
|
|
99
|
+
analyte's molar mass, a clinical computation this engine refuses).
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { validateUcum, ucumEqual } from "@cosyte/terminology";
|
|
103
|
+
|
|
104
|
+
const v = validateUcum("mmol/L");
|
|
105
|
+
v.valid; // true — v.canonical is a stable canonical descriptor
|
|
106
|
+
validateUcum("mg/dl/").valid; // false — code "TERM_UCUM_INVALID", never a guessed "nearest" unit
|
|
107
|
+
|
|
108
|
+
ucumEqual("N", "kg.m/s2"); // true — same unit, different spelling
|
|
109
|
+
ucumEqual("mmol/L", "mmol.L-1"); // true — annotations inert, operators normalized
|
|
110
|
+
ucumEqual("mg", "g"); // false — different units (this is not conversion)
|
|
111
|
+
ucumEqual("Cel", "K"); // false — a special (non-linear) unit is never equated with a linear one
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The UCUM table is the official `ucum-essence.xml`, vendored **verbatim** and parsed at runtime; the
|
|
115
|
+
engine ships no derivative of it and passes the official UCUM functional-test suite. See
|
|
116
|
+
`vendor/ucum/NOTICE.md`.
|
|
117
|
+
|
|
118
|
+
> **Case-sensitive (c/s) mode only.** UCUM's normative interchange mode — the one FHIR/HL7 bind to —
|
|
119
|
+
> is case-sensitive (`m` = metre, `M` = mega, `Pa` = pascal, `pA` = picoampere). The
|
|
120
|
+
> case-**insensitive** (c/i) spelling variant is **not** supported; a c/i-only string returns a typed
|
|
121
|
+
> `TERM_UCUM_INVALID` (fail-safe — never silently reinterpreted). Magnitude conversion is likewise a
|
|
122
|
+
> deliberate non-goal (recognition and canonicalization only).
|
|
123
|
+
|
|
124
|
+
## Navigate the RxNorm drug graph
|
|
125
|
+
|
|
126
|
+
Load a **bring-your-own** RxNorm RRF release and walk the ingredient / brand / clinical-drug /
|
|
127
|
+
dose-form graph. Relationships are read in RxNorm's documented direction (`RELA` is the relationship
|
|
128
|
+
the **second** `RXCUI` has to the **first**); navigation follows only authored edges — the engine
|
|
129
|
+
never synthesizes an inverse, and never fabricates a concept, edge, or NDC.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { loadRxNormGraph, ingredientsOf, genericFor, resolveNdc } from "@cosyte/terminology";
|
|
133
|
+
|
|
134
|
+
const graph = loadRxNormGraph({
|
|
135
|
+
conso: rxnconsoRrf,
|
|
136
|
+
rel: rxnrelRrf,
|
|
137
|
+
sat: rxnsatRrf,
|
|
138
|
+
version: "RXNORM_2026AA",
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
ingredientsOf(graph, "314076"); // { found: true, targets: [ { rxcui: "29046", tty: "IN", name: "lisinopril", … } ] }
|
|
142
|
+
genericFor(graph, "311354"); // an SBD → its SCD, via the authored tradename_of edge
|
|
143
|
+
resolveNdc(graph, "00000000001"); // { resolved: true, rxcui: "314076", status: "active", asOf: "RXNORM_2026AA" }
|
|
144
|
+
ingredientsOf(graph, "99999999"); // { found: false, code: "TERM_RXNORM_UNKNOWN_RXCUI" } — never a guess
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The engine ships **no** RxNorm content — you supply the release (the public-domain Current Prescribable
|
|
148
|
+
Content subset, or the full BYO release). Bundling the public-domain pack is a later phase.
|
|
149
|
+
|
|
150
|
+
## The invariants this engine is built on
|
|
151
|
+
|
|
152
|
+
- **Never fabricate.** An unmapped source is a typed `unmapped`, never a guessed target. Every target
|
|
153
|
+
returned is drawn verbatim from the supplied map. An unrecognized system resolves to a typed
|
|
154
|
+
`unknown`, never a guessed URI.
|
|
155
|
+
- **Never invert.** A directional map is read in its authored direction only; reverse translation
|
|
156
|
+
needs an explicit inverse map, never a mechanical inversion.
|
|
157
|
+
- **BYO data, engine-only.** Zero copyrighted terminology content is bundled (a licensing wall, not a
|
|
158
|
+
gap); the engine operates over your own FHIR resources.
|
|
159
|
+
- **Liberal load, conservative assertion.** Malformed input degrades to a typed diagnostic; a 1:many
|
|
160
|
+
mapping returns the full candidate set, never collapsed to one.
|
|
161
|
+
- **Value-free diagnostics.** A diagnostic carries a code + system + version, never patient context —
|
|
162
|
+
a code _in patient context_ can be PHI.
|
|
163
|
+
- **Zero runtime dependencies. Dual ESM + CJS.** Node stdlib only; built with `tsup`, validated with
|
|
164
|
+
`attw`. Immutable by construction (every returned value is deep-frozen).
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT © Cosyte
|