@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 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