@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/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 (roadmap §4.4 — a false "not a member" is a clinical error).
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 hazard,
84
- * roadmap §4.4). The engine **never treats a truncated expansion as complete membership**: a code
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 — roadmap §4.3). The
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 — roadmap §4.1). Distinct from
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 (roadmap §4.1) — never a silently-chosen target.
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 — roadmap §4.2). Distinct from a *present*
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 (roadmap §4.2), so a resolution
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 (roadmap §4.1). Thrown by {@link ../crosswalk/gems.invertGem} so the
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: "Branded Pack",
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 lines = content.split(/\r?\n/);
4394
- for (let i = 0; i < lines.length; i++) {
4395
- const raw = lines[i] ?? "";
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
- warnings.push(
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
- warnings.push(
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 lines = content.split(/\r?\n/);
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 lines = content.split(/\r?\n/);
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
- /** `has_ingredient` — subject (a drug/component) *has ingredient* object (an `IN`/`PIN`). */
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
- /** `ingredient_of` — subject (an `IN`/`PIN`) *is an ingredient of* object (a drug/component). */
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.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;