@cosyte/terminology 0.1.5 → 0.1.6

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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 43eea2f: An internal tidy with no change to any result, warning, or diagnostic. The value set diagnostic factories now take the path every caller already gives them, a few expressions became statements so a coverage note can name the one arm no input reaches, and those arms now carry that note.
8
+ - 43eea2f: A CSV source whose `columns.properties` list has a hole now fails with the typed `TERM_CODESYSTEM_MALFORMED` fatal for a missing property column, the same as an explicit `undefined` entry, instead of a `TypeError` from inside the loader.
9
+ - 0cb5b88: Doc comments, which your editor shows on hover and the generated API reference prints, now match the code. `resolveSystem` lists the forms it accepts in the order it tries them, and no longer says it accepts a `v2-0203` mnemonic on its own (it does not; pass the table's OID or URI). `SYSTEM_IDENTITIES` and `resolveSystem` no longer say every accepted mnemonic comes from HL7 Table 0396: aliases such as `LOINC` and `CPT` are accepted too but are not Table 0396 codes. `NdcStatus` says that `loadRxNormGraph` reads no NDC history, so every NDC it resolves is `active`. `RxNormGraph.concepts` says a concept is typed by its first defining atom. The complex-map outcome types and `MAP_CATEGORIES.AMBIGUOUS` no longer say an ambiguous or context-dependent category stops a group on its own: the group's rules decide. Links that printed as a source path, such as `../crosswalk/gems.invertGem`, now link the symbol. No code changes.
10
+ - e16570e: An RRF or fixed-width source whose property list has an entry that is not an object (a hole, `undefined`, or `null`) now fails with the typed `TERM_CODESYSTEM_MALFORMED` fatal, as a CSV source with a hole in its property columns does, instead of a `TypeError` from inside the loader.
11
+ - 43eea2f: `validateCodeInValueSet` no longer admits a code through a `compose` component that selects nothing. A component with no `system`, `concept`, `filter`, or referenced value set (the loader accepts one with an empty `valueSet` list) matched every code, so an include admitted any code and an exclude removed every code, while `expand` gave the same component no members. Membership now agrees with `expand`: such a component matches no code, and a `valueSet` list that yields no reference is an intersection with nothing.
12
+
3
13
  ## 0.1.5
4
14
 
5
15
  ### Patch Changes
package/README.md CHANGED
@@ -379,6 +379,9 @@ Questions, bug reports, and an answer the engine got wrong go in
379
379
  system, map, or value set that reproduces it. Use synthetic data only: a code in patient context can
380
380
  be PHI. [SUPPORT.md](https://github.com/cosyte/.github/blob/main/SUPPORT.md) says where to ask what.
381
381
 
382
+ Need a terminology integration built, or hosted and run for you? See our
383
+ [integration services](https://cosyte.com/integration-services).
384
+
382
385
  ## Security
383
386
 
384
387
  Report a vulnerability privately, never in a public issue, and with no real patient data in it:
package/dist/index.cjs CHANGED
@@ -212,7 +212,7 @@ var FATAL_CODES = {
212
212
  /**
213
213
  * An **inversion** of a directional crosswalk was requested: a forbidden operation. The GEMs and
214
214
  * the SNOMED→ICD-10-CM map are authoritative in **one direction only**; a forward map is not the
215
- * inverse of the backward map. Thrown by {@link ../crosswalk/gems.invertGem} so the
215
+ * inverse of the backward map. Thrown by {@link invertGem} so the
216
216
  * never-invert refusal is a first-class, discoverable contract rather than a silent absence.
217
217
  */
218
218
  TERM_MAP_NOT_INVERTIBLE: "TERM_MAP_NOT_INVERTIBLE"
@@ -764,10 +764,10 @@ function parseCsvSource(source) {
764
764
  const codeIdx = requireHeader(header, source.columns.code, "code");
765
765
  const displayIdx = requireHeader(header, source.columns.display, "display");
766
766
  const statusIdx = source.columns.status !== void 0 ? requireHeader(header, source.columns.status, "status") : void 0;
767
- const propIdx = (source.columns.properties ?? []).map((n) => ({
768
- name: n,
769
- index: requireHeader(header, n, "property")
770
- }));
767
+ const propIdx = [];
768
+ for (const n of source.columns.properties ?? []) {
769
+ propIdx.push({ name: n, index: requireHeader(header, n, "property") });
770
+ }
771
771
  const needCols = Math.max(codeIdx, displayIdx, statusIdx ?? 0, ...propIdx.map((p) => p.index)) + 1;
772
772
  const concepts = /* @__PURE__ */ new Map();
773
773
  for (let r = 1; r < rows.length; r++) {
@@ -856,9 +856,10 @@ function collectConcepts(rawConcepts, into, warnings, parentCode) {
856
856
  if (code !== void 0 && code !== "" && !into.has(code)) {
857
857
  const props = [];
858
858
  for (const p of getArray(raw, "property") ?? []) {
859
+ if (!isJsonObject(p)) continue;
859
860
  const pcode = getString(p, "code");
860
861
  if (pcode === void 0) continue;
861
- const value = isJsonObject(p) ? readPropertyValue(p) : void 0;
862
+ const value = readPropertyValue(p);
862
863
  if (value !== void 0) props.push(Object.freeze({ code: pcode, value }));
863
864
  }
864
865
  if (parentCode !== void 0 && !props.some((p) => p.code === "parent" && p.value === parentCode)) {
@@ -921,9 +922,21 @@ var ICD10CM_ORDER_FILE_FIELDS = Object.freeze({
921
922
  Object.freeze({ name: "shortDescription", slice: Object.freeze({ start: 16, end: 76 }) })
922
923
  ])
923
924
  });
925
+ function requirePropertyFields(fields) {
926
+ for (const p of fields.properties ?? []) {
927
+ const entry = p;
928
+ if (typeof entry !== "object" || entry === null) {
929
+ throw new TerminologyError(
930
+ FATAL_CODES.TERM_CODESYSTEM_MALFORMED,
931
+ "fixed-width source: a configured property field is not an object"
932
+ );
933
+ }
934
+ }
935
+ }
924
936
  function parseFixedWidth(source) {
925
937
  const statusMap = source.statusMap ?? defaultStatusMapper;
926
938
  const fields = source.fields;
939
+ requirePropertyFields(fields);
927
940
  const concepts = /* @__PURE__ */ new Map();
928
941
  const warnings = [];
929
942
  const lines = source.content.split(/\r?\n/);
@@ -985,7 +998,16 @@ function parseRrfLine(line) {
985
998
  function maxColumn(source) {
986
999
  const { code, display, status, properties } = source.columns;
987
1000
  let max = Math.max(code, display, status ?? 0);
988
- for (const p of properties ?? []) max = Math.max(max, p.column);
1001
+ for (const p of properties ?? []) {
1002
+ const entry = p;
1003
+ if (typeof entry !== "object" || entry === null) {
1004
+ throw new TerminologyError(
1005
+ FATAL_CODES.TERM_CODESYSTEM_MALFORMED,
1006
+ "RRF source: a configured property column is not an object"
1007
+ );
1008
+ }
1009
+ max = Math.max(max, p.column);
1010
+ }
989
1011
  return max;
990
1012
  }
991
1013
  function cell(cells, index) {
@@ -1392,8 +1414,7 @@ function makeCoding(system, code, display, version) {
1392
1414
  return coding(init);
1393
1415
  }
1394
1416
  function cannotExpand(detail, path) {
1395
- const d = { code: "TERM_VALUESET_CANNOT_EXPAND", detail };
1396
- if (path !== void 0) d.path = path;
1417
+ const d = { code: "TERM_VALUESET_CANNOT_EXPAND", detail, path };
1397
1418
  return Object.freeze(d);
1398
1419
  }
1399
1420
  function truncated(detail, path) {
@@ -1407,9 +1428,9 @@ function truncated(detail, path) {
1407
1428
  function enumeratedUndefined(detail, path) {
1408
1429
  const d = {
1409
1430
  code: "TERM_VALUESET_ENUMERATED_CODE_UNDEFINED",
1410
- detail
1431
+ detail,
1432
+ path
1411
1433
  };
1412
- if (path !== void 0) d.path = path;
1413
1434
  return Object.freeze(d);
1414
1435
  }
1415
1436
  function expansionIncompleteDetail(unclosed) {
@@ -1443,7 +1464,8 @@ function screenActive(members, component, ctx) {
1443
1464
  }
1444
1465
  function underPath(d, prefix) {
1445
1466
  const out = { code: d.code, detail: d.detail };
1446
- out.path = d.path === void 0 ? prefix : `${prefix}/${d.path}`;
1467
+ if (d.path === void 0) out.path = prefix;
1468
+ else out.path = `${prefix}/${d.path}`;
1447
1469
  return Object.freeze(out);
1448
1470
  }
1449
1471
  function intersectInto(into, other) {
@@ -1639,8 +1661,7 @@ function expand(vs, ctx = {}) {
1639
1661
 
1640
1662
  // src/valueset/validate.ts
1641
1663
  function cannotExpand2(detail, path) {
1642
- const d = { code: "TERM_VALUESET_CANNOT_EXPAND", detail };
1643
- if (path !== void 0) d.path = path;
1664
+ const d = { code: "TERM_VALUESET_CANNOT_EXPAND", detail, path };
1644
1665
  return Object.freeze(d);
1645
1666
  }
1646
1667
  function expansionIncompleteDetail2(unclosed) {
@@ -1658,7 +1679,8 @@ function usableRelease2(component, system, ctx) {
1658
1679
  }
1659
1680
  function underPath2(d, prefix) {
1660
1681
  const out = { code: d.code, detail: d.detail };
1661
- out.path = d.path === void 0 ? prefix : `${prefix}/${d.path}`;
1682
+ if (d.path === void 0) out.path = prefix;
1683
+ else out.path = `${prefix}/${d.path}`;
1662
1684
  return Object.freeze(out);
1663
1685
  }
1664
1686
  function matchComponent(target, component, ctx, visited, path, activeOnly = false) {
@@ -1718,10 +1740,13 @@ function matchComponent(target, component, ctx, visited, path, activeOnly = fals
1718
1740
  }
1719
1741
  }
1720
1742
  if (system !== void 0 && target.system === void 0 && baseComplete) baseMatched = false;
1743
+ if (baseMatched === null && !hasVs) return { matched: false, complete: true, diagnostics };
1721
1744
  let refDefiniteFalse = false;
1722
1745
  let refUndetermined = false;
1723
1746
  if (hasVs) {
1747
+ let refsRead = 0;
1724
1748
  for (const [i, url] of valueSet2.entries()) {
1749
+ refsRead++;
1725
1750
  const refPath = `${path}.valueSet[${String(i)}]`;
1726
1751
  if (visited.has(url)) {
1727
1752
  diagnostics.push(cannotExpand2("cyclic value set reference", refPath));
@@ -1742,6 +1767,7 @@ function matchComponent(target, component, ctx, visited, path, activeOnly = fals
1742
1767
  refDefiniteFalse = true;
1743
1768
  }
1744
1769
  }
1770
+ if (refsRead === 0) refDefiniteFalse = true;
1745
1771
  }
1746
1772
  const baseDefiniteFalse = baseMatched === false && baseComplete;
1747
1773
  const baseDefiniteTrue = baseMatched === null || baseMatched === true && baseComplete;
@@ -3993,6 +4019,9 @@ function atBoundary(s, pos) {
3993
4019
  const c = s[pos];
3994
4020
  return c === "." || c === "/" || c === ")";
3995
4021
  }
4022
+ function prefixRank(m) {
4023
+ return m.prefix ? 1 : 0;
4024
+ }
3996
4025
  function simpleMatches(essence, s, pos) {
3997
4026
  const out = [];
3998
4027
  for (const atom of essence.atoms) {
@@ -4007,7 +4036,7 @@ function simpleMatches(essence, s, pos) {
4007
4036
  }
4008
4037
  }
4009
4038
  }
4010
- out.sort((a, b) => b.len - a.len || (a.prefix ? 1 : 0) - (b.prefix ? 1 : 0));
4039
+ out.sort((a, b) => b.len - a.len || prefixRank(a) - prefixRank(b));
4011
4040
  return out;
4012
4041
  }
4013
4042
  var MAX_PAREN_DEPTH = 100;
@@ -4412,7 +4441,7 @@ var MAP_CATEGORIES = {
4412
4441
  NO_MAP: "447638001",
4413
4442
  /** `447639009`: "Map of source concept is context dependent" (needs runtime patient context). */
4414
4443
  CONTEXT_DEPENDENT: "447639009",
4415
- /** `447640006`: "Source concept is ambiguous" (surface candidates, never pick one). */
4444
+ /** `447640006`: "Source concept is ambiguous" (carried through on the result; the rule decides). */
4416
4445
  AMBIGUOUS: "447640006"
4417
4446
  };
4418
4447
  var NO_MAP_CATEGORIES = /* @__PURE__ */ new Set([MAP_CATEGORIES.NO_MAP]);
@@ -4545,8 +4574,15 @@ function evaluateConjunct(clause, context) {
4545
4574
  const bound = Number(ageMatch[2]);
4546
4575
  if (context.ageYears === void 0 || !Number.isFinite(bound)) return "unknown";
4547
4576
  const age = context.ageYears;
4548
- const ok = op === "<=" ? age <= bound : op === ">=" ? age >= bound : op === "<" ? age < bound : op === ">" ? age > bound : op === "=" ? age === bound : void 0;
4549
- if (ok === void 0) return "unknown";
4577
+ let ok;
4578
+ if (op === "<=") ok = age <= bound;
4579
+ else if (op === ">=") ok = age >= bound;
4580
+ else if (op === "<") ok = age < bound;
4581
+ else if (op === ">") ok = age > bound;
4582
+ else {
4583
+ if (op === "=") ok = age === bound;
4584
+ else return "unknown";
4585
+ }
4550
4586
  return ok ? "match" : "no-match";
4551
4587
  }
4552
4588
  return "unknown";
@@ -4965,7 +5001,8 @@ function jaccard(a, b) {
4965
5001
  let inter = 0;
4966
5002
  for (const t of a) if (b.has(t)) inter++;
4967
5003
  const union = a.size + b.size - inter;
4968
- return union === 0 ? 0 : inter / union;
5004
+ if (union === 0) return 0;
5005
+ return inter / union;
4969
5006
  }
4970
5007
  function approximateMatch(graph, query, options = {}) {
4971
5008
  const minScore = options.minScore ?? 0.34;
@@ -4982,7 +5019,7 @@ function approximateMatch(graph, query, options = {}) {
4982
5019
  }
4983
5020
 
4984
5021
  // src/index.ts
4985
- var VERSION = "0.1.5";
5022
+ var VERSION = "0.1.6";
4986
5023
 
4987
5024
  exports.COMPLEX_MAP_SOURCE_SYSTEM = COMPLEX_MAP_SOURCE_SYSTEM;
4988
5025
  exports.DIAGNOSTIC_CODES = DIAGNOSTIC_CODES;