@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/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The immutable FHIR value types the engine operates over: {@link Coding} and
|
|
3
|
+
* {@link CodeableConcept}.
|
|
4
|
+
*
|
|
5
|
+
* These mirror the FHIR R4 datatypes of the same names (the interchange lingua franca the whole
|
|
6
|
+
* ecosystem speaks), reduced to the fields the terminology operations need. Every instance the
|
|
7
|
+
* engine hands back is deep-frozen, so a translated result is safe to share across a pipeline
|
|
8
|
+
* without defensive copying — the immutability discipline the parsers follow.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* A single coded value — a `code` drawn from a code `system`, optionally versioned and displayed.
|
|
14
|
+
*
|
|
15
|
+
* Models FHIR R4 `Coding`. `system` is the code system's **canonical URI** (e.g.
|
|
16
|
+
* `http://loinc.org`), not a mnemonic or OID — use {@link ../systems/resolve.resolveSystem} to
|
|
17
|
+
* canonicalize before constructing one. `system` is optional because a raw wire code sometimes
|
|
18
|
+
* arrives without one; the engine surfaces that rather than inventing a system.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { coding } from "@cosyte/terminology";
|
|
23
|
+
*
|
|
24
|
+
* const c = coding({ system: "http://loinc.org", code: "2160-0", display: "Creatinine" });
|
|
25
|
+
* c.code; // => "2160-0"
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
interface Coding {
|
|
29
|
+
/** The code system's canonical URI (e.g. `http://snomed.info/sct`). Optional when unknown. */
|
|
30
|
+
readonly system?: string;
|
|
31
|
+
/** The symbol in the code system's syntax. Required — a coding without a code is meaningless. */
|
|
32
|
+
readonly code: string;
|
|
33
|
+
/** The code system version this code is drawn from, when known (mappings are release-scoped). */
|
|
34
|
+
readonly version?: string;
|
|
35
|
+
/** The human-readable label. Carried verbatim from the source; never fabricated by the engine. */
|
|
36
|
+
readonly display?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A concept described by one or more {@link Coding}s plus optional free text — FHIR R4
|
|
40
|
+
* `CodeableConcept`. Included so the value layer is complete for downstream consumers even though
|
|
41
|
+
* Phase 1's {@link ../conceptmap/translate.translate} operates on a single {@link Coding}.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* import { codeableConcept } from "@cosyte/terminology";
|
|
46
|
+
*
|
|
47
|
+
* const cc = codeableConcept({
|
|
48
|
+
* coding: [{ system: "http://loinc.org", code: "2160-0" }],
|
|
49
|
+
* text: "Serum creatinine",
|
|
50
|
+
* });
|
|
51
|
+
* cc.coding[0].code; // => "2160-0"
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
interface CodeableConcept {
|
|
55
|
+
/** The codings for this concept, in order of preference. Frozen and never empty-by-fabrication. */
|
|
56
|
+
readonly coding: readonly Coding[];
|
|
57
|
+
/** Plain-text representation of the concept, when supplied. */
|
|
58
|
+
readonly text?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Construct a deep-frozen {@link Coding}.
|
|
62
|
+
*
|
|
63
|
+
* The returned object is `Object.freeze`d so it cannot be mutated in place — parsed/translated
|
|
64
|
+
* values are immutable by default. Optional fields are only present when supplied (no `undefined`
|
|
65
|
+
* keys), so structural equality checks stay clean.
|
|
66
|
+
*
|
|
67
|
+
* @param init - The coding fields; `code` is required.
|
|
68
|
+
* @returns A frozen {@link Coding}.
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* import { coding } from "@cosyte/terminology";
|
|
72
|
+
*
|
|
73
|
+
* const frozen = coding({ system: "http://loinc.org", code: "2160-0" });
|
|
74
|
+
* Object.isFrozen(frozen); // => true
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
declare function coding(init: Coding): Coding;
|
|
78
|
+
/**
|
|
79
|
+
* Construct a deep-frozen {@link CodeableConcept} (its `coding` array and every member frozen).
|
|
80
|
+
*
|
|
81
|
+
* @param init - The concept fields; `coding` is required (may be empty).
|
|
82
|
+
* @returns A frozen {@link CodeableConcept}.
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* import { codeableConcept } from "@cosyte/terminology";
|
|
86
|
+
*
|
|
87
|
+
* const cc = codeableConcept({ coding: [{ code: "2160-0" }] });
|
|
88
|
+
* Object.isFrozen(cc.coding); // => true
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
declare function codeableConcept(init: CodeableConcept): CodeableConcept;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The engine's **stable diagnostic + fatal code registries** and its typed error.
|
|
95
|
+
*
|
|
96
|
+
* `@cosyte/terminology` is an engine, not a lenient wire-format parser, so its stable codes are
|
|
97
|
+
* **diagnostics** (typed, surfaced outcomes a consumer branches on) and **fatals** (thrown on
|
|
98
|
+
* structurally unusable input), rather than the parsers' "Tier-2 tolerance warnings". The
|
|
99
|
+
* discipline is identical: the code strings are part of the public contract — consumers narrow on
|
|
100
|
+
* them, so renaming or removing one is a **breaking change**, and the full set is snapshotted via
|
|
101
|
+
* `sortedCodeSet` (`@cosyte/test-utils`) as a reviewable tripwire.
|
|
102
|
+
*
|
|
103
|
+
* **Value-free by construction.** A diagnostic carries a stable code plus, at most, a
|
|
104
|
+
* code + system + version — **never** a surrounding patient identifier or clinical narrative. A
|
|
105
|
+
* code *in patient context* can be PHI (roadmap §7), so messages describe *structure* (a resource
|
|
106
|
+
* path, a code system), never echo arbitrary input *values*.
|
|
107
|
+
*
|
|
108
|
+
* @packageDocumentation
|
|
109
|
+
*/
|
|
110
|
+
/**
|
|
111
|
+
* Stable **diagnostic codes** — typed, surfaced, non-throwing outcomes.
|
|
112
|
+
*
|
|
113
|
+
* These are not errors: they are first-class results a caller inspects (an unmapped translation,
|
|
114
|
+
* an unrecognized code system). `key === value` so `Object.values(...)` yields the snapshot set.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* import { DIAGNOSTIC_CODES } from "@cosyte/terminology";
|
|
119
|
+
*
|
|
120
|
+
* DIAGNOSTIC_CODES.TERM_TRANSLATE_UNMAPPED; // => "TERM_TRANSLATE_UNMAPPED"
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
declare const DIAGNOSTIC_CODES: {
|
|
124
|
+
/**
|
|
125
|
+
* A `$translate` found no target for the source code. A **first-class outcome, never an error** —
|
|
126
|
+
* the source is surfaced as `unmapped`, never replaced with a guessed target (the never-fabricate
|
|
127
|
+
* invariant).
|
|
128
|
+
*/
|
|
129
|
+
readonly TERM_TRANSLATE_UNMAPPED: "TERM_TRANSLATE_UNMAPPED";
|
|
130
|
+
/**
|
|
131
|
+
* A code-system identifier (URI, OID, or mnemonic) was not recognized by the identity resolver.
|
|
132
|
+
* Surfaced as a typed `unknown`, never coerced to a guessed canonical URI.
|
|
133
|
+
*/
|
|
134
|
+
readonly TERM_SYSTEM_UNRECOGNIZED: "TERM_SYSTEM_UNRECOGNIZED";
|
|
135
|
+
/**
|
|
136
|
+
* An RRF (pipe-delimited RxNorm/UMLS release) row was structurally unusable — too few columns to
|
|
137
|
+
* reach a configured field, or a missing code. The row is **skipped and surfaced** as a load
|
|
138
|
+
* warning (liberal on load), never kept as a partially-parsed concept.
|
|
139
|
+
*/
|
|
140
|
+
readonly TERM_RRF_MALFORMED_ROW: "TERM_RRF_MALFORMED_ROW";
|
|
141
|
+
/**
|
|
142
|
+
* A CSV (RFC-4180, e.g. LOINC `Loinc.csv`) row was structurally unusable — too few fields to reach
|
|
143
|
+
* a configured column. The row is **skipped and surfaced** as a load warning, never partial.
|
|
144
|
+
*/
|
|
145
|
+
readonly TERM_CSV_MALFORMED: "TERM_CSV_MALFORMED";
|
|
146
|
+
/**
|
|
147
|
+
* A fixed-width (e.g. ICD-10-CM order file) row was structurally unusable — too short to contain
|
|
148
|
+
* the configured code field, or a missing code. Skipped and surfaced, never partial.
|
|
149
|
+
*/
|
|
150
|
+
readonly TERM_FIXED_WIDTH_MALFORMED: "TERM_FIXED_WIDTH_MALFORMED";
|
|
151
|
+
/**
|
|
152
|
+
* A concept inside a FHIR `CodeSystem` resource was unusable — not an object, or missing its
|
|
153
|
+
* required `code`. Skipped and surfaced (the whole resource still loads); `line` is `0` since a
|
|
154
|
+
* JSON concept is not line-addressable.
|
|
155
|
+
*/
|
|
156
|
+
readonly TERM_FHIR_CONCEPT_MALFORMED: "TERM_FHIR_CONCEPT_MALFORMED";
|
|
157
|
+
/**
|
|
158
|
+
* A `$lookup`/`$validate-code` found no concept for the code in the loaded release. A **first-class
|
|
159
|
+
* typed outcome, never a guess** — the engine returns `unknown`/`valid: false`, never a fabricated
|
|
160
|
+
* display or a coerced `valid: true` (the never-fabricate invariant, applied to code identity).
|
|
161
|
+
*/
|
|
162
|
+
readonly TERM_CODE_UNKNOWN: "TERM_CODE_UNKNOWN";
|
|
163
|
+
/**
|
|
164
|
+
* A resolved concept is **deprecated / no longer current** (e.g. LOINC `STATUS = DEPRECATED`, or a
|
|
165
|
+
* FHIR concept flagged `inactive`). Surfaced on the concept's status so a caller never presents a
|
|
166
|
+
* non-current code as clean; the concept is still *found* (a lookup succeeds), it is just flagged.
|
|
167
|
+
*/
|
|
168
|
+
readonly TERM_CONCEPT_DEPRECATED: "TERM_CONCEPT_DEPRECATED";
|
|
169
|
+
/**
|
|
170
|
+
* A resolved concept is a **classification header, not a billable/valid leaf** (e.g. an ICD-10-CM
|
|
171
|
+
* order-file row with the position-15 flag `0`). Surfaced on the concept's status: a header code is
|
|
172
|
+
* found but is **not valid for claim submission**, and must never be presented as billable.
|
|
173
|
+
*/
|
|
174
|
+
readonly TERM_CONCEPT_HEADER_NOT_BILLABLE: "TERM_CONCEPT_HEADER_NOT_BILLABLE";
|
|
175
|
+
/**
|
|
176
|
+
* A ValueSet `compose` part could **not be expanded** — an intensional `include`/`exclude` whose
|
|
177
|
+
* code system was not supplied, an unresolvable referenced value set, or a `filter` operator the
|
|
178
|
+
* engine does not implement. Surfaced as a typed outcome that marks the expansion **incomplete**;
|
|
179
|
+
* the engine **never** fabricates a member and **never** returns a silently-empty "no members"
|
|
180
|
+
* answer for a part it could not compute (roadmap §4.4 — a false "not a member" is a clinical error).
|
|
181
|
+
*/
|
|
182
|
+
readonly TERM_VALUESET_CANNOT_EXPAND: "TERM_VALUESET_CANNOT_EXPAND";
|
|
183
|
+
/**
|
|
184
|
+
* A **pre-computed** `ValueSet.expansion` is **incomplete** — its `total` exceeds the number of
|
|
185
|
+
* `contains` entries, or it is flagged too-costly (the VSAC `$expand` >1200-code truncation hazard,
|
|
186
|
+
* roadmap §4.4). The engine **never treats a truncated expansion as complete membership**: a code
|
|
187
|
+
* absent from a truncated expansion is `undetermined`, never a confident "not a member".
|
|
188
|
+
*/
|
|
189
|
+
readonly TERM_VALUESET_EXPANSION_TRUNCATED: "TERM_VALUESET_EXPANSION_TRUNCATED";
|
|
190
|
+
/**
|
|
191
|
+
* A UCUM unit expression is **not valid** — it does not parse against the UCUM grammar, or it
|
|
192
|
+
* names an atom absent from the vendored UCUM table. A **first-class typed outcome, never an
|
|
193
|
+
* error and never a guess**: {@link validateUcum} returns `{ valid: false, reason }`, never a
|
|
194
|
+
* coerced or "nearest" unit (the never-fabricate invariant, applied to units — roadmap §4.3). The
|
|
195
|
+
* `reason` is value-free (a grammar fault or an unknown-atom shape), never PHI.
|
|
196
|
+
*/
|
|
197
|
+
readonly TERM_UCUM_INVALID: "TERM_UCUM_INVALID";
|
|
198
|
+
/**
|
|
199
|
+
* A crosswalk source code has an **authored No-Map** — the steward declares it has no valid target
|
|
200
|
+
* in the other classification (a GEM `NoDx`/`NoPCS` sentinel, or a SNOMED complex-map `447638001`
|
|
201
|
+
* "cannot be classified" category). A **first-class typed outcome, never an error and never a
|
|
202
|
+
* guess** (the never-fabricate invariant, applied to crosswalks — roadmap §4.1). Distinct from
|
|
203
|
+
* {@link TERM_CROSSWALK_UNMAPPED}: the source *is* in the map, and the map says "no target".
|
|
204
|
+
*/
|
|
205
|
+
readonly TERM_CROSSWALK_NO_MAP: "TERM_CROSSWALK_NO_MAP";
|
|
206
|
+
/**
|
|
207
|
+
* A crosswalk source code is **absent from the map entirely** — not an authored No-Map, simply not
|
|
208
|
+
* present. Surfaced as a typed outcome (never a fabricated target, never a silent success), and
|
|
209
|
+
* kept distinct from {@link TERM_CROSSWALK_NO_MAP} so a caller can tell "the steward said no target"
|
|
210
|
+
* from "this code was not in the file".
|
|
211
|
+
*/
|
|
212
|
+
readonly TERM_CROSSWALK_UNMAPPED: "TERM_CROSSWALK_UNMAPPED";
|
|
213
|
+
/**
|
|
214
|
+
* A SNOMED→ICD-10-CM complex-map group's decision needs **patient context** (an `IFA` age band or
|
|
215
|
+
* gender) the caller did not supply. The engine surfaces the candidate rules + advice and **refuses
|
|
216
|
+
* to pick a branch** it lacks the data for (roadmap §4.1) — never a silently-chosen target.
|
|
217
|
+
*/
|
|
218
|
+
readonly TERM_CROSSWALK_CONTEXT_REQUIRED: "TERM_CROSSWALK_CONTEXT_REQUIRED";
|
|
219
|
+
/**
|
|
220
|
+
* A GEM file line was structurally unusable — not three whitespace-delimited fields, or a flag
|
|
221
|
+
* field that is not a 5-digit code. The row is **skipped and surfaced** as a load warning (liberal
|
|
222
|
+
* on load), never kept as a partially-parsed entry.
|
|
223
|
+
*/
|
|
224
|
+
readonly TERM_GEM_MALFORMED_ROW: "TERM_GEM_MALFORMED_ROW";
|
|
225
|
+
/**
|
|
226
|
+
* A SNOMED complex-map RF2 refset row was structurally unusable — missing its
|
|
227
|
+
* `referencedComponentId`/`mapGroup`/`mapPriority`. Skipped and surfaced, never partial.
|
|
228
|
+
*/
|
|
229
|
+
readonly TERM_COMPLEX_MAP_MALFORMED_ROW: "TERM_COMPLEX_MAP_MALFORMED_ROW";
|
|
230
|
+
/**
|
|
231
|
+
* A RxNorm RRF row (`RXNCONSO` / `RXNREL` / `RXNSAT`) was structurally unusable — too few columns to
|
|
232
|
+
* reach a required field, or a missing `RXCUI`/NDC value. The row is **skipped and surfaced** as a
|
|
233
|
+
* load warning (liberal on load), never kept as a partial concept/edge. Rows that are simply *not of
|
|
234
|
+
* interest* (a non-`RXNORM` atom, an atom-level relationship, a non-`NDC` attribute) are skipped
|
|
235
|
+
* **silently** — they are expected, not faults.
|
|
236
|
+
*/
|
|
237
|
+
readonly TERM_RXNORM_MALFORMED_ROW: "TERM_RXNORM_MALFORMED_ROW";
|
|
238
|
+
/**
|
|
239
|
+
* A RxNorm graph navigation was asked about an `RXCUI` **absent from the loaded release** — a
|
|
240
|
+
* first-class typed outcome, never a fabricated concept and never an empty "success" (the
|
|
241
|
+
* never-fabricate invariant, applied to the drug graph — roadmap §4.2). Distinct from a *present*
|
|
242
|
+
* concept that simply has no edge of the requested relationship (that is a found result with empty
|
|
243
|
+
* targets).
|
|
244
|
+
*/
|
|
245
|
+
readonly TERM_RXNORM_UNKNOWN_RXCUI: "TERM_RXNORM_UNKNOWN_RXCUI";
|
|
246
|
+
/**
|
|
247
|
+
* An NDC could **not be resolved** to any `RXCUI` in the loaded RxNorm release — a typed, surfaced
|
|
248
|
+
* absence, never a guessed `RXCUI`. NDC↔RXCUI is many:1 and temporal (roadmap §4.2), so a resolution
|
|
249
|
+
* is always release-scoped; an NDC not present in the loaded release is this diagnostic.
|
|
250
|
+
*/
|
|
251
|
+
readonly TERM_RXNORM_NDC_UNMAPPED: "TERM_RXNORM_NDC_UNMAPPED";
|
|
252
|
+
};
|
|
253
|
+
/**
|
|
254
|
+
* A value from {@link DIAGNOSTIC_CODES} — the type consumers narrow a diagnostic's `code` against.
|
|
255
|
+
*/
|
|
256
|
+
type DiagnosticCode = (typeof DIAGNOSTIC_CODES)[keyof typeof DIAGNOSTIC_CODES];
|
|
257
|
+
/**
|
|
258
|
+
* Stable **fatal codes** — thrown (as a {@link TerminologyError}) when input cannot be loaded into
|
|
259
|
+
* a usable model at all. Always thrown, never swallowed. `key === value`, part of the contract.
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* ```ts
|
|
263
|
+
* import { FATAL_CODES } from "@cosyte/terminology";
|
|
264
|
+
*
|
|
265
|
+
* FATAL_CODES.TERM_CONCEPTMAP_MALFORMED; // => "TERM_CONCEPTMAP_MALFORMED"
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
declare const FATAL_CODES: {
|
|
269
|
+
/**
|
|
270
|
+
* A `ConceptMap` resource was structurally unusable — wrong `resourceType`, or a group/element
|
|
271
|
+
* missing a required field. Thrown rather than silently loading a partial, misleading map.
|
|
272
|
+
*/
|
|
273
|
+
readonly TERM_CONCEPTMAP_MALFORMED: "TERM_CONCEPTMAP_MALFORMED";
|
|
274
|
+
/**
|
|
275
|
+
* A CodeSystem *source* was structurally unusable — a FHIR resource with the wrong `resourceType`,
|
|
276
|
+
* or a delimited source whose configured code/display column is absent from the header. Thrown
|
|
277
|
+
* rather than silently loading an empty or mis-keyed release. (Malformed individual **rows** are
|
|
278
|
+
* skipped-and-surfaced load warnings, not fatals — only an unusable *source* is fatal.)
|
|
279
|
+
*/
|
|
280
|
+
readonly TERM_CODESYSTEM_MALFORMED: "TERM_CODESYSTEM_MALFORMED";
|
|
281
|
+
/**
|
|
282
|
+
* A `ValueSet` resource was structurally unusable — wrong `resourceType`, or a `compose`/`expansion`
|
|
283
|
+
* whose required shape (an `include` that is not an object, a `contains` entry missing its `code`)
|
|
284
|
+
* is corrupt. Thrown rather than silently loading a partial value set that would bind *some* codes
|
|
285
|
+
* and quietly drop others. (A `filter` operator the engine does not implement is **not** fatal — it
|
|
286
|
+
* is the surfaced `TERM_VALUESET_CANNOT_EXPAND` diagnostic at expansion time.)
|
|
287
|
+
*/
|
|
288
|
+
readonly TERM_VALUESET_MALFORMED: "TERM_VALUESET_MALFORMED";
|
|
289
|
+
/**
|
|
290
|
+
* A crosswalk map *source* was structurally unusable — e.g. a SNOMED complex-map RF2 refset with
|
|
291
|
+
* empty content or a header missing a required column. Thrown rather than silently loading an empty
|
|
292
|
+
* or mis-keyed map. (Malformed individual **rows** are skipped-and-surfaced warnings, not fatals.)
|
|
293
|
+
*/
|
|
294
|
+
readonly TERM_CROSSWALK_MALFORMED: "TERM_CROSSWALK_MALFORMED";
|
|
295
|
+
/**
|
|
296
|
+
* An **inversion** of a directional crosswalk was requested — a forbidden operation. The GEMs and
|
|
297
|
+
* the SNOMED→ICD-10-CM map are authoritative in **one direction only**; a forward map is not the
|
|
298
|
+
* inverse of the backward map (roadmap §4.1). Thrown by {@link ../crosswalk/gems.invertGem} so the
|
|
299
|
+
* never-invert refusal is a first-class, discoverable contract rather than a silent absence.
|
|
300
|
+
*/
|
|
301
|
+
readonly TERM_MAP_NOT_INVERTIBLE: "TERM_MAP_NOT_INVERTIBLE";
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
304
|
+
* A value from {@link FATAL_CODES} — the type carried by a thrown {@link TerminologyError}.
|
|
305
|
+
*/
|
|
306
|
+
type FatalCode = (typeof FATAL_CODES)[keyof typeof FATAL_CODES];
|
|
307
|
+
/**
|
|
308
|
+
* The engine's typed error. Carries a stable {@link FatalCode} so callers branch on `err.code`
|
|
309
|
+
* without string-matching the message. Thrown only for {@link FATAL_CODES} conditions.
|
|
310
|
+
*
|
|
311
|
+
* The `message` is **value-free**: it names the structural fault (a resource path, a missing
|
|
312
|
+
* field) and never echoes input *values*.
|
|
313
|
+
*
|
|
314
|
+
* @example
|
|
315
|
+
* ```ts
|
|
316
|
+
* import { loadConceptMap, TerminologyError, FATAL_CODES } from "@cosyte/terminology";
|
|
317
|
+
*
|
|
318
|
+
* try {
|
|
319
|
+
* loadConceptMap({ resourceType: "Patient" });
|
|
320
|
+
* } catch (err) {
|
|
321
|
+
* (err as TerminologyError).code === FATAL_CODES.TERM_CONCEPTMAP_MALFORMED; // => true
|
|
322
|
+
* }
|
|
323
|
+
* ```
|
|
324
|
+
*/
|
|
325
|
+
declare class TerminologyError extends Error {
|
|
326
|
+
/** The stable fatal code. Branch on this, not on {@link message}. */
|
|
327
|
+
readonly code: FatalCode;
|
|
328
|
+
/**
|
|
329
|
+
* @param code - The stable {@link FatalCode} describing the fault.
|
|
330
|
+
* @param message - A **value-free** structural description (path + fault; never an input value).
|
|
331
|
+
*/
|
|
332
|
+
constructor(code: FatalCode, message: string);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Minimal, zero-dependency navigation helpers for **untrusted FHIR JSON** (`unknown` inputs
|
|
337
|
+
* decoded from a consumer-supplied `ConceptMap`/`CodeSystem`/`ValueSet`).
|
|
338
|
+
*
|
|
339
|
+
* The engine never trusts the shape of its inputs: every accessor here narrows an `unknown`
|
|
340
|
+
* to a concrete type or returns `undefined`, so a malformed resource degrades to a typed
|
|
341
|
+
* diagnostic (see {@link ../conceptmap/load}) rather than a `TypeError` deep in a getter. This
|
|
342
|
+
* is the terminology-engine analogue of a parser's lenient tokenizer — liberal on load.
|
|
343
|
+
*
|
|
344
|
+
* These helpers are **value-free**: they read structure, never log or echo field *values*, so a
|
|
345
|
+
* code-in-context (which can be PHI, per the roadmap's PHI posture) never flows into a message.
|
|
346
|
+
*
|
|
347
|
+
* @packageDocumentation
|
|
348
|
+
*/
|
|
349
|
+
/**
|
|
350
|
+
* Narrow an `unknown` to a plain JSON object (a non-null, non-array `object`).
|
|
351
|
+
*
|
|
352
|
+
* @param value - The value to test.
|
|
353
|
+
* @returns `true` when `value` is a non-null, non-array object.
|
|
354
|
+
* @example
|
|
355
|
+
* ```ts
|
|
356
|
+
* import { isJsonObject } from "@cosyte/terminology";
|
|
357
|
+
*
|
|
358
|
+
* isJsonObject({ resourceType: "ConceptMap" }); // => true
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
declare function isJsonObject(value: unknown): value is Record<string, unknown>;
|
|
362
|
+
/**
|
|
363
|
+
* Read a **string** property from an untrusted object, or `undefined` if absent/not a string.
|
|
364
|
+
*
|
|
365
|
+
* @param obj - The source object (may be any `unknown`).
|
|
366
|
+
* @param key - The property name to read.
|
|
367
|
+
* @returns The string value, or `undefined` when the key is missing or not a string.
|
|
368
|
+
* @example
|
|
369
|
+
* ```ts
|
|
370
|
+
* import { getString } from "@cosyte/terminology";
|
|
371
|
+
*
|
|
372
|
+
* getString({ url: "http://loinc.org" }, "url"); // => "http://loinc.org"
|
|
373
|
+
* ```
|
|
374
|
+
*/
|
|
375
|
+
declare function getString(obj: unknown, key: string): string | undefined;
|
|
376
|
+
/**
|
|
377
|
+
* Read an **array** property from an untrusted object as a `readonly unknown[]`, or `undefined`.
|
|
378
|
+
*
|
|
379
|
+
* The elements are left as `unknown` — callers narrow each one. A non-array (including a missing
|
|
380
|
+
* key) yields `undefined`, which callers treat as "no entries" rather than an error.
|
|
381
|
+
*
|
|
382
|
+
* @param obj - The source object (may be any `unknown`).
|
|
383
|
+
* @param key - The property name to read.
|
|
384
|
+
* @returns The array (elements untyped), or `undefined` when the key is missing or not an array.
|
|
385
|
+
* @example
|
|
386
|
+
* ```ts
|
|
387
|
+
* import { getArray } from "@cosyte/terminology";
|
|
388
|
+
*
|
|
389
|
+
* getArray({ group: [{ source: "a" }] }, "group")?.length; // => 1
|
|
390
|
+
* ```
|
|
391
|
+
*/
|
|
392
|
+
declare function getArray(obj: unknown, key: string): readonly unknown[] | undefined;
|
|
393
|
+
/**
|
|
394
|
+
* Read a **boolean** property from an untrusted object, or `undefined` if absent/not a boolean.
|
|
395
|
+
*
|
|
396
|
+
* A non-boolean (including a missing key, or the strings `"true"`/`"false"`) yields `undefined` —
|
|
397
|
+
* the accessor never coerces, so a caller can distinguish "explicitly `false`" from "absent".
|
|
398
|
+
*
|
|
399
|
+
* @param obj - The source object (may be any `unknown`).
|
|
400
|
+
* @param key - The property name to read.
|
|
401
|
+
* @returns The boolean value, or `undefined` when the key is missing or not a boolean.
|
|
402
|
+
* @example
|
|
403
|
+
* ```ts
|
|
404
|
+
* import { getBoolean } from "@cosyte/terminology";
|
|
405
|
+
*
|
|
406
|
+
* getBoolean({ inactive: true }, "inactive"); // => true
|
|
407
|
+
* ```
|
|
408
|
+
*/
|
|
409
|
+
declare function getBoolean(obj: unknown, key: string): boolean | undefined;
|
|
410
|
+
/**
|
|
411
|
+
* Read a **number** property from an untrusted object, or `undefined` if absent/not a finite number.
|
|
412
|
+
*
|
|
413
|
+
* @param obj - The source object (may be any `unknown`).
|
|
414
|
+
* @param key - The property name to read.
|
|
415
|
+
* @returns The number value, or `undefined` when the key is missing or not a finite number.
|
|
416
|
+
* @example
|
|
417
|
+
* ```ts
|
|
418
|
+
* import { getNumber } from "@cosyte/terminology";
|
|
419
|
+
*
|
|
420
|
+
* getNumber({ valueInteger: 3 }, "valueInteger"); // => 3
|
|
421
|
+
* ```
|
|
422
|
+
*/
|
|
423
|
+
declare function getNumber(obj: unknown, key: string): number | undefined;
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* The **code-system identity registry** — the grounded OID ↔ canonical-URI ↔ mnemonic facts the
|
|
427
|
+
* ecosystem uses.
|
|
428
|
+
*
|
|
429
|
+
* A code-system *identity* (the triple of canonical URI, OID, and HL7 v2 mnemonic that all name
|
|
430
|
+
* the same terminology) is a **published fact**, not copyrighted content — so it is encoded here,
|
|
431
|
+
* cited firsthand. This is the license line the roadmap draws: identities are facts we may encode;
|
|
432
|
+
* the code *lists themselves* (SNOMED/CPT/LOINC content) are BYO and never bundled.
|
|
433
|
+
*
|
|
434
|
+
* **Every identity below is grounded firsthand against a primary source** (verified 2026-07-21):
|
|
435
|
+
*
|
|
436
|
+
* - The FHIR R4 terminology-systems registry — `https://hl7.org/fhir/R4/terminologies-systems.html`
|
|
437
|
+
* — for LOINC, SNOMED CT, RxNorm, UCUM, CPT, NDC, CVX (canonical URI + OID), and for the HL7 v2
|
|
438
|
+
* table URI/OID patterns (`http://terminology.hl7.org/CodeSystem/v2-[X]` ↔
|
|
439
|
+
* `2.16.840.1.113883.12.[X]`).
|
|
440
|
+
* - The FHIR R4 ICD page — `https://hl7.org/fhir/R4/icd.html` — for ICD-10-CM
|
|
441
|
+
* (`http://hl7.org/fhir/sid/icd-10-cm` ↔ `2.16.840.1.113883.6.90`).
|
|
442
|
+
* - HL7 Table 0396 (coding-system mnemonics) — the THO `v2-0396` code system — for the mnemonics
|
|
443
|
+
* (`LN`, `SCT`, `I10C`, `RXNORM`, `UCUM`, `C4`, `NDC`, `CVX`).
|
|
444
|
+
*
|
|
445
|
+
* Identities that could not be pinned to a primary source firsthand this pass are **omitted**, not
|
|
446
|
+
* guessed (roadmap: "do not guess an identity") — notably ICD-9-CM and ICD-10-PCS, whose OID
|
|
447
|
+
* assignments were ambiguous across sources.
|
|
448
|
+
*
|
|
449
|
+
* @packageDocumentation
|
|
450
|
+
*/
|
|
451
|
+
/**
|
|
452
|
+
* A resolved code-system identity — the canonical URI plus the alternate identifiers that name the
|
|
453
|
+
* same system.
|
|
454
|
+
*/
|
|
455
|
+
interface SystemIdentity {
|
|
456
|
+
/** The canonical URI — the single value the engine treats as the system's identity. */
|
|
457
|
+
readonly url: string;
|
|
458
|
+
/** The registered OID (bare, no `urn:oid:` prefix), when the system has one. */
|
|
459
|
+
readonly oid?: string;
|
|
460
|
+
/** A short human-readable name for diagnostics/logs (value-free — a system name, never a value). */
|
|
461
|
+
readonly name: string;
|
|
462
|
+
/** HL7 v2 Table 0396 mnemonic(s) and other accepted short aliases for this system. */
|
|
463
|
+
readonly mnemonics: readonly string[];
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* The seed identity table. Order is irrelevant; lookup indexes are built from it in
|
|
467
|
+
* {@link ../systems/resolve}. Each entry is one firsthand-grounded system (see the module doc).
|
|
468
|
+
*/
|
|
469
|
+
declare const SYSTEM_IDENTITIES: readonly SystemIdentity[];
|
|
470
|
+
/**
|
|
471
|
+
* The OID root under which every HL7 v2 table code system is registered:
|
|
472
|
+
* `2.16.840.1.113883.12.[table]` — grounded against the FHIR R4 terminology-systems registry.
|
|
473
|
+
* The table number is appended with no leading zeros (e.g. table 0203 → `…12.203`).
|
|
474
|
+
*/
|
|
475
|
+
declare const HL7_V2_OID_ROOT = "2.16.840.1.113883.12";
|
|
476
|
+
/**
|
|
477
|
+
* The canonical URI prefix for HL7 v2 table code systems in HL7 Terminology (THO):
|
|
478
|
+
* `http://terminology.hl7.org/CodeSystem/v2-[XXXX]`, where `[XXXX]` is the zero-padded 4-digit
|
|
479
|
+
* table id — grounded against the FHIR R4 terminology-systems registry.
|
|
480
|
+
*/
|
|
481
|
+
declare const HL7_V2_URL_PREFIX = "http://terminology.hl7.org/CodeSystem/v2-";
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* The **code-system identity / canonical-URI resolver** — `resolveSystem`.
|
|
485
|
+
*
|
|
486
|
+
* Given a code system named any of the ways the ecosystem names it — a canonical URI, an OID (bare
|
|
487
|
+
* or `urn:oid:`-prefixed), or an HL7 v2 mnemonic (`LN`, `SCT`, …) — it returns the one canonical
|
|
488
|
+
* {@link SystemIdentity}. This is the piece `@cosyte/transform` pins so a `Coding` off any parser
|
|
489
|
+
* (HL7 v2 CE/CWE, C-CDA, FHIR) can be canonicalized to a single system URI before translation.
|
|
490
|
+
*
|
|
491
|
+
* **Never guesses.** An identifier that matches nothing known returns a typed
|
|
492
|
+
* `{ unknown: true, input }` — never a fabricated URI (the never-fabricate invariant, applied to
|
|
493
|
+
* system identity).
|
|
494
|
+
*
|
|
495
|
+
* @packageDocumentation
|
|
496
|
+
*/
|
|
497
|
+
|
|
498
|
+
/** The typed "not recognized" outcome from {@link resolveSystem} — surfaced, never a guess. */
|
|
499
|
+
interface UnknownSystem {
|
|
500
|
+
/** Discriminant: the identifier was not recognized. */
|
|
501
|
+
readonly unknown: true;
|
|
502
|
+
/** The identifier as supplied, echoed back so the caller can surface/log it. */
|
|
503
|
+
readonly input: string;
|
|
504
|
+
}
|
|
505
|
+
/** The result of {@link resolveSystem}: a resolved {@link SystemIdentity} or a typed unknown. */
|
|
506
|
+
type ResolveSystemResult = SystemIdentity | UnknownSystem;
|
|
507
|
+
/**
|
|
508
|
+
* Resolve a code-system identifier to its canonical {@link SystemIdentity}.
|
|
509
|
+
*
|
|
510
|
+
* Accepts, in order of attempt: a canonical URI (case-insensitive), a `v2-XXXX` mnemonic or a
|
|
511
|
+
* v2 table URI/OID (resolved structurally), a bare or `urn:oid:`-prefixed OID, or an HL7 v2
|
|
512
|
+
* Table 0396 mnemonic. Whitespace is trimmed. An empty or unrecognized identifier yields a typed
|
|
513
|
+
* {@link UnknownSystem} — the resolver **never** invents a URI.
|
|
514
|
+
*
|
|
515
|
+
* @param id - The identifier (canonical URI, OID, `urn:oid:` OID, or mnemonic).
|
|
516
|
+
* @returns The resolved {@link SystemIdentity}, or `{ unknown: true, input }`.
|
|
517
|
+
* @example
|
|
518
|
+
* ```ts
|
|
519
|
+
* import { resolveSystem } from "@cosyte/terminology";
|
|
520
|
+
*
|
|
521
|
+
* resolveSystem("2.16.840.1.113883.6.1"); // => { url: "http://loinc.org", … }
|
|
522
|
+
* resolveSystem("SCT"); // => { url: "http://snomed.info/sct", … }
|
|
523
|
+
* resolveSystem("http://example.com/nope"); // => { unknown: true, input: "http://example.com/nope" }
|
|
524
|
+
* ```
|
|
525
|
+
*/
|
|
526
|
+
declare function resolveSystem(id: string): ResolveSystemResult;
|
|
527
|
+
/**
|
|
528
|
+
* Type guard: did {@link resolveSystem} fail to recognize the identifier?
|
|
529
|
+
*
|
|
530
|
+
* @param result - A {@link resolveSystem} result.
|
|
531
|
+
* @returns `true` when the result is a typed {@link UnknownSystem}.
|
|
532
|
+
* @example
|
|
533
|
+
* ```ts
|
|
534
|
+
* import { resolveSystem, isUnknownSystem } from "@cosyte/terminology";
|
|
535
|
+
*
|
|
536
|
+
* isUnknownSystem(resolveSystem("nope")); // => true
|
|
537
|
+
* ```
|
|
538
|
+
*/
|
|
539
|
+
declare function isUnknownSystem(result: ResolveSystemResult): result is UnknownSystem;
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* The types for the ConceptMap `$translate` engine — the loaded {@link ConceptMap} model, the
|
|
543
|
+
* normalized {@link Relationship} enum, and the fail-safe {@link TranslateResult}.
|
|
544
|
+
*
|
|
545
|
+
* The engine targets **FHIR R4** ConceptMap input (`element.target.equivalence`), but carries an
|
|
546
|
+
* internal, version-neutral {@link Relationship} model that maps cleanly to **both** R4
|
|
547
|
+
* `equivalence` and R5 `relationship` (the enum was renamed and re-valued between versions). Each
|
|
548
|
+
* match surfaces both the normalized relationship and the verbatim R4 token it came from, so no
|
|
549
|
+
* information is dropped and a consumer can render either vocabulary.
|
|
550
|
+
*
|
|
551
|
+
* @packageDocumentation
|
|
552
|
+
*/
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* The verbatim **FHIR R4** `ConceptMap.group.element.target.equivalence` value, carried through
|
|
556
|
+
* untouched so nothing the map author asserted is lost. (R4 enum:
|
|
557
|
+
* `relatedto | equivalent | equal | wider | subsumes | narrower | specializes | inexact |
|
|
558
|
+
* unmatched | disjoint`.)
|
|
559
|
+
*/
|
|
560
|
+
type R4Equivalence = "relatedto" | "equivalent" | "equal" | "wider" | "subsumes" | "narrower" | "specializes" | "inexact" | "unmatched" | "disjoint";
|
|
561
|
+
/**
|
|
562
|
+
* The engine's **normalized, version-neutral relationship** between a source and a target concept —
|
|
563
|
+
* the FHIR **R5** vocabulary, which the R4 `equivalence` tokens fold into cleanly. Surfaced on every
|
|
564
|
+
* match alongside the verbatim R4 token.
|
|
565
|
+
*
|
|
566
|
+
* - `equivalent` ← R4 `equivalent` / `equal`
|
|
567
|
+
* - `source-is-narrower-than-target` ← R4 `wider` / `subsumes` (the source is-a the target)
|
|
568
|
+
* - `source-is-broader-than-target` ← R4 `narrower` / `specializes` (the target is-a the source)
|
|
569
|
+
* - `related-to` ← R4 `relatedto` / `inexact`
|
|
570
|
+
* - `not-related-to` ← R4 `disjoint` / `unmatched`
|
|
571
|
+
*/
|
|
572
|
+
type Relationship = "equivalent" | "source-is-narrower-than-target" | "source-is-broader-than-target" | "related-to" | "not-related-to";
|
|
573
|
+
/**
|
|
574
|
+
* One target concept a {@link translate} call produced, with its relationship to the source and
|
|
575
|
+
* the verbatim R4 equivalence token. Never fabricated — every field is drawn from the loaded map.
|
|
576
|
+
*/
|
|
577
|
+
interface TranslateMatch {
|
|
578
|
+
/** The target {@link Coding} exactly as declared in the map (frozen). */
|
|
579
|
+
readonly target: Coding;
|
|
580
|
+
/** The normalized, version-neutral relationship (R5 vocabulary). */
|
|
581
|
+
readonly relationship: Relationship;
|
|
582
|
+
/** The verbatim FHIR R4 `equivalence` token this match was declared with. */
|
|
583
|
+
readonly equivalence: R4Equivalence;
|
|
584
|
+
/** Author's free-text comment on this mapping, when present (carried verbatim, e.g. map advice). */
|
|
585
|
+
readonly comment?: string;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* The FHIR R4 `group.unmapped.mode` an unmapped source fell through to — the map author's declared
|
|
589
|
+
* fallback behavior. Surfaced (never silently acted on): the caller decides whether to trust a
|
|
590
|
+
* fallback.
|
|
591
|
+
*
|
|
592
|
+
* - `provided` — echo the source code as the target (the map declares no real mapping).
|
|
593
|
+
* - `fixed` — a single fixed fallback code the map declares for *all* unmapped sources.
|
|
594
|
+
* - `other-map` — consult the referenced ConceptMap (which this engine does not auto-follow).
|
|
595
|
+
* - `none` — no `group.unmapped` directive applied (plain no-match, or an explicit `unmatched`).
|
|
596
|
+
*/
|
|
597
|
+
type UnmappedMode = "provided" | "fixed" | "other-map" | "none";
|
|
598
|
+
/** Provenance of a translation — which map (and which group's systems) produced it. */
|
|
599
|
+
interface MapProvenance {
|
|
600
|
+
/** The ConceptMap's canonical `url`, when declared. */
|
|
601
|
+
readonly conceptMapUrl?: string;
|
|
602
|
+
/** The ConceptMap's business `version`, when declared (mappings are release-scoped). */
|
|
603
|
+
readonly conceptMapVersion?: string;
|
|
604
|
+
/** The source code system of the group that matched (or was consulted), when known. */
|
|
605
|
+
readonly sourceSystem?: string;
|
|
606
|
+
/** The target code system of the group that matched, when known. */
|
|
607
|
+
readonly targetSystem?: string;
|
|
608
|
+
}
|
|
609
|
+
/** A successful translation: one or more matches, never empty (an empty result is `unmapped`). */
|
|
610
|
+
interface TranslateMatched {
|
|
611
|
+
/** Discriminant: the source mapped. */
|
|
612
|
+
readonly unmapped: false;
|
|
613
|
+
/** The target concepts, in declared order. Non-empty and frozen. */
|
|
614
|
+
readonly matches: readonly TranslateMatch[];
|
|
615
|
+
/** Where this translation came from. */
|
|
616
|
+
readonly provenance: MapProvenance;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* A **fail-safe unmapped** translation — the never-fabricate outcome. The source is surfaced as-is;
|
|
620
|
+
* **no target is ever guessed**. Any `group.unmapped` fallback the author declared is reported via
|
|
621
|
+
* {@link UnmappedMode} (and `fixedTarget`/`otherMapUrl`) for the caller to accept or reject — the
|
|
622
|
+
* engine does not silently substitute it.
|
|
623
|
+
*/
|
|
624
|
+
interface TranslateUnmapped {
|
|
625
|
+
/** Discriminant: the source did not map. */
|
|
626
|
+
readonly unmapped: true;
|
|
627
|
+
/** The stable diagnostic code for an unmapped translation. */
|
|
628
|
+
readonly code: "TERM_TRANSLATE_UNMAPPED";
|
|
629
|
+
/** The map author's declared fallback mode (or `none`). */
|
|
630
|
+
readonly mode: UnmappedMode;
|
|
631
|
+
/** The original source {@link Coding}, surfaced untouched. */
|
|
632
|
+
readonly source: Coding;
|
|
633
|
+
/** For `mode: "fixed"`, the author's fixed fallback coding — reported, not auto-applied. */
|
|
634
|
+
readonly fixedTarget?: Coding;
|
|
635
|
+
/** For `mode: "other-map"`, the referenced map's URL — reported, not auto-followed. */
|
|
636
|
+
readonly otherMapUrl?: string;
|
|
637
|
+
/** Where the (failed) translation was attempted. */
|
|
638
|
+
readonly provenance: MapProvenance;
|
|
639
|
+
}
|
|
640
|
+
/** The result of {@link translate}: a matched result or a typed {@link TranslateUnmapped}. */
|
|
641
|
+
type TranslateResult = TranslateMatched | TranslateUnmapped;
|
|
642
|
+
/**
|
|
643
|
+
* A single `group.unmapped` directive from a loaded ConceptMap group (FHIR R4).
|
|
644
|
+
*/
|
|
645
|
+
interface ConceptMapUnmapped {
|
|
646
|
+
/** The fallback mode. */
|
|
647
|
+
readonly mode: UnmappedMode;
|
|
648
|
+
/** For `fixed`, the fixed target code. */
|
|
649
|
+
readonly code?: string;
|
|
650
|
+
/** For `fixed`, the fixed target display. */
|
|
651
|
+
readonly display?: string;
|
|
652
|
+
/** For `other-map`, the canonical URL of the map to consult. */
|
|
653
|
+
readonly url?: string;
|
|
654
|
+
}
|
|
655
|
+
/** One `group.element.target` — a candidate target for a source concept (FHIR R4). */
|
|
656
|
+
interface ConceptMapTarget {
|
|
657
|
+
/** The target code (absent for a pure `unmatched` assertion). */
|
|
658
|
+
readonly code?: string;
|
|
659
|
+
/** The target display, carried verbatim when present. */
|
|
660
|
+
readonly display?: string;
|
|
661
|
+
/** The verbatim R4 equivalence token. */
|
|
662
|
+
readonly equivalence: R4Equivalence;
|
|
663
|
+
/** Author's comment (e.g. steward map advice), carried verbatim. */
|
|
664
|
+
readonly comment?: string;
|
|
665
|
+
}
|
|
666
|
+
/** One `group.element` — a source concept and its candidate targets (FHIR R4). */
|
|
667
|
+
interface ConceptMapElement {
|
|
668
|
+
/** The source code. */
|
|
669
|
+
readonly code?: string;
|
|
670
|
+
/** The source display. */
|
|
671
|
+
readonly display?: string;
|
|
672
|
+
/** The candidate targets. */
|
|
673
|
+
readonly target: readonly ConceptMapTarget[];
|
|
674
|
+
}
|
|
675
|
+
/** One `group` — a source-system/target-system pair with its element mappings (FHIR R4). */
|
|
676
|
+
interface ConceptMapGroup {
|
|
677
|
+
/** The group's source code system (canonical URI), when declared. */
|
|
678
|
+
readonly source?: string;
|
|
679
|
+
/** The group's source system version, when declared. */
|
|
680
|
+
readonly sourceVersion?: string;
|
|
681
|
+
/** The group's target code system (canonical URI), when declared. */
|
|
682
|
+
readonly target?: string;
|
|
683
|
+
/** The group's target system version, when declared. */
|
|
684
|
+
readonly targetVersion?: string;
|
|
685
|
+
/** The element mappings. */
|
|
686
|
+
readonly element: readonly ConceptMapElement[];
|
|
687
|
+
/** The `group.unmapped` fallback directive, when declared. */
|
|
688
|
+
readonly unmapped?: ConceptMapUnmapped;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* A loaded, immutable ConceptMap — the subset of the FHIR R4 `ConceptMap` resource the
|
|
692
|
+
* `$translate` engine needs. Produced by {@link loadConceptMap}; every field frozen.
|
|
693
|
+
*/
|
|
694
|
+
interface ConceptMap {
|
|
695
|
+
/** The resource's canonical `url`, when declared. */
|
|
696
|
+
readonly url?: string;
|
|
697
|
+
/** The resource's business `version`, when declared. */
|
|
698
|
+
readonly version?: string;
|
|
699
|
+
/** The map's `sourceUri`/`sourceCanonical` scope, when declared. */
|
|
700
|
+
readonly sourceScope?: string;
|
|
701
|
+
/** The map's `targetUri`/`targetCanonical` scope, when declared. */
|
|
702
|
+
readonly targetScope?: string;
|
|
703
|
+
/** The translation groups. */
|
|
704
|
+
readonly group: readonly ConceptMapGroup[];
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* {@link loadConceptMap} — validate and normalize an untrusted FHIR R4 `ConceptMap` JSON resource
|
|
709
|
+
* into the immutable {@link ConceptMap} model the `$translate` engine runs over.
|
|
710
|
+
*
|
|
711
|
+
* **Conservative on load** (unlike a wire parser's lenient posture): a ConceptMap drives a clinical
|
|
712
|
+
* translation, so a structurally unusable map is a typed **fatal**
|
|
713
|
+
* ({@link FATAL_CODES.TERM_CONCEPTMAP_MALFORMED}), never a silently partial map that would translate
|
|
714
|
+
* *some* codes and quietly drop others. Fault messages are **value-free** — they name the resource
|
|
715
|
+
* *path* and the fault, never echo a code or display *value*.
|
|
716
|
+
*
|
|
717
|
+
* @packageDocumentation
|
|
718
|
+
*/
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Load an untrusted FHIR R4 `ConceptMap` JSON value into an immutable {@link ConceptMap}.
|
|
722
|
+
*
|
|
723
|
+
* Accepts the standard resource shape: a top-level object with `resourceType: "ConceptMap"` and an
|
|
724
|
+
* optional `group[]`. `source[x]`/`target[x]` scopes are read from either the `Uri` or `Canonical`
|
|
725
|
+
* variant. The result is deep-frozen. Anything structurally unusable throws a
|
|
726
|
+
* {@link TerminologyError} carrying {@link FATAL_CODES.TERM_CONCEPTMAP_MALFORMED}.
|
|
727
|
+
*
|
|
728
|
+
* @param json - The untrusted resource (typically `JSON.parse` output — hence `unknown`).
|
|
729
|
+
* @returns The immutable, validated {@link ConceptMap}.
|
|
730
|
+
* @throws {TerminologyError} `TERM_CONCEPTMAP_MALFORMED` when the resource is not a usable ConceptMap.
|
|
731
|
+
* @example
|
|
732
|
+
* ```ts
|
|
733
|
+
* import { loadConceptMap } from "@cosyte/terminology";
|
|
734
|
+
*
|
|
735
|
+
* const map = loadConceptMap({
|
|
736
|
+
* resourceType: "ConceptMap",
|
|
737
|
+
* url: "http://example.org/cm/gender",
|
|
738
|
+
* group: [
|
|
739
|
+
* {
|
|
740
|
+
* source: "http://hl7.org/fhir/administrative-gender",
|
|
741
|
+
* target: "http://terminology.hl7.org/CodeSystem/v2-0001",
|
|
742
|
+
* element: [{ code: "male", target: [{ code: "M", equivalence: "equivalent" }] }],
|
|
743
|
+
* },
|
|
744
|
+
* ],
|
|
745
|
+
* });
|
|
746
|
+
* map.group.length; // => 1
|
|
747
|
+
* ```
|
|
748
|
+
*/
|
|
749
|
+
declare function loadConceptMap(json: unknown): ConceptMap;
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* {@link translate} — the ConceptMap `$translate` engine, modeled on the FHIR
|
|
753
|
+
* `ConceptMap/$translate` operation.
|
|
754
|
+
*
|
|
755
|
+
* Given a source {@link Coding} and a loaded {@link ConceptMap}, it returns the declared target
|
|
756
|
+
* code(s) with their FHIR relationship and the map's provenance — or, when the source does not map,
|
|
757
|
+
* a **typed {@link TranslateUnmapped}**. This slice births the invariants every later phase
|
|
758
|
+
* inherits:
|
|
759
|
+
*
|
|
760
|
+
* 1. **Never fabricate.** An unmapped source yields a surfaced `unmapped` result — never a guessed
|
|
761
|
+
* target. Every target returned is drawn verbatim from the map; the engine invents nothing.
|
|
762
|
+
* 2. **Never invert.** Translation reads the map in its authored direction only: a source coding is
|
|
763
|
+
* matched against `group.element.code` (the source side) and **never** against target codes. A
|
|
764
|
+
* directional map therefore cannot be run backwards — reverse translation requires an explicit
|
|
765
|
+
* inverse map (a later phase), it is never synthesized here.
|
|
766
|
+
*
|
|
767
|
+
* @packageDocumentation
|
|
768
|
+
*/
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Translate a source {@link Coding} through a loaded {@link ConceptMap}.
|
|
772
|
+
*
|
|
773
|
+
* Matches the coding's `code` against each applicable group's `element.code` (the **source** side —
|
|
774
|
+
* never targets, so the map is never inverted) and returns every declared target for it. A target
|
|
775
|
+
* with an `unmatched` equivalence, or with no `code`, is treated as "no target" — the FHIR way of
|
|
776
|
+
* asserting a non-mapping — and does not count as a match. When nothing matches, the map author's
|
|
777
|
+
* `group.unmapped` directive (if any) is *reported* via the result's {@link UnmappedMode} but never
|
|
778
|
+
* silently applied.
|
|
779
|
+
*
|
|
780
|
+
* @param sourceCoding - The source concept to translate.
|
|
781
|
+
* @param map - A {@link ConceptMap} from {@link loadConceptMap}.
|
|
782
|
+
* @returns A {@link TranslateResult} — matched targets, or a typed unmapped outcome.
|
|
783
|
+
* @example
|
|
784
|
+
* ```ts
|
|
785
|
+
* import { loadConceptMap, translate } from "@cosyte/terminology";
|
|
786
|
+
*
|
|
787
|
+
* const map = loadConceptMap({
|
|
788
|
+
* resourceType: "ConceptMap",
|
|
789
|
+
* group: [
|
|
790
|
+
* {
|
|
791
|
+
* source: "http://hl7.org/fhir/administrative-gender",
|
|
792
|
+
* target: "http://terminology.hl7.org/CodeSystem/v2-0001",
|
|
793
|
+
* element: [{ code: "male", target: [{ code: "M", equivalence: "equivalent" }] }],
|
|
794
|
+
* },
|
|
795
|
+
* ],
|
|
796
|
+
* });
|
|
797
|
+
* const r = translate(
|
|
798
|
+
* { system: "http://hl7.org/fhir/administrative-gender", code: "male" },
|
|
799
|
+
* map,
|
|
800
|
+
* );
|
|
801
|
+
* r.unmapped; // => false
|
|
802
|
+
* ```
|
|
803
|
+
*/
|
|
804
|
+
declare function translate(sourceCoding: Coding, map: ConceptMap): TranslateResult;
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* The types for the **CodeSystem load layer** and the FHIR `$lookup` / `$validate-code` operations
|
|
808
|
+
* (roadmap Phase 2): the immutable {@link CodeSystem} model, the per-concept {@link ConceptStatus},
|
|
809
|
+
* and the discriminated {@link CodeSystemSource} union the loaders accept.
|
|
810
|
+
*
|
|
811
|
+
* The model is **content-agnostic**: it is fed a code system's *release* (a consumer-supplied RRF,
|
|
812
|
+
* CSV, fixed-width file, or FHIR `CodeSystem` JSON) and answers the two identity questions. It ships
|
|
813
|
+
* **zero copyrighted content** — every concept comes from the caller's data (roadmap §5, the BYO-data
|
|
814
|
+
* posture). The FHIR operation shapes are grounded firsthand on the FHIR R4 Terminology Module
|
|
815
|
+
* (`https://hl7.org/fhir/R4/codesystem-operation-lookup.html`,
|
|
816
|
+
* `https://hl7.org/fhir/R4/codesystem-operation-validate-code.html`).
|
|
817
|
+
*
|
|
818
|
+
* @packageDocumentation
|
|
819
|
+
*/
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* A concept's normalized activity — whether it is a clean, current, usable code, or carries a
|
|
823
|
+
* steward status that a caller must not ignore. `active` is the boolean convenience derived from it.
|
|
824
|
+
*
|
|
825
|
+
* - `active` — current and usable.
|
|
826
|
+
* - `deprecated` — no longer current (LOINC `DEPRECATED`; a FHIR `inactive`/`deprecated` concept).
|
|
827
|
+
* - `discouraged` / `trial` — LOINC lifecycle states (usable, but not settled).
|
|
828
|
+
* - `obsolete` — withdrawn (RxNorm `SUPPRESS = O`).
|
|
829
|
+
* - `suppressed` — editor-suppressed (RxNorm/UMLS `SUPPRESS = Y`/`E`).
|
|
830
|
+
* - `header` — a classification header, **not a billable/valid leaf** (ICD-10-CM flag `0`).
|
|
831
|
+
* - `unknown` — a status token the mapper did not recognize; carried verbatim, never guessed clean.
|
|
832
|
+
*/
|
|
833
|
+
type ConceptActivity = "active" | "deprecated" | "discouraged" | "trial" | "obsolete" | "suppressed" | "header" | "unknown";
|
|
834
|
+
/**
|
|
835
|
+
* The status flags a `$lookup`/`$validate-code` carries on a concept — the safety surface that stops
|
|
836
|
+
* a non-current or non-billable code from being presented as clean (roadmap §4.4). Present only when
|
|
837
|
+
* the loaded release supplied status information; the engine never fabricates a status.
|
|
838
|
+
*/
|
|
839
|
+
interface ConceptStatus {
|
|
840
|
+
/** The normalized {@link ConceptActivity}. */
|
|
841
|
+
readonly activity: ConceptActivity;
|
|
842
|
+
/** Convenience: `activity === "active"`. A caller can gate on this without a switch. */
|
|
843
|
+
readonly active: boolean;
|
|
844
|
+
/** For classification systems (ICD-10-CM): is this a billable/valid leaf? A header is `false`. */
|
|
845
|
+
readonly billable?: boolean;
|
|
846
|
+
/** The steward's raw status token, carried verbatim (e.g. `"DEPRECATED"`, `"O"`, `"0"`). */
|
|
847
|
+
readonly raw?: string;
|
|
848
|
+
/** A stable {@link DiagnosticCode} naming the concern, when the concept is not clean-active. */
|
|
849
|
+
readonly code?: DiagnosticCode;
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* One property value on a concept — models FHIR R4 `$lookup` `property` ("one or more properties
|
|
853
|
+
* that contain additional information about the code, including status"). Carried verbatim from the
|
|
854
|
+
* release; never fabricated.
|
|
855
|
+
*/
|
|
856
|
+
interface Property {
|
|
857
|
+
/** Identifies the property (FHIR `property.code`), e.g. `"shortDescription"`, `"TTY"`, `"parent"`. */
|
|
858
|
+
readonly code: string;
|
|
859
|
+
/** The property value (FHIR `property.value[x]`), reduced to the primitive kinds the readers emit. */
|
|
860
|
+
readonly value: string | boolean | number;
|
|
861
|
+
/** Optional human-readable rendering of the value (FHIR `property.description`). */
|
|
862
|
+
readonly description?: string;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* One loaded, immutable concept — the subset of a `CodeSystem.concept` the operations need.
|
|
866
|
+
*/
|
|
867
|
+
interface Concept {
|
|
868
|
+
/** The code, exactly as it appears in the release. */
|
|
869
|
+
readonly code: string;
|
|
870
|
+
/** The preferred display, carried verbatim. Absent when the release supplied none (never invented). */
|
|
871
|
+
readonly display?: string;
|
|
872
|
+
/** The formal definition, when the release supplied one. */
|
|
873
|
+
readonly definition?: string;
|
|
874
|
+
/** The concept's status flags, when the release carried status information. */
|
|
875
|
+
readonly status?: ConceptStatus;
|
|
876
|
+
/** Additional properties, in load order. Frozen; may be empty, never fabricated. */
|
|
877
|
+
readonly properties: readonly Property[];
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* A surfaced, non-fatal load warning — a malformed row that was **skipped**, reported with its line
|
|
881
|
+
* number and a **value-free** structural reason (never the row's content). Liberal on load.
|
|
882
|
+
*/
|
|
883
|
+
interface LoadWarning {
|
|
884
|
+
/** The stable diagnostic code (`TERM_RRF_MALFORMED_ROW` / `_CSV_MALFORMED` / `_FIXED_WIDTH_MALFORMED`). */
|
|
885
|
+
readonly code: DiagnosticCode;
|
|
886
|
+
/** The 1-based source line the fault was found on (positional context; value-free). */
|
|
887
|
+
readonly line: number;
|
|
888
|
+
/** A value-free description of the structural fault (never echoes a field value). */
|
|
889
|
+
readonly detail: string;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* A loaded, immutable code-system release — the queryable model {@link loadCodeSystem} produces and
|
|
893
|
+
* {@link lookup} / {@link validateCode} run over. Concepts are keyed by code for O(1) identity; the
|
|
894
|
+
* map is exposed `readonly` and every concept is frozen.
|
|
895
|
+
*/
|
|
896
|
+
interface CodeSystem {
|
|
897
|
+
/** The code system's canonical URI, when supplied by the source. */
|
|
898
|
+
readonly url?: string;
|
|
899
|
+
/** The release version, when supplied — mappings and displays are release-scoped. */
|
|
900
|
+
readonly version?: string;
|
|
901
|
+
/** A short human-readable name, when supplied. */
|
|
902
|
+
readonly name?: string;
|
|
903
|
+
/** The number of loaded concepts. */
|
|
904
|
+
readonly count: number;
|
|
905
|
+
/** The concepts, keyed by `code`. Read-only; every value frozen. */
|
|
906
|
+
readonly concepts: ReadonlyMap<string, Concept>;
|
|
907
|
+
/** The skipped-row warnings surfaced during load, in line order. Frozen; may be empty. */
|
|
908
|
+
readonly warnings: readonly LoadWarning[];
|
|
909
|
+
}
|
|
910
|
+
/** A successful {@link lookup}: the concept's details plus its release provenance. */
|
|
911
|
+
interface LookupResult {
|
|
912
|
+
/** Discriminant: the code was found. */
|
|
913
|
+
readonly found: true;
|
|
914
|
+
/** The code, echoed. */
|
|
915
|
+
readonly code: string;
|
|
916
|
+
/** The preferred display, when the release carried one (never fabricated). */
|
|
917
|
+
readonly display?: string;
|
|
918
|
+
/** The formal definition, when present. */
|
|
919
|
+
readonly definition?: string;
|
|
920
|
+
/** The concept's status flags, when present. */
|
|
921
|
+
readonly status?: ConceptStatus;
|
|
922
|
+
/** The concept's properties, in load order (frozen, may be empty). */
|
|
923
|
+
readonly properties: readonly Property[];
|
|
924
|
+
/** The code system's canonical URI, when known. */
|
|
925
|
+
readonly system?: string;
|
|
926
|
+
/** The release version, when known. */
|
|
927
|
+
readonly version?: string;
|
|
928
|
+
}
|
|
929
|
+
/** A **fail-safe unknown** {@link lookup} — the never-fabricate outcome; no display is ever guessed. */
|
|
930
|
+
interface LookupUnknown {
|
|
931
|
+
/** Discriminant: the code was not found. */
|
|
932
|
+
readonly found: false;
|
|
933
|
+
/** The stable diagnostic code for an unknown code. */
|
|
934
|
+
readonly code: "TERM_CODE_UNKNOWN";
|
|
935
|
+
/** The code that was looked up, echoed so the caller can surface it. */
|
|
936
|
+
readonly input: string;
|
|
937
|
+
}
|
|
938
|
+
/** The result of {@link lookup}: a found concept or a typed {@link LookupUnknown}. */
|
|
939
|
+
type LookupOutcome = LookupResult | LookupUnknown;
|
|
940
|
+
/**
|
|
941
|
+
* The result of {@link validateCode} — models FHIR R4 `$validate-code`'s `result` boolean. A found
|
|
942
|
+
* concept is `valid: true` **carrying its status** (a deprecated or header code validates as present
|
|
943
|
+
* but is flagged, never presented clean); an absent code is `valid: false`, never a guessed `true`.
|
|
944
|
+
*/
|
|
945
|
+
interface ValidateCodeResult {
|
|
946
|
+
/** FHIR `result`: true if the code is a valid member of the code system. */
|
|
947
|
+
readonly valid: boolean;
|
|
948
|
+
/** The concept's status flags, when the code was found and the release carried status. */
|
|
949
|
+
readonly status?: ConceptStatus;
|
|
950
|
+
/** For an invalid code, the stable diagnostic (`TERM_CODE_UNKNOWN`); absent when valid. */
|
|
951
|
+
readonly code?: DiagnosticCode;
|
|
952
|
+
}
|
|
953
|
+
/** A mapper from a raw steward status token to a normalized {@link ConceptActivity}. Overridable. */
|
|
954
|
+
type StatusMapper = (raw: string) => ConceptActivity;
|
|
955
|
+
/** A half-open `[start, end)` slice of a fixed-width line (0-based char indices; `end` optional). */
|
|
956
|
+
interface FieldSlice {
|
|
957
|
+
/** The 0-based start char index (inclusive). */
|
|
958
|
+
readonly start: number;
|
|
959
|
+
/** The 0-based end char index (exclusive). Omit to slice to end of line. */
|
|
960
|
+
readonly end?: number;
|
|
961
|
+
}
|
|
962
|
+
/** One extra RRF column to capture as a {@link Property}. */
|
|
963
|
+
interface RrfPropertyColumn {
|
|
964
|
+
/** The property code to emit. */
|
|
965
|
+
readonly name: string;
|
|
966
|
+
/** The 0-based column index. */
|
|
967
|
+
readonly column: number;
|
|
968
|
+
}
|
|
969
|
+
/** The column mapping for an {@link RrfSource} (0-based column indices into the pipe-split row). */
|
|
970
|
+
interface RrfColumnMap {
|
|
971
|
+
/** The column holding the concept code (e.g. RxNorm `RXCUI` = 0, or `CODE` = 13). */
|
|
972
|
+
readonly code: number;
|
|
973
|
+
/** The column holding the display string (e.g. `STR` = 14). */
|
|
974
|
+
readonly display: number;
|
|
975
|
+
/** The column holding a status/suppress token (e.g. `SUPPRESS` = 16), when present. */
|
|
976
|
+
readonly status?: number;
|
|
977
|
+
/** Extra columns to surface as properties. */
|
|
978
|
+
readonly properties?: readonly RrfPropertyColumn[];
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* An **RRF** (Rich Release Format — pipe-delimited RxNorm/UMLS release) source. One parameterized
|
|
982
|
+
* reader serves both: RxNorm `RXNCONSO`/`RXNSAT` and UMLS `MRCONSO`/`MRSAT` share the RRF shape
|
|
983
|
+
* (pipe-delimited, one trailing pipe per line). BYO — the consumer supplies the file.
|
|
984
|
+
*/
|
|
985
|
+
interface RrfSource {
|
|
986
|
+
/** Discriminant. */
|
|
987
|
+
readonly format: "rrf";
|
|
988
|
+
/** The raw RRF file content. */
|
|
989
|
+
readonly content: string;
|
|
990
|
+
/** Which columns hold the code/display/status/properties. */
|
|
991
|
+
readonly columns: RrfColumnMap;
|
|
992
|
+
/** The code system's canonical URI, attached to the loaded release. */
|
|
993
|
+
readonly url?: string;
|
|
994
|
+
/** A human-readable name. */
|
|
995
|
+
readonly name?: string;
|
|
996
|
+
/** The release version (RRF carries none inline — supply it so displays stay release-scoped). */
|
|
997
|
+
readonly version?: string;
|
|
998
|
+
/** Override the default status interpretation of the `status` column's raw token. */
|
|
999
|
+
readonly statusMap?: StatusMapper;
|
|
1000
|
+
}
|
|
1001
|
+
/** The column mapping for a {@link CsvSource} (header names, matched against the header row). */
|
|
1002
|
+
interface CsvColumnMap {
|
|
1003
|
+
/** The header name of the code column (e.g. LOINC `LOINC_NUM`). */
|
|
1004
|
+
readonly code: string;
|
|
1005
|
+
/** The header name of the display column (e.g. LOINC `LONG_COMMON_NAME`). */
|
|
1006
|
+
readonly display: string;
|
|
1007
|
+
/** The header name of a status column (e.g. LOINC `STATUS`), when present. */
|
|
1008
|
+
readonly status?: string;
|
|
1009
|
+
/** Header names to surface as properties. */
|
|
1010
|
+
readonly properties?: readonly string[];
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* An **RFC-4180 CSV** source (quoted fields, embedded commas/quotes/newlines) — e.g. LOINC's
|
|
1014
|
+
* `Loinc.csv`. The one release format that needs real quote handling; hand-rolled, zero-dep. BYO.
|
|
1015
|
+
*/
|
|
1016
|
+
interface CsvSource {
|
|
1017
|
+
/** Discriminant. */
|
|
1018
|
+
readonly format: "csv";
|
|
1019
|
+
/** The raw CSV file content. */
|
|
1020
|
+
readonly content: string;
|
|
1021
|
+
/** Which header-named columns hold the code/display/status/properties. */
|
|
1022
|
+
readonly columns: CsvColumnMap;
|
|
1023
|
+
/** The code system's canonical URI. */
|
|
1024
|
+
readonly url?: string;
|
|
1025
|
+
/** A human-readable name. */
|
|
1026
|
+
readonly name?: string;
|
|
1027
|
+
/** The release version. */
|
|
1028
|
+
readonly version?: string;
|
|
1029
|
+
/** Override the default status interpretation of the `status` column's raw token. */
|
|
1030
|
+
readonly statusMap?: StatusMapper;
|
|
1031
|
+
}
|
|
1032
|
+
/** The billable-flag descriptor for a {@link FixedWidthFieldMap}. */
|
|
1033
|
+
interface BillableFlag {
|
|
1034
|
+
/** The 0-based char index of the flag. */
|
|
1035
|
+
readonly at: number;
|
|
1036
|
+
/** The character value that means "billable/valid leaf" (ICD-10-CM: `"1"`; `"0"` is a header). */
|
|
1037
|
+
readonly billableValue: string;
|
|
1038
|
+
}
|
|
1039
|
+
/** One extra fixed-width field to capture as a {@link Property}. */
|
|
1040
|
+
interface FixedWidthPropertyField {
|
|
1041
|
+
/** The property code to emit. */
|
|
1042
|
+
readonly name: string;
|
|
1043
|
+
/** The field slice. */
|
|
1044
|
+
readonly slice: FieldSlice;
|
|
1045
|
+
}
|
|
1046
|
+
/** The field mapping for a {@link FixedWidthSource} (0-based char slices). */
|
|
1047
|
+
interface FixedWidthFieldMap {
|
|
1048
|
+
/** The slice holding the concept code. */
|
|
1049
|
+
readonly code: FieldSlice;
|
|
1050
|
+
/** The slice holding the display string. */
|
|
1051
|
+
readonly display: FieldSlice;
|
|
1052
|
+
/** The billable/header flag (ICD-10-CM position-15), when the format has one. */
|
|
1053
|
+
readonly billable?: BillableFlag;
|
|
1054
|
+
/** A slice holding a status token, when the format has one (alternative to `billable`). */
|
|
1055
|
+
readonly status?: FieldSlice;
|
|
1056
|
+
/** Extra slices to surface as properties. */
|
|
1057
|
+
readonly properties?: readonly FixedWidthPropertyField[];
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* A **fixed-width** source (slice by column) — e.g. the ICD-10-CM order file. Fully parameterized:
|
|
1061
|
+
* the consumer supplies the field slices (see {@link ICD10CM_ORDER_FILE_FIELDS} for the documented
|
|
1062
|
+
* ICD-10-CM preset, which they confirm against their release's README — the byte offsets are a
|
|
1063
|
+
* flagged open question in the roadmap). BYO.
|
|
1064
|
+
*/
|
|
1065
|
+
interface FixedWidthSource {
|
|
1066
|
+
/** Discriminant. */
|
|
1067
|
+
readonly format: "fixed-width";
|
|
1068
|
+
/** The raw fixed-width file content. */
|
|
1069
|
+
readonly content: string;
|
|
1070
|
+
/** Which char slices hold the code/display/billable/status/properties. */
|
|
1071
|
+
readonly fields: FixedWidthFieldMap;
|
|
1072
|
+
/** The code system's canonical URI. */
|
|
1073
|
+
readonly url?: string;
|
|
1074
|
+
/** A human-readable name. */
|
|
1075
|
+
readonly name?: string;
|
|
1076
|
+
/** The release version. */
|
|
1077
|
+
readonly version?: string;
|
|
1078
|
+
/** Override the default status interpretation of the `status` slice's raw token. */
|
|
1079
|
+
readonly statusMap?: StatusMapper;
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* A **FHIR R4 `CodeSystem`** JSON source (native, license-clean model). The consumer supplies the
|
|
1083
|
+
* untrusted resource; nested `concept` hierarchies are flattened, and the standard `inactive` /
|
|
1084
|
+
* `deprecated` concept properties are read into {@link ConceptStatus}.
|
|
1085
|
+
*/
|
|
1086
|
+
interface FhirCodeSystemSource {
|
|
1087
|
+
/** Discriminant. */
|
|
1088
|
+
readonly format: "fhir";
|
|
1089
|
+
/** The untrusted FHIR `CodeSystem` resource (typically `JSON.parse` output — hence `unknown`). */
|
|
1090
|
+
readonly resource: unknown;
|
|
1091
|
+
}
|
|
1092
|
+
/** The discriminated union {@link loadCodeSystem} accepts. */
|
|
1093
|
+
type CodeSystemSource = RrfSource | CsvSource | FixedWidthSource | FhirCodeSystemSource;
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* {@link loadCodeSystem} — the single entry point that dispatches a {@link CodeSystemSource} to the
|
|
1097
|
+
* right reader (RRF / CSV / fixed-width / FHIR) and assembles the immutable {@link CodeSystem}.
|
|
1098
|
+
*
|
|
1099
|
+
* The engine is **content-agnostic and license-clean**: it loads a *consumer-supplied* release and
|
|
1100
|
+
* ships **zero** copyrighted content (roadmap §5). The loaded model is deep-frozen and keyed by code
|
|
1101
|
+
* for O(1) {@link lookup}/{@link validateCode}.
|
|
1102
|
+
*
|
|
1103
|
+
* @packageDocumentation
|
|
1104
|
+
*/
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Load a consumer-supplied code-system release into an immutable, queryable {@link CodeSystem}.
|
|
1108
|
+
*
|
|
1109
|
+
* Dispatches on `source.format`:
|
|
1110
|
+
* - `"rrf"` — pipe-delimited RxNorm/UMLS release (one parameterized reader for both).
|
|
1111
|
+
* - `"csv"` — RFC-4180 CSV (e.g. LOINC `Loinc.csv`), with real quote handling.
|
|
1112
|
+
* - `"fixed-width"` — slice-by-column order files (e.g. ICD-10-CM; see `ICD10CM_ORDER_FILE_FIELDS`).
|
|
1113
|
+
* - `"fhir"` — a native FHIR R4 `CodeSystem` JSON resource.
|
|
1114
|
+
*
|
|
1115
|
+
* **Liberal on load:** a malformed *row* is skipped and surfaced in `warnings` (never a crash, never a
|
|
1116
|
+
* partial concept). **Conservative on structure:** an unusable *source* (wrong FHIR `resourceType`, or
|
|
1117
|
+
* a CSV whose configured columns are absent from the header) throws a typed `TerminologyError`
|
|
1118
|
+
* carrying `TERM_CODESYSTEM_MALFORMED`.
|
|
1119
|
+
*
|
|
1120
|
+
* @param source - The release descriptor (a discriminated {@link CodeSystemSource}).
|
|
1121
|
+
* @returns The immutable, deep-frozen {@link CodeSystem}.
|
|
1122
|
+
* @throws {TerminologyError} `TERM_CODESYSTEM_MALFORMED` when the source is structurally unusable.
|
|
1123
|
+
* @example
|
|
1124
|
+
* ```ts
|
|
1125
|
+
* import { loadCodeSystem } from "@cosyte/terminology";
|
|
1126
|
+
*
|
|
1127
|
+
* const cs = loadCodeSystem({
|
|
1128
|
+
* format: "fhir",
|
|
1129
|
+
* resource: {
|
|
1130
|
+
* resourceType: "CodeSystem",
|
|
1131
|
+
* url: "http://example.org/cs",
|
|
1132
|
+
* concept: [{ code: "A", display: "Alpha" }],
|
|
1133
|
+
* },
|
|
1134
|
+
* });
|
|
1135
|
+
* cs.count; // => 1
|
|
1136
|
+
* ```
|
|
1137
|
+
*/
|
|
1138
|
+
declare function loadCodeSystem(source: CodeSystemSource): CodeSystem;
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* {@link lookup} and {@link validateCode} — the FHIR R4 CodeSystem `$lookup` and `$validate-code`
|
|
1142
|
+
* operations over a loaded {@link CodeSystem}.
|
|
1143
|
+
*
|
|
1144
|
+
* Grounded firsthand on FHIR R4 (`https://hl7.org/fhir/R4/codesystem-operation-lookup.html`,
|
|
1145
|
+
* `https://hl7.org/fhir/R4/codesystem-operation-validate-code.html`):
|
|
1146
|
+
*
|
|
1147
|
+
* - `$lookup` returns the concept's `display` (*"The preferred display for this concept"*) and its
|
|
1148
|
+
* `property` values (*"additional information about the code, including status"*).
|
|
1149
|
+
* - `$validate-code` returns `result` (*"True if the concept details supplied are valid"*).
|
|
1150
|
+
*
|
|
1151
|
+
* Both honor the **never-fabricate invariant** born in Phase 1, now applied to code identity: an
|
|
1152
|
+
* unknown code yields a typed `unknown` / `valid: false` — **never** a fabricated display and
|
|
1153
|
+
* **never** a guessed `valid: true`. A found-but-non-current concept (deprecated, or an ICD-10-CM
|
|
1154
|
+
* header) validates as present **but carries its status flag**, so a caller never treats it as clean.
|
|
1155
|
+
*
|
|
1156
|
+
* @packageDocumentation
|
|
1157
|
+
*/
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* `$lookup`: resolve a code to its display, definition, status, and properties in a loaded release.
|
|
1161
|
+
*
|
|
1162
|
+
* @param cs - The loaded {@link CodeSystem}.
|
|
1163
|
+
* @param code - The code to look up.
|
|
1164
|
+
* @returns A {@link LookupResult} when found, or a typed {@link LookupUnknown} — never a guess.
|
|
1165
|
+
* @example
|
|
1166
|
+
* ```ts
|
|
1167
|
+
* import { loadCodeSystem, lookup } from "@cosyte/terminology";
|
|
1168
|
+
*
|
|
1169
|
+
* const cs = loadCodeSystem({
|
|
1170
|
+
* format: "fhir",
|
|
1171
|
+
* resource: { resourceType: "CodeSystem", concept: [{ code: "A", display: "Alpha" }] },
|
|
1172
|
+
* });
|
|
1173
|
+
* const r = lookup(cs, "A");
|
|
1174
|
+
* r.found === true && r.display; // => "Alpha"
|
|
1175
|
+
* ```
|
|
1176
|
+
*/
|
|
1177
|
+
declare function lookup(cs: CodeSystem, code: string): LookupOutcome;
|
|
1178
|
+
/**
|
|
1179
|
+
* `$validate-code`: is `code` a valid member of the loaded code system?
|
|
1180
|
+
*
|
|
1181
|
+
* A found code is `valid: true`, **carrying its status** (a deprecated or header code is present but
|
|
1182
|
+
* flagged — never silently clean). An absent code is `valid: false` with `TERM_CODE_UNKNOWN`, never a
|
|
1183
|
+
* guessed `true`.
|
|
1184
|
+
*
|
|
1185
|
+
* @param cs - The loaded {@link CodeSystem}.
|
|
1186
|
+
* @param code - The code to validate.
|
|
1187
|
+
* @returns A {@link ValidateCodeResult}.
|
|
1188
|
+
* @example
|
|
1189
|
+
* ```ts
|
|
1190
|
+
* import { loadCodeSystem, validateCode } from "@cosyte/terminology";
|
|
1191
|
+
*
|
|
1192
|
+
* const cs = loadCodeSystem({
|
|
1193
|
+
* format: "fhir",
|
|
1194
|
+
* resource: { resourceType: "CodeSystem", concept: [{ code: "A", display: "Alpha" }] },
|
|
1195
|
+
* });
|
|
1196
|
+
* validateCode(cs, "A").valid; // => true
|
|
1197
|
+
* validateCode(cs, "ZZZ").valid; // => false
|
|
1198
|
+
* ```
|
|
1199
|
+
*/
|
|
1200
|
+
declare function validateCode(cs: CodeSystem, code: string): ValidateCodeResult;
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* The **RRF** (Rich Release Format) reader — the pipe-delimited layer shared by RxNorm (`RXNCONSO`/
|
|
1204
|
+
* `RXNSAT`) and UMLS (`MRCONSO`/`MRSAT`). One parameterized reader serves both: they share the RRF
|
|
1205
|
+
* shape — fields separated by `|`, with **one trailing pipe per line** (grounded firsthand on the
|
|
1206
|
+
* UMLS Reference Manual, `https://www.ncbi.nlm.nih.gov/books/NBK9685/`: *"values … are separated by
|
|
1207
|
+
* vertical bars (|)"* and each row ends with *"a vertical bar and line termination"*).
|
|
1208
|
+
*
|
|
1209
|
+
* **Liberal on load** (roadmap §6): a row too short to reach a configured column, or with an empty
|
|
1210
|
+
* code, is **skipped and surfaced** as a `TERM_RRF_MALFORMED_ROW` warning — never kept as a partial
|
|
1211
|
+
* concept, never a crash. RRF reserves `|` as the delimiter (fields never contain a literal pipe), so
|
|
1212
|
+
* a plain split is exact.
|
|
1213
|
+
*
|
|
1214
|
+
* @packageDocumentation
|
|
1215
|
+
*/
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* Split one RRF line into its fields, dropping the single reserved trailing pipe.
|
|
1219
|
+
*
|
|
1220
|
+
* @param line - One RRF line (no line terminator).
|
|
1221
|
+
* @returns The fields, in column order.
|
|
1222
|
+
* @example
|
|
1223
|
+
* ```ts
|
|
1224
|
+
* import { parseRrfLine } from "@cosyte/terminology";
|
|
1225
|
+
*
|
|
1226
|
+
* parseRrfLine("314076|ENG|lisinopril 10 MG|"); // => ["314076", "ENG", "lisinopril 10 MG"]
|
|
1227
|
+
* ```
|
|
1228
|
+
*/
|
|
1229
|
+
declare function parseRrfLine(line: string): readonly string[];
|
|
1230
|
+
/**
|
|
1231
|
+
* Parse an {@link RrfSource} into concepts (first row per code wins) and skipped-row warnings.
|
|
1232
|
+
*
|
|
1233
|
+
* A code that recurs across rows (RRF repeats a code once per atom/attribute) keeps its **first**
|
|
1234
|
+
* occurrence — a documented v1 limitation: preferred-atom selection (`TS`/`STT`/`ISPREF`) and
|
|
1235
|
+
* attribute merging are later work. The result is data only; {@link loadCodeSystem} freezes it.
|
|
1236
|
+
*
|
|
1237
|
+
* @param source - The RRF source.
|
|
1238
|
+
* @returns The parsed concepts (by code) and the load warnings.
|
|
1239
|
+
* @example
|
|
1240
|
+
* ```ts
|
|
1241
|
+
* import { parseRrf } from "@cosyte/terminology";
|
|
1242
|
+
*
|
|
1243
|
+
* const { concepts } = parseRrf({
|
|
1244
|
+
* format: "rrf",
|
|
1245
|
+
* content: "1|Aspirin|",
|
|
1246
|
+
* columns: { code: 0, display: 1 },
|
|
1247
|
+
* });
|
|
1248
|
+
* concepts.get("1")?.display; // => "Aspirin"
|
|
1249
|
+
* ```
|
|
1250
|
+
*/
|
|
1251
|
+
declare function parseRrf(source: RrfSource): {
|
|
1252
|
+
concepts: Map<string, Concept>;
|
|
1253
|
+
warnings: LoadWarning[];
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* The **RFC-4180 CSV** reader — the one release format that needs real quote handling (LOINC's
|
|
1258
|
+
* `Loinc.csv`). Fields may be quoted with `"`; inside a quoted field a literal quote is escaped as
|
|
1259
|
+
* `""`, and commas and newlines are data. Hand-rolled, zero-dep (roadmap §1: the CSV surface is small
|
|
1260
|
+
* and well-specified enough to hand-roll rather than pull a dependency).
|
|
1261
|
+
*
|
|
1262
|
+
* **Liberal on load:** a data row with too few fields to reach a configured column is **skipped and
|
|
1263
|
+
* surfaced** as a `TERM_CSV_MALFORMED` warning; an unterminated final quote is surfaced too — never a
|
|
1264
|
+
* crash. A configured code/display header that is **absent from the header row** is a *fatal*
|
|
1265
|
+
* (`TERM_CODESYSTEM_MALFORMED`): the source config does not match the file, so nothing loads cleanly.
|
|
1266
|
+
*
|
|
1267
|
+
* @packageDocumentation
|
|
1268
|
+
*/
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* Parse RFC-4180 CSV content into rows of fields (the `.rows` of {@link parseCsvRows}). Lenient: a
|
|
1272
|
+
* stray mid-field quote is literal, so a malformed quote never silently swallows subsequent rows.
|
|
1273
|
+
*
|
|
1274
|
+
* @param content - The raw CSV text.
|
|
1275
|
+
* @returns The rows, each an array of field strings (untrimmed).
|
|
1276
|
+
* @example
|
|
1277
|
+
* ```ts
|
|
1278
|
+
* import { parseCsv } from "@cosyte/terminology";
|
|
1279
|
+
*
|
|
1280
|
+
* parseCsv('a,b\n"x,y","z""q"'); // => [["a", "b"], ["x,y", 'z"q']]
|
|
1281
|
+
* ```
|
|
1282
|
+
*/
|
|
1283
|
+
declare function parseCsv(content: string): string[][];
|
|
1284
|
+
/**
|
|
1285
|
+
* Parse a {@link CsvSource} into concepts and skipped-row warnings.
|
|
1286
|
+
*
|
|
1287
|
+
* @param source - The CSV source.
|
|
1288
|
+
* @returns The parsed concepts (by code) and the load warnings.
|
|
1289
|
+
* @throws {TerminologyError} `TERM_CODESYSTEM_MALFORMED` when the file has no header row, or a
|
|
1290
|
+
* configured `code`/`display`/`status`/property column is absent from the header.
|
|
1291
|
+
* @example
|
|
1292
|
+
* ```ts
|
|
1293
|
+
* import { parseCsvSource } from "@cosyte/terminology";
|
|
1294
|
+
*
|
|
1295
|
+
* const { concepts } = parseCsvSource({
|
|
1296
|
+
* format: "csv",
|
|
1297
|
+
* content: "CODE,NAME\n2160-0,Creatinine",
|
|
1298
|
+
* columns: { code: "CODE", display: "NAME" },
|
|
1299
|
+
* });
|
|
1300
|
+
* concepts.get("2160-0")?.display; // => "Creatinine"
|
|
1301
|
+
* ```
|
|
1302
|
+
*/
|
|
1303
|
+
declare function parseCsvSource(source: CsvSource): {
|
|
1304
|
+
concepts: Map<string, Concept>;
|
|
1305
|
+
warnings: LoadWarning[];
|
|
1306
|
+
};
|
|
1307
|
+
|
|
1308
|
+
/**
|
|
1309
|
+
* The **fixed-width** reader — slice a line by column, for order-file layouts like the ICD-10-CM
|
|
1310
|
+
* order file (`icd10cm_order_YYYY.txt`). Fully parameterized: the consumer supplies the field slices,
|
|
1311
|
+
* so the *engine* is content-agnostic and the byte offsets live in the caller's config, not baked in.
|
|
1312
|
+
*
|
|
1313
|
+
* The documented ICD-10-CM preset is {@link ICD10CM_ORDER_FILE_FIELDS} — the position-15 flag
|
|
1314
|
+
* (0-based char index 14) is `1` for a **billable/valid** code and `0` for a **header** (grounded on
|
|
1315
|
+
* the CMS/CDC order-file layout). Per the roadmap's flagged open question (§10 #4), a consumer should
|
|
1316
|
+
* confirm the exact offsets against their release's README before relying on the preset — which is
|
|
1317
|
+
* exactly why the reader takes the slices as a parameter.
|
|
1318
|
+
*
|
|
1319
|
+
* **Liberal on load:** a line too short to contain the code slice, or with an empty code, is skipped
|
|
1320
|
+
* and surfaced as a `TERM_FIXED_WIDTH_MALFORMED` warning.
|
|
1321
|
+
*
|
|
1322
|
+
* @packageDocumentation
|
|
1323
|
+
*/
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* Extract and trim a {@link FieldSlice} from a line (`end` omitted slices to end of line).
|
|
1327
|
+
*
|
|
1328
|
+
* @param line - The source line.
|
|
1329
|
+
* @param slice - The half-open `[start, end)` slice.
|
|
1330
|
+
* @returns The trimmed field text (empty when the slice is entirely past the line's end).
|
|
1331
|
+
* @example
|
|
1332
|
+
* ```ts
|
|
1333
|
+
* import { sliceField } from "@cosyte/terminology";
|
|
1334
|
+
*
|
|
1335
|
+
* sliceField("00001 A00 1 Cholera", { start: 6, end: 13 }); // => "A00"
|
|
1336
|
+
* ```
|
|
1337
|
+
*/
|
|
1338
|
+
declare function sliceField(line: string, slice: FieldSlice): string;
|
|
1339
|
+
/**
|
|
1340
|
+
* The documented **ICD-10-CM order file** field layout (0-based char indices).
|
|
1341
|
+
*
|
|
1342
|
+
* - `code` — the ICD-10-CM code (no decimal point).
|
|
1343
|
+
* - `billable` — position-15 flag at index 14: `"1"` = billable/valid leaf, `"0"` = header.
|
|
1344
|
+
* - `display` — the long description (to end of line).
|
|
1345
|
+
* - property `shortDescription` — the 60-char short description.
|
|
1346
|
+
*
|
|
1347
|
+
* Grounded on the CMS/CDC order-file layout; **verify against your release's README** before relying
|
|
1348
|
+
* on the exact offsets (roadmap §10 #4 — the CDC PDF resisted authoritative decode). The reader takes
|
|
1349
|
+
* the layout as a parameter precisely so this preset is a starting point, not a hardcoded assumption.
|
|
1350
|
+
*/
|
|
1351
|
+
declare const ICD10CM_ORDER_FILE_FIELDS: FixedWidthFieldMap;
|
|
1352
|
+
/**
|
|
1353
|
+
* Parse a {@link FixedWidthSource} into concepts and skipped-row warnings.
|
|
1354
|
+
*
|
|
1355
|
+
* @param source - The fixed-width source.
|
|
1356
|
+
* @returns The parsed concepts (by code) and the load warnings.
|
|
1357
|
+
* @example
|
|
1358
|
+
* ```ts
|
|
1359
|
+
* import { parseFixedWidth } from "@cosyte/terminology";
|
|
1360
|
+
*
|
|
1361
|
+
* const { concepts } = parseFixedWidth({
|
|
1362
|
+
* format: "fixed-width",
|
|
1363
|
+
* content: "A00 1 Cholera",
|
|
1364
|
+
* fields: { code: { start: 0, end: 3 }, display: { start: 6 } },
|
|
1365
|
+
* });
|
|
1366
|
+
* concepts.get("A00")?.display; // => "Cholera"
|
|
1367
|
+
* ```
|
|
1368
|
+
*/
|
|
1369
|
+
declare function parseFixedWidth(source: FixedWidthSource): {
|
|
1370
|
+
concepts: Map<string, Concept>;
|
|
1371
|
+
warnings: LoadWarning[];
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
/**
|
|
1375
|
+
* The **FHIR R4 `CodeSystem`** JSON loader — the native, license-clean model. Reads the untrusted
|
|
1376
|
+
* resource into the engine's {@link Concept} map, flattening nested `concept` hierarchies and mapping
|
|
1377
|
+
* the standard concept properties to {@link ConceptStatus}.
|
|
1378
|
+
*
|
|
1379
|
+
* Grounded firsthand on FHIR R4 (`https://hl7.org/fhir/R4/codesystem.html` and the concept-properties
|
|
1380
|
+
* code system, `https://hl7.org/fhir/R4/codesystem-concept-properties.html`): a concept has
|
|
1381
|
+
* `code`/`display`/`definition`/`designation`/`property`/nested `concept`; the base property
|
|
1382
|
+
* **`inactive`** (boolean) is *"True if the concept is not considered active — e.g. not a valid
|
|
1383
|
+
* concept any more"*, **`deprecated`** (dateTime) marks a deprecation, and a `status` string property
|
|
1384
|
+
* is common in real systems.
|
|
1385
|
+
*
|
|
1386
|
+
* **Conservative on load:** the wrong `resourceType` is a *fatal* (`TERM_CODESYSTEM_MALFORMED`); a
|
|
1387
|
+
* concept missing its required `code` is skipped and surfaced (a malformed row, not a whole-file
|
|
1388
|
+
* fatal).
|
|
1389
|
+
*
|
|
1390
|
+
* @packageDocumentation
|
|
1391
|
+
*/
|
|
1392
|
+
|
|
1393
|
+
/**
|
|
1394
|
+
* Parse a {@link FhirCodeSystemSource} into concepts, a resolved url/version/name, and warnings.
|
|
1395
|
+
*
|
|
1396
|
+
* @param source - The FHIR CodeSystem source.
|
|
1397
|
+
* @returns The parsed concepts (by code), the load warnings, and the resource metadata.
|
|
1398
|
+
* @throws {TerminologyError} `TERM_CODESYSTEM_MALFORMED` when the resource is not a FHIR `CodeSystem`.
|
|
1399
|
+
* @example
|
|
1400
|
+
* ```ts
|
|
1401
|
+
* import { parseFhirCodeSystem } from "@cosyte/terminology";
|
|
1402
|
+
*
|
|
1403
|
+
* const { concepts } = parseFhirCodeSystem({
|
|
1404
|
+
* resource: { resourceType: "CodeSystem", concept: [{ code: "A", display: "Alpha" }] },
|
|
1405
|
+
* format: "fhir",
|
|
1406
|
+
* });
|
|
1407
|
+
* concepts.get("A")?.display; // => "Alpha"
|
|
1408
|
+
* ```
|
|
1409
|
+
*/
|
|
1410
|
+
declare function parseFhirCodeSystem(source: FhirCodeSystemSource): {
|
|
1411
|
+
concepts: Map<string, Concept>;
|
|
1412
|
+
warnings: LoadWarning[];
|
|
1413
|
+
url?: string;
|
|
1414
|
+
version?: string;
|
|
1415
|
+
name?: string;
|
|
1416
|
+
};
|
|
1417
|
+
|
|
1418
|
+
/**
|
|
1419
|
+
* Status interpretation shared by the loaders — the {@link defaultStatusMapper} that turns a raw
|
|
1420
|
+
* steward status token into a normalized {@link ConceptActivity}, and {@link makeStatus} which
|
|
1421
|
+
* assembles the frozen {@link ConceptStatus} (attaching the stable diagnostic code for the concerns
|
|
1422
|
+
* the roadmap names: deprecated and header-not-billable).
|
|
1423
|
+
*
|
|
1424
|
+
* The mapper is deliberately **conservative**: it recognizes only the tokens grounded in the format
|
|
1425
|
+
* docs (LOINC `STATUS` lifecycle values, RxNorm/UMLS `SUPPRESS` values — roadmap §4.4/§11), and maps
|
|
1426
|
+
* everything else to `unknown` — a non-active state carrying the raw token, **never** a guessed
|
|
1427
|
+
* clean-active. Every loader lets the consumer override it for a release with bespoke semantics.
|
|
1428
|
+
*
|
|
1429
|
+
* @packageDocumentation
|
|
1430
|
+
*/
|
|
1431
|
+
|
|
1432
|
+
/**
|
|
1433
|
+
* The default raw-token → {@link ConceptActivity} mapping, case-insensitive.
|
|
1434
|
+
*
|
|
1435
|
+
* Recognized (grounded in the format docs): `""`/`N`/`ACTIVE` → active; `DEPRECATED` → deprecated;
|
|
1436
|
+
* `DISCOURAGED`; `TRIAL`; `O`/`OBSOLETE` → obsolete; `Y`/`E`/`SUPPRESSED` → suppressed. Anything else
|
|
1437
|
+
* → `unknown` (surfaced with its raw token, never coerced to active).
|
|
1438
|
+
*
|
|
1439
|
+
* @param raw - The steward's raw status token.
|
|
1440
|
+
* @returns The normalized activity.
|
|
1441
|
+
* @example
|
|
1442
|
+
* ```ts
|
|
1443
|
+
* import { defaultStatusMapper } from "@cosyte/terminology";
|
|
1444
|
+
*
|
|
1445
|
+
* defaultStatusMapper("DEPRECATED"); // => "deprecated"
|
|
1446
|
+
* defaultStatusMapper("N"); // => "active"
|
|
1447
|
+
* ```
|
|
1448
|
+
*/
|
|
1449
|
+
declare const defaultStatusMapper: StatusMapper;
|
|
1450
|
+
/**
|
|
1451
|
+
* Assemble a frozen {@link ConceptStatus} from a normalized activity, carrying the raw token and (for
|
|
1452
|
+
* classification systems) the billable flag, and attaching the stable diagnostic code for the two
|
|
1453
|
+
* concerns the roadmap names — `TERM_CONCEPT_DEPRECATED` and `TERM_CONCEPT_HEADER_NOT_BILLABLE`.
|
|
1454
|
+
*
|
|
1455
|
+
* @param activity - The normalized activity.
|
|
1456
|
+
* @param raw - The verbatim steward token, if any (omitted from the result when empty).
|
|
1457
|
+
* @param billable - The billable flag for a classification system, if applicable.
|
|
1458
|
+
* @returns A frozen {@link ConceptStatus}.
|
|
1459
|
+
* @example
|
|
1460
|
+
* ```ts
|
|
1461
|
+
* import { makeStatus } from "@cosyte/terminology";
|
|
1462
|
+
*
|
|
1463
|
+
* makeStatus("deprecated", "DEPRECATED").code; // => "TERM_CONCEPT_DEPRECATED"
|
|
1464
|
+
* ```
|
|
1465
|
+
*/
|
|
1466
|
+
declare function makeStatus(activity: ConceptActivity, raw?: string, billable?: boolean): ConceptStatus;
|
|
1467
|
+
|
|
1468
|
+
/**
|
|
1469
|
+
* The types for the **ValueSet binding layer** (roadmap Phase 3): the immutable {@link ValueSet}
|
|
1470
|
+
* model, the `compose` component/filter shapes, the {@link ExpansionContext} that supplies the loaded
|
|
1471
|
+
* code systems and referenced value sets, and the fail-safe {@link ExpandResult} /
|
|
1472
|
+
* {@link ValueSetMembership} outcomes.
|
|
1473
|
+
*
|
|
1474
|
+
* The model is **content-agnostic and license-clean**: a `ValueSet` names codes by system URI and is
|
|
1475
|
+
* expanded over **consumer-supplied** {@link CodeSystem} releases — the engine ships **zero** encoded
|
|
1476
|
+
* value-set content (SNOMED/CPT/full-UMLS/VSAC value sets are strictly BYO, roadmap §5). The FHIR
|
|
1477
|
+
* operation shapes are grounded firsthand on the FHIR R4 Terminology Module
|
|
1478
|
+
* (`https://hl7.org/fhir/R4/valueset.html`, `https://hl7.org/fhir/R4/valueset-operation-expand.html`,
|
|
1479
|
+
* `https://hl7.org/fhir/R4/valueset-operation-validate-code.html`).
|
|
1480
|
+
*
|
|
1481
|
+
* @packageDocumentation
|
|
1482
|
+
*/
|
|
1483
|
+
|
|
1484
|
+
/**
|
|
1485
|
+
* A FHIR R4 `ValueSet.compose.include.filter` operator — the subset the engine implements plus the
|
|
1486
|
+
* ones it explicitly does not. Grounded on the FHIR `filter-operator` code system
|
|
1487
|
+
* (`https://hl7.org/fhir/R4/valueset-filter-operator.html`).
|
|
1488
|
+
*
|
|
1489
|
+
* **Implemented:** `is-a` / `descendent-of` / `is-not-a` (subsumption over the loaded release's
|
|
1490
|
+
* hierarchy), `=` / `in` / `not-in` / `exists` (property predicates). **Not implemented** (surfaced as
|
|
1491
|
+
* a typed {@link DiagnosticCode.TERM_VALUESET_CANNOT_EXPAND}, never silently mis-expanded): `regex`,
|
|
1492
|
+
* `generalizes`. Anything unrecognized is treated as not-implemented — never as "matches nothing".
|
|
1493
|
+
*/
|
|
1494
|
+
type FilterOperator = "=" | "is-a" | "descendent-of" | "is-not-a" | "regex" | "in" | "not-in" | "generalizes" | "exists";
|
|
1495
|
+
/** One `ValueSet.compose.include.filter` — a `property op value` predicate over a code system. */
|
|
1496
|
+
interface ConceptSetFilter {
|
|
1497
|
+
/** The concept property the predicate is on (e.g. `"concept"` for the code hierarchy, or a code). */
|
|
1498
|
+
readonly property: string;
|
|
1499
|
+
/** The filter operator. */
|
|
1500
|
+
readonly op: FilterOperator;
|
|
1501
|
+
/** The comparison value (for `in`/`not-in`, a comma-separated list; for `is-a`, an anchor code). */
|
|
1502
|
+
readonly value: string;
|
|
1503
|
+
}
|
|
1504
|
+
/** One explicit `ValueSet.compose.include.concept` — an enumerated code (extensional). */
|
|
1505
|
+
interface ConceptRef {
|
|
1506
|
+
/** The code, drawn verbatim from the value set. */
|
|
1507
|
+
readonly code: string;
|
|
1508
|
+
/** The display, when the value set supplied one (carried verbatim, never fabricated). */
|
|
1509
|
+
readonly display?: string;
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* One `ValueSet.compose.include` / `.exclude` component — a `ConceptSet`. Selects codes by an explicit
|
|
1513
|
+
* `concept` list, by `filter`s over a `system`, by referencing other value sets (`valueSet`), or a
|
|
1514
|
+
* combination (the combination is an **intersection**, per FHIR).
|
|
1515
|
+
*/
|
|
1516
|
+
interface ConceptSetComponent {
|
|
1517
|
+
/** The code system these codes are drawn from (canonical URI), when the component names one. */
|
|
1518
|
+
readonly system?: string;
|
|
1519
|
+
/** The code system version to pin the selection to, when declared. */
|
|
1520
|
+
readonly version?: string;
|
|
1521
|
+
/** An explicit, enumerated code list (extensional). Mutually exclusive with {@link filter} in FHIR. */
|
|
1522
|
+
readonly concept?: readonly ConceptRef[];
|
|
1523
|
+
/** Intensional filters over {@link system}. Each code must satisfy **all** filters. */
|
|
1524
|
+
readonly filter?: readonly ConceptSetFilter[];
|
|
1525
|
+
/** Canonical URLs of other value sets; a code must be a member of **every** one (intersection). */
|
|
1526
|
+
readonly valueSet?: readonly string[];
|
|
1527
|
+
}
|
|
1528
|
+
/** A `ValueSet.compose` — the intensional/extensional definition of membership. */
|
|
1529
|
+
interface ValueSetCompose {
|
|
1530
|
+
/** The `include` components (their union, minus every {@link exclude}, is the membership). */
|
|
1531
|
+
readonly include: readonly ConceptSetComponent[];
|
|
1532
|
+
/** The `exclude` components — codes removed from the include union. */
|
|
1533
|
+
readonly exclude: readonly ConceptSetComponent[];
|
|
1534
|
+
}
|
|
1535
|
+
/** One `ValueSet.expansion.contains` entry from a **pre-computed** expansion. */
|
|
1536
|
+
interface ExpansionContains {
|
|
1537
|
+
/** The code system URI, when the entry names one. */
|
|
1538
|
+
readonly system?: string;
|
|
1539
|
+
/** The code. Required — a `contains` entry without a code is a malformed expansion. */
|
|
1540
|
+
readonly code: string;
|
|
1541
|
+
/** The display, carried verbatim when present. */
|
|
1542
|
+
readonly display?: string;
|
|
1543
|
+
/** The code system version, when the entry pins one. */
|
|
1544
|
+
readonly version?: string;
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* A **pre-computed** `ValueSet.expansion` — a cached membership snapshot, used as-is (extensional).
|
|
1548
|
+
* The engine never re-derives it; it only checks whether it is **complete** (roadmap §4.4 — a
|
|
1549
|
+
* truncated expansion must never read as full membership).
|
|
1550
|
+
*/
|
|
1551
|
+
interface ValueSetExpansion {
|
|
1552
|
+
/** The server-reported total membership, when present (FHIR `expansion.total`). */
|
|
1553
|
+
readonly total?: number;
|
|
1554
|
+
/** The snapshot members, in expansion order (frozen). */
|
|
1555
|
+
readonly contains: readonly ExpansionContains[];
|
|
1556
|
+
/**
|
|
1557
|
+
* Derived at load: the expansion is **incomplete**. True when `total` exceeds `contains.length`, or
|
|
1558
|
+
* the `http://hl7.org/fhir/StructureDefinition/valueset-toocostly` extension flagged it too-costly
|
|
1559
|
+
* (the VSAC >1200-code truncation hazard). A truncated expansion never reads as complete membership.
|
|
1560
|
+
*/
|
|
1561
|
+
readonly truncated: boolean;
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* A loaded, immutable `ValueSet` — the subset of the FHIR R4 `ValueSet` resource the binding layer
|
|
1565
|
+
* needs. Produced by {@link loadValueSet}; every field deep-frozen.
|
|
1566
|
+
*/
|
|
1567
|
+
interface ValueSet {
|
|
1568
|
+
/** The resource's canonical `url`, when declared (the key referenced value sets resolve against). */
|
|
1569
|
+
readonly url?: string;
|
|
1570
|
+
/** The resource's business `version`, when declared. */
|
|
1571
|
+
readonly version?: string;
|
|
1572
|
+
/** A short human-readable name, when declared. */
|
|
1573
|
+
readonly name?: string;
|
|
1574
|
+
/** The intensional/extensional `compose`, when the resource defines membership by composition. */
|
|
1575
|
+
readonly compose?: ValueSetCompose;
|
|
1576
|
+
/** A **pre-computed** `expansion`, when the resource carries a cached membership snapshot. */
|
|
1577
|
+
readonly expansion?: ValueSetExpansion;
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* The data an {@link expand}/{@link validateCodeInValueSet} call draws on: the loaded
|
|
1581
|
+
* {@link CodeSystem} releases (for intensional `filter`/`system` includes) and any referenced
|
|
1582
|
+
* {@link ValueSet}s (for `compose.include.valueSet`), each keyed by its **canonical URL**.
|
|
1583
|
+
*
|
|
1584
|
+
* The engine makes **no network call** and holds no bundled content: everything intensional resolves
|
|
1585
|
+
* through this map, and a system or value set the caller did not supply becomes a typed
|
|
1586
|
+
* {@link DiagnosticCode.TERM_VALUESET_CANNOT_EXPAND}, never a fabricated member (roadmap §2, §4.4).
|
|
1587
|
+
*/
|
|
1588
|
+
interface ExpansionContext {
|
|
1589
|
+
/** Loaded code-system releases, keyed by canonical URI (e.g. `"http://loinc.org"`). */
|
|
1590
|
+
readonly codeSystems?: ReadonlyMap<string, CodeSystem>;
|
|
1591
|
+
/** Referenced value sets, keyed by canonical URL, for `compose.include.valueSet`. */
|
|
1592
|
+
readonly valueSets?: ReadonlyMap<string, ValueSet>;
|
|
1593
|
+
}
|
|
1594
|
+
/** A surfaced expansion concern — a part that could not be expanded, or a truncated snapshot. */
|
|
1595
|
+
interface ExpansionDiagnostic {
|
|
1596
|
+
/** The stable code (`TERM_VALUESET_CANNOT_EXPAND` / `TERM_VALUESET_EXPANSION_TRUNCATED`). */
|
|
1597
|
+
readonly code: DiagnosticCode;
|
|
1598
|
+
/** A **value-free** structural description of the concern (never echoes a patient value). */
|
|
1599
|
+
readonly detail: string;
|
|
1600
|
+
/** The code system / value set URI the concern is about (a published identity — not PHI). */
|
|
1601
|
+
readonly system?: string;
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* The result of {@link expand}: the flattened membership plus an honest **completeness** signal.
|
|
1605
|
+
*
|
|
1606
|
+
* `complete` is `false` whenever any part could not be fully computed (a missing code system, an
|
|
1607
|
+
* unresolved referenced value set, an unimplemented `filter` op, or a truncated pre-computed
|
|
1608
|
+
* expansion). A `false` here is the never-fabricate contract in action: the `contains` set is a
|
|
1609
|
+
* **lower bound**, so it must never be read as exhaustive membership.
|
|
1610
|
+
*/
|
|
1611
|
+
interface ExpandResult {
|
|
1612
|
+
/** Whether the expansion is exhaustive. When `false`, `contains` is a lower bound, not the whole set. */
|
|
1613
|
+
readonly complete: boolean;
|
|
1614
|
+
/** The expanded, de-duplicated members, in first-seen order (frozen; every member frozen). */
|
|
1615
|
+
readonly contains: readonly Coding[];
|
|
1616
|
+
/** The server-reported total, when the expansion came from a pre-computed snapshot. */
|
|
1617
|
+
readonly total?: number;
|
|
1618
|
+
/** The surfaced concerns, in encounter order (frozen; may be empty). */
|
|
1619
|
+
readonly diagnostics: readonly ExpansionDiagnostic[];
|
|
1620
|
+
}
|
|
1621
|
+
/** A definitive membership answer — the code is (or is not) a member of the value set. */
|
|
1622
|
+
interface ValueSetMemberDecided {
|
|
1623
|
+
/** Discriminant: membership was decided. */
|
|
1624
|
+
readonly undetermined: false;
|
|
1625
|
+
/** FHIR `$validate-code` `result`: true iff the code is a member of the value set. */
|
|
1626
|
+
readonly result: boolean;
|
|
1627
|
+
/** The coding that was tested, echoed. */
|
|
1628
|
+
readonly coding: Coding;
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* A **fail-safe undetermined** membership — the never-fabricate outcome for a value set that could
|
|
1632
|
+
* not be fully evaluated (a missing code system, a truncated expansion). The engine refuses to guess:
|
|
1633
|
+
* it returns neither `true` nor `false`, so a caller can never mistake "we could not check" for "not a
|
|
1634
|
+
* member" (roadmap §4.4 — a false "not a member" is a clinical error).
|
|
1635
|
+
*/
|
|
1636
|
+
interface ValueSetMemberUndetermined {
|
|
1637
|
+
/** Discriminant: membership could not be decided. */
|
|
1638
|
+
readonly undetermined: true;
|
|
1639
|
+
/** The stable diagnostic code (`TERM_VALUESET_CANNOT_EXPAND`). */
|
|
1640
|
+
readonly code: "TERM_VALUESET_CANNOT_EXPAND";
|
|
1641
|
+
/** The coding that was tested, echoed. */
|
|
1642
|
+
readonly coding: Coding;
|
|
1643
|
+
/** The surfaced reasons the check could not be completed. */
|
|
1644
|
+
readonly diagnostics: readonly ExpansionDiagnostic[];
|
|
1645
|
+
}
|
|
1646
|
+
/** The result of {@link validateCodeInValueSet}: a decided membership or a typed undetermined. */
|
|
1647
|
+
type ValueSetMembership = ValueSetMemberDecided | ValueSetMemberUndetermined;
|
|
1648
|
+
|
|
1649
|
+
/**
|
|
1650
|
+
* {@link loadValueSet} — validate and normalize an untrusted FHIR R4 `ValueSet` JSON resource into
|
|
1651
|
+
* the immutable {@link ValueSet} model the `$expand` / `$validate-code` binding operations run over.
|
|
1652
|
+
*
|
|
1653
|
+
* **Conservative on load** (like {@link ../conceptmap/load.loadConceptMap}, unlike a lenient wire
|
|
1654
|
+
* parser): a value set *binds* whether a code is allowed in a clinical slot, so a structurally
|
|
1655
|
+
* unusable resource is a typed **fatal** ({@link FATAL_CODES.TERM_VALUESET_MALFORMED}) rather than a
|
|
1656
|
+
* silently partial value set that would admit *some* codes and quietly drop others. A `filter`
|
|
1657
|
+
* operator the engine does not implement is **not** fatal — the value set loads, and the gap surfaces
|
|
1658
|
+
* at expansion time as a {@link DIAGNOSTIC_CODES.TERM_VALUESET_CANNOT_EXPAND} diagnostic. Fault
|
|
1659
|
+
* messages are **value-free**: they name the resource *path* and the fault, never echo a code value.
|
|
1660
|
+
*
|
|
1661
|
+
* @packageDocumentation
|
|
1662
|
+
*/
|
|
1663
|
+
|
|
1664
|
+
/**
|
|
1665
|
+
* Load an untrusted FHIR R4 `ValueSet` JSON value into an immutable {@link ValueSet}.
|
|
1666
|
+
*
|
|
1667
|
+
* Accepts the standard resource shape: `resourceType: "ValueSet"` with an optional intensional
|
|
1668
|
+
* `compose` (`include`/`exclude` of `system`/`concept`/`filter`/`valueSet`) and/or a **pre-computed**
|
|
1669
|
+
* `expansion` (`contains`, with `total` and the `valueset-toocostly` extension read into a derived
|
|
1670
|
+
* `truncated` flag). The result is deep-frozen. Anything structurally unusable throws a
|
|
1671
|
+
* {@link TerminologyError} carrying {@link FATAL_CODES.TERM_VALUESET_MALFORMED}.
|
|
1672
|
+
*
|
|
1673
|
+
* @param json - The untrusted resource (typically `JSON.parse` output — hence `unknown`).
|
|
1674
|
+
* @returns The immutable, validated {@link ValueSet}.
|
|
1675
|
+
* @throws {TerminologyError} `TERM_VALUESET_MALFORMED` when the resource is not a usable ValueSet.
|
|
1676
|
+
* @example
|
|
1677
|
+
* ```ts
|
|
1678
|
+
* import { loadValueSet } from "@cosyte/terminology";
|
|
1679
|
+
*
|
|
1680
|
+
* const vs = loadValueSet({
|
|
1681
|
+
* resourceType: "ValueSet",
|
|
1682
|
+
* url: "http://example.org/vs/colors",
|
|
1683
|
+
* compose: {
|
|
1684
|
+
* include: [{ system: "http://example.org/cs", concept: [{ code: "red" }, { code: "green" }] }],
|
|
1685
|
+
* },
|
|
1686
|
+
* });
|
|
1687
|
+
* vs.compose?.include.length; // => 1
|
|
1688
|
+
* ```
|
|
1689
|
+
*/
|
|
1690
|
+
declare function loadValueSet(json: unknown): ValueSet;
|
|
1691
|
+
|
|
1692
|
+
/**
|
|
1693
|
+
* {@link expand} — the FHIR R4 ValueSet `$expand` operation over a loaded {@link ValueSet}.
|
|
1694
|
+
*
|
|
1695
|
+
* Grounded firsthand on FHIR R4 (`https://hl7.org/fhir/R4/valueset-operation-expand.html`,
|
|
1696
|
+
* `https://hl7.org/fhir/R4/valueset.html#compose`):
|
|
1697
|
+
*
|
|
1698
|
+
* - A **pre-computed** `expansion` is used as-is (extensional), and its completeness is checked — a
|
|
1699
|
+
* truncated snapshot is flagged, never treated as full membership (roadmap §4.4).
|
|
1700
|
+
* - An intensional `compose` is expanded over **consumer-supplied** {@link ../codesystem/types.CodeSystem}
|
|
1701
|
+
* releases: the union of `include` components minus the union of `exclude` components. Each component
|
|
1702
|
+
* selects by explicit `concept`, by `filter` (subsumption + property predicates), by the whole
|
|
1703
|
+
* `system`, and/or by intersecting referenced value sets.
|
|
1704
|
+
*
|
|
1705
|
+
* **Never fabricate.** A part that cannot be computed — a missing code system, an unresolved
|
|
1706
|
+
* referenced value set, an unimplemented `filter` operator — yields a typed
|
|
1707
|
+
* {@link ../common/diagnostics.DIAGNOSTIC_CODES.TERM_VALUESET_CANNOT_EXPAND} and marks the result
|
|
1708
|
+
* `complete: false`, so the `contains` set is an explicit **lower bound**, never a silently-empty or
|
|
1709
|
+
* fabricated membership.
|
|
1710
|
+
*
|
|
1711
|
+
* @packageDocumentation
|
|
1712
|
+
*/
|
|
1713
|
+
|
|
1714
|
+
/**
|
|
1715
|
+
* `$expand`: compute the flattened membership of a {@link ValueSet} over the supplied code systems.
|
|
1716
|
+
*
|
|
1717
|
+
* @param vs - The loaded value set.
|
|
1718
|
+
* @param ctx - The loaded code systems / referenced value sets the intensional parts resolve against.
|
|
1719
|
+
* @returns An {@link ExpandResult} — the `contains` members plus an honest `complete` flag. When
|
|
1720
|
+
* `complete` is `false`, `contains` is a lower bound (never treat it as exhaustive membership).
|
|
1721
|
+
* @example
|
|
1722
|
+
* ```ts
|
|
1723
|
+
* import { loadValueSet, loadCodeSystem, expand } from "@cosyte/terminology";
|
|
1724
|
+
*
|
|
1725
|
+
* const cs = loadCodeSystem({
|
|
1726
|
+
* format: "fhir",
|
|
1727
|
+
* resource: {
|
|
1728
|
+
* resourceType: "CodeSystem",
|
|
1729
|
+
* url: "http://example.org/cs",
|
|
1730
|
+
* concept: [{ code: "a" }, { code: "b" }, { code: "c" }],
|
|
1731
|
+
* },
|
|
1732
|
+
* });
|
|
1733
|
+
* const vs = loadValueSet({
|
|
1734
|
+
* resourceType: "ValueSet",
|
|
1735
|
+
* compose: { include: [{ system: "http://example.org/cs", concept: [{ code: "a" }, { code: "b" }] }] },
|
|
1736
|
+
* });
|
|
1737
|
+
* const result = expand(vs, { codeSystems: new Map([["http://example.org/cs", cs]]) });
|
|
1738
|
+
* result.complete; // => true
|
|
1739
|
+
* result.contains.length; // => 2
|
|
1740
|
+
* ```
|
|
1741
|
+
*/
|
|
1742
|
+
declare function expand(vs: ValueSet, ctx?: ExpansionContext): ExpandResult;
|
|
1743
|
+
|
|
1744
|
+
/**
|
|
1745
|
+
* {@link validateCodeInValueSet} — the FHIR R4 ValueSet `$validate-code` operation (value-set
|
|
1746
|
+
* binding): is a {@link Coding} a member of a {@link ValueSet}?
|
|
1747
|
+
*
|
|
1748
|
+
* Grounded firsthand on FHIR R4 (`https://hl7.org/fhir/R4/valueset-operation-validate-code.html`): a
|
|
1749
|
+
* binding decides whether a code is *allowed* in a slot. The engine returns a **decided** membership
|
|
1750
|
+
* (`result: true`/`false`) **only when it can prove it** — a code found in a computable `include` and
|
|
1751
|
+
* not removed by any computable `exclude` is a definite member; a code absent from a fully-evaluated
|
|
1752
|
+
* value set is a definite non-member. When any relevant part cannot be evaluated (a missing code
|
|
1753
|
+
* system, a truncated pre-computed expansion, an unimplemented `filter`), the answer is a typed
|
|
1754
|
+
* {@link ValueSetMemberUndetermined} — **never** a fabricated `false` (roadmap §4.4: a false "not a
|
|
1755
|
+
* member" is a clinical error).
|
|
1756
|
+
*
|
|
1757
|
+
* @packageDocumentation
|
|
1758
|
+
*/
|
|
1759
|
+
|
|
1760
|
+
/**
|
|
1761
|
+
* `$validate-code` (ValueSet): is `coding` a member of `valueSet`?
|
|
1762
|
+
*
|
|
1763
|
+
* @param coding - The code to test (system + code).
|
|
1764
|
+
* @param valueSet - The loaded value set.
|
|
1765
|
+
* @param ctx - The loaded code systems / referenced value sets the intensional parts resolve against.
|
|
1766
|
+
* @returns A decided {@link ValueSetMembership} (`result: true`/`false`), or a typed
|
|
1767
|
+
* {@link ValueSetMemberUndetermined} when membership could not be proven — never a fabricated `false`.
|
|
1768
|
+
* @example
|
|
1769
|
+
* ```ts
|
|
1770
|
+
* import { loadValueSet, validateCodeInValueSet } from "@cosyte/terminology";
|
|
1771
|
+
*
|
|
1772
|
+
* const vs = loadValueSet({
|
|
1773
|
+
* resourceType: "ValueSet",
|
|
1774
|
+
* compose: { include: [{ system: "http://example.org/cs", concept: [{ code: "a" }, { code: "b" }] }] },
|
|
1775
|
+
* });
|
|
1776
|
+
* const r = validateCodeInValueSet({ system: "http://example.org/cs", code: "a" }, vs);
|
|
1777
|
+
* r.undetermined === false && r.result; // => true
|
|
1778
|
+
* ```
|
|
1779
|
+
*/
|
|
1780
|
+
declare function validateCodeInValueSet(coding: Coding, valueSet: ValueSet, ctx?: ExpansionContext): ValueSetMembership;
|
|
1781
|
+
|
|
1782
|
+
/**
|
|
1783
|
+
* Subsumption + `compose.include.filter` evaluation — the shared machinery {@link ./expand.expand}
|
|
1784
|
+
* and {@link ./validate.validateCodeInValueSet} both draw on.
|
|
1785
|
+
*
|
|
1786
|
+
* **Subsumption** is read from the loaded {@link CodeSystem}'s **`parent`** concept-properties
|
|
1787
|
+
* (`http://hl7.org/fhir/concept-properties#parent`) — which the FHIR loader synthesizes from a nested
|
|
1788
|
+
* `concept` hierarchy and carries verbatim from an explicit `parent` property. This is deliberately
|
|
1789
|
+
* the release's **own** hierarchy (roadmap: "hierarchy/`$subsumes` minimal; deep subsumption is
|
|
1790
|
+
* Phase 5"); a code system that encodes no parent edges has no descendants beyond identity, and that
|
|
1791
|
+
* is treated as complete — not guessed deeper.
|
|
1792
|
+
*
|
|
1793
|
+
* **Filter support is explicit.** `is-a` / `descendent-of` / `is-not-a` / `=` / `in` / `not-in` /
|
|
1794
|
+
* `exists` are implemented; `regex` / `generalizes` / anything unrecognized report `supported: false`
|
|
1795
|
+
* so the caller surfaces a typed {@link DIAGNOSTIC_CODES.TERM_VALUESET_CANNOT_EXPAND} rather than
|
|
1796
|
+
* silently mis-including or dropping codes.
|
|
1797
|
+
*
|
|
1798
|
+
* @packageDocumentation
|
|
1799
|
+
*/
|
|
1800
|
+
|
|
1801
|
+
/**
|
|
1802
|
+
* The filters whose operator the engine does **not** implement (`regex` / `generalizes` / unknown) —
|
|
1803
|
+
* a non-empty result means the caller must surface a typed cannot-expand, never mis-expand.
|
|
1804
|
+
*
|
|
1805
|
+
* @param filters - The component's filters.
|
|
1806
|
+
* @returns The subset with an unimplemented operator (empty when all are supported).
|
|
1807
|
+
* @example
|
|
1808
|
+
* ```ts
|
|
1809
|
+
* import { unsupportedOps } from "@cosyte/terminology";
|
|
1810
|
+
*
|
|
1811
|
+
* unsupportedOps([{ property: "concept", op: "is-a", value: "x" }]).length; // => 0
|
|
1812
|
+
* ```
|
|
1813
|
+
*/
|
|
1814
|
+
declare function unsupportedOps(filters: readonly ConceptSetFilter[]): readonly ConceptSetFilter[];
|
|
1815
|
+
/** An immutable ancestor index over a {@link CodeSystem}'s `parent` edges. */
|
|
1816
|
+
interface Subsumption {
|
|
1817
|
+
/** All (transitive) ancestor codes of `code`, cycle-safe. Excludes `code` itself. */
|
|
1818
|
+
ancestorsOf(code: string): ReadonlySet<string>;
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Build a cycle-safe ancestor index from a loaded {@link CodeSystem}'s `parent` concept-properties.
|
|
1822
|
+
*
|
|
1823
|
+
* @param cs - The loaded code system.
|
|
1824
|
+
* @returns A {@link Subsumption} answering transitive-ancestor queries.
|
|
1825
|
+
* @example
|
|
1826
|
+
* ```ts
|
|
1827
|
+
* import { loadCodeSystem, buildSubsumption } from "@cosyte/terminology";
|
|
1828
|
+
*
|
|
1829
|
+
* const cs = loadCodeSystem({
|
|
1830
|
+
* format: "fhir",
|
|
1831
|
+
* resource: { resourceType: "CodeSystem", concept: [{ code: "p", concept: [{ code: "c" }] }] },
|
|
1832
|
+
* });
|
|
1833
|
+
* buildSubsumption(cs).ancestorsOf("c").has("p"); // => true
|
|
1834
|
+
* ```
|
|
1835
|
+
*/
|
|
1836
|
+
declare function buildSubsumption(cs: CodeSystem): Subsumption;
|
|
1837
|
+
/**
|
|
1838
|
+
* True when `code` is-a `anchor` — the same code, or a (transitive) descendant of it.
|
|
1839
|
+
*
|
|
1840
|
+
* @param sub - The subsumption index (from {@link buildSubsumption}).
|
|
1841
|
+
* @param code - The candidate code.
|
|
1842
|
+
* @param anchor - The ancestor code to test against.
|
|
1843
|
+
* @returns Whether `code` is `anchor` or a descendant of it.
|
|
1844
|
+
* @example
|
|
1845
|
+
* ```ts
|
|
1846
|
+
* import { loadCodeSystem, buildSubsumption, isA } from "@cosyte/terminology";
|
|
1847
|
+
*
|
|
1848
|
+
* const cs = loadCodeSystem({
|
|
1849
|
+
* format: "fhir",
|
|
1850
|
+
* resource: { resourceType: "CodeSystem", concept: [{ code: "p", concept: [{ code: "c" }] }] },
|
|
1851
|
+
* });
|
|
1852
|
+
* isA(buildSubsumption(cs), "c", "p"); // => true
|
|
1853
|
+
* ```
|
|
1854
|
+
*/
|
|
1855
|
+
declare function isA(sub: Subsumption, code: string, anchor: string): boolean;
|
|
1856
|
+
/** The outcome of evaluating a single filter against a single concept. */
|
|
1857
|
+
interface FilterMatch {
|
|
1858
|
+
/** Whether the concept satisfies the filter (meaningful only when {@link supported}). */
|
|
1859
|
+
readonly matched: boolean;
|
|
1860
|
+
/** Whether the engine implements this filter operator; `false` ⇒ the caller must surface it. */
|
|
1861
|
+
readonly supported: boolean;
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Evaluate one {@link ConceptSetFilter} against one {@link Concept}, using the release's hierarchy.
|
|
1865
|
+
*
|
|
1866
|
+
* The `concept` filter property (and the hierarchy operators regardless of property) reads the code
|
|
1867
|
+
* itself / its subsumption; other properties read the concept's `property` values. An unimplemented
|
|
1868
|
+
* operator returns `{ supported: false }` — never a coerced `matched`.
|
|
1869
|
+
*
|
|
1870
|
+
* @param concept - The concept under test.
|
|
1871
|
+
* @param filter - The filter predicate.
|
|
1872
|
+
* @param sub - The subsumption index for the concept's code system.
|
|
1873
|
+
* @returns A {@link FilterMatch}.
|
|
1874
|
+
* @example
|
|
1875
|
+
* ```ts
|
|
1876
|
+
* import { loadCodeSystem, buildSubsumption, matchesFilter } from "@cosyte/terminology";
|
|
1877
|
+
*
|
|
1878
|
+
* const cs = loadCodeSystem({
|
|
1879
|
+
* format: "fhir",
|
|
1880
|
+
* resource: { resourceType: "CodeSystem", concept: [{ code: "p", concept: [{ code: "c" }] }] },
|
|
1881
|
+
* });
|
|
1882
|
+
* const dog = cs.concepts.get("c");
|
|
1883
|
+
* dog && matchesFilter(dog, { property: "concept", op: "is-a", value: "p" }, buildSubsumption(cs)).matched; // => true
|
|
1884
|
+
* ```
|
|
1885
|
+
*/
|
|
1886
|
+
declare function matchesFilter(concept: Concept, filter: ConceptSetFilter, sub: Subsumption): FilterMatch;
|
|
1887
|
+
|
|
1888
|
+
/**
|
|
1889
|
+
* Types for the **UCUM unit layer** (roadmap Phase 4): the parsed unit AST, the reduced
|
|
1890
|
+
* (canonical) dimensional form, and the public {@link UcumValidation} result.
|
|
1891
|
+
*
|
|
1892
|
+
* The layer is **recognition + validation + representation canonicalization only** — it answers
|
|
1893
|
+
* "is this a valid UCUM expression" and "are these two expressions the same unit", and it
|
|
1894
|
+
* canonicalizes a unit's *representation*. It performs **no magnitude conversion** (`mg/dL` →
|
|
1895
|
+
* `mmol/L` needs an analyte's molar mass — a clinical computation the engine refuses, roadmap
|
|
1896
|
+
* §2/§4.3). The tables are the vendored, verbatim UCUM `ucum-essence.xml` (roadmap §5), transformed
|
|
1897
|
+
* to an in-memory model at runtime; the engine ships **no derivative** of the UCUM table.
|
|
1898
|
+
*
|
|
1899
|
+
* @packageDocumentation
|
|
1900
|
+
*/
|
|
1901
|
+
/** A UCUM metric prefix (e.g. `k` = kilo, factor `1e3`), from the essence `<prefix>` table. */
|
|
1902
|
+
interface UcumPrefix {
|
|
1903
|
+
/** The case-sensitive prefix symbol (e.g. `"k"`, `"da"`, `"Ki"`). */
|
|
1904
|
+
readonly code: string;
|
|
1905
|
+
/** The multiplicative factor (e.g. `1000` for `k`, `1024` for `Ki`). */
|
|
1906
|
+
readonly factor: number;
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* A UCUM unit atom, from the essence `<base-unit>` / `<unit>` table. An atom is a symbol like `m`,
|
|
1910
|
+
* `L`, `[in_i]`, `10*`, or `Cel`, **before** any prefix or exponent is applied.
|
|
1911
|
+
*/
|
|
1912
|
+
interface UcumAtom {
|
|
1913
|
+
/** The case-sensitive atom symbol (the essence `Code`, e.g. `"m"`, `"[iU]"`, `"m[Hg]"`). */
|
|
1914
|
+
readonly code: string;
|
|
1915
|
+
/** Whether a metric prefix may attach to this atom (essence `isMetric`). Only metric atoms prefix. */
|
|
1916
|
+
readonly metric: boolean;
|
|
1917
|
+
/** A **base** unit (one of the 7: `m s g rad K C cd`) — its own dimension, factor 1. */
|
|
1918
|
+
readonly base: boolean;
|
|
1919
|
+
/** A base unit's dimension key (its own `code`), or `undefined` for derived atoms. */
|
|
1920
|
+
readonly dim?: string;
|
|
1921
|
+
/**
|
|
1922
|
+
* A **special** unit (essence `isSpecial`, e.g. `Cel`, `B`, `[pH]`) — defined by a non-linear
|
|
1923
|
+
* function, so it has **no** linear factor/dimension and is never reduced to one. Compared only by
|
|
1924
|
+
* identity of its normalized representation (never claimed equal to a linear unit).
|
|
1925
|
+
*/
|
|
1926
|
+
readonly special: boolean;
|
|
1927
|
+
/**
|
|
1928
|
+
* An **arbitrary** unit (essence `isArbitrary`, e.g. `[IU]`, `[arb'U]`) — not commensurable with
|
|
1929
|
+
* any other unit. Modeled as its own dimension axis keyed by the atom `code`, so `[IU]/[IU]`
|
|
1930
|
+
* reduces to unity but `[IU]` is never equated with a mass or another arbitrary unit.
|
|
1931
|
+
*/
|
|
1932
|
+
readonly arbitrary: boolean;
|
|
1933
|
+
/**
|
|
1934
|
+
* The linear definition: `1 <atom> = value × <unit>` (essence `<value value Unit>`), for derived
|
|
1935
|
+
* atoms. `undefined` for base units (they define the dimensions) and for special units (non-linear).
|
|
1936
|
+
*/
|
|
1937
|
+
readonly value?: {
|
|
1938
|
+
readonly factor: number;
|
|
1939
|
+
readonly unit: string;
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
/** The parsed UCUM essence table — the engine's in-memory unit model, built lazily and cached. */
|
|
1943
|
+
interface UcumEssence {
|
|
1944
|
+
/** Prefix symbols → prefix, longest symbols first (for greedy matching). */
|
|
1945
|
+
readonly prefixes: readonly UcumPrefix[];
|
|
1946
|
+
/** Atom symbols → atom, longest symbols first (for greedy longest-match). */
|
|
1947
|
+
readonly atoms: readonly UcumAtom[];
|
|
1948
|
+
/** Atom lookup by exact `code`. */
|
|
1949
|
+
readonly atomByCode: ReadonlyMap<string, UcumAtom>;
|
|
1950
|
+
/** Prefix lookup by exact `code`. */
|
|
1951
|
+
readonly prefixByCode: ReadonlyMap<string, UcumPrefix>;
|
|
1952
|
+
}
|
|
1953
|
+
/** One parsed component of a unit term: a simple unit (prefix?+atom) with an optional exponent. */
|
|
1954
|
+
interface SimpleUnitNode {
|
|
1955
|
+
readonly kind: "simple";
|
|
1956
|
+
/** The applied prefix, when present. */
|
|
1957
|
+
readonly prefix?: UcumPrefix;
|
|
1958
|
+
/** The resolved atom. */
|
|
1959
|
+
readonly atom: UcumAtom;
|
|
1960
|
+
/** The integer exponent (default 1). Negative for the denominator side. */
|
|
1961
|
+
readonly exponent: number;
|
|
1962
|
+
}
|
|
1963
|
+
/** A numeric factor component (a bare integer, e.g. the `4` in `4.[pi]`). */
|
|
1964
|
+
interface FactorNode {
|
|
1965
|
+
readonly kind: "factor";
|
|
1966
|
+
/** The integer value. */
|
|
1967
|
+
readonly value: number;
|
|
1968
|
+
}
|
|
1969
|
+
/** A parenthesised sub-term or an annotation-only component. */
|
|
1970
|
+
interface GroupNode {
|
|
1971
|
+
readonly kind: "group";
|
|
1972
|
+
/** The parsed inner term. */
|
|
1973
|
+
readonly term: UnitNode;
|
|
1974
|
+
}
|
|
1975
|
+
/** A component that carries **only** an annotation (e.g. `{cells}`), dimensionless and inert. */
|
|
1976
|
+
interface AnnotationNode {
|
|
1977
|
+
readonly kind: "annotation";
|
|
1978
|
+
}
|
|
1979
|
+
/** A component: a simple unit, a factor, a group, or an annotation — each optionally exponentiated. */
|
|
1980
|
+
type ComponentNode = SimpleUnitNode | FactorNode | GroupNode | AnnotationNode;
|
|
1981
|
+
/**
|
|
1982
|
+
* A parsed UCUM term: an ordered list of components, each combined into the product with the given
|
|
1983
|
+
* sign (+1 for `.` / the leading operand, -1 for the `/` denominator).
|
|
1984
|
+
*/
|
|
1985
|
+
interface UnitNode {
|
|
1986
|
+
readonly kind: "term";
|
|
1987
|
+
/** The components and their operator sign, in source order. */
|
|
1988
|
+
readonly factors: ReadonlyArray<{
|
|
1989
|
+
readonly sign: 1 | -1;
|
|
1990
|
+
readonly node: ComponentNode;
|
|
1991
|
+
}>;
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* A unit reduced to base dimensions: a scalar `factor` times a map of dimension axis → exponent.
|
|
1995
|
+
* Dimension keys are the seven UCUM base symbols (`m s g rad K C cd`) plus `arb:<code>` axes for
|
|
1996
|
+
* arbitrary units. Two `linear` forms are the **same unit** iff equal dims and equal factor.
|
|
1997
|
+
*/
|
|
1998
|
+
interface LinearReduction {
|
|
1999
|
+
readonly kind: "linear";
|
|
2000
|
+
/** The accumulated scalar factor in base units. */
|
|
2001
|
+
readonly factor: number;
|
|
2002
|
+
/** Dimension axis → non-zero exponent (zero exponents omitted). */
|
|
2003
|
+
readonly dims: Readonly<Record<string, number>>;
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* A **non-linear** (special) reduction — the expression involves a special unit (`Cel`, `B`, …) and
|
|
2007
|
+
* cannot be reduced to a scalar×dimension form. Carried by its normalized representation `form`;
|
|
2008
|
+
* two special reductions are the same unit iff their `form`s are identical (never equated with a
|
|
2009
|
+
* linear unit — the never-fabricate posture applied to units).
|
|
2010
|
+
*/
|
|
2011
|
+
interface SpecialReduction {
|
|
2012
|
+
readonly kind: "special";
|
|
2013
|
+
/** The normalized expression string (prefixes/atoms/exponents; annotations stripped). */
|
|
2014
|
+
readonly form: string;
|
|
2015
|
+
}
|
|
2016
|
+
/** The reduced canonical form of a unit expression. */
|
|
2017
|
+
type Reduction = LinearReduction | SpecialReduction;
|
|
2018
|
+
/**
|
|
2019
|
+
* The result of {@link validateUcum}: a valid unit with its canonical descriptor, or a typed
|
|
2020
|
+
* invalid carrying the stable {@link TERM_UCUM_INVALID} diagnostic and a value-free reason.
|
|
2021
|
+
*/
|
|
2022
|
+
type UcumValidation = {
|
|
2023
|
+
/** The expression is a well-formed UCUM unit over known atoms. */
|
|
2024
|
+
readonly valid: true;
|
|
2025
|
+
/**
|
|
2026
|
+
* A stable, normalized **descriptor** of the unit's canonical form — equal iff the units are
|
|
2027
|
+
* the same (see {@link ucumEqual}). Note: this is a comparison/debug descriptor, **not**
|
|
2028
|
+
* guaranteed to be a re-parseable UCUM expression (UCUM cannot express arbitrary numeric
|
|
2029
|
+
* factors as literals).
|
|
2030
|
+
*/
|
|
2031
|
+
readonly canonical: string;
|
|
2032
|
+
} | {
|
|
2033
|
+
/** The expression is not a valid UCUM unit. */
|
|
2034
|
+
readonly valid: false;
|
|
2035
|
+
/** The stable diagnostic code — never a guessed "nearest" unit (the never-fabricate rule). */
|
|
2036
|
+
readonly code: "TERM_UCUM_INVALID";
|
|
2037
|
+
/** A **value-free** structural reason (a grammar fault or an unknown atom; never PHI). */
|
|
2038
|
+
readonly reason: string;
|
|
2039
|
+
};
|
|
2040
|
+
|
|
2041
|
+
/**
|
|
2042
|
+
* The public **UCUM** surface (roadmap Phase 4): {@link validateUcum} (recognition + a canonical
|
|
2043
|
+
* descriptor) and {@link ucumEqual} (are two expressions the *same unit*).
|
|
2044
|
+
*
|
|
2045
|
+
* Both are **recognition / validation / representation-canonicalization only** — never magnitude
|
|
2046
|
+
* conversion (roadmap §2/§4.3). The never-fabricate posture holds: an unparseable or unknown unit is
|
|
2047
|
+
* a typed {@link TERM_UCUM_INVALID} outcome carrying a value-free reason, **never** a guessed
|
|
2048
|
+
* "nearest" unit; and a special (non-linear) unit is never claimed equal to a linear one.
|
|
2049
|
+
*
|
|
2050
|
+
* @packageDocumentation
|
|
2051
|
+
*/
|
|
2052
|
+
|
|
2053
|
+
/**
|
|
2054
|
+
* Validate a UCUM unit expression and, when valid, return its canonical descriptor.
|
|
2055
|
+
*
|
|
2056
|
+
* Validation **is** parsing against the UCUM grammar over the known atom table: an expression is
|
|
2057
|
+
* valid iff it parses completely. An invalid expression returns a typed {@link TERM_UCUM_INVALID}
|
|
2058
|
+
* with a value-free reason — never a coerced or guessed unit.
|
|
2059
|
+
*
|
|
2060
|
+
* @param unit - The UCUM unit expression (e.g. `"mmol/L"`, `"kg.m/s2"`, `"Cel"`).
|
|
2061
|
+
* @returns `{ valid: true, canonical }` or `{ valid: false, code: "TERM_UCUM_INVALID", reason }`.
|
|
2062
|
+
* @example
|
|
2063
|
+
* ```ts
|
|
2064
|
+
* import { validateUcum } from "@cosyte/terminology";
|
|
2065
|
+
*
|
|
2066
|
+
* validateUcum("mmol/L").valid; // => true
|
|
2067
|
+
* const bad = validateUcum("mg/dl/"); // trailing operator
|
|
2068
|
+
* bad.valid; // => false
|
|
2069
|
+
* ```
|
|
2070
|
+
*/
|
|
2071
|
+
declare function validateUcum(unit: string): UcumValidation;
|
|
2072
|
+
/**
|
|
2073
|
+
* Whether two UCUM expressions denote the **same unit** — i.e. they reduce to the same canonical
|
|
2074
|
+
* form (equal base dimensions *and* equal scale). This recognizes representational equivalences
|
|
2075
|
+
* (`N` ≡ `kg.m/s2`, `mmol/L` ≡ `mmol.L-1`, annotations inert) but is **not** magnitude conversion:
|
|
2076
|
+
* `mg` and `g` are different units and compare unequal.
|
|
2077
|
+
*
|
|
2078
|
+
* A unit is never equal to an invalid expression, and a special (non-linear) unit (`Cel`, `[pH]`) is
|
|
2079
|
+
* equal only to a structurally identical special unit — never to a linear unit (the never-fabricate
|
|
2080
|
+
* posture: the engine will not assert an equivalence it cannot prove).
|
|
2081
|
+
*
|
|
2082
|
+
* @param a - The first UCUM unit expression.
|
|
2083
|
+
* @param b - The second UCUM unit expression.
|
|
2084
|
+
* @returns `true` iff both are valid and denote the same unit.
|
|
2085
|
+
* @example
|
|
2086
|
+
* ```ts
|
|
2087
|
+
* import { ucumEqual } from "@cosyte/terminology";
|
|
2088
|
+
*
|
|
2089
|
+
* ucumEqual("N", "kg.m/s2"); // => true
|
|
2090
|
+
* ucumEqual("mmol/L", "mmol.L-1"); // => true
|
|
2091
|
+
* ucumEqual("mg", "g"); // => false
|
|
2092
|
+
* ```
|
|
2093
|
+
*/
|
|
2094
|
+
declare function ucumEqual(a: string, b: string): boolean;
|
|
2095
|
+
|
|
2096
|
+
/**
|
|
2097
|
+
* The **UCUM grammar parser** (roadmap Phase 4): a hand-rolled, zero-dependency recursive-descent
|
|
2098
|
+
* parser from a unit expression string to a {@link UnitNode} AST, grounded firsthand on the UCUM
|
|
2099
|
+
* specification's syntax (base units, metric prefixes attached with no delimiter and resolved by
|
|
2100
|
+
* longest-match, `.` multiply / `/` divide, signed integer exponents, `{…}` inert annotations, and
|
|
2101
|
+
* the `10*` / `10^` powers-of-ten). Validation **is** parsing: an expression is valid iff it parses
|
|
2102
|
+
* completely over **known** atoms — an unknown atom yields no match and fails, never a guessed unit.
|
|
2103
|
+
*
|
|
2104
|
+
* @packageDocumentation
|
|
2105
|
+
*/
|
|
2106
|
+
|
|
2107
|
+
/** A successful parse (the AST) or a value-free failure reason. */
|
|
2108
|
+
type ParseResult = {
|
|
2109
|
+
readonly ok: true;
|
|
2110
|
+
readonly node: UnitNode;
|
|
2111
|
+
} | ParseFailure;
|
|
2112
|
+
/** A parse failure carrying a value-free structural reason. */
|
|
2113
|
+
interface ParseFailure {
|
|
2114
|
+
readonly ok: false;
|
|
2115
|
+
/** A structural description of the fault (never echoes an input *value* beyond the offending token shape). */
|
|
2116
|
+
readonly reason: string;
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Parse a UCUM unit expression into an AST.
|
|
2120
|
+
*
|
|
2121
|
+
* @param unit - The unit expression (e.g. `"mmol/L"`, `"kg.m/s2"`, `"10*3/ul"`).
|
|
2122
|
+
* @returns The parsed {@link UnitNode}, or a typed {@link ParseFailure} with a value-free reason.
|
|
2123
|
+
* @example
|
|
2124
|
+
* ```ts
|
|
2125
|
+
* import { parseUcum } from "@cosyte/terminology";
|
|
2126
|
+
*
|
|
2127
|
+
* parseUcum("mmol/L").ok; // => true
|
|
2128
|
+
* parseUcum("m/").ok; // => false
|
|
2129
|
+
* ```
|
|
2130
|
+
*/
|
|
2131
|
+
declare function parseUcum(unit: string): ParseResult;
|
|
2132
|
+
|
|
2133
|
+
/**
|
|
2134
|
+
* **Dimensional reduction** of a parsed UCUM unit (roadmap Phase 4): reduce an expression to its
|
|
2135
|
+
* canonical base-unit form — a scalar factor times a map of the seven UCUM base dimensions
|
|
2136
|
+
* (`m s g rad K C cd`), recursively substituting each atom's essence definition down to base units.
|
|
2137
|
+
*
|
|
2138
|
+
* This is **representation** canonicalization, not magnitude *conversion*: it answers "are these two
|
|
2139
|
+
* expressions the same unit" (`N` ≡ `kg.m/s2`, `mmol/L` ≡ `mmol.L-1`), never "what is 5 mg/dL in
|
|
2140
|
+
* mmol/L" (that needs an analyte's molar mass — refused, roadmap §2/§4.3). **Special** units (`Cel`,
|
|
2141
|
+
* `B`, `[pH]`) are non-linear and carry no factor/dimension — they reduce to an opaque normalized
|
|
2142
|
+
* form and are never equated with a linear unit. **Arbitrary** units (`[IU]`, `[arb'U]`) are modeled
|
|
2143
|
+
* as their own dimension axis, so they cancel with themselves but convert to nothing else.
|
|
2144
|
+
*
|
|
2145
|
+
* @packageDocumentation
|
|
2146
|
+
*/
|
|
2147
|
+
|
|
2148
|
+
/**
|
|
2149
|
+
* Reduce a parsed UCUM unit to its canonical {@link Reduction} — a linear scalar×dimension form, or
|
|
2150
|
+
* an opaque `special` form for expressions involving a non-linear special unit.
|
|
2151
|
+
*
|
|
2152
|
+
* @param node - The AST from {@link parseUcum}.
|
|
2153
|
+
* @returns The reduced form.
|
|
2154
|
+
* @example
|
|
2155
|
+
* ```ts
|
|
2156
|
+
* import { parseUcum, reduce } from "@cosyte/terminology";
|
|
2157
|
+
*
|
|
2158
|
+
* const parsed = parseUcum("N");
|
|
2159
|
+
* if (parsed.ok) {
|
|
2160
|
+
* const r = reduce(parsed.node);
|
|
2161
|
+
* r.kind; // => "linear" (1000 × g·m·s⁻²)
|
|
2162
|
+
* }
|
|
2163
|
+
* ```
|
|
2164
|
+
*/
|
|
2165
|
+
declare function reduce(node: UnitNode): Reduction;
|
|
2166
|
+
|
|
2167
|
+
/**
|
|
2168
|
+
* Parse the vendored, verbatim UCUM `ucum-essence.xml` (embedded as {@link UCUM_ESSENCE_XML}) into
|
|
2169
|
+
* the engine's in-memory unit model — the {@link UcumEssence} table of prefixes and atoms.
|
|
2170
|
+
*
|
|
2171
|
+
* Hand-rolled, **zero-dependency** scanning of a known-well-formed document (not a general XML
|
|
2172
|
+
* parser): the essence file is a flat, regular list of `<prefix>` / `<base-unit>` / `<unit>`
|
|
2173
|
+
* elements. Parsed **once**, lazily, and cached — the engine reads no file at runtime; the table is
|
|
2174
|
+
* a pure transform of the embedded verbatim content (a permitted UCUM use, not a derivative — see
|
|
2175
|
+
* `vendor/ucum/NOTICE.md`).
|
|
2176
|
+
*
|
|
2177
|
+
* @packageDocumentation
|
|
2178
|
+
*/
|
|
2179
|
+
|
|
2180
|
+
/**
|
|
2181
|
+
* Load the UCUM essence table, parsing the embedded verbatim XML on first call and caching it.
|
|
2182
|
+
*
|
|
2183
|
+
* @returns The immutable in-memory {@link UcumEssence} model (7 base units, 24 prefixes, ~305 atoms).
|
|
2184
|
+
* @example
|
|
2185
|
+
* ```ts
|
|
2186
|
+
* import { loadUcumEssence } from "@cosyte/terminology";
|
|
2187
|
+
*
|
|
2188
|
+
* const essence = loadUcumEssence();
|
|
2189
|
+
* essence.atomByCode.has("m"); // => true
|
|
2190
|
+
* ```
|
|
2191
|
+
*/
|
|
2192
|
+
declare function loadUcumEssence(): UcumEssence;
|
|
2193
|
+
|
|
2194
|
+
/**
|
|
2195
|
+
* The types for the **crosswalk resolvers** (roadmap Phase 5) — the CMS ICD-9↔ICD-10 **GEMs**
|
|
2196
|
+
* (General Equivalence Mappings) and the NLM **SNOMED CT → ICD-10-CM complex map**.
|
|
2197
|
+
*
|
|
2198
|
+
* Both are the library's highest-risk surface, and their types are built around the stewards' own
|
|
2199
|
+
* warnings: these maps are **approximate, 1:many, context-dependent, and non-invertible** — *"not
|
|
2200
|
+
* crosswalks… reference mappings"* (CMS on the GEMs), *"semi-automated"* (NLM on the SNOMED map). So
|
|
2201
|
+
* every result type here is a **discriminated, fail-safe outcome** carrying the steward's flags,
|
|
2202
|
+
* never a single collapsed target:
|
|
2203
|
+
*
|
|
2204
|
+
* - a No-Map is a typed {@link CrosswalkNoMap} — the steward said "cannot be classified", surfaced,
|
|
2205
|
+
* never a guessed code;
|
|
2206
|
+
* - a 1:many source yields the **whole** ordered candidate set, never one confident pick;
|
|
2207
|
+
* - a context-dependent rule with no runtime context is a typed {@link ComplexMapContextRequired} —
|
|
2208
|
+
* the rule + advice ride through, the engine never picks a branch it wasn't given the data for;
|
|
2209
|
+
* - all Map Advice / GEM flags ride through **verbatim** ("advice flags must not be swallowed",
|
|
2210
|
+
* roadmap §4.1);
|
|
2211
|
+
* - a directional map is **never inverted** (see {@link ../crosswalk/gems.invertGem}).
|
|
2212
|
+
*
|
|
2213
|
+
* @packageDocumentation
|
|
2214
|
+
*/
|
|
2215
|
+
|
|
2216
|
+
/**
|
|
2217
|
+
* The **direction** of a loaded GEM file. CMS ships the forward (ICD-9→ICD-10, `…_I9gem.txt`) and
|
|
2218
|
+
* backward (ICD-10→ICD-9, `…_I10gem.txt`) maps as **separate, non-inverse artifacts** — a forward map
|
|
2219
|
+
* composed with a backward map is *"not a mirror image"* (roadmap §4.1). A `GemMap` therefore records
|
|
2220
|
+
* which direction it is and is only ever applied in that direction.
|
|
2221
|
+
*/
|
|
2222
|
+
type GemDirection = "9-to-10" | "10-to-9";
|
|
2223
|
+
/**
|
|
2224
|
+
* The decoded **5-position flag field** of a GEM entry (CMS Dx GEM User's Guide). Each digit is a
|
|
2225
|
+
* steward signal a caller must not ignore; the verbatim field rides through as {@link raw}.
|
|
2226
|
+
*
|
|
2227
|
+
* Positions (left to right): `approximate | no-map | combination | scenario | choice-list`.
|
|
2228
|
+
*/
|
|
2229
|
+
interface GemFlags {
|
|
2230
|
+
/**
|
|
2231
|
+
* Position 1 — **approximate** flag. `true` when the mapping is approximate (the common case: the
|
|
2232
|
+
* source and target are not identical in meaning); `false` when the entry is an exact identity. A
|
|
2233
|
+
* GEM entry is a *reference* mapping either way — never an assertion of clinical equivalence.
|
|
2234
|
+
*/
|
|
2235
|
+
readonly approximate: boolean;
|
|
2236
|
+
/**
|
|
2237
|
+
* Position 2 — **no-map** flag. `true` when the source code has **no valid target** in the other
|
|
2238
|
+
* classification (the target field is the `NoDx`/`NoPCS` sentinel). A first-class typed outcome —
|
|
2239
|
+
* surfaced as a {@link CrosswalkNoMap}, never a fabricated target (roadmap §4.1).
|
|
2240
|
+
*/
|
|
2241
|
+
readonly noMap: boolean;
|
|
2242
|
+
/**
|
|
2243
|
+
* Position 3 — **combination** flag. `true` when the source requires a **combination** of target
|
|
2244
|
+
* codes to be fully represented; the entry is one candidate within a scenario/choice-list cluster
|
|
2245
|
+
* (see {@link scenario} / {@link choiceList}), never a standalone 1:1 target.
|
|
2246
|
+
*/
|
|
2247
|
+
readonly combination: boolean;
|
|
2248
|
+
/**
|
|
2249
|
+
* Position 4 — **scenario** number (`0` for a non-combination entry). Within a combination, all
|
|
2250
|
+
* entries sharing a scenario describe **one** valid way to represent the source; distinct scenarios
|
|
2251
|
+
* are alternative representations.
|
|
2252
|
+
*/
|
|
2253
|
+
readonly scenario: number;
|
|
2254
|
+
/**
|
|
2255
|
+
* Position 5 — **choice-list** number (`0` for a non-combination entry). Within a scenario, a valid
|
|
2256
|
+
* cluster is built by taking **one** target from each distinct choice list.
|
|
2257
|
+
*/
|
|
2258
|
+
readonly choiceList: number;
|
|
2259
|
+
/** The verbatim 5-character flag field, carried through untouched. */
|
|
2260
|
+
readonly raw: string;
|
|
2261
|
+
}
|
|
2262
|
+
/**
|
|
2263
|
+
* One loaded GEM entry — a source code, its (optional) target, and the decoded {@link GemFlags}. A
|
|
2264
|
+
* No-Map entry ({@link GemFlags.noMap}) carries **no** {@link target}: the source has no equivalent,
|
|
2265
|
+
* and the engine never invents one.
|
|
2266
|
+
*/
|
|
2267
|
+
interface GemEntry {
|
|
2268
|
+
/** The source code (in the map's source classification), verbatim. */
|
|
2269
|
+
readonly source: string;
|
|
2270
|
+
/** The target code (in the map's target classification), verbatim — **absent** for a No-Map entry. */
|
|
2271
|
+
readonly target?: string;
|
|
2272
|
+
/** The decoded steward flags. */
|
|
2273
|
+
readonly flags: GemFlags;
|
|
2274
|
+
}
|
|
2275
|
+
/**
|
|
2276
|
+
* A loaded, immutable GEM map. Entries are keyed by source code (a source may have many entries — the
|
|
2277
|
+
* maps are 1:many). Applied **only** in its {@link direction}; never inverted.
|
|
2278
|
+
*/
|
|
2279
|
+
interface GemMap {
|
|
2280
|
+
/** Which direction this file maps. Applied only this way. */
|
|
2281
|
+
readonly direction: GemDirection;
|
|
2282
|
+
/** The GEM version/release (e.g. `"2018"`), when supplied — mappings are release-scoped. */
|
|
2283
|
+
readonly version?: string;
|
|
2284
|
+
/** The number of loaded entries. */
|
|
2285
|
+
readonly count: number;
|
|
2286
|
+
/** Entries keyed by source code, each list in file order. Read-only; frozen. */
|
|
2287
|
+
readonly entries: ReadonlyMap<string, readonly GemEntry[]>;
|
|
2288
|
+
/** The skipped-row warnings surfaced during load, in line order. Frozen; may be empty. */
|
|
2289
|
+
readonly warnings: readonly GemLoadWarning[];
|
|
2290
|
+
}
|
|
2291
|
+
/**
|
|
2292
|
+
* A surfaced, non-fatal GEM load warning — a malformed line that was **skipped**, reported with its
|
|
2293
|
+
* line number and a value-free structural reason. Liberal on load (roadmap §6, the parsers' posture).
|
|
2294
|
+
*/
|
|
2295
|
+
interface GemLoadWarning {
|
|
2296
|
+
/** The stable diagnostic code for a skipped GEM row. */
|
|
2297
|
+
readonly code: "TERM_GEM_MALFORMED_ROW";
|
|
2298
|
+
/** The 1-based source line the fault was found on. */
|
|
2299
|
+
readonly line: number;
|
|
2300
|
+
/** A value-free description of the structural fault (never echoes a field value). */
|
|
2301
|
+
readonly detail: string;
|
|
2302
|
+
}
|
|
2303
|
+
/** One choice list within a combination scenario — pick **one** of its targets to build a cluster. */
|
|
2304
|
+
interface GemChoiceList {
|
|
2305
|
+
/** The choice-list number ({@link GemFlags.choiceList}). */
|
|
2306
|
+
readonly choiceList: number;
|
|
2307
|
+
/** The candidate target codes for this choice list, in file order. Frozen; never empty. */
|
|
2308
|
+
readonly targets: readonly string[];
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* One combination **scenario** — a single valid way to represent the source as a combination of
|
|
2312
|
+
* target codes. A valid **cluster** is one target taken from each of its {@link choiceLists}.
|
|
2313
|
+
*/
|
|
2314
|
+
interface GemScenario {
|
|
2315
|
+
/** The scenario number ({@link GemFlags.scenario}). */
|
|
2316
|
+
readonly scenario: number;
|
|
2317
|
+
/** The choice lists composing this scenario, ordered by choice-list number. Frozen. */
|
|
2318
|
+
readonly choiceLists: readonly GemChoiceList[];
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* A successful GEM application — one or more candidate targets for the source. The **full** candidate
|
|
2322
|
+
* set is surfaced (never collapsed to one); when the source is a combination, {@link combinations}
|
|
2323
|
+
* carries the scenario/choice-list structure needed to build valid clusters.
|
|
2324
|
+
*/
|
|
2325
|
+
interface GemMatched {
|
|
2326
|
+
/** Discriminant: the source mapped to at least one target. */
|
|
2327
|
+
readonly mapped: true;
|
|
2328
|
+
/** The source code, echoed. */
|
|
2329
|
+
readonly source: string;
|
|
2330
|
+
/** The direction the map was applied in. */
|
|
2331
|
+
readonly direction: GemDirection;
|
|
2332
|
+
/** Every candidate entry for the source, in file order. Frozen; never empty here. */
|
|
2333
|
+
readonly entries: readonly GemEntry[];
|
|
2334
|
+
/** For a combination source, the scenario→choice-list structure. Absent for a plain 1:many source. */
|
|
2335
|
+
readonly combinations?: readonly GemScenario[];
|
|
2336
|
+
}
|
|
2337
|
+
/**
|
|
2338
|
+
* One row of the SNOMED CT → ICD-10-CM **complex/extended map** refset — the caller-supplied,
|
|
2339
|
+
* SNOMED-licensed content the resolver runs over (**no SNOMED content is bundled**; roadmap §5). The
|
|
2340
|
+
* field names paraphrase the SNOMED RF2 extended-map refset columns.
|
|
2341
|
+
*/
|
|
2342
|
+
interface ComplexMapEntry {
|
|
2343
|
+
/** The SNOMED CT source concept id (`referencedComponentId`), verbatim. */
|
|
2344
|
+
readonly source: string;
|
|
2345
|
+
/**
|
|
2346
|
+
* The **map group** (1-based). Distinct groups on one source are an **AND** — the source needs a
|
|
2347
|
+
* target from *each* group (e.g. a manifestation code plus an etiology code). Every group resolves
|
|
2348
|
+
* independently.
|
|
2349
|
+
*/
|
|
2350
|
+
readonly group: number;
|
|
2351
|
+
/**
|
|
2352
|
+
* The **map priority** within a group (1-based). Rules are evaluated in ascending priority; the
|
|
2353
|
+
* first whose {@link rule} matches the caller's context wins that group (an if-then-else chain).
|
|
2354
|
+
*/
|
|
2355
|
+
readonly priority: number;
|
|
2356
|
+
/**
|
|
2357
|
+
* The **map rule**, verbatim — `"TRUE"` / `"OTHERWISE TRUE"` (unconditional fall-through), or an
|
|
2358
|
+
* `IFA` predicate (e.g. an age-band or gender test) evaluated against caller-supplied context.
|
|
2359
|
+
*/
|
|
2360
|
+
readonly rule: string;
|
|
2361
|
+
/**
|
|
2362
|
+
* The **map advice**, verbatim (pipe-delimited in the source) — steward instructions to a human
|
|
2363
|
+
* (`CONSIDER LATERALITY`, `EPISODE OF CARE INFORMATION NEEDED`, `THIS IS A MANIFESTATION CODE…`).
|
|
2364
|
+
* Carried through untouched: advice flags must **never** be swallowed (roadmap §4.1).
|
|
2365
|
+
*/
|
|
2366
|
+
readonly advice: string;
|
|
2367
|
+
/** The ICD-10-CM **map target** code, verbatim — **absent** for a No-Map / empty-target row. */
|
|
2368
|
+
readonly target?: string;
|
|
2369
|
+
/** The steward **map category** id (a `MAP_CATEGORIES` value, e.g. `447638001`), when supplied. */
|
|
2370
|
+
readonly category?: string;
|
|
2371
|
+
/** The map **correlation** id (SNOMED map-correlation concept), when supplied. Carried verbatim. */
|
|
2372
|
+
readonly correlation?: string;
|
|
2373
|
+
}
|
|
2374
|
+
/**
|
|
2375
|
+
* A loaded, immutable SNOMED→ICD-10-CM complex map. Entries are keyed by source concept id; within a
|
|
2376
|
+
* source they are ordered by (group, priority). Applied **only** SNOMED→ICD-10-CM; never inverted.
|
|
2377
|
+
*/
|
|
2378
|
+
interface ComplexMap {
|
|
2379
|
+
/** The map version/release, when supplied — mappings are release-scoped. */
|
|
2380
|
+
readonly version?: string;
|
|
2381
|
+
/** The number of loaded rows. */
|
|
2382
|
+
readonly count: number;
|
|
2383
|
+
/** Rows keyed by SNOMED source concept id, each ordered by (group, priority). Frozen. */
|
|
2384
|
+
readonly entries: ReadonlyMap<string, readonly ComplexMapEntry[]>;
|
|
2385
|
+
/** The skipped-row warnings surfaced during load, in line order. Frozen; may be empty. */
|
|
2386
|
+
readonly warnings: readonly ComplexMapLoadWarning[];
|
|
2387
|
+
}
|
|
2388
|
+
/** A surfaced, non-fatal complex-map load warning — a malformed refset row that was skipped. */
|
|
2389
|
+
interface ComplexMapLoadWarning {
|
|
2390
|
+
/** The stable diagnostic code for a skipped complex-map row. */
|
|
2391
|
+
readonly code: "TERM_COMPLEX_MAP_MALFORMED_ROW";
|
|
2392
|
+
/** The 1-based source line the fault was found on. */
|
|
2393
|
+
readonly line: number;
|
|
2394
|
+
/** A value-free description of the structural fault. */
|
|
2395
|
+
readonly detail: string;
|
|
2396
|
+
}
|
|
2397
|
+
/**
|
|
2398
|
+
* The **caller-supplied patient context** an `IFA` rule is evaluated against. The resolver evaluates
|
|
2399
|
+
* only against what it is given: a rule needing context absent here surfaces as a
|
|
2400
|
+
* {@link ComplexMapContextRequired}, never a silently-picked branch (roadmap §4.1).
|
|
2401
|
+
*/
|
|
2402
|
+
interface PatientContext {
|
|
2403
|
+
/** The patient's age **in years**, when known (for `IFA … Age …` bands). */
|
|
2404
|
+
readonly ageYears?: number;
|
|
2405
|
+
/** The patient's administrative gender, when known (for gender `IFA` rules). */
|
|
2406
|
+
readonly gender?: "male" | "female";
|
|
2407
|
+
}
|
|
2408
|
+
/**
|
|
2409
|
+
* One map group's resolution — a discriminated outcome. A group resolves to a target, a No-Map, an
|
|
2410
|
+
* ambiguity (candidates surfaced), or a context-required stall; it is **never** a silently-picked
|
|
2411
|
+
* single target when the steward flagged otherwise.
|
|
2412
|
+
*/
|
|
2413
|
+
type ComplexMapGroupResult = ComplexMapGroupResolved | ComplexMapGroupNoMap | ComplexMapContextRequired;
|
|
2414
|
+
/** A group that resolved to a concrete ICD-10-CM target via its winning rule. */
|
|
2415
|
+
interface ComplexMapGroupResolved {
|
|
2416
|
+
/** Discriminant. */
|
|
2417
|
+
readonly outcome: "resolved";
|
|
2418
|
+
/** The 1-based map group. */
|
|
2419
|
+
readonly group: number;
|
|
2420
|
+
/** The resolved ICD-10-CM target coding (drawn verbatim from the map). */
|
|
2421
|
+
readonly target: Coding;
|
|
2422
|
+
/** The steward map category, when supplied (e.g. `447637006`). */
|
|
2423
|
+
readonly category?: string;
|
|
2424
|
+
/** The winning rule's advice, verbatim. */
|
|
2425
|
+
readonly advice: string;
|
|
2426
|
+
/** The winning rule text, verbatim (`"TRUE"`, an `IFA` predicate, …). */
|
|
2427
|
+
readonly rule: string;
|
|
2428
|
+
}
|
|
2429
|
+
/**
|
|
2430
|
+
* A group the steward marked **No-Map** (`447638001` "cannot be classified", or an empty target) — a
|
|
2431
|
+
* typed, surfaced outcome, never a fabricated target.
|
|
2432
|
+
*/
|
|
2433
|
+
interface ComplexMapGroupNoMap {
|
|
2434
|
+
/** Discriminant. */
|
|
2435
|
+
readonly outcome: "no-map";
|
|
2436
|
+
/** The stable diagnostic code. */
|
|
2437
|
+
readonly code: "TERM_CROSSWALK_NO_MAP";
|
|
2438
|
+
/** The 1-based map group. */
|
|
2439
|
+
readonly group: number;
|
|
2440
|
+
/** The steward map category, when supplied (`447638001`, or absent for an empty-target row). */
|
|
2441
|
+
readonly category?: string;
|
|
2442
|
+
/** The advice on the No-Map row, verbatim. */
|
|
2443
|
+
readonly advice: string;
|
|
2444
|
+
}
|
|
2445
|
+
/**
|
|
2446
|
+
* A group whose winning path is a **context-dependent** `IFA` rule (or `447639009`/`447640006`
|
|
2447
|
+
* category) for which the caller supplied **no** matching context. The engine surfaces the rules +
|
|
2448
|
+
* advice and refuses to pick a branch (roadmap §4.1) — the never-fabricate rule for runtime context.
|
|
2449
|
+
*/
|
|
2450
|
+
interface ComplexMapContextRequired {
|
|
2451
|
+
/** Discriminant. */
|
|
2452
|
+
readonly outcome: "context-required";
|
|
2453
|
+
/** The stable diagnostic code. */
|
|
2454
|
+
readonly code: "TERM_CROSSWALK_CONTEXT_REQUIRED";
|
|
2455
|
+
/** The 1-based map group. */
|
|
2456
|
+
readonly group: number;
|
|
2457
|
+
/** The unresolved candidate rules in this group (priority order), each with its target + advice. */
|
|
2458
|
+
readonly rules: readonly ComplexMapEntry[];
|
|
2459
|
+
}
|
|
2460
|
+
/** A successful complex-map application — one resolution **per map group**, in group order. */
|
|
2461
|
+
interface ComplexMapMatched {
|
|
2462
|
+
/** Discriminant: the source concept is present in the map. */
|
|
2463
|
+
readonly mapped: true;
|
|
2464
|
+
/** The SNOMED source concept id, echoed. */
|
|
2465
|
+
readonly source: string;
|
|
2466
|
+
/** One resolution per map group, ordered by group. A source needing N codes has N groups. Frozen. */
|
|
2467
|
+
readonly groups: readonly ComplexMapGroupResult[];
|
|
2468
|
+
}
|
|
2469
|
+
/**
|
|
2470
|
+
* A **No-Map** outcome — the source is present in the map but the steward declares it has no valid
|
|
2471
|
+
* target ("cannot be classified with available data"). A first-class typed result, **never** a
|
|
2472
|
+
* fabricated target (roadmap §4.1). Distinct from {@link CrosswalkUnmapped} (source absent entirely).
|
|
2473
|
+
*/
|
|
2474
|
+
interface CrosswalkNoMap {
|
|
2475
|
+
/** Discriminant: the source did not map. */
|
|
2476
|
+
readonly mapped: false;
|
|
2477
|
+
/** Sub-discriminant: this is an authored No-Map, not a plain absence. */
|
|
2478
|
+
readonly noMap: true;
|
|
2479
|
+
/** The stable diagnostic code. */
|
|
2480
|
+
readonly code: "TERM_CROSSWALK_NO_MAP";
|
|
2481
|
+
/** The source code, echoed. */
|
|
2482
|
+
readonly source: string;
|
|
2483
|
+
/** The steward flags/advice that declared the No-Map, carried verbatim. */
|
|
2484
|
+
readonly entries: readonly GemEntry[];
|
|
2485
|
+
}
|
|
2486
|
+
/**
|
|
2487
|
+
* An **unmapped** outcome — the source code is **not present in the map at all** (distinct from an
|
|
2488
|
+
* authored No-Map). Surfaced as a typed result, never a guessed target and never a silent success.
|
|
2489
|
+
*/
|
|
2490
|
+
interface CrosswalkUnmapped {
|
|
2491
|
+
/** Discriminant: the source did not map. */
|
|
2492
|
+
readonly mapped: false;
|
|
2493
|
+
/** Sub-discriminant: a plain absence, not an authored No-Map. */
|
|
2494
|
+
readonly noMap: false;
|
|
2495
|
+
/** The stable diagnostic code. */
|
|
2496
|
+
readonly code: "TERM_CROSSWALK_UNMAPPED";
|
|
2497
|
+
/** The source code, echoed. */
|
|
2498
|
+
readonly source: string;
|
|
2499
|
+
}
|
|
2500
|
+
/** The result of {@link ../crosswalk/gems.applyGem}: a match, an authored No-Map, or an absence. */
|
|
2501
|
+
type GemApplyResult = GemMatched | CrosswalkNoMap | CrosswalkUnmapped;
|
|
2502
|
+
/**
|
|
2503
|
+
* The result of {@link ../crosswalk/complex-map.applyComplexMap}: a per-group match, or a typed
|
|
2504
|
+
* absence when the source concept is not in the map.
|
|
2505
|
+
*/
|
|
2506
|
+
type ComplexMapApplyResult = ComplexMapMatched | CrosswalkUnmapped;
|
|
2507
|
+
|
|
2508
|
+
/**
|
|
2509
|
+
* The CMS **ICD-9 ↔ ICD-10 GEMs** (General Equivalence Mappings) loader + applier (roadmap Phase 5).
|
|
2510
|
+
*
|
|
2511
|
+
* The GEMs are **CMS/NCHS public-domain** (US-government work) reference mappings between the ICD-9-CM
|
|
2512
|
+
* and ICD-10-CM diagnosis classifications. CMS is emphatic about what they are — *"GEMs are not
|
|
2513
|
+
* crosswalks. They are reference mappings… the raw material from which providers… derive specific
|
|
2514
|
+
* applied mappings"* — so this resolver treats a GEM hit as a **candidate**, never an equivalence:
|
|
2515
|
+
*
|
|
2516
|
+
* - **Approximate by default.** Position-1 flag: most entries are approximate (source and target are
|
|
2517
|
+
* not identical in meaning). The flag rides through; the engine asserts nothing beyond it.
|
|
2518
|
+
* - **No-Map is first-class.** A source with no valid target carries the `NoDx` sentinel (and the
|
|
2519
|
+
* position-2 flag); {@link applyGem} returns a typed {@link CrosswalkNoMap}, never a guessed code.
|
|
2520
|
+
* - **1:many, never collapsed.** A source with several targets returns the **whole** candidate set.
|
|
2521
|
+
* - **Combination clusters.** A combination source (position-3 flag) maps to a *combination* of
|
|
2522
|
+
* targets grouped by scenario (position 4) and choice list (position 5); the result surfaces that
|
|
2523
|
+
* structure so a caller can build valid clusters, never a single fabricated target.
|
|
2524
|
+
* - **Directional, never inverted.** The forward (9→10) and backward (10→9) files are **separate,
|
|
2525
|
+
* non-inverse** artifacts; {@link invertGem} refuses to synthesize one from the other.
|
|
2526
|
+
*
|
|
2527
|
+
* The engine ships **no GEM content**: the caller supplies the public-domain GEM file (BYO — roadmap
|
|
2528
|
+
* §2/§5). Bundling the public-domain packs is the separate content-packs phase.
|
|
2529
|
+
*
|
|
2530
|
+
* @packageDocumentation
|
|
2531
|
+
*/
|
|
2532
|
+
|
|
2533
|
+
/**
|
|
2534
|
+
* A raw GEM source file, plus the direction it maps and its release version. BYO — the caller
|
|
2535
|
+
* supplies the public-domain CMS GEM file content.
|
|
2536
|
+
*/
|
|
2537
|
+
interface GemSource {
|
|
2538
|
+
/** The raw GEM file content (whitespace-delimited: `source target flags`, one entry per line). */
|
|
2539
|
+
readonly content: string;
|
|
2540
|
+
/** Which direction this file maps (`"9-to-10"` for `…_I9gem.txt`, `"10-to-9"` for `…_I10gem.txt`). */
|
|
2541
|
+
readonly direction: GemDirection;
|
|
2542
|
+
/** The GEM release/version (e.g. `"2018"`), when known — mappings are release-scoped. */
|
|
2543
|
+
readonly version?: string;
|
|
2544
|
+
}
|
|
2545
|
+
/**
|
|
2546
|
+
* Load a CMS GEM file into an immutable {@link GemMap}, keyed by source code.
|
|
2547
|
+
*
|
|
2548
|
+
* Liberal on load: a structurally unusable *line* (not three whitespace-delimited fields, or a
|
|
2549
|
+
* malformed 5-digit flag) is **skipped and surfaced** as a {@link GemLoadWarning}, never a
|
|
2550
|
+
* partially-parsed entry. An unusable *source* is not possible here (an all-malformed file loads to an
|
|
2551
|
+
* empty map with warnings) — there is no fatal for GEM load. Ships **no** GEM content: the entries are
|
|
2552
|
+
* the caller's public-domain file (roadmap §5).
|
|
2553
|
+
*
|
|
2554
|
+
* @param source - The {@link GemSource} (raw content + direction + version).
|
|
2555
|
+
* @returns The immutable {@link GemMap}.
|
|
2556
|
+
* @example
|
|
2557
|
+
* ```ts
|
|
2558
|
+
* import { loadGems, applyGem } from "@cosyte/terminology";
|
|
2559
|
+
*
|
|
2560
|
+
* // Synthetic rows in the CMS GEM format (source target flags): an exact map and a No-Map.
|
|
2561
|
+
* const gems = loadGems({
|
|
2562
|
+
* direction: "9-to-10",
|
|
2563
|
+
* version: "2018",
|
|
2564
|
+
* content: "0010 A000 00000\nV290 NoDx 11000\n",
|
|
2565
|
+
* });
|
|
2566
|
+
* applyGem(gems, "0010").mapped; // => true
|
|
2567
|
+
* ```
|
|
2568
|
+
*/
|
|
2569
|
+
declare function loadGems(source: GemSource): GemMap;
|
|
2570
|
+
/**
|
|
2571
|
+
* Apply a loaded GEM map to a source code, in the map's authored direction only.
|
|
2572
|
+
*
|
|
2573
|
+
* Returns the **full** candidate set (never collapsed to one), with each entry's steward flags; a
|
|
2574
|
+
* combination source additionally surfaces its scenario/choice-list structure. A source the steward
|
|
2575
|
+
* marked No-Map (`NoDx` sentinel) is a typed {@link CrosswalkNoMap}; a source **absent** from the map
|
|
2576
|
+
* is a typed {@link CrosswalkUnmapped} — the two are distinct, and neither is ever a fabricated code.
|
|
2577
|
+
*
|
|
2578
|
+
* @param map - A {@link GemMap} from {@link loadGems}.
|
|
2579
|
+
* @param source - The source code (in the map's source classification).
|
|
2580
|
+
* @returns A {@link GemApplyResult}.
|
|
2581
|
+
* @example
|
|
2582
|
+
* ```ts
|
|
2583
|
+
* import { loadGems, applyGem } from "@cosyte/terminology";
|
|
2584
|
+
*
|
|
2585
|
+
* const gems = loadGems({ direction: "9-to-10", content: "V290 NoDx 11000\n" });
|
|
2586
|
+
* const r = applyGem(gems, "V290");
|
|
2587
|
+
* r.mapped; // => false
|
|
2588
|
+
* ```
|
|
2589
|
+
*/
|
|
2590
|
+
declare function applyGem(map: GemMap, source: string): GemApplyResult;
|
|
2591
|
+
/**
|
|
2592
|
+
* Refuse to invert a directional GEM map. **Always throws** {@link FATAL_CODES.TERM_MAP_NOT_INVERTIBLE}.
|
|
2593
|
+
*
|
|
2594
|
+
* The never-invert invariant made explicit and discoverable: a forward GEM (9→10) is **not** the
|
|
2595
|
+
* inverse of the backward GEM (10→9) — CMS ships them as separate artifacts and *"forward and
|
|
2596
|
+
* backward mappings are not simply the reverse"* (roadmap §4.1). To resolve the other direction, load
|
|
2597
|
+
* the other file with {@link loadGems}; the engine never synthesizes an inverse.
|
|
2598
|
+
*
|
|
2599
|
+
* @param map - The map an inversion was (incorrectly) requested for.
|
|
2600
|
+
* @returns Never — always throws.
|
|
2601
|
+
* @throws {TerminologyError} `TERM_MAP_NOT_INVERTIBLE`, always.
|
|
2602
|
+
* @example
|
|
2603
|
+
* ```ts runnable throws
|
|
2604
|
+
* import { loadGems, invertGem } from "@cosyte/terminology";
|
|
2605
|
+
*
|
|
2606
|
+
* const gems = loadGems({ direction: "9-to-10", content: "0010 A000 00000\n" });
|
|
2607
|
+
* invertGem(gems); // throws TERM_MAP_NOT_INVERTIBLE
|
|
2608
|
+
* ```
|
|
2609
|
+
*/
|
|
2610
|
+
declare function invertGem(map: GemMap): never;
|
|
2611
|
+
|
|
2612
|
+
/**
|
|
2613
|
+
* The **SNOMED CT → ICD-10-CM complex-map** resolver (roadmap Phase 5) — a rule-based, **BYO**
|
|
2614
|
+
* resolver over the caller-supplied complex/extended-map refset. **No SNOMED CT content is bundled**
|
|
2615
|
+
* (roadmap §5): the engine ships the rule machinery; the caller supplies the map rows from their own
|
|
2616
|
+
* SNOMED-licensed release.
|
|
2617
|
+
*
|
|
2618
|
+
* The map is *"semi-automated"* by the steward's own statement — 1:many, context-dependent, with
|
|
2619
|
+
* first-class No-Map states. The resolver honours that:
|
|
2620
|
+
*
|
|
2621
|
+
* - **Map groups are an AND.** A source needing a manifestation *and* an etiology code has two groups;
|
|
2622
|
+
* {@link applyComplexMap} resolves each independently and returns one result per group.
|
|
2623
|
+
* - **Priorities are an if-then-else.** Within a group, rules run in ascending priority; the first
|
|
2624
|
+
* whose `IFA` predicate the caller's context satisfies wins.
|
|
2625
|
+
* - **Context it wasn't given, it never guesses.** If deciding a group requires an `IFA` predicate
|
|
2626
|
+
* (an age band, a gender) the caller left unspecified, the group is a typed
|
|
2627
|
+
* {@link ComplexMapContextRequired} carrying the candidate rules + advice — never a silently-picked
|
|
2628
|
+
* branch (roadmap §4.1).
|
|
2629
|
+
* - **No-Map is typed.** A `447638001` "cannot be classified" row (or an empty target) is a typed
|
|
2630
|
+
* No-Map, never a fabricated code.
|
|
2631
|
+
* - **Advice rides through verbatim.** `CONSIDER LATERALITY`, `EPISODE OF CARE INFORMATION NEEDED`,
|
|
2632
|
+
* manifestation-code advice — carried untouched to the caller.
|
|
2633
|
+
* - **Never inverted.** The map is applied SNOMED→ICD-10-CM only; the reverse is a separate artifact.
|
|
2634
|
+
*
|
|
2635
|
+
* @packageDocumentation
|
|
2636
|
+
*/
|
|
2637
|
+
|
|
2638
|
+
/**
|
|
2639
|
+
* The discriminated input {@link loadComplexMap} accepts — already-structured refset rows, or a raw
|
|
2640
|
+
* RF2 tab-delimited extended-map refset the caller exported from their SNOMED release.
|
|
2641
|
+
*/
|
|
2642
|
+
type ComplexMapInput = {
|
|
2643
|
+
readonly format: "rows";
|
|
2644
|
+
readonly rows: readonly ComplexMapEntry[];
|
|
2645
|
+
readonly version?: string;
|
|
2646
|
+
} | {
|
|
2647
|
+
readonly format: "rf2";
|
|
2648
|
+
readonly content: string;
|
|
2649
|
+
readonly version?: string;
|
|
2650
|
+
};
|
|
2651
|
+
/**
|
|
2652
|
+
* Load a SNOMED→ICD-10-CM complex map from **caller-supplied** rows or a raw RF2 refset into an
|
|
2653
|
+
* immutable {@link ComplexMap}, keyed by source concept and ordered by (group, priority).
|
|
2654
|
+
*
|
|
2655
|
+
* Liberal on load: a structurally unusable RF2 *row* is skipped and surfaced as a warning; only an
|
|
2656
|
+
* unusable *source* (empty content, header missing required columns) is a fatal
|
|
2657
|
+
* {@link FATAL_CODES.TERM_CROSSWALK_MALFORMED}. Ships **no** SNOMED content — the rows are the
|
|
2658
|
+
* caller's, under their own licence (roadmap §5).
|
|
2659
|
+
*
|
|
2660
|
+
* @param input - Structured `rows`, or a raw RF2 extended-map refset `content`.
|
|
2661
|
+
* @returns The immutable {@link ComplexMap}.
|
|
2662
|
+
* @throws {TerminologyError} `TERM_CROSSWALK_MALFORMED` when the source is structurally unusable.
|
|
2663
|
+
* @example
|
|
2664
|
+
* ```ts
|
|
2665
|
+
* import { loadComplexMap } from "@cosyte/terminology";
|
|
2666
|
+
*
|
|
2667
|
+
* const map = loadComplexMap({
|
|
2668
|
+
* format: "rows",
|
|
2669
|
+
* rows: [
|
|
2670
|
+
* { source: "195967001", group: 1, priority: 1, rule: "TRUE", advice: "ALWAYS J45.909", target: "J45.909", category: "447637006" },
|
|
2671
|
+
* ],
|
|
2672
|
+
* });
|
|
2673
|
+
* map.count; // => 1
|
|
2674
|
+
* ```
|
|
2675
|
+
*/
|
|
2676
|
+
declare function loadComplexMap(input: ComplexMapInput): ComplexMap;
|
|
2677
|
+
/**
|
|
2678
|
+
* Apply a loaded SNOMED→ICD-10-CM complex map to a source concept and patient context.
|
|
2679
|
+
*
|
|
2680
|
+
* Resolves **each map group** independently (groups are an AND — a source needing multiple ICD-10-CM
|
|
2681
|
+
* codes has multiple groups) by running its rules in priority order against `context`. A group whose
|
|
2682
|
+
* decision needs `IFA` context the caller did not supply is a typed
|
|
2683
|
+
* {@link ComplexMapContextRequired}; a `447638001` row is a typed No-Map; every result carries the
|
|
2684
|
+
* steward's advice verbatim. **Never** fabricates a target, **never** inverts the map.
|
|
2685
|
+
*
|
|
2686
|
+
* @param map - A {@link ComplexMap} from {@link loadComplexMap}.
|
|
2687
|
+
* @param source - The SNOMED CT source concept id.
|
|
2688
|
+
* @param context - The caller-supplied {@link PatientContext} (age / gender). Optional; a missing
|
|
2689
|
+
* field that a rule needs yields a `context-required` group, never a guessed branch.
|
|
2690
|
+
* @returns A {@link ComplexMapApplyResult} — per-group resolutions, or a typed absence.
|
|
2691
|
+
* @example
|
|
2692
|
+
* ```ts
|
|
2693
|
+
* import { loadComplexMap, applyComplexMap } from "@cosyte/terminology";
|
|
2694
|
+
*
|
|
2695
|
+
* const map = loadComplexMap({
|
|
2696
|
+
* format: "rows",
|
|
2697
|
+
* rows: [
|
|
2698
|
+
* { source: "72098002", group: 1, priority: 1, rule: "IFA 248152002 | Female (finding) |", advice: "MAP IS CONTEXT DEPENDENT FOR GENDER", target: "O00.9", category: "447639009" },
|
|
2699
|
+
* { source: "72098002", group: 1, priority: 2, rule: "OTHERWISE TRUE", advice: "CANNOT BE CLASSIFIED", category: "447638001" },
|
|
2700
|
+
* ],
|
|
2701
|
+
* });
|
|
2702
|
+
* const r = applyComplexMap(map, "72098002", { gender: "female" });
|
|
2703
|
+
* r.mapped; // => true
|
|
2704
|
+
* ```
|
|
2705
|
+
*/
|
|
2706
|
+
declare function applyComplexMap(map: ComplexMap, source: string, context?: PatientContext): ComplexMapApplyResult;
|
|
2707
|
+
/** The SNOMED CT source-system URI a complex-map source concept is drawn from (re-exported for docs). */
|
|
2708
|
+
declare const COMPLEX_MAP_SOURCE_SYSTEM = "http://snomed.info/sct";
|
|
2709
|
+
|
|
2710
|
+
/**
|
|
2711
|
+
* Steward-defined **map category** and **map-relationship** identifiers the crosswalk resolvers carry
|
|
2712
|
+
* through verbatim — the SNOMED CT complex-map `mapCategoryId` concepts and the map-correlation
|
|
2713
|
+
* concepts. These are **published SNOMED CT metadata identifiers** (concept ids in the SNOMED CT
|
|
2714
|
+
* Model Component module), encoded here as identity facts (like the code-system OID↔URI facts in
|
|
2715
|
+
* {@link ../systems/registry}); **no SNOMED CT content is bundled** — a caller still supplies the map
|
|
2716
|
+
* refset itself under their own SNOMED licence (roadmap §5, the BYO posture).
|
|
2717
|
+
*
|
|
2718
|
+
* The category on a resolved SNOMED→ICD-10-CM map row is the steward telling the caller *how much to
|
|
2719
|
+
* trust the target*: a `447638001` "cannot be classified" row is a **No-Map**, a `447640006`
|
|
2720
|
+
* "ambiguous" row must surface its candidates rather than pick one, and a `447639009`
|
|
2721
|
+
* "context-dependent" row needs runtime patient context. The resolver never collapses these to a
|
|
2722
|
+
* single confident target (roadmap §4.1).
|
|
2723
|
+
*
|
|
2724
|
+
* @packageDocumentation
|
|
2725
|
+
*/
|
|
2726
|
+
/**
|
|
2727
|
+
* The SNOMED CT complex-map **map category** concept ids — the `mapCategoryId` column of the
|
|
2728
|
+
* SNOMED→ICD-10-CM extended-map refset. Encoded as published identity facts; carried through a
|
|
2729
|
+
* resolution verbatim so a caller sees the steward's own trust signal.
|
|
2730
|
+
*
|
|
2731
|
+
* @example
|
|
2732
|
+
* ```ts
|
|
2733
|
+
* import { MAP_CATEGORIES } from "@cosyte/terminology";
|
|
2734
|
+
*
|
|
2735
|
+
* MAP_CATEGORIES.NO_MAP; // => "447638001"
|
|
2736
|
+
* ```
|
|
2737
|
+
*/
|
|
2738
|
+
declare const MAP_CATEGORIES: {
|
|
2739
|
+
/** `447637006` — "Map source concept is properly classified" (a normal, usable target). */
|
|
2740
|
+
readonly PROPERLY_CLASSIFIED: "447637006";
|
|
2741
|
+
/** `447638001` — "Map source concept cannot be classified with available data" — a **No-Map**. */
|
|
2742
|
+
readonly NO_MAP: "447638001";
|
|
2743
|
+
/** `447639009` — "Map of source concept is context dependent" (needs runtime patient context). */
|
|
2744
|
+
readonly CONTEXT_DEPENDENT: "447639009";
|
|
2745
|
+
/** `447640006` — "Source concept is ambiguous" — surface candidates, never pick one. */
|
|
2746
|
+
readonly AMBIGUOUS: "447640006";
|
|
2747
|
+
};
|
|
2748
|
+
/** A value from {@link MAP_CATEGORIES} — the SNOMED map-category id carried on a resolution. */
|
|
2749
|
+
type MapCategoryId = (typeof MAP_CATEGORIES)[keyof typeof MAP_CATEGORIES];
|
|
2750
|
+
/**
|
|
2751
|
+
* The set of {@link MAP_CATEGORIES} ids the resolver treats as a **No-Map** (a typed, surfaced
|
|
2752
|
+
* "cannot be classified", never a fabricated target).
|
|
2753
|
+
*/
|
|
2754
|
+
declare const NO_MAP_CATEGORIES: ReadonlySet<string>;
|
|
2755
|
+
/**
|
|
2756
|
+
* The canonical code-system URI for **ICD-10-CM** — the target system of both crosswalks. A published
|
|
2757
|
+
* identity fact (mirrors {@link ../systems/registry.SYSTEM_IDENTITIES}); attached to a resolved
|
|
2758
|
+
* target {@link ../common/coding.Coding} so a translation is self-describing.
|
|
2759
|
+
*/
|
|
2760
|
+
declare const ICD10CM_SYSTEM = "http://hl7.org/fhir/sid/icd-10-cm";
|
|
2761
|
+
/**
|
|
2762
|
+
* The canonical code-system URI for **ICD-9-CM** (the legacy diagnosis classification the GEMs bridge
|
|
2763
|
+
* to/from). A published identity fact; attached to a GEM target/source coding.
|
|
2764
|
+
*/
|
|
2765
|
+
declare const ICD9CM_SYSTEM = "http://hl7.org/fhir/sid/icd-9-cm";
|
|
2766
|
+
/**
|
|
2767
|
+
* The canonical code-system URI for **SNOMED CT** — the source system of the SNOMED→ICD-10-CM complex
|
|
2768
|
+
* map. Recognised by URI only; **no SNOMED CT content is bundled** (roadmap §5).
|
|
2769
|
+
*/
|
|
2770
|
+
declare const SNOMEDCT_SYSTEM = "http://snomed.info/sct";
|
|
2771
|
+
|
|
2772
|
+
/**
|
|
2773
|
+
* The types for the **RxNorm drug relationship graph** (roadmap Phase 6) — the ingredient / brand /
|
|
2774
|
+
* clinical-drug / dose-form graph navigated over a **caller-supplied** RxNorm RRF release.
|
|
2775
|
+
*
|
|
2776
|
+
* RxNorm is the US NLM's normalized drug nomenclature. Its concepts (`RXCUI`s) are typed by **term
|
|
2777
|
+
* type** ({@link TermType}: `IN` ingredient, `SCD` semantic clinical drug, `SBD` semantic branded
|
|
2778
|
+
* drug, `BN` brand name, `DF` dose form, …) and wired together by **directed relationships** carried
|
|
2779
|
+
* in `RXNREL.RRF` and labeled by a `RELA` (`has_ingredient`, `tradename_of`, `has_dose_form`,
|
|
2780
|
+
* `consists_of`, …). The graph these form is the whole point of this phase: resolve an `SCD` to its
|
|
2781
|
+
* ingredients, a branded drug to its generic, a drug to its dose form.
|
|
2782
|
+
*
|
|
2783
|
+
* **The direction convention is a documented medication-safety trap** (roadmap §4.2 / §10 Q5), and it
|
|
2784
|
+
* is grounded here firsthand on the NLM RxNorm Technical Documentation (§12.7) and the UMLS Reference
|
|
2785
|
+
* Manual: *"the direction of REL — the relationship which the SECOND concept or atom (with … RXCUI2
|
|
2786
|
+
* …) HAS TO the FIRST concept or atom (with … RXCUI1 …)."* So every `RXNREL` row `(RXCUI1, …, RELA,
|
|
2787
|
+
* RXCUI2, …)` is read **`RXCUI2 ⟶RELA⟶ RXCUI1`** and normalized at load into a
|
|
2788
|
+
* {@link RxNormEdge} with `subject = RXCUI2`, `predicate = RELA`, `object = RXCUI1`. Navigation only
|
|
2789
|
+
* ever follows **authored** edges by their predicate; the engine **never synthesizes an inverse edge**
|
|
2790
|
+
* (RxNorm already ships both directions of an asymmetric relationship as separate rows) and **never
|
|
2791
|
+
* fabricates** an `RXCUI` or a relationship absent from the loaded data — an absent concept is a typed
|
|
2792
|
+
* {@link RxNormUnknown}, an unresolvable NDC a typed {@link NdcUnmapped}, never a guess.
|
|
2793
|
+
*
|
|
2794
|
+
* **No RxNorm content is bundled** (roadmap §5): the engine ships the graph *machinery*; the caller
|
|
2795
|
+
* supplies the RRF release (the public-domain Current Prescribable Content, or the full BYO release).
|
|
2796
|
+
* Bundling the public-domain Prescribable pack is the separate content-packs phase (Phase 7).
|
|
2797
|
+
*
|
|
2798
|
+
* @packageDocumentation
|
|
2799
|
+
*/
|
|
2800
|
+
/**
|
|
2801
|
+
* A RxNorm **term type** (`TTY`) — the kind of drug concept an `RXCUI` names. These are the
|
|
2802
|
+
* `RXNCONSO.RRF` `TTY` values relevant to the drug graph, grounded on the NLM RxNorm Technical
|
|
2803
|
+
* Documentation (Appendix 5). Carried verbatim on a {@link RxNormConcept}; the engine classifies but
|
|
2804
|
+
* never re-labels a concept.
|
|
2805
|
+
*/
|
|
2806
|
+
type TermType = "IN" | "PIN" | "MIN" | "BN" | "SCDC" | "SBDC" | "SCD" | "SBD" | "SCDG" | "SBDG" | "GPCK" | "BPCK" | "DF" | "DFG" | "PSN" | "SY" | "TMSY" | "ET";
|
|
2807
|
+
/**
|
|
2808
|
+
* One RxNorm concept (`RXCUI`) as loaded from `RXNCONSO.RRF` — its normalized name and term type.
|
|
2809
|
+
* Built from the concept's `SAB=RXNORM` atom (the normalized form); the verbatim `RXCUI`, `TTY`, and
|
|
2810
|
+
* `STR` (name) ride through untouched.
|
|
2811
|
+
*/
|
|
2812
|
+
interface RxNormConcept {
|
|
2813
|
+
/** The RxNorm concept unique identifier (`RXCUI`), verbatim. */
|
|
2814
|
+
readonly rxcui: string;
|
|
2815
|
+
/** The concept's term type ({@link TermType}), verbatim from `RXNCONSO.TTY`. */
|
|
2816
|
+
readonly tty: TermType;
|
|
2817
|
+
/** The concept's normalized name (`RXNCONSO.STR`), verbatim. */
|
|
2818
|
+
readonly name: string;
|
|
2819
|
+
/** `true` when the source atom is suppressed (`RXNCONSO.SUPPRESS` ≠ `N`) — surfaced, never hidden. */
|
|
2820
|
+
readonly suppressed: boolean;
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* One directed relationship edge, normalized from an `RXNREL.RRF` row under the documented direction
|
|
2824
|
+
* convention (see the module doc): the row `(RXCUI1, …, RELA, RXCUI2, …)` becomes
|
|
2825
|
+
* `subject = RXCUI2`, `predicate = RELA`, `object = RXCUI1` — read **`subject ⟶predicate⟶ object`**.
|
|
2826
|
+
*/
|
|
2827
|
+
interface RxNormEdge {
|
|
2828
|
+
/** The subject `RXCUI` — `RXCUI2` in the source row. The concept the relationship is *from*. */
|
|
2829
|
+
readonly subject: string;
|
|
2830
|
+
/** The relationship label (`RXNREL.RELA`, e.g. `has_ingredient`), verbatim. */
|
|
2831
|
+
readonly predicate: string;
|
|
2832
|
+
/** The object `RXCUI` — `RXCUI1` in the source row. The concept the relationship is *to*. */
|
|
2833
|
+
readonly object: string;
|
|
2834
|
+
}
|
|
2835
|
+
/**
|
|
2836
|
+
* A resolved **NDC → RXCUI** mapping (from an `RXNSAT.RRF` `ATN=NDC` attribute), carrying the
|
|
2837
|
+
* temporal {@link NdcStatus} and the **as-of release** it is valid for. NDC↔RXCUI is many:1 and
|
|
2838
|
+
* changes across releases (roadmap §4.2), so a resolution is never timeless — the release rides
|
|
2839
|
+
* through on {@link asOf}.
|
|
2840
|
+
*/
|
|
2841
|
+
interface NdcResolution {
|
|
2842
|
+
/** Discriminant: the NDC resolved to a concept in the loaded release. */
|
|
2843
|
+
readonly resolved: true;
|
|
2844
|
+
/** The 11-digit NDC (as supplied), echoed. */
|
|
2845
|
+
readonly ndc: string;
|
|
2846
|
+
/** The `RXCUI` the NDC attribute is attached to in the loaded release. */
|
|
2847
|
+
readonly rxcui: string;
|
|
2848
|
+
/** The temporal status of the NDC ({@link NdcStatus}). */
|
|
2849
|
+
readonly status: NdcStatus;
|
|
2850
|
+
/** The release version the mapping is valid **as of**, when the graph carries one. */
|
|
2851
|
+
readonly asOf?: string;
|
|
2852
|
+
}
|
|
2853
|
+
/**
|
|
2854
|
+
* The temporal status of an NDC↔RXCUI mapping (RxNav `getNDCStatus`: `ACTIVE` / `OBSOLETE` / `ALIEN`
|
|
2855
|
+
* / `UNKNOWN`). An NDC found in the loaded release's `RXNSAT` attributes is `active` **as of that
|
|
2856
|
+
* release**; `obsolete` / `alien` come from the RxNav NDC-history data (a differential / BYO source,
|
|
2857
|
+
* not the base RRF concept files) and are surfaced only when the caller supplies them — the engine
|
|
2858
|
+
* **never fabricates** a non-current status (roadmap §4.2).
|
|
2859
|
+
*/
|
|
2860
|
+
type NdcStatus = "active" | "obsolete" | "alien" | "unknown";
|
|
2861
|
+
/** An NDC that could not be resolved to any `RXCUI` in the loaded release — a typed absence. */
|
|
2862
|
+
interface NdcUnmapped {
|
|
2863
|
+
/** Discriminant: the NDC is not present in the loaded release. */
|
|
2864
|
+
readonly resolved: false;
|
|
2865
|
+
/** The stable diagnostic code. */
|
|
2866
|
+
readonly code: "TERM_RXNORM_NDC_UNMAPPED";
|
|
2867
|
+
/** The NDC (as supplied), echoed. */
|
|
2868
|
+
readonly ndc: string;
|
|
2869
|
+
}
|
|
2870
|
+
/** The result of {@link ../rxnorm/navigate.resolveNdc}: a resolution or a typed absence. */
|
|
2871
|
+
type NdcResult = NdcResolution | NdcUnmapped;
|
|
2872
|
+
/**
|
|
2873
|
+
* A successful graph-navigation result — the concept, the predicate followed, and the **full** set of
|
|
2874
|
+
* related concepts drawn verbatim from the loaded edges. `targets` may be **empty**: that is an
|
|
2875
|
+
* honest, data-grounded "the release has no such edge for this concept", never a fabricated relation.
|
|
2876
|
+
*/
|
|
2877
|
+
interface RxNormRelated {
|
|
2878
|
+
/** Discriminant: the source `RXCUI` is present in the graph. */
|
|
2879
|
+
readonly found: true;
|
|
2880
|
+
/** The source `RXCUI`, echoed. */
|
|
2881
|
+
readonly rxcui: string;
|
|
2882
|
+
/** The relationship predicate(s) that were followed. */
|
|
2883
|
+
readonly predicates: readonly string[];
|
|
2884
|
+
/** The related concepts, in edge order, each drawn verbatim from the loaded graph. May be empty. */
|
|
2885
|
+
readonly targets: readonly RxNormConcept[];
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* A typed **unknown** navigation result — the queried `RXCUI` is **absent from the loaded graph**. A
|
|
2889
|
+
* first-class outcome, never a fabricated concept or an empty success (the never-fabricate invariant,
|
|
2890
|
+
* applied to the drug graph — roadmap §4.2). Distinct from a {@link RxNormRelated} with empty
|
|
2891
|
+
* `targets`, which means "present, but no such relationship".
|
|
2892
|
+
*/
|
|
2893
|
+
interface RxNormUnknown {
|
|
2894
|
+
/** Discriminant: the source `RXCUI` is not in the graph. */
|
|
2895
|
+
readonly found: false;
|
|
2896
|
+
/** The stable diagnostic code. */
|
|
2897
|
+
readonly code: "TERM_RXNORM_UNKNOWN_RXCUI";
|
|
2898
|
+
/** The queried `RXCUI`, echoed. */
|
|
2899
|
+
readonly rxcui: string;
|
|
2900
|
+
}
|
|
2901
|
+
/** The result of a graph-navigation query: a match (possibly empty) or a typed unknown source. */
|
|
2902
|
+
type RxNormNavResult = RxNormRelated | RxNormUnknown;
|
|
2903
|
+
/**
|
|
2904
|
+
* One **approximate**-match candidate (the opt-in, never-default path — roadmap §4.2). Every field is
|
|
2905
|
+
* marked so a caller can never mistake it for an exact identity: {@link approximate} is always `true`
|
|
2906
|
+
* and {@link score} is the labeled, derived similarity. The concept itself is drawn verbatim from the
|
|
2907
|
+
* loaded release.
|
|
2908
|
+
*/
|
|
2909
|
+
interface RxNormApproximateMatch {
|
|
2910
|
+
/** Always `true` — this is a similarity match, never an exact code resolution. */
|
|
2911
|
+
readonly approximate: true;
|
|
2912
|
+
/** The matched concept, verbatim from the loaded release. */
|
|
2913
|
+
readonly concept: RxNormConcept;
|
|
2914
|
+
/** A derived, labeled similarity score in `[0, 1]` (token overlap). Never a clinical assertion. */
|
|
2915
|
+
readonly score: number;
|
|
2916
|
+
}
|
|
2917
|
+
/**
|
|
2918
|
+
* A surfaced, non-fatal RxNorm load warning — a malformed `RXNCONSO` / `RXNREL` / `RXNSAT` row that
|
|
2919
|
+
* was **skipped**, reported with its line number and a value-free structural reason (liberal on load,
|
|
2920
|
+
* roadmap §6). Never echoes a field value.
|
|
2921
|
+
*/
|
|
2922
|
+
interface RxNormLoadWarning {
|
|
2923
|
+
/** The stable diagnostic code for a skipped RxNorm row. */
|
|
2924
|
+
readonly code: "TERM_RXNORM_MALFORMED_ROW";
|
|
2925
|
+
/** Which source file the fault was in. */
|
|
2926
|
+
readonly file: "RXNCONSO" | "RXNREL" | "RXNSAT";
|
|
2927
|
+
/** The 1-based source line the fault was found on. */
|
|
2928
|
+
readonly line: number;
|
|
2929
|
+
/** A value-free description of the structural fault. */
|
|
2930
|
+
readonly detail: string;
|
|
2931
|
+
}
|
|
2932
|
+
/**
|
|
2933
|
+
* A loaded, immutable RxNorm drug graph: concepts keyed by `RXCUI`, edges indexed by subject, and the
|
|
2934
|
+
* NDC→RXCUI attribute index — all frozen. Applied only in the authored edge direction; never inverted.
|
|
2935
|
+
*/
|
|
2936
|
+
interface RxNormGraph {
|
|
2937
|
+
/** The release version (e.g. `"RXNORM_2026AA"`), when supplied — mappings are release-scoped. */
|
|
2938
|
+
readonly version?: string;
|
|
2939
|
+
/** Concepts keyed by `RXCUI` (first `SAB=RXNORM` atom per `RXCUI` wins). Frozen. */
|
|
2940
|
+
readonly concepts: ReadonlyMap<string, RxNormConcept>;
|
|
2941
|
+
/** Directed edges indexed by `subject` `RXCUI`, each list in file order. Frozen. */
|
|
2942
|
+
readonly edges: ReadonlyMap<string, readonly RxNormEdge[]>;
|
|
2943
|
+
/** NDC → { rxcui, status } index from `RXNSAT` `ATN=NDC` attributes. Frozen; may be empty. */
|
|
2944
|
+
readonly ndcs: ReadonlyMap<string, {
|
|
2945
|
+
readonly rxcui: string;
|
|
2946
|
+
readonly status: NdcStatus;
|
|
2947
|
+
}>;
|
|
2948
|
+
/** The number of loaded concepts. */
|
|
2949
|
+
readonly conceptCount: number;
|
|
2950
|
+
/** The number of loaded edges. */
|
|
2951
|
+
readonly edgeCount: number;
|
|
2952
|
+
/** The skipped-row warnings surfaced during load, in file/line order. Frozen; may be empty. */
|
|
2953
|
+
readonly warnings: readonly RxNormLoadWarning[];
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
/**
|
|
2957
|
+
* The **RxNorm drug graph loader** (roadmap Phase 6) — build an immutable {@link RxNormGraph} from a
|
|
2958
|
+
* **caller-supplied** RxNorm RRF release (`RXNCONSO.RRF` + `RXNREL.RRF`, and optionally `RXNSAT.RRF`
|
|
2959
|
+
* for NDC attributes). Reuses the shared, zero-dep {@link ../codesystem/rrf.parseRrfLine} (pipe split,
|
|
2960
|
+
* reserved trailing pipe) — the same RRF reader the CodeSystem layer uses.
|
|
2961
|
+
*
|
|
2962
|
+
* **Column layouts, grounded firsthand** on the NLM RxNorm Technical Documentation (§ file
|
|
2963
|
+
* descriptions):
|
|
2964
|
+
* - `RXNCONSO.RRF` (18 cols): `RXCUI`(0) `LAT`(1) `TS`(2) `LUI`(3) `STT`(4) `SUI`(5) `ISPREF`(6)
|
|
2965
|
+
* `RXAUI`(7) `SAUI`(8) `SCUI`(9) `SDUI`(10) `SAB`(11) `TTY`(12) `CODE`(13) `STR`(14) `SRL`(15)
|
|
2966
|
+
* `SUPPRESS`(16) `CVF`(17). Only the **normalized** atoms (`SAB=RXNORM`) with a drug-graph `TTY`
|
|
2967
|
+
* become graph concepts; the first such atom per `RXCUI` wins.
|
|
2968
|
+
* - `RXNREL.RRF` (16 cols): `RXCUI1`(0) `RXAUI1`(1) `STYPE1`(2) `REL`(3) `RXCUI2`(4) `RXAUI2`(5)
|
|
2969
|
+
* `STYPE2`(6) `RELA`(7) `RUI`(8) `SRUI`(9) `SAB`(10) `SL`(11) `RG`(12) `DIR`(13) `SUPPRESS`(14)
|
|
2970
|
+
* `CVF`(15). Read `subject = RXCUI2`, `predicate = RELA`, `object = RXCUI1` (the direction
|
|
2971
|
+
* convention — see {@link ../rxnorm/rela}). Only concept-level rows (both `RXCUI`s present) with a
|
|
2972
|
+
* non-empty `RELA` become edges; atom-level (`RXAUI`) rows are not part of the concept graph.
|
|
2973
|
+
* - `RXNSAT.RRF` (13 cols): `RXCUI`(0) … `ATN`(8) `SAB`(9) `ATV`(10) `SUPPRESS`(11) `CVF`(12). Rows
|
|
2974
|
+
* with `ATN=NDC` index `ATV`(the NDC) → `RXCUI`, marked `active` **as of** the loaded release.
|
|
2975
|
+
*
|
|
2976
|
+
* **Liberal on load** (roadmap §6): a structurally unusable *row* (too few columns, missing a required
|
|
2977
|
+
* value) is **skipped and surfaced** as a `TERM_RXNORM_MALFORMED_ROW` warning, never a partial
|
|
2978
|
+
* concept/edge, never a crash. Rows that are simply *not of interest* (a non-`RXNORM` atom, an
|
|
2979
|
+
* atom-level relationship, a non-`NDC` attribute) are skipped **silently** — they are expected, not
|
|
2980
|
+
* faults. Ships **no** RxNorm content: every concept/edge is the caller's release (roadmap §5).
|
|
2981
|
+
*
|
|
2982
|
+
* @packageDocumentation
|
|
2983
|
+
*/
|
|
2984
|
+
|
|
2985
|
+
/**
|
|
2986
|
+
* A raw RxNorm RRF release, BYO. The caller supplies the public-domain Current Prescribable Content
|
|
2987
|
+
* subset or the full (licensed) release; the engine bundles **no** RxNorm content (roadmap §5).
|
|
2988
|
+
*/
|
|
2989
|
+
interface RxNormGraphSource {
|
|
2990
|
+
/** The raw `RXNCONSO.RRF` content (pipe-delimited concept/atom names). */
|
|
2991
|
+
readonly conso: string;
|
|
2992
|
+
/** The raw `RXNREL.RRF` content (pipe-delimited relationships). */
|
|
2993
|
+
readonly rel: string;
|
|
2994
|
+
/** The raw `RXNSAT.RRF` content (pipe-delimited attributes) — supply to resolve NDCs. Optional. */
|
|
2995
|
+
readonly sat?: string;
|
|
2996
|
+
/** The release version (e.g. `"RXNORM_2026AA"`), when known — mappings are release-scoped. */
|
|
2997
|
+
readonly version?: string;
|
|
2998
|
+
}
|
|
2999
|
+
/**
|
|
3000
|
+
* Load a **caller-supplied** RxNorm RRF release into an immutable {@link RxNormGraph}.
|
|
3001
|
+
*
|
|
3002
|
+
* Parses `RXNCONSO` (concepts), `RXNREL` (directed edges, normalized to the documented direction), and
|
|
3003
|
+
* — when supplied — `RXNSAT` (NDC attributes). Liberal on load: a structurally unusable row is a
|
|
3004
|
+
* skipped, surfaced {@link RxNormLoadWarning}, never partial. Ships **no** RxNorm content — the graph
|
|
3005
|
+
* is entirely the caller's release (roadmap §5).
|
|
3006
|
+
*
|
|
3007
|
+
* @param source - The {@link RxNormGraphSource} (RRF file contents + optional version).
|
|
3008
|
+
* @returns The immutable {@link RxNormGraph}.
|
|
3009
|
+
* @example
|
|
3010
|
+
* ```ts
|
|
3011
|
+
* import { loadRxNormGraph } from "@cosyte/terminology";
|
|
3012
|
+
*
|
|
3013
|
+
* // Synthetic rows in the RxNorm RRF wire format (no real RxNorm content is bundled).
|
|
3014
|
+
* const graph = loadRxNormGraph({
|
|
3015
|
+
* conso: "1|ENG||||||||||RXNORM|IN||lisinopril||N||\n2|ENG||||||||||RXNORM|SCD||lisinopril 10 MG Oral Tablet||N||",
|
|
3016
|
+
* // "SCD(2) has_ingredient IN(1)": subject=RXCUI2=2, object=RXCUI1=1.
|
|
3017
|
+
* rel: "1|||RN|2|||has_ingredient|||RXNORM||||||",
|
|
3018
|
+
* });
|
|
3019
|
+
* graph.conceptCount; // => 2
|
|
3020
|
+
* ```
|
|
3021
|
+
*/
|
|
3022
|
+
declare function loadRxNormGraph(source: RxNormGraphSource): RxNormGraph;
|
|
3023
|
+
|
|
3024
|
+
/**
|
|
3025
|
+
* **RxNorm drug-graph navigation** (roadmap Phase 6) — resolve an `SCD` to its ingredients, a branded
|
|
3026
|
+
* drug to its generic, a drug to its dose form, and an NDC to its `RXCUI`, over a loaded
|
|
3027
|
+
* {@link RxNormGraph}. Every query is **fail-safe and never-fabricate** (roadmap §4.2):
|
|
3028
|
+
*
|
|
3029
|
+
* - a queried `RXCUI` absent from the graph is a typed {@link RxNormUnknown}, never a guessed concept;
|
|
3030
|
+
* - a present concept with no such relationship is a {@link RxNormRelated} with **empty** `targets` —
|
|
3031
|
+
* an honest, data-grounded "no such edge", distinct from "unknown concept";
|
|
3032
|
+
* - navigation follows only **authored** edges by their predicate; the engine **never synthesizes an
|
|
3033
|
+
* inverse** (RxNorm ships both directions as separate rows, so brand→generic follows the authored
|
|
3034
|
+
* `tradename_of` and generic→brand follows the authored `has_tradename`);
|
|
3035
|
+
* - an NDC not in the loaded release is a typed {@link NdcUnmapped}, and a resolution carries the
|
|
3036
|
+
* **as-of release** and temporal {@link ../rxnorm/types.NdcStatus} (never a timeless assertion);
|
|
3037
|
+
* - approximate matching is an **opt-in, explicitly labeled** path ({@link approximateMatch}), never
|
|
3038
|
+
* the default and never presented as an exact code resolution.
|
|
3039
|
+
*
|
|
3040
|
+
* @packageDocumentation
|
|
3041
|
+
*/
|
|
3042
|
+
|
|
3043
|
+
/**
|
|
3044
|
+
* Look up a single concept by `RXCUI`.
|
|
3045
|
+
*
|
|
3046
|
+
* @param graph - A loaded {@link RxNormGraph}.
|
|
3047
|
+
* @param rxcui - The concept unique identifier.
|
|
3048
|
+
* @returns The {@link RxNormConcept}, or `undefined` when the `RXCUI` is not in the release.
|
|
3049
|
+
* @example
|
|
3050
|
+
* ```ts
|
|
3051
|
+
* import { loadRxNormGraph, getConcept } from "@cosyte/terminology";
|
|
3052
|
+
*
|
|
3053
|
+
* const g = loadRxNormGraph({ conso: "1|ENG||||||||||RXNORM|IN||aspirin||N||", rel: "" });
|
|
3054
|
+
* getConcept(g, "1")?.tty; // => "IN"
|
|
3055
|
+
* ```
|
|
3056
|
+
*/
|
|
3057
|
+
declare function getConcept(graph: RxNormGraph, rxcui: string): RxNormConcept | undefined;
|
|
3058
|
+
/**
|
|
3059
|
+
* Follow one or more `RELA` predicates out of a concept (the generic navigation primitive). Direction
|
|
3060
|
+
* is the **authored** edge direction (`subject ⟶predicate⟶ object`); the engine never inverts.
|
|
3061
|
+
*
|
|
3062
|
+
* @param graph - A loaded {@link RxNormGraph}.
|
|
3063
|
+
* @param rxcui - The source concept.
|
|
3064
|
+
* @param predicate - A `RELA` name, or an array of them (any-of).
|
|
3065
|
+
* @returns A {@link RxNormNavResult}: the related concepts (possibly empty), or a typed unknown source.
|
|
3066
|
+
* @example
|
|
3067
|
+
* ```ts
|
|
3068
|
+
* import { loadRxNormGraph, relatedByRela, RELA } from "@cosyte/terminology";
|
|
3069
|
+
*
|
|
3070
|
+
* // "SCD(2) has_ingredient IN(1)": subject=RXCUI2=2, object=RXCUI1=1.
|
|
3071
|
+
* const g = loadRxNormGraph({
|
|
3072
|
+
* conso: "1|ENG||||||||||RXNORM|IN||lisinopril||N||\n2|ENG||||||||||RXNORM|SCD||lisinopril 10 MG Oral Tablet||N||",
|
|
3073
|
+
* rel: "1|||RN|2|||has_ingredient|||RXNORM||||||",
|
|
3074
|
+
* });
|
|
3075
|
+
* const r = relatedByRela(g, "2", RELA.HAS_INGREDIENT);
|
|
3076
|
+
* r.found && r.targets[0]?.name; // => "lisinopril"
|
|
3077
|
+
* ```
|
|
3078
|
+
*/
|
|
3079
|
+
declare function relatedByRela(graph: RxNormGraph, rxcui: string, predicate: string | readonly string[]): RxNormNavResult;
|
|
3080
|
+
/**
|
|
3081
|
+
* The **ingredients** of a concept — the concepts it links to by a **direct** `has_ingredient` /
|
|
3082
|
+
* `has_precise_ingredient` edge in the loaded release.
|
|
3083
|
+
*
|
|
3084
|
+
* This follows the *authored, direct* edge only (never-fabricate, never a synthesized path). Note the
|
|
3085
|
+
* release's own topology: a clinical component (`SCDC`) links directly to its `IN`, whereas some `SCD`
|
|
3086
|
+
* releases route the ingredient through the component (`SCD ⟶consists_of⟶ SCDC ⟶has_ingredient⟶ IN`).
|
|
3087
|
+
* When a drug's ingredient is not a *direct* edge, this honestly returns found-with-empty-targets —
|
|
3088
|
+
* traverse {@link consistsOf} then `ingredientsOf` to reach the ingredient through the component. The
|
|
3089
|
+
* engine never invents the transitive edge.
|
|
3090
|
+
*
|
|
3091
|
+
* @param graph - A loaded {@link RxNormGraph}.
|
|
3092
|
+
* @param rxcui - The concept (a drug or a clinical/branded component).
|
|
3093
|
+
* @returns The directly-linked ingredient concepts (possibly empty), or a typed unknown source.
|
|
3094
|
+
* @example
|
|
3095
|
+
* ```ts
|
|
3096
|
+
* import { loadRxNormGraph, ingredientsOf } from "@cosyte/terminology";
|
|
3097
|
+
*
|
|
3098
|
+
* const g = loadRxNormGraph({
|
|
3099
|
+
* conso: "1|ENG||||||||||RXNORM|IN||lisinopril||N||\n2|ENG||||||||||RXNORM|SCD||lisinopril 10 MG Oral Tablet||N||",
|
|
3100
|
+
* rel: "1|||RN|2|||has_ingredient|||RXNORM||||||",
|
|
3101
|
+
* });
|
|
3102
|
+
* ingredientsOf(g, "2").found; // => true
|
|
3103
|
+
* ```
|
|
3104
|
+
*/
|
|
3105
|
+
declare function ingredientsOf(graph: RxNormGraph, rxcui: string): RxNormNavResult;
|
|
3106
|
+
/**
|
|
3107
|
+
* The **generic** form(s) of a branded concept — an `SBD`/`BN`/`BPCK` `tradename_of` its generic. The
|
|
3108
|
+
* authored forward edge; the engine never inverts a `has_tradename` edge to synthesize this.
|
|
3109
|
+
*
|
|
3110
|
+
* @param graph - A loaded {@link RxNormGraph}.
|
|
3111
|
+
* @param rxcui - The branded concept.
|
|
3112
|
+
* @returns The generic concept(s) (possibly empty), or a typed unknown source.
|
|
3113
|
+
* @example
|
|
3114
|
+
* ```ts
|
|
3115
|
+
* import { loadRxNormGraph, genericFor } from "@cosyte/terminology";
|
|
3116
|
+
*
|
|
3117
|
+
* // "SBD(2) tradename_of SCD(1)": subject=RXCUI2=2, object=RXCUI1=1.
|
|
3118
|
+
* const g = loadRxNormGraph({
|
|
3119
|
+
* conso: "1|ENG||||||||||RXNORM|SCD||lisinopril 10 MG Oral Tablet||N||\n2|ENG||||||||||RXNORM|SBD||Zestril 10 MG Oral Tablet||N||",
|
|
3120
|
+
* rel: "1|||RN|2|||tradename_of|||RXNORM||||||",
|
|
3121
|
+
* });
|
|
3122
|
+
* genericFor(g, "2").found; // => true
|
|
3123
|
+
* ```
|
|
3124
|
+
*/
|
|
3125
|
+
declare function genericFor(graph: RxNormGraph, rxcui: string): RxNormNavResult;
|
|
3126
|
+
/**
|
|
3127
|
+
* The **branded** form(s) of a generic concept — an `SCD`/`IN`/`GPCK` that `has_tradename` a brand.
|
|
3128
|
+
* The authored forward edge; not a synthesized inverse of `tradename_of`.
|
|
3129
|
+
*
|
|
3130
|
+
* @param graph - A loaded {@link RxNormGraph}.
|
|
3131
|
+
* @param rxcui - The generic concept.
|
|
3132
|
+
* @returns The branded concept(s) (possibly empty), or a typed unknown source.
|
|
3133
|
+
* @example
|
|
3134
|
+
* ```ts
|
|
3135
|
+
* import { loadRxNormGraph, brandsFor } from "@cosyte/terminology";
|
|
3136
|
+
*
|
|
3137
|
+
* const g = loadRxNormGraph({
|
|
3138
|
+
* conso: "1|ENG||||||||||RXNORM|SCD||lisinopril 10 MG Oral Tablet||N||\n2|ENG||||||||||RXNORM|SBD||Zestril 10 MG Oral Tablet||N||",
|
|
3139
|
+
* rel: "2|||RN|1|||has_tradename|||RXNORM||||||",
|
|
3140
|
+
* });
|
|
3141
|
+
* brandsFor(g, "1").found; // => true
|
|
3142
|
+
* ```
|
|
3143
|
+
*/
|
|
3144
|
+
declare function brandsFor(graph: RxNormGraph, rxcui: string): RxNormNavResult;
|
|
3145
|
+
/**
|
|
3146
|
+
* The **dose form(s)** of a drug — the `DF`/`DFG` concepts it `has_dose_form` / `has_doseformgroup`.
|
|
3147
|
+
*
|
|
3148
|
+
* @param graph - A loaded {@link RxNormGraph}.
|
|
3149
|
+
* @param rxcui - The drug concept.
|
|
3150
|
+
* @returns The dose-form concept(s) (possibly empty), or a typed unknown source.
|
|
3151
|
+
* @example
|
|
3152
|
+
* ```ts
|
|
3153
|
+
* import { loadRxNormGraph, doseFormsOf } from "@cosyte/terminology";
|
|
3154
|
+
*
|
|
3155
|
+
* const g = loadRxNormGraph({
|
|
3156
|
+
* conso: "1|ENG||||||||||RXNORM|SCD||lisinopril 10 MG Oral Tablet||N||\n9|ENG||||||||||RXNORM|DF||Oral Tablet||N||",
|
|
3157
|
+
* rel: "9|||RN|1|||has_dose_form|||RXNORM||||||",
|
|
3158
|
+
* });
|
|
3159
|
+
* doseFormsOf(g, "1").found; // => true
|
|
3160
|
+
* ```
|
|
3161
|
+
*/
|
|
3162
|
+
declare function doseFormsOf(graph: RxNormGraph, rxcui: string): RxNormNavResult;
|
|
3163
|
+
/**
|
|
3164
|
+
* The **components** a drug `consists_of` — an `SCD`/`SBD` → its `SCDC`/`SBDC` dose-form components.
|
|
3165
|
+
*
|
|
3166
|
+
* @param graph - A loaded {@link RxNormGraph}.
|
|
3167
|
+
* @param rxcui - The drug concept.
|
|
3168
|
+
* @returns The component concept(s) (possibly empty), or a typed unknown source.
|
|
3169
|
+
* @example
|
|
3170
|
+
* ```ts
|
|
3171
|
+
* import { loadRxNormGraph, consistsOf } from "@cosyte/terminology";
|
|
3172
|
+
*
|
|
3173
|
+
* const g = loadRxNormGraph({
|
|
3174
|
+
* conso: "2|ENG||||||||||RXNORM|SCD||lisinopril 10 MG Oral Tablet||N||\n3|ENG||||||||||RXNORM|SCDC||lisinopril 10 MG||N||",
|
|
3175
|
+
* rel: "3|||RN|2|||consists_of|||RXNORM||||||",
|
|
3176
|
+
* });
|
|
3177
|
+
* consistsOf(g, "2").found; // => true
|
|
3178
|
+
* ```
|
|
3179
|
+
*/
|
|
3180
|
+
declare function consistsOf(graph: RxNormGraph, rxcui: string): RxNormNavResult;
|
|
3181
|
+
/**
|
|
3182
|
+
* Resolve an **NDC** (National Drug Code) to its `RXCUI` in the loaded release, carrying the temporal
|
|
3183
|
+
* status and the as-of release. NDC↔RXCUI is many:1 and changes across releases (roadmap §4.2), so an
|
|
3184
|
+
* NDC present in the release is `active` **as of that release**; an absent NDC is a typed
|
|
3185
|
+
* {@link NdcUnmapped}, never a guessed `RXCUI`.
|
|
3186
|
+
*
|
|
3187
|
+
* @param graph - A loaded {@link RxNormGraph} (load with `sat` supplied to populate NDC attributes).
|
|
3188
|
+
* @param ndc - The NDC (as it appears in the release, e.g. 11-digit).
|
|
3189
|
+
* @returns An {@link NdcResult} — a resolution with status + `asOf`, or a typed absence.
|
|
3190
|
+
* @example
|
|
3191
|
+
* ```ts
|
|
3192
|
+
* import { loadRxNormGraph, resolveNdc } from "@cosyte/terminology";
|
|
3193
|
+
*
|
|
3194
|
+
* const g = loadRxNormGraph({
|
|
3195
|
+
* conso: "2|ENG||||||||||RXNORM|SCD||lisinopril 10 MG Oral Tablet||N||",
|
|
3196
|
+
* rel: "",
|
|
3197
|
+
* sat: "2||||||||NDC|RXNORM|00000000000|N||",
|
|
3198
|
+
* version: "RXNORM_2026AA",
|
|
3199
|
+
* });
|
|
3200
|
+
* const r = resolveNdc(g, "00000000000");
|
|
3201
|
+
* r.resolved && r.status; // => "active"
|
|
3202
|
+
* ```
|
|
3203
|
+
*/
|
|
3204
|
+
declare function resolveNdc(graph: RxNormGraph, ndc: string): NdcResult;
|
|
3205
|
+
/** Options for {@link approximateMatch}. */
|
|
3206
|
+
interface ApproximateMatchOptions {
|
|
3207
|
+
/** Minimum similarity (`0`–`1`) for a candidate to be returned. Default `0.34`. */
|
|
3208
|
+
readonly minScore?: number;
|
|
3209
|
+
/** Maximum number of candidates to return, best-first. Default `20`. */
|
|
3210
|
+
readonly limit?: number;
|
|
3211
|
+
}
|
|
3212
|
+
/**
|
|
3213
|
+
* **Opt-in, explicitly labeled** approximate name matching (roadmap §4.2) — find loaded concepts whose
|
|
3214
|
+
* names are token-similar to `query`. This is **never** the default resolution path and its results
|
|
3215
|
+
* are **never** an exact code assertion: every candidate is marked `approximate: true` with a derived
|
|
3216
|
+
* {@link RxNormApproximateMatch.score}. A caller opts in by calling this function explicitly.
|
|
3217
|
+
*
|
|
3218
|
+
* Coverage is not correctness (roadmap §4.2): a "no match" is an **empty** array (typed, honest),
|
|
3219
|
+
* distinct from a fabricated nearest guess. The engine never promotes an approximate hit to an exact
|
|
3220
|
+
* one.
|
|
3221
|
+
*
|
|
3222
|
+
* @param graph - A loaded {@link RxNormGraph}.
|
|
3223
|
+
* @param query - A free-text drug name to match against loaded concept names.
|
|
3224
|
+
* @param options - {@link ApproximateMatchOptions} (min score, limit).
|
|
3225
|
+
* @returns The labeled approximate candidates, best-first (possibly empty). Never an exact resolution.
|
|
3226
|
+
* @example
|
|
3227
|
+
* ```ts
|
|
3228
|
+
* import { loadRxNormGraph, approximateMatch } from "@cosyte/terminology";
|
|
3229
|
+
*
|
|
3230
|
+
* const g = loadRxNormGraph({
|
|
3231
|
+
* conso: "1|ENG||||||||||RXNORM|IN||lisinopril||N||",
|
|
3232
|
+
* rel: "",
|
|
3233
|
+
* });
|
|
3234
|
+
* approximateMatch(g, "lisinopril")[0]?.approximate; // => true
|
|
3235
|
+
* ```
|
|
3236
|
+
*/
|
|
3237
|
+
declare function approximateMatch(graph: RxNormGraph, query: string, options?: ApproximateMatchOptions): readonly RxNormApproximateMatch[];
|
|
3238
|
+
|
|
3239
|
+
/**
|
|
3240
|
+
* The RxNorm **relationship (`RELA`) vocabulary** and the **direction convention** the drug graph is
|
|
3241
|
+
* built on — encoded here as published identity facts (like the code-system OID↔URI facts in
|
|
3242
|
+
* {@link ../systems/registry}), grounded firsthand on the NLM RxNorm Technical Documentation and the
|
|
3243
|
+
* UMLS Reference Manual. **No RxNorm content is bundled** — only the relationship *names* and the
|
|
3244
|
+
* *rule* for reading a row's direction (roadmap §5).
|
|
3245
|
+
*
|
|
3246
|
+
* **The direction rule (roadmap §4.2 / §10 Q5 — a documented medication-safety trap).** From the
|
|
3247
|
+
* RxNorm Technical Documentation (§12.7) and the UMLS Reference Manual, verbatim: *"the direction of
|
|
3248
|
+
* REL — the relationship which the SECOND concept or atom (with … RXCUI2 …) HAS TO the FIRST concept
|
|
3249
|
+
* or atom (with … RXCUI1 …)."* The doc's worked example — a row with `RXCUI1=1155862`, `RELA=isa`,
|
|
3250
|
+
* `RXCUI2=197589` — reads *"197589 **isa** 1155862"* (the second concept is-a the first).
|
|
3251
|
+
*
|
|
3252
|
+
* So an `RXNREL.RRF` row is read **`RXCUI2 ⟶RELA⟶ RXCUI1`**, and {@link ../rxnorm/load.loadRxNormGraph}
|
|
3253
|
+
* normalizes every row to `subject = RXCUI2`, `predicate = RELA`, `object = RXCUI1`. Concretely, a
|
|
3254
|
+
* `has_ingredient` row places the **drug** in `RXCUI2` and the **ingredient** in `RXCUI1` (the drug
|
|
3255
|
+
* *has* the ingredient); a `tradename_of` row places the **branded** concept in `RXCUI2` and the
|
|
3256
|
+
* **generic** in `RXCUI1` (the brand *is a tradename of* the generic). Getting this backwards is a
|
|
3257
|
+
* wrong-medication bug, so the fixtures pin it against the doc's convention.
|
|
3258
|
+
*
|
|
3259
|
+
* @packageDocumentation
|
|
3260
|
+
*/
|
|
3261
|
+
/**
|
|
3262
|
+
* The drug-graph `RELA` relationship names, grounded on the NLM RxNorm Technical Documentation
|
|
3263
|
+
* (Appendix 1). Each is directional and read `subject ⟶RELA⟶ object` after normalization (see the
|
|
3264
|
+
* module doc). RxNorm ships both members of an inverse pair as **separate rows**, so the graph
|
|
3265
|
+
* traverses the authored predicate for the direction it wants — it **never synthesizes an inverse**.
|
|
3266
|
+
*
|
|
3267
|
+
* @example
|
|
3268
|
+
* ```ts
|
|
3269
|
+
* import { RELA } from "@cosyte/terminology";
|
|
3270
|
+
*
|
|
3271
|
+
* RELA.HAS_INGREDIENT; // => "has_ingredient"
|
|
3272
|
+
* ```
|
|
3273
|
+
*/
|
|
3274
|
+
declare const RELA: {
|
|
3275
|
+
/** `has_ingredient` — subject (a drug/component) *has ingredient* object (an `IN`/`PIN`). */
|
|
3276
|
+
readonly HAS_INGREDIENT: "has_ingredient";
|
|
3277
|
+
/** `ingredient_of` — subject (an `IN`/`PIN`) *is an ingredient of* object (a drug/component). */
|
|
3278
|
+
readonly INGREDIENT_OF: "ingredient_of";
|
|
3279
|
+
/** `has_precise_ingredient` — subject *has precise ingredient* object (a `PIN`). */
|
|
3280
|
+
readonly HAS_PRECISE_INGREDIENT: "has_precise_ingredient";
|
|
3281
|
+
/** `precise_ingredient_of` — inverse of `has_precise_ingredient`. */
|
|
3282
|
+
readonly PRECISE_INGREDIENT_OF: "precise_ingredient_of";
|
|
3283
|
+
/** `tradename_of` — subject (a branded concept) *is a tradename of* object (its generic). */
|
|
3284
|
+
readonly TRADENAME_OF: "tradename_of";
|
|
3285
|
+
/** `has_tradename` — subject (a generic concept) *has tradename* object (a branded concept). */
|
|
3286
|
+
readonly HAS_TRADENAME: "has_tradename";
|
|
3287
|
+
/** `has_dose_form` — subject (a drug) *has dose form* object (a `DF`). */
|
|
3288
|
+
readonly HAS_DOSE_FORM: "has_dose_form";
|
|
3289
|
+
/** `dose_form_of` — subject (a `DF`) *is the dose form of* object (a drug). */
|
|
3290
|
+
readonly DOSE_FORM_OF: "dose_form_of";
|
|
3291
|
+
/** `has_doseformgroup` — subject *has dose-form group* object (a `DFG`). */
|
|
3292
|
+
readonly HAS_DOSE_FORM_GROUP: "has_doseformgroup";
|
|
3293
|
+
/** `doseformgroup_of` — inverse of `has_doseformgroup`. */
|
|
3294
|
+
readonly DOSE_FORM_GROUP_OF: "doseformgroup_of";
|
|
3295
|
+
/** `consists_of` — subject (an `SCD`/`SBD`) *consists of* object (its `SCDC`/`SBDC` component(s)). */
|
|
3296
|
+
readonly CONSISTS_OF: "consists_of";
|
|
3297
|
+
/** `constitutes` — subject (a component) *constitutes* object (the drug). Inverse of `consists_of`. */
|
|
3298
|
+
readonly CONSTITUTES: "constitutes";
|
|
3299
|
+
/** `contains` — subject (a pack) *contains* object (a member drug). */
|
|
3300
|
+
readonly CONTAINS: "contains";
|
|
3301
|
+
/** `contained_in` — subject (a drug) *is contained in* object (a pack). Inverse of `contains`. */
|
|
3302
|
+
readonly CONTAINED_IN: "contained_in";
|
|
3303
|
+
/** `has_quantified_form` — subject *has quantified form* object. */
|
|
3304
|
+
readonly HAS_QUANTIFIED_FORM: "has_quantified_form";
|
|
3305
|
+
/** `quantified_form_of` — inverse of `has_quantified_form`. */
|
|
3306
|
+
readonly QUANTIFIED_FORM_OF: "quantified_form_of";
|
|
3307
|
+
/** `isa` — subject *is a* object (hierarchical, e.g. `SCD isa SCDG`). */
|
|
3308
|
+
readonly ISA: "isa";
|
|
3309
|
+
/** `inverse_isa` — subject *is a parent of* object. Inverse of `isa`. */
|
|
3310
|
+
readonly INVERSE_ISA: "inverse_isa";
|
|
3311
|
+
};
|
|
3312
|
+
/** A value from {@link RELA} — a drug-graph relationship name. */
|
|
3313
|
+
type RelaName = (typeof RELA)[keyof typeof RELA];
|
|
3314
|
+
/**
|
|
3315
|
+
* The authored **inverse pairs** of the drug-graph `RELA`s — a published fact (RxNorm Appendix 1),
|
|
3316
|
+
* used only to *document* and *validate* that a caller is traversing a real relationship. The graph
|
|
3317
|
+
* **never** uses this to synthesize a missing edge: RxNorm ships both directions, so
|
|
3318
|
+
* {@link ../rxnorm/navigate} follows whichever authored predicate points the way it wants to go.
|
|
3319
|
+
*
|
|
3320
|
+
* @example
|
|
3321
|
+
* ```ts
|
|
3322
|
+
* import { RELA_INVERSE, RELA } from "@cosyte/terminology";
|
|
3323
|
+
*
|
|
3324
|
+
* RELA_INVERSE[RELA.HAS_INGREDIENT]; // => "ingredient_of"
|
|
3325
|
+
* ```
|
|
3326
|
+
*/
|
|
3327
|
+
declare const RELA_INVERSE: Readonly<Record<string, string>>;
|
|
3328
|
+
/** The canonical code-system URI for **RxNorm** — a published identity fact, attached to graph codings. */
|
|
3329
|
+
declare const RXNORM_SYSTEM = "http://www.nlm.nih.gov/research/umls/rxnorm";
|
|
3330
|
+
|
|
3331
|
+
/**
|
|
3332
|
+
* The RxNorm **term-type (`TTY`) vocabulary** relevant to the drug graph — the normal-form ladder and
|
|
3333
|
+
* the cross-link kinds — encoded as published identity facts (grounded firsthand on the NLM RxNorm
|
|
3334
|
+
* Technical Documentation, Appendix 5). **No RxNorm content is bundled** (roadmap §5): these are the
|
|
3335
|
+
* *labels*, not the drug list.
|
|
3336
|
+
*
|
|
3337
|
+
* The RxNorm normal-form graph runs `IN → SCDC → SCD → SBD` (ingredient → clinical-drug component →
|
|
3338
|
+
* semantic clinical drug → semantic branded drug), with `BN` (brand name) and `DF` (dose form) as
|
|
3339
|
+
* cross-links (roadmap §4.2).
|
|
3340
|
+
*
|
|
3341
|
+
* @packageDocumentation
|
|
3342
|
+
*/
|
|
3343
|
+
|
|
3344
|
+
/**
|
|
3345
|
+
* The drug-graph term types (`RXNCONSO.TTY`), each mapped to its short gloss. Encoded as an identity
|
|
3346
|
+
* fact; a concept's `TTY` is carried verbatim on its {@link ../rxnorm/types.RxNormConcept}, never
|
|
3347
|
+
* re-labeled.
|
|
3348
|
+
*
|
|
3349
|
+
* @example
|
|
3350
|
+
* ```ts
|
|
3351
|
+
* import { TERM_TYPES } from "@cosyte/terminology";
|
|
3352
|
+
*
|
|
3353
|
+
* TERM_TYPES.SCD; // => "Semantic Clinical Drug"
|
|
3354
|
+
* ```
|
|
3355
|
+
*/
|
|
3356
|
+
declare const TERM_TYPES: Readonly<Record<TermType, string>>;
|
|
3357
|
+
/**
|
|
3358
|
+
* Narrow a raw `RXNCONSO.TTY` string to a known {@link TermType}, or `undefined` when it is not a
|
|
3359
|
+
* drug-graph term type this engine models (the concept is then skipped at load, liberal-on-load —
|
|
3360
|
+
* never coerced to a wrong type).
|
|
3361
|
+
*
|
|
3362
|
+
* @param tty - The raw `TTY` value from an `RXNCONSO` row.
|
|
3363
|
+
* @returns The narrowed {@link TermType}, or `undefined` when unrecognized.
|
|
3364
|
+
* @example
|
|
3365
|
+
* ```ts
|
|
3366
|
+
* import { asTermType } from "@cosyte/terminology";
|
|
3367
|
+
*
|
|
3368
|
+
* asTermType("SCD"); // => "SCD"
|
|
3369
|
+
* asTermType("ZZZ"); // => undefined
|
|
3370
|
+
* ```
|
|
3371
|
+
*/
|
|
3372
|
+
declare function asTermType(tty: string): TermType | undefined;
|
|
3373
|
+
|
|
3374
|
+
/**
|
|
3375
|
+
* Public entry point for **`@cosyte/terminology`** — a zero-dependency, developer-focused
|
|
3376
|
+
* **terminology engine** for US healthcare code systems.
|
|
3377
|
+
*
|
|
3378
|
+
* Unlike its sibling `@cosyte/*` packages, this is **not a wire-format parser**: it mirrors the
|
|
3379
|
+
* **FHIR Terminology Module** (`$translate`, `$lookup`, `$validate-code`, `$expand`, …), operating
|
|
3380
|
+
* over **consumer-supplied** FHIR resources. It ships the *engine*, never copyrighted terminology
|
|
3381
|
+
* *content* (SNOMED/CPT/full-LOINC/UMLS are strictly BYO); code-system *identities* (OID ↔ URI) are
|
|
3382
|
+
* published facts, encoded here and cited firsthand.
|
|
3383
|
+
*
|
|
3384
|
+
* **Phase 1** ships exactly the surface `@cosyte/transform` pins:
|
|
3385
|
+
*
|
|
3386
|
+
* - {@link resolveSystem} — the code-system identity / canonical-URI resolver (mnemonic | OID → URI).
|
|
3387
|
+
* - {@link loadConceptMap} + {@link translate} — the ConceptMap `$translate` engine, with the
|
|
3388
|
+
* **never-fabricate / never-invert** invariants: an unmapped source is a typed
|
|
3389
|
+
* {@link TranslateUnmapped}, never a guessed target; a directional map is never run backwards.
|
|
3390
|
+
*
|
|
3391
|
+
* **Phase 2** adds the CodeSystem load layer and the FHIR `$lookup` / `$validate-code` operations:
|
|
3392
|
+
*
|
|
3393
|
+
* - {@link loadCodeSystem} — load a **consumer-supplied** release (RRF / CSV / fixed-width / FHIR
|
|
3394
|
+
* `CodeSystem` JSON) into an immutable model. Ships the engine, **never** copyrighted content.
|
|
3395
|
+
* - {@link lookup} — code → display + properties, carrying status (deprecated / header-not-billable).
|
|
3396
|
+
* - {@link validateCode} — is a code a valid member of the system, with its status flags.
|
|
3397
|
+
*
|
|
3398
|
+
* The never-fabricate invariant holds here too: an unknown code is a typed `unknown` / `valid: false`,
|
|
3399
|
+
* never a fabricated display or a guessed `valid: true`.
|
|
3400
|
+
*
|
|
3401
|
+
* **Phase 3** adds the ValueSet binding layer — FHIR `$expand` / `$validate-code` over a `compose`:
|
|
3402
|
+
*
|
|
3403
|
+
* - {@link loadValueSet} — load a **consumer-supplied** FHIR `ValueSet` (intensional `compose` and/or
|
|
3404
|
+
* a pre-computed `expansion`) into an immutable model.
|
|
3405
|
+
* - {@link expand} — flatten membership over the supplied {@link CodeSystem}s (`include`/`exclude`,
|
|
3406
|
+
* explicit `concept` lists, `is-a`/property `filter`s, referenced value sets), with an honest
|
|
3407
|
+
* `complete` flag: an unresolvable part is a typed `TERM_VALUESET_CANNOT_EXPAND`, never a guess.
|
|
3408
|
+
* - {@link validateCodeInValueSet} — binding membership, returning a **decided** `result` only when
|
|
3409
|
+
* proven and a typed `undetermined` otherwise (a truncated expansion never reads as complete).
|
|
3410
|
+
*
|
|
3411
|
+
* **Phase 4** adds the **UCUM** unit layer — a hand-rolled UCUM grammar parser + validation +
|
|
3412
|
+
* representation canonicalization (recognition only, **no** magnitude conversion):
|
|
3413
|
+
*
|
|
3414
|
+
* - {@link validateUcum} — is a string a valid UCUM unit; if so, its canonical descriptor. An
|
|
3415
|
+
* invalid unit is a typed `TERM_UCUM_INVALID`, never a guessed "nearest" unit.
|
|
3416
|
+
* - {@link ucumEqual} — do two expressions denote the *same unit* (`N` ≡ `kg.m/s2`), by reducing
|
|
3417
|
+
* both to base dimensions. Not magnitude conversion (`mg/dL` → `mmol/L` is refused).
|
|
3418
|
+
* - {@link parseUcum} / {@link reduce} / {@link loadUcumEssence} — the underlying grammar parser,
|
|
3419
|
+
* dimensional reducer, and the in-memory model of the vendored, verbatim UCUM table.
|
|
3420
|
+
*
|
|
3421
|
+
* **Phase 5** adds the **crosswalk resolvers** — the never-fabricate/never-invert invariant applied
|
|
3422
|
+
* to the published, directional reference maps (roadmap §4.1, the safety crown):
|
|
3423
|
+
*
|
|
3424
|
+
* - {@link loadGems} + {@link applyGem} — the CMS **ICD-9↔ICD-10 GEMs** (public-domain reference
|
|
3425
|
+
* mappings), honoring the steward's Approximate / No-Map / Combination (scenario→choice-list) flags.
|
|
3426
|
+
* A No-Map source is a typed {@link CrosswalkNoMap}; a 1:many source returns the full candidate set.
|
|
3427
|
+
* - {@link loadComplexMap} + {@link applyComplexMap} — the NLM **SNOMED CT → ICD-10-CM complex map**
|
|
3428
|
+
* (BYO, SNOMED-licensed content — **zero bundled**), evaluating Map Group / Priority / `IFA` Rule /
|
|
3429
|
+
* Advice / Category against caller-supplied {@link PatientContext}; a rule needing context the
|
|
3430
|
+
* caller lacks is a typed {@link ComplexMapContextRequired}, never a guessed branch.
|
|
3431
|
+
* - {@link invertGem} — the never-invert refusal made a first-class, thrown contract
|
|
3432
|
+
* ({@link FATAL_CODES.TERM_MAP_NOT_INVERTIBLE}).
|
|
3433
|
+
*
|
|
3434
|
+
* **Phase 6** adds the **RxNorm drug relationship graph** — ingredient / brand / clinical-drug /
|
|
3435
|
+
* dose-form navigation over a **caller-supplied** RxNorm RRF release (roadmap §4.2):
|
|
3436
|
+
*
|
|
3437
|
+
* - {@link loadRxNormGraph} — load `RXNCONSO` (concepts, typed by `TTY`), `RXNREL` (directed `RELA`
|
|
3438
|
+
* edges, normalized to the documented `RXCUI2 ⟶RELA⟶ RXCUI1` direction), and optionally `RXNSAT`
|
|
3439
|
+
* (NDC attributes) into an immutable graph. Ships **no** RxNorm content — BYO release (roadmap §5).
|
|
3440
|
+
* - {@link ingredientsOf} / {@link genericFor} / {@link brandsFor} / {@link doseFormsOf} /
|
|
3441
|
+
* {@link consistsOf} / {@link relatedByRela} — graph navigation following **authored** edges only
|
|
3442
|
+
* (the engine never synthesizes an inverse). An absent `RXCUI` is a typed {@link RxNormUnknown}.
|
|
3443
|
+
* - {@link resolveNdc} — NDC → `RXCUI` carrying the temporal status and the as-of release; an absent
|
|
3444
|
+
* NDC is a typed {@link NdcUnmapped}, never a guess.
|
|
3445
|
+
* - {@link approximateMatch} — the **opt-in, explicitly labeled** similarity path (never the default,
|
|
3446
|
+
* never an exact code assertion).
|
|
3447
|
+
*
|
|
3448
|
+
* Deferred to later phases: the bundleable public-domain content packs, including the RxNorm Current
|
|
3449
|
+
* Prescribable Content pack and a GEM pack (the content-packs phase, Phase 7). The GEMs and the
|
|
3450
|
+
* Prescribable subset are public-domain, so a caller may supply them today (BYO); SNOMED/CPT/full-UMLS
|
|
3451
|
+
* content stays BYO permanently (a licensing wall — roadmap §5).
|
|
3452
|
+
*
|
|
3453
|
+
* @packageDocumentation
|
|
3454
|
+
*/
|
|
3455
|
+
/**
|
|
3456
|
+
* Library version string, synced with `package.json#version` at release time.
|
|
3457
|
+
*
|
|
3458
|
+
* @example
|
|
3459
|
+
* ```ts
|
|
3460
|
+
* import { VERSION } from "@cosyte/terminology";
|
|
3461
|
+
* typeof VERSION; // => "string"
|
|
3462
|
+
* ```
|
|
3463
|
+
*/
|
|
3464
|
+
declare const VERSION = "0.0.0";
|
|
3465
|
+
|
|
3466
|
+
export { type AnnotationNode, type ApproximateMatchOptions, type BillableFlag, COMPLEX_MAP_SOURCE_SYSTEM, type CodeSystem, type CodeSystemSource, type CodeableConcept, type Coding, type ComplexMap, type ComplexMapApplyResult, type ComplexMapContextRequired, type ComplexMapEntry, type ComplexMapGroupNoMap, type ComplexMapGroupResolved, type ComplexMapGroupResult, type ComplexMapInput, type ComplexMapLoadWarning, type ComplexMapMatched, type ComponentNode, type Concept, type ConceptActivity, type ConceptMap, type ConceptMapElement, type ConceptMapGroup, type ConceptMapTarget, type ConceptMapUnmapped, type ConceptRef, type ConceptSetComponent, type ConceptSetFilter, type ConceptStatus, type CrosswalkNoMap, type CrosswalkUnmapped, type CsvColumnMap, type CsvSource, DIAGNOSTIC_CODES, type DiagnosticCode, type ExpandResult, type ExpansionContains, type ExpansionContext, type ExpansionDiagnostic, FATAL_CODES, type FactorNode, type FatalCode, type FhirCodeSystemSource, type FieldSlice, type FilterMatch, type FilterOperator, type FixedWidthFieldMap, type FixedWidthPropertyField, type FixedWidthSource, type GemApplyResult, type GemChoiceList, type GemDirection, type GemEntry, type GemFlags, type GemLoadWarning, type GemMap, type GemMatched, type GemScenario, type GemSource, type GroupNode, HL7_V2_OID_ROOT, HL7_V2_URL_PREFIX, ICD10CM_ORDER_FILE_FIELDS, ICD10CM_SYSTEM, ICD9CM_SYSTEM, type LinearReduction, type LoadWarning, type LookupOutcome, type LookupResult, type LookupUnknown, MAP_CATEGORIES, type MapCategoryId, type MapProvenance, NO_MAP_CATEGORIES, type NdcResolution, type NdcResult, type NdcStatus, type NdcUnmapped, type ParseFailure, type ParseResult, type PatientContext, type Property, type R4Equivalence, RELA, RELA_INVERSE, RXNORM_SYSTEM, type Reduction, type RelaName, type Relationship, type ResolveSystemResult, type RrfColumnMap, type RrfPropertyColumn, type RrfSource, type RxNormApproximateMatch, type RxNormConcept, type RxNormEdge, type RxNormGraph, type RxNormGraphSource, type RxNormLoadWarning, type RxNormNavResult, type RxNormRelated, type RxNormUnknown, SNOMEDCT_SYSTEM, SYSTEM_IDENTITIES, type SimpleUnitNode, type SpecialReduction, type StatusMapper, type Subsumption, type SystemIdentity, TERM_TYPES, type TermType, TerminologyError, type TranslateMatch, type TranslateMatched, type TranslateResult, type TranslateUnmapped, type UcumAtom, type UcumEssence, type UcumPrefix, type UcumValidation, type UnitNode, type UnknownSystem, type UnmappedMode, VERSION, type ValidateCodeResult, type ValueSet, type ValueSetCompose, type ValueSetExpansion, type ValueSetMemberDecided, type ValueSetMemberUndetermined, type ValueSetMembership, applyComplexMap, applyGem, approximateMatch, asTermType, brandsFor, buildSubsumption, codeableConcept, coding, consistsOf, defaultStatusMapper, doseFormsOf, expand, genericFor, getArray, getBoolean, getConcept, getNumber, getString, ingredientsOf, invertGem, isA, isJsonObject, isUnknownSystem, loadCodeSystem, loadComplexMap, loadConceptMap, loadGems, loadRxNormGraph, loadUcumEssence, loadValueSet, lookup, makeStatus, matchesFilter, parseCsv, parseCsvSource, parseFhirCodeSystem, parseFixedWidth, parseRrf, parseRrfLine, parseUcum, reduce, relatedByRela, resolveNdc, resolveSystem, sliceField, translate, ucumEqual, unsupportedOps, validateCode, validateCodeInValueSet, validateUcum };
|