@cosyte/terminology 0.0.1 → 0.0.4
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 +280 -7
- package/LICENSE +83 -0
- package/README.md +96 -25
- package/dist/index.cjs +66 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +348 -188
- package/dist/index.d.ts +348 -188
- package/dist/index.mjs +66 -27
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -6
- package/vendor/ucum/NOTICE.md +48 -0
package/dist/index.cjs
CHANGED
|
@@ -75,13 +75,13 @@ var DIAGNOSTIC_CODES = {
|
|
|
75
75
|
* code system was not supplied, an unresolvable referenced value set, or a `filter` operator the
|
|
76
76
|
* engine does not implement. Surfaced as a typed outcome that marks the expansion **incomplete**;
|
|
77
77
|
* the engine **never** fabricates a member and **never** returns a silently-empty "no members"
|
|
78
|
-
* answer for a part it could not compute (
|
|
78
|
+
* answer for a part it could not compute (a false "not a member" is a clinical error).
|
|
79
79
|
*/
|
|
80
80
|
TERM_VALUESET_CANNOT_EXPAND: "TERM_VALUESET_CANNOT_EXPAND",
|
|
81
81
|
/**
|
|
82
82
|
* A **pre-computed** `ValueSet.expansion` is **incomplete** — its `total` exceeds the number of
|
|
83
|
-
* `contains` entries, or it is flagged too-costly (the VSAC `$expand` >1200-code truncation
|
|
84
|
-
*
|
|
83
|
+
* `contains` entries, or it is flagged too-costly (the VSAC `$expand` >1200-code truncation
|
|
84
|
+
* hazard). The engine **never treats a truncated expansion as complete membership**: a code
|
|
85
85
|
* absent from a truncated expansion is `undetermined`, never a confident "not a member".
|
|
86
86
|
*/
|
|
87
87
|
TERM_VALUESET_EXPANSION_TRUNCATED: "TERM_VALUESET_EXPANSION_TRUNCATED",
|
|
@@ -89,7 +89,7 @@ var DIAGNOSTIC_CODES = {
|
|
|
89
89
|
* A UCUM unit expression is **not valid** — it does not parse against the UCUM grammar, or it
|
|
90
90
|
* names an atom absent from the vendored UCUM table. A **first-class typed outcome, never an
|
|
91
91
|
* error and never a guess**: {@link validateUcum} returns `{ valid: false, reason }`, never a
|
|
92
|
-
* coerced or "nearest" unit (the never-fabricate invariant, applied to units
|
|
92
|
+
* coerced or "nearest" unit (the never-fabricate invariant, applied to units). The
|
|
93
93
|
* `reason` is value-free (a grammar fault or an unknown-atom shape), never PHI.
|
|
94
94
|
*/
|
|
95
95
|
TERM_UCUM_INVALID: "TERM_UCUM_INVALID",
|
|
@@ -97,7 +97,7 @@ var DIAGNOSTIC_CODES = {
|
|
|
97
97
|
* A crosswalk source code has an **authored No-Map** — the steward declares it has no valid target
|
|
98
98
|
* in the other classification (a GEM `NoDx`/`NoPCS` sentinel, or a SNOMED complex-map `447638001`
|
|
99
99
|
* "cannot be classified" category). A **first-class typed outcome, never an error and never a
|
|
100
|
-
* guess** (the never-fabricate invariant, applied to crosswalks
|
|
100
|
+
* guess** (the never-fabricate invariant, applied to crosswalks). Distinct from
|
|
101
101
|
* {@link TERM_CROSSWALK_UNMAPPED}: the source *is* in the map, and the map says "no target".
|
|
102
102
|
*/
|
|
103
103
|
TERM_CROSSWALK_NO_MAP: "TERM_CROSSWALK_NO_MAP",
|
|
@@ -111,7 +111,7 @@ var DIAGNOSTIC_CODES = {
|
|
|
111
111
|
/**
|
|
112
112
|
* A SNOMED→ICD-10-CM complex-map group's decision needs **patient context** (an `IFA` age band or
|
|
113
113
|
* gender) the caller did not supply. The engine surfaces the candidate rules + advice and **refuses
|
|
114
|
-
* to pick a branch** it lacks the data for
|
|
114
|
+
* to pick a branch** it lacks the data for — never a silently-chosen target.
|
|
115
115
|
*/
|
|
116
116
|
TERM_CROSSWALK_CONTEXT_REQUIRED: "TERM_CROSSWALK_CONTEXT_REQUIRED",
|
|
117
117
|
/**
|
|
@@ -133,17 +133,27 @@ var DIAGNOSTIC_CODES = {
|
|
|
133
133
|
* **silently** — they are expected, not faults.
|
|
134
134
|
*/
|
|
135
135
|
TERM_RXNORM_MALFORMED_ROW: "TERM_RXNORM_MALFORMED_ROW",
|
|
136
|
+
/**
|
|
137
|
+
* An `RXCUI` appeared in the release's `SAB=RXNORM` atoms but **none of them could establish its
|
|
138
|
+
* term type**, so it is **not** in the graph. Either every atom carried a synonym-class `TTY`
|
|
139
|
+
* (`PSN`/`SY`/`TMSY`, which Appendix 5 defines as a "synonym of another TTY" and which therefore
|
|
140
|
+
* types a *name* rather than a concept) or every atom carried a `TTY` this engine does not model.
|
|
141
|
+
* The concept is skipped rather than typed from a synonym atom: an absent concept is already a
|
|
142
|
+
* first-class typed answer (`TERM_RXNORM_UNKNOWN_RXCUI`), while a concept whose term type reads
|
|
143
|
+
* `TMSY` is a fabricated claim about what a drug *is* that a caller cannot detect.
|
|
144
|
+
*/
|
|
145
|
+
TERM_RXNORM_UNTYPED_CONCEPT: "TERM_RXNORM_UNTYPED_CONCEPT",
|
|
136
146
|
/**
|
|
137
147
|
* A RxNorm graph navigation was asked about an `RXCUI` **absent from the loaded release** — a
|
|
138
148
|
* first-class typed outcome, never a fabricated concept and never an empty "success" (the
|
|
139
|
-
* never-fabricate invariant, applied to the drug graph
|
|
149
|
+
* never-fabricate invariant, applied to the drug graph). Distinct from a *present*
|
|
140
150
|
* concept that simply has no edge of the requested relationship (that is a found result with empty
|
|
141
151
|
* targets).
|
|
142
152
|
*/
|
|
143
153
|
TERM_RXNORM_UNKNOWN_RXCUI: "TERM_RXNORM_UNKNOWN_RXCUI",
|
|
144
154
|
/**
|
|
145
155
|
* An NDC could **not be resolved** to any `RXCUI` in the loaded RxNorm release — a typed, surfaced
|
|
146
|
-
* absence, never a guessed `RXCUI`. NDC↔RXCUI is many:1 and temporal
|
|
156
|
+
* absence, never a guessed `RXCUI`. NDC↔RXCUI is many:1 and temporal, so a resolution
|
|
147
157
|
* is always release-scoped; an NDC not present in the loaded release is this diagnostic.
|
|
148
158
|
*/
|
|
149
159
|
TERM_RXNORM_NDC_UNMAPPED: "TERM_RXNORM_NDC_UNMAPPED"
|
|
@@ -178,7 +188,7 @@ var FATAL_CODES = {
|
|
|
178
188
|
/**
|
|
179
189
|
* An **inversion** of a directional crosswalk was requested — a forbidden operation. The GEMs and
|
|
180
190
|
* the SNOMED→ICD-10-CM map are authoritative in **one direction only**; a forward map is not the
|
|
181
|
-
* inverse of the backward map
|
|
191
|
+
* inverse of the backward map. Thrown by {@link ../crosswalk/gems.invertGem} so the
|
|
182
192
|
* never-invert refusal is a first-class, discoverable contract rather than a silent absence.
|
|
183
193
|
*/
|
|
184
194
|
TERM_MAP_NOT_INVERTIBLE: "TERM_MAP_NOT_INVERTIBLE"
|
|
@@ -4360,12 +4370,17 @@ var TERM_TYPES = Object.freeze({
|
|
|
4360
4370
|
BN: "Brand Name",
|
|
4361
4371
|
SCDC: "Semantic Clinical Drug Component",
|
|
4362
4372
|
SBDC: "Semantic Branded Drug Component",
|
|
4373
|
+
SCDF: "Semantic Clinical Drug Form",
|
|
4374
|
+
SBDF: "Semantic Branded Drug Form",
|
|
4375
|
+
SCDFP: "Semantic Clinical Drug Form Precise",
|
|
4376
|
+
SBDFP: "Semantic Branded Drug Form Precise",
|
|
4363
4377
|
SCD: "Semantic Clinical Drug",
|
|
4364
4378
|
SBD: "Semantic Branded Drug",
|
|
4365
4379
|
SCDG: "Semantic Clinical Drug Group",
|
|
4366
4380
|
SBDG: "Semantic Branded Drug Group",
|
|
4381
|
+
SCDGP: "Semantic Clinical Drug Form Group Precise",
|
|
4367
4382
|
GPCK: "Generic Pack",
|
|
4368
|
-
BPCK: "
|
|
4383
|
+
BPCK: "Brand Name Pack",
|
|
4369
4384
|
DF: "Dose Form",
|
|
4370
4385
|
DFG: "Dose Form Group",
|
|
4371
4386
|
PSN: "Prescribable Name",
|
|
@@ -4374,9 +4389,13 @@ var TERM_TYPES = Object.freeze({
|
|
|
4374
4389
|
ET: "Entry Term"
|
|
4375
4390
|
});
|
|
4376
4391
|
var KNOWN = new Set(Object.keys(TERM_TYPES));
|
|
4392
|
+
var SYNONYM = /* @__PURE__ */ new Set(["PSN", "SY", "TMSY"]);
|
|
4377
4393
|
function asTermType(tty) {
|
|
4378
4394
|
return KNOWN.has(tty) ? tty : void 0;
|
|
4379
4395
|
}
|
|
4396
|
+
function isSynonymTermType(tty) {
|
|
4397
|
+
return SYNONYM.has(tty);
|
|
4398
|
+
}
|
|
4380
4399
|
|
|
4381
4400
|
// src/rxnorm/load.ts
|
|
4382
4401
|
var SAB_RXNORM = "RXNORM";
|
|
@@ -4390,13 +4409,13 @@ function cell2(cells, index) {
|
|
|
4390
4409
|
}
|
|
4391
4410
|
function parseConso(content, warnings) {
|
|
4392
4411
|
const concepts = /* @__PURE__ */ new Map();
|
|
4393
|
-
const
|
|
4394
|
-
|
|
4395
|
-
|
|
4412
|
+
const conso = [];
|
|
4413
|
+
const untyped = /* @__PURE__ */ new Map();
|
|
4414
|
+
for (const [i, raw] of content.split(/\r?\n/).entries()) {
|
|
4396
4415
|
if (raw === "") continue;
|
|
4397
4416
|
const cells = parseRrfLine(raw);
|
|
4398
4417
|
if (cells.length <= CONSO.STR) {
|
|
4399
|
-
|
|
4418
|
+
conso.push(
|
|
4400
4419
|
Object.freeze({
|
|
4401
4420
|
code: "TERM_RXNORM_MALFORMED_ROW",
|
|
4402
4421
|
file: "RXNCONSO",
|
|
@@ -4407,11 +4426,9 @@ function parseConso(content, warnings) {
|
|
|
4407
4426
|
continue;
|
|
4408
4427
|
}
|
|
4409
4428
|
if (cell2(cells, CONSO.SAB) !== SAB_RXNORM) continue;
|
|
4410
|
-
const tty = asTermType(cell2(cells, CONSO.TTY) ?? "");
|
|
4411
|
-
if (tty === void 0) continue;
|
|
4412
4429
|
const rxcui = cell2(cells, CONSO.RXCUI);
|
|
4413
4430
|
if (rxcui === void 0 || rxcui === "") {
|
|
4414
|
-
|
|
4431
|
+
conso.push(
|
|
4415
4432
|
Object.freeze({
|
|
4416
4433
|
code: "TERM_RXNORM_MALFORMED_ROW",
|
|
4417
4434
|
file: "RXNCONSO",
|
|
@@ -4421,7 +4438,13 @@ function parseConso(content, warnings) {
|
|
|
4421
4438
|
);
|
|
4422
4439
|
continue;
|
|
4423
4440
|
}
|
|
4441
|
+
const tty = asTermType(cell2(cells, CONSO.TTY) ?? "");
|
|
4442
|
+
if (tty === void 0 || isSynonymTermType(tty)) {
|
|
4443
|
+
if (!concepts.has(rxcui) && !untyped.has(rxcui)) untyped.set(rxcui, i + 1);
|
|
4444
|
+
continue;
|
|
4445
|
+
}
|
|
4424
4446
|
if (concepts.has(rxcui)) continue;
|
|
4447
|
+
untyped.delete(rxcui);
|
|
4425
4448
|
const name = cell2(cells, CONSO.STR) ?? "";
|
|
4426
4449
|
const suppress = cell2(cells, CONSO.SUPPRESS) ?? "N";
|
|
4427
4450
|
concepts.set(
|
|
@@ -4429,14 +4452,24 @@ function parseConso(content, warnings) {
|
|
|
4429
4452
|
Object.freeze({ rxcui, tty, name, suppressed: suppress !== "" && suppress !== "N" })
|
|
4430
4453
|
);
|
|
4431
4454
|
}
|
|
4455
|
+
for (const line of untyped.values()) {
|
|
4456
|
+
conso.push(
|
|
4457
|
+
Object.freeze({
|
|
4458
|
+
code: "TERM_RXNORM_UNTYPED_CONCEPT",
|
|
4459
|
+
file: "RXNCONSO",
|
|
4460
|
+
line,
|
|
4461
|
+
detail: "no RXNORM atom established this RXCUI's term type (every atom was synonym-class or an unmodelled TTY); skipped rather than typed from a synonym"
|
|
4462
|
+
})
|
|
4463
|
+
);
|
|
4464
|
+
}
|
|
4465
|
+
conso.sort((a, b) => a.line - b.line);
|
|
4466
|
+
for (const w of conso) warnings.push(w);
|
|
4432
4467
|
return concepts;
|
|
4433
4468
|
}
|
|
4434
4469
|
function parseRel(content, warnings) {
|
|
4435
4470
|
const edges = /* @__PURE__ */ new Map();
|
|
4436
4471
|
let edgeCount = 0;
|
|
4437
|
-
const
|
|
4438
|
-
for (let i = 0; i < lines.length; i++) {
|
|
4439
|
-
const raw = lines[i] ?? "";
|
|
4472
|
+
for (const [i, raw] of content.split(/\r?\n/).entries()) {
|
|
4440
4473
|
if (raw === "") continue;
|
|
4441
4474
|
const cells = parseRrfLine(raw);
|
|
4442
4475
|
if (cells.length <= REL.RELA) {
|
|
@@ -4465,9 +4498,7 @@ function parseRel(content, warnings) {
|
|
|
4465
4498
|
}
|
|
4466
4499
|
function parseNdcs(content, warnings) {
|
|
4467
4500
|
const ndcs = /* @__PURE__ */ new Map();
|
|
4468
|
-
const
|
|
4469
|
-
for (let i = 0; i < lines.length; i++) {
|
|
4470
|
-
const raw = lines[i] ?? "";
|
|
4501
|
+
for (const [i, raw] of content.split(/\r?\n/).entries()) {
|
|
4471
4502
|
if (raw === "") continue;
|
|
4472
4503
|
const cells = parseRrfLine(raw);
|
|
4473
4504
|
if (cells.length <= SAT.ATV) {
|
|
@@ -4519,9 +4550,17 @@ function loadRxNormGraph(source) {
|
|
|
4519
4550
|
|
|
4520
4551
|
// src/rxnorm/rela.ts
|
|
4521
4552
|
var RELA = {
|
|
4522
|
-
/**
|
|
4553
|
+
/**
|
|
4554
|
+
* `has_ingredient`: subject *has ingredient* object. Authored from the clinical side
|
|
4555
|
+
* (`SCDC`/`SCDF`/`SCDG`) to an `IN` and from the branded side (`SBD`/`SBDC`/`SBDF`/`SBDG`) to a
|
|
4556
|
+
* `BN`; RxNorm authors no `SCD ⟶ IN` row.
|
|
4557
|
+
*/
|
|
4523
4558
|
HAS_INGREDIENT: "has_ingredient",
|
|
4524
|
-
/**
|
|
4559
|
+
/**
|
|
4560
|
+
* `ingredient_of`: subject *is an ingredient of* object. The authored inverse, so an `IN` reaches
|
|
4561
|
+
* clinical components and dose forms and a `BN` reaches branded concepts (the `SBD` included).
|
|
4562
|
+
* Neither reaches an `SCD`.
|
|
4563
|
+
*/
|
|
4525
4564
|
INGREDIENT_OF: "ingredient_of",
|
|
4526
4565
|
/** `has_precise_ingredient` — subject *has precise ingredient* object (a `PIN`). */
|
|
4527
4566
|
HAS_PRECISE_INGREDIENT: "has_precise_ingredient",
|
|
@@ -4664,7 +4703,7 @@ function approximateMatch(graph, query, options = {}) {
|
|
|
4664
4703
|
}
|
|
4665
4704
|
|
|
4666
4705
|
// src/index.ts
|
|
4667
|
-
var VERSION = "0.0.
|
|
4706
|
+
var VERSION = "0.0.4";
|
|
4668
4707
|
|
|
4669
4708
|
exports.COMPLEX_MAP_SOURCE_SYSTEM = COMPLEX_MAP_SOURCE_SYSTEM;
|
|
4670
4709
|
exports.DIAGNOSTIC_CODES = DIAGNOSTIC_CODES;
|
|
@@ -4706,6 +4745,7 @@ exports.ingredientsOf = ingredientsOf;
|
|
|
4706
4745
|
exports.invertGem = invertGem;
|
|
4707
4746
|
exports.isA = isA;
|
|
4708
4747
|
exports.isJsonObject = isJsonObject;
|
|
4748
|
+
exports.isSynonymTermType = isSynonymTermType;
|
|
4709
4749
|
exports.isUnknownSystem = isUnknownSystem;
|
|
4710
4750
|
exports.loadCodeSystem = loadCodeSystem;
|
|
4711
4751
|
exports.loadComplexMap = loadComplexMap;
|