@cosyte/terminology 0.1.5 → 0.1.7

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
@@ -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"
@@ -446,6 +446,10 @@ function loadUnmapped(raw, path) {
446
446
  assignString(out, raw, "url");
447
447
  return Object.freeze(out);
448
448
  }
449
+ var loadedGroups = /* @__PURE__ */ new WeakSet();
450
+ function isLoadedGroup(group) {
451
+ return loadedGroups.has(group);
452
+ }
449
453
  function loadGroup(raw, path) {
450
454
  if (!isJsonObject(raw)) malformed(path, "group is not an object");
451
455
  const rawElements = getArray(raw, "element") ?? [];
@@ -460,7 +464,9 @@ function loadGroup(raw, path) {
460
464
  if (isJsonObject(raw) && raw["unmapped"] !== void 0) {
461
465
  out.unmapped = loadUnmapped(raw["unmapped"], `${path}.unmapped`);
462
466
  }
463
- return Object.freeze(out);
467
+ const group = Object.freeze(out);
468
+ loadedGroups.add(group);
469
+ return group;
464
470
  }
465
471
  function loadConceptMap(json) {
466
472
  if (!isJsonObject(json)) malformed("", "resource is not a JSON object");
@@ -504,6 +510,24 @@ function groupApplies(group, system) {
504
510
  if (system === void 0) return true;
505
511
  return group.source === void 0 || group.source === system;
506
512
  }
513
+ var elementIndex = /* @__PURE__ */ new WeakMap();
514
+ var NO_ELEMENTS = Object.freeze([]);
515
+ function candidates(group, code) {
516
+ if (typeof code !== "string" || !isLoadedGroup(group)) return group.element;
517
+ let index = elementIndex.get(group);
518
+ if (index === void 0) {
519
+ const built = /* @__PURE__ */ new Map();
520
+ for (const element of group.element) {
521
+ if (element.code === void 0) continue;
522
+ const list = built.get(element.code);
523
+ if (list === void 0) built.set(element.code, [element]);
524
+ else list.push(element);
525
+ }
526
+ index = built;
527
+ elementIndex.set(group, index);
528
+ }
529
+ return index.get(code) ?? NO_ELEMENTS;
530
+ }
507
531
  function provenanceOf(map, group, sourceSystem) {
508
532
  const out = {};
509
533
  if (map.url !== void 0) out.conceptMapUrl = map.url;
@@ -537,7 +561,7 @@ function translate(sourceCoding, map) {
537
561
  for (const group of map.group) {
538
562
  if (!groupApplies(group, system)) continue;
539
563
  if (fallbackGroup === void 0 && group.unmapped !== void 0) fallbackGroup = group;
540
- for (const element of group.element) {
564
+ for (const element of candidates(group, source.code)) {
541
565
  if (element.code !== source.code) continue;
542
566
  sourceCodePresent = true;
543
567
  for (const t of element.target) {
@@ -640,6 +664,12 @@ function frozenMap(source, what) {
640
664
  return Object.freeze(view);
641
665
  }
642
666
 
667
+ // src/common/own-string.ts
668
+ var VIEW_MIN_LENGTH = 13;
669
+ function ownString(s) {
670
+ return s.length < VIEW_MIN_LENGTH ? s : ` ${s}`.slice(1);
671
+ }
672
+
643
673
  // src/codesystem/status.ts
644
674
  var defaultStatusMapper = (raw) => {
645
675
  switch (raw.trim().toUpperCase()) {
@@ -674,7 +704,79 @@ function makeStatus(activity, raw, billable) {
674
704
  }
675
705
 
676
706
  // src/codesystem/csv.ts
677
- function parseCsvRows(content) {
707
+ var QUOTE = 34;
708
+ var COMMA = 44;
709
+ var LINE_FEED = 10;
710
+ var CARRIAGE_RETURN = 13;
711
+ function scanCsv(content, sink) {
712
+ if (typeof content !== "string") return readCsvByCharAt(content, sink);
713
+ const n = content.length;
714
+ const cells = [];
715
+ let column = 0;
716
+ const keeps = (col) => sink.wanted === void 0 || sink.wanted[col] === true;
717
+ let keep = keeps(0);
718
+ let field = "";
719
+ let runStart = 0;
720
+ let inQuotes = false;
721
+ let fieldStart = true;
722
+ let sawAny = false;
723
+ const endField = (end) => {
724
+ if (keep) cells[column] = field + content.slice(runStart, end);
725
+ field = "";
726
+ };
727
+ const endRow = () => {
728
+ sink.row(cells, column + 1);
729
+ column = 0;
730
+ keep = keeps(0);
731
+ };
732
+ for (let i = 0; i < n; i++) {
733
+ const ch = content.charCodeAt(i);
734
+ if (inQuotes) {
735
+ if (ch === QUOTE) {
736
+ if (content.charCodeAt(i + 1) === QUOTE) {
737
+ if (keep) field += content.slice(runStart, i + 1);
738
+ i++;
739
+ } else {
740
+ if (keep) field += content.slice(runStart, i);
741
+ inQuotes = false;
742
+ }
743
+ runStart = i + 1;
744
+ }
745
+ continue;
746
+ }
747
+ if (ch === QUOTE && fieldStart) {
748
+ inQuotes = true;
749
+ fieldStart = false;
750
+ sawAny = true;
751
+ runStart = i + 1;
752
+ } else if (ch === COMMA) {
753
+ endField(i);
754
+ column++;
755
+ keep = keeps(column);
756
+ runStart = i + 1;
757
+ fieldStart = true;
758
+ sawAny = true;
759
+ } else if (ch === LINE_FEED) {
760
+ endField(i);
761
+ endRow();
762
+ runStart = i + 1;
763
+ fieldStart = true;
764
+ sawAny = false;
765
+ } else if (ch === CARRIAGE_RETURN) {
766
+ if (keep) field += content.slice(runStart, i);
767
+ runStart = i + 1;
768
+ } else {
769
+ fieldStart = false;
770
+ sawAny = true;
771
+ }
772
+ }
773
+ if (sawAny) {
774
+ endField(n);
775
+ endRow();
776
+ }
777
+ return inQuotes;
778
+ }
779
+ function readCsvByCharAt(content, sink) {
678
780
  const rows = [];
679
781
  let row = [];
680
782
  let field = "";
@@ -713,20 +815,30 @@ function parseCsvRows(content) {
713
815
  field = "";
714
816
  fieldStart = true;
715
817
  sawAny = false;
716
- } else if (ch === "\r") ; else {
818
+ } else if (ch !== "\r") {
717
819
  field += ch;
718
820
  fieldStart = false;
719
821
  sawAny = true;
720
822
  }
721
823
  }
722
- if (sawAny || field !== "" || row.length > 0) {
824
+ if (sawAny) {
723
825
  row.push(field);
724
826
  rows.push(row);
725
827
  }
726
- return { rows, unterminatedQuote: inQuotes };
828
+ for (const r of rows) sink.row(r, r.length);
829
+ return inQuotes;
727
830
  }
728
831
  function parseCsv(content) {
729
- return parseCsvRows(content).rows;
832
+ const rows = [];
833
+ scanCsv(content, {
834
+ wanted: void 0,
835
+ row: (cells, count) => {
836
+ const row = [];
837
+ for (let c = 0; c < count; c++) row.push(ownString(cells[c]));
838
+ rows.push(row);
839
+ }
840
+ });
841
+ return rows;
730
842
  }
731
843
  var MISSING_COLUMN_MESSAGE = Object.freeze({
732
844
  code: "CSV source: the configured 'code' column is not present in the header row",
@@ -743,47 +855,44 @@ function requireHeader(header, name, role) {
743
855
  }
744
856
  function parseCsvSource(source) {
745
857
  const statusMap = source.statusMap ?? defaultStatusMapper;
746
- const { rows, unterminatedQuote } = parseCsvRows(source.content);
747
- const header = rows[0];
748
- if (header === void 0) {
749
- throw new TerminologyError(
750
- FATAL_CODES.TERM_CODESYSTEM_MALFORMED,
751
- "CSV source: the file has no header row"
752
- );
753
- }
754
- const warnings = [];
755
- if (unterminatedQuote) {
756
- warnings.push(
757
- Object.freeze({
758
- code: DIAGNOSTIC_CODES.TERM_CSV_MALFORMED,
759
- line: rows.length,
760
- detail: "an opened quoted field was not terminated before end-of-input"
761
- })
762
- );
763
- }
764
- const codeIdx = requireHeader(header, source.columns.code, "code");
765
- const displayIdx = requireHeader(header, source.columns.display, "display");
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
- }));
771
- const needCols = Math.max(codeIdx, displayIdx, statusIdx ?? 0, ...propIdx.map((p) => p.index)) + 1;
858
+ const content = source.content;
772
859
  const concepts = /* @__PURE__ */ new Map();
773
- for (let r = 1; r < rows.length; r++) {
774
- const cells = rows[r] ?? [];
775
- const lineNo = r + 1;
776
- if (cells.length < needCols) {
860
+ const warnings = [];
861
+ let rowCount = 0;
862
+ let codeIdx = 0;
863
+ let displayIdx = 0;
864
+ let statusIdx;
865
+ const propIdx = [];
866
+ let needCols = 0;
867
+ const header = (cells, count) => {
868
+ const names = cells.slice(0, count);
869
+ codeIdx = requireHeader(names, source.columns.code, "code");
870
+ displayIdx = requireHeader(names, source.columns.display, "display");
871
+ statusIdx = source.columns.status !== void 0 ? requireHeader(names, source.columns.status, "status") : void 0;
872
+ for (const n of source.columns.properties ?? []) {
873
+ propIdx.push({ name: n, index: requireHeader(names, n, "property") });
874
+ }
875
+ needCols = Math.max(codeIdx, displayIdx, statusIdx ?? 0, ...propIdx.map((p) => p.index)) + 1;
876
+ const wanted = [];
877
+ wanted[codeIdx] = true;
878
+ wanted[displayIdx] = true;
879
+ if (statusIdx !== void 0) wanted[statusIdx] = true;
880
+ for (const p of propIdx) wanted[p.index] = true;
881
+ sink.wanted = wanted;
882
+ };
883
+ const dataRow = (cells, count, lineNo) => {
884
+ if (count < needCols) {
777
885
  warnings.push(
778
886
  Object.freeze({
779
887
  code: DIAGNOSTIC_CODES.TERM_CSV_MALFORMED,
780
888
  line: lineNo,
781
- detail: `row has ${String(cells.length)} fields, need ${String(needCols)}`
889
+ detail: `row has ${String(count)} fields, need ${String(needCols)}`
782
890
  })
783
891
  );
784
- continue;
892
+ return;
785
893
  }
786
- const code = (cells[codeIdx] ?? "").trim();
894
+ const cell3 = (index) => cells[index];
895
+ const code = cell3(codeIdx).trim();
787
896
  if (code === "") {
788
897
  warnings.push(
789
898
  Object.freeze({
@@ -792,23 +901,47 @@ function parseCsvSource(source) {
792
901
  detail: "row is missing its code"
793
902
  })
794
903
  );
795
- continue;
904
+ return;
796
905
  }
797
- if (concepts.has(code)) continue;
798
- const out = { code, properties: [] };
799
- const display = (cells[displayIdx] ?? "").trim();
800
- if (display !== "") out.display = display;
906
+ if (concepts.has(code)) return;
907
+ const out = { code: ownString(code), properties: [] };
908
+ const display = cell3(displayIdx).trim();
909
+ if (display !== "") out.display = ownString(display);
801
910
  if (statusIdx !== void 0) {
802
- const rawStatus = (cells[statusIdx] ?? "").trim();
911
+ const rawStatus = ownString(cell3(statusIdx).trim());
803
912
  out.status = makeStatus(statusMap(rawStatus), rawStatus);
804
913
  }
805
914
  const props = [];
806
915
  for (const p of propIdx) {
807
- const value = (cells[p.index] ?? "").trim();
808
- if (value !== "") props.push(Object.freeze({ code: p.name, value }));
916
+ const value = cell3(p.index).trim();
917
+ if (value !== "") props.push(Object.freeze({ code: p.name, value: ownString(value) }));
809
918
  }
810
919
  out.properties = Object.freeze(props);
811
- concepts.set(code, Object.freeze(out));
920
+ concepts.set(out.code, Object.freeze(out));
921
+ };
922
+ const sink = {
923
+ wanted: void 0,
924
+ row: (cells, count) => {
925
+ rowCount++;
926
+ if (rowCount === 1) header(cells, count);
927
+ else dataRow(cells, count, rowCount);
928
+ }
929
+ };
930
+ const unterminatedQuote = scanCsv(content, sink);
931
+ if (rowCount === 0) {
932
+ throw new TerminologyError(
933
+ FATAL_CODES.TERM_CODESYSTEM_MALFORMED,
934
+ "CSV source: the file has no header row"
935
+ );
936
+ }
937
+ if (unterminatedQuote) {
938
+ warnings.unshift(
939
+ Object.freeze({
940
+ code: DIAGNOSTIC_CODES.TERM_CSV_MALFORMED,
941
+ line: rowCount,
942
+ detail: "an opened quoted field was not terminated before end-of-input"
943
+ })
944
+ );
812
945
  }
813
946
  return { concepts, warnings };
814
947
  }
@@ -856,9 +989,10 @@ function collectConcepts(rawConcepts, into, warnings, parentCode) {
856
989
  if (code !== void 0 && code !== "" && !into.has(code)) {
857
990
  const props = [];
858
991
  for (const p of getArray(raw, "property") ?? []) {
992
+ if (!isJsonObject(p)) continue;
859
993
  const pcode = getString(p, "code");
860
994
  if (pcode === void 0) continue;
861
- const value = isJsonObject(p) ? readPropertyValue(p) : void 0;
995
+ const value = readPropertyValue(p);
862
996
  if (value !== void 0) props.push(Object.freeze({ code: pcode, value }));
863
997
  }
864
998
  if (parentCode !== void 0 && !props.some((p) => p.code === "parent" && p.value === parentCode)) {
@@ -921,9 +1055,21 @@ var ICD10CM_ORDER_FILE_FIELDS = Object.freeze({
921
1055
  Object.freeze({ name: "shortDescription", slice: Object.freeze({ start: 16, end: 76 }) })
922
1056
  ])
923
1057
  });
1058
+ function requirePropertyFields(fields) {
1059
+ for (const p of fields.properties ?? []) {
1060
+ const entry = p;
1061
+ if (typeof entry !== "object" || entry === null) {
1062
+ throw new TerminologyError(
1063
+ FATAL_CODES.TERM_CODESYSTEM_MALFORMED,
1064
+ "fixed-width source: a configured property field is not an object"
1065
+ );
1066
+ }
1067
+ }
1068
+ }
924
1069
  function parseFixedWidth(source) {
925
1070
  const statusMap = source.statusMap ?? defaultStatusMapper;
926
1071
  const fields = source.fields;
1072
+ requirePropertyFields(fields);
927
1073
  const concepts = /* @__PURE__ */ new Map();
928
1074
  const warnings = [];
929
1075
  const lines = source.content.split(/\r?\n/);
@@ -985,7 +1131,16 @@ function parseRrfLine(line) {
985
1131
  function maxColumn(source) {
986
1132
  const { code, display, status, properties } = source.columns;
987
1133
  let max = Math.max(code, display, status ?? 0);
988
- for (const p of properties ?? []) max = Math.max(max, p.column);
1134
+ for (const p of properties ?? []) {
1135
+ const entry = p;
1136
+ if (typeof entry !== "object" || entry === null) {
1137
+ throw new TerminologyError(
1138
+ FATAL_CODES.TERM_CODESYSTEM_MALFORMED,
1139
+ "RRF source: a configured property column is not an object"
1140
+ );
1141
+ }
1142
+ max = Math.max(max, p.column);
1143
+ }
989
1144
  return max;
990
1145
  }
991
1146
  function cell(cells, index) {
@@ -1046,6 +1201,10 @@ function parseRrf(source) {
1046
1201
  }
1047
1202
 
1048
1203
  // src/codesystem/load.ts
1204
+ var loadedConcepts = /* @__PURE__ */ new WeakSet();
1205
+ function isLoadedConcepts(concepts) {
1206
+ return loadedConcepts.has(concepts);
1207
+ }
1049
1208
  function loadCodeSystem(source) {
1050
1209
  let concepts;
1051
1210
  let warnings;
@@ -1084,6 +1243,7 @@ function loadCodeSystem(source) {
1084
1243
  }
1085
1244
  out.count = concepts.size;
1086
1245
  out.concepts = frozenMap(concepts, "a loaded CodeSystem's concepts map");
1246
+ loadedConcepts.add(out.concepts);
1087
1247
  out.warnings = Object.freeze(warnings);
1088
1248
  return Object.freeze(out);
1089
1249
  }
@@ -1286,8 +1446,11 @@ function directParents(concept) {
1286
1446
  return parents;
1287
1447
  }
1288
1448
  function buildSubsumption(cs) {
1449
+ return subsumptionOver(cs.concepts);
1450
+ }
1451
+ function subsumptionOver(concepts) {
1289
1452
  const parentsOf = /* @__PURE__ */ new Map();
1290
- for (const concept of cs.concepts.values()) {
1453
+ for (const concept of concepts.values()) {
1291
1454
  const parents = directParents(concept);
1292
1455
  if (parents.length > 0) parentsOf.set(concept.code, parents);
1293
1456
  }
@@ -1308,6 +1471,17 @@ function buildSubsumption(cs) {
1308
1471
  };
1309
1472
  return { ancestorsOf };
1310
1473
  }
1474
+ var loadedSubsumption = /* @__PURE__ */ new WeakMap();
1475
+ function subsumptionOf(cs) {
1476
+ const concepts = cs.concepts;
1477
+ if (!isLoadedConcepts(concepts)) return subsumptionOver(concepts);
1478
+ let sub = loadedSubsumption.get(concepts);
1479
+ if (sub === void 0) {
1480
+ sub = subsumptionOver(concepts);
1481
+ loadedSubsumption.set(concepts, sub);
1482
+ }
1483
+ return sub;
1484
+ }
1311
1485
  function isA(sub, code, anchor) {
1312
1486
  return code === anchor || sub.ancestorsOf(code).has(anchor);
1313
1487
  }
@@ -1392,8 +1566,7 @@ function makeCoding(system, code, display, version) {
1392
1566
  return coding(init);
1393
1567
  }
1394
1568
  function cannotExpand(detail, path) {
1395
- const d = { code: "TERM_VALUESET_CANNOT_EXPAND", detail };
1396
- if (path !== void 0) d.path = path;
1569
+ const d = { code: "TERM_VALUESET_CANNOT_EXPAND", detail, path };
1397
1570
  return Object.freeze(d);
1398
1571
  }
1399
1572
  function truncated(detail, path) {
@@ -1407,9 +1580,9 @@ function truncated(detail, path) {
1407
1580
  function enumeratedUndefined(detail, path) {
1408
1581
  const d = {
1409
1582
  code: "TERM_VALUESET_ENUMERATED_CODE_UNDEFINED",
1410
- detail
1583
+ detail,
1584
+ path
1411
1585
  };
1412
- if (path !== void 0) d.path = path;
1413
1586
  return Object.freeze(d);
1414
1587
  }
1415
1588
  function expansionIncompleteDetail(unclosed) {
@@ -1443,7 +1616,8 @@ function screenActive(members, component, ctx) {
1443
1616
  }
1444
1617
  function underPath(d, prefix) {
1445
1618
  const out = { code: d.code, detail: d.detail };
1446
- out.path = d.path === void 0 ? prefix : `${prefix}/${d.path}`;
1619
+ if (d.path === void 0) out.path = prefix;
1620
+ else out.path = `${prefix}/${d.path}`;
1447
1621
  return Object.freeze(out);
1448
1622
  }
1449
1623
  function intersectInto(into, other) {
@@ -1639,8 +1813,7 @@ function expand(vs, ctx = {}) {
1639
1813
 
1640
1814
  // src/valueset/validate.ts
1641
1815
  function cannotExpand2(detail, path) {
1642
- const d = { code: "TERM_VALUESET_CANNOT_EXPAND", detail };
1643
- if (path !== void 0) d.path = path;
1816
+ const d = { code: "TERM_VALUESET_CANNOT_EXPAND", detail, path };
1644
1817
  return Object.freeze(d);
1645
1818
  }
1646
1819
  function expansionIncompleteDetail2(unclosed) {
@@ -1658,7 +1831,8 @@ function usableRelease2(component, system, ctx) {
1658
1831
  }
1659
1832
  function underPath2(d, prefix) {
1660
1833
  const out = { code: d.code, detail: d.detail };
1661
- out.path = d.path === void 0 ? prefix : `${prefix}/${d.path}`;
1834
+ if (d.path === void 0) out.path = prefix;
1835
+ else out.path = `${prefix}/${d.path}`;
1662
1836
  return Object.freeze(out);
1663
1837
  }
1664
1838
  function matchComponent(target, component, ctx, visited, path, activeOnly = false) {
@@ -1710,7 +1884,7 @@ function matchComponent(target, component, ctx, visited, path, activeOnly = fals
1710
1884
  baseComplete = false;
1711
1885
  } else {
1712
1886
  const concpt = cs.concepts.get(target.code);
1713
- baseMatched = concpt !== void 0 && matchesAllFilters(concpt, filter, buildSubsumption(cs)).matched;
1887
+ baseMatched = concpt !== void 0 && matchesAllFilters(concpt, filter, subsumptionOf(cs)).matched;
1714
1888
  }
1715
1889
  } else {
1716
1890
  baseMatched = cs.concepts.get(target.code) !== void 0;
@@ -1718,10 +1892,13 @@ function matchComponent(target, component, ctx, visited, path, activeOnly = fals
1718
1892
  }
1719
1893
  }
1720
1894
  if (system !== void 0 && target.system === void 0 && baseComplete) baseMatched = false;
1895
+ if (baseMatched === null && !hasVs) return { matched: false, complete: true, diagnostics };
1721
1896
  let refDefiniteFalse = false;
1722
1897
  let refUndetermined = false;
1723
1898
  if (hasVs) {
1899
+ let refsRead = 0;
1724
1900
  for (const [i, url] of valueSet2.entries()) {
1901
+ refsRead++;
1725
1902
  const refPath = `${path}.valueSet[${String(i)}]`;
1726
1903
  if (visited.has(url)) {
1727
1904
  diagnostics.push(cannotExpand2("cyclic value set reference", refPath));
@@ -1742,6 +1919,7 @@ function matchComponent(target, component, ctx, visited, path, activeOnly = fals
1742
1919
  refDefiniteFalse = true;
1743
1920
  }
1744
1921
  }
1922
+ if (refsRead === 0) refDefiniteFalse = true;
1745
1923
  }
1746
1924
  const baseDefiniteFalse = baseMatched === false && baseComplete;
1747
1925
  const baseDefiniteTrue = baseMatched === null || baseMatched === true && baseComplete;
@@ -3993,7 +4171,32 @@ function atBoundary(s, pos) {
3993
4171
  const c = s[pos];
3994
4172
  return c === "." || c === "/" || c === ")";
3995
4173
  }
4174
+ function prefixRank(m) {
4175
+ return m.prefix ? 1 : 0;
4176
+ }
4177
+ function byPreference(a, b) {
4178
+ return b.len - a.len || prefixRank(a) - prefixRank(b);
4179
+ }
3996
4180
  function simpleMatches(essence, s, pos) {
4181
+ if (typeof s !== "string") return simpleMatchesByScan(essence, s, pos);
4182
+ const index = indexOf(essence);
4183
+ const out = [];
4184
+ for (const atom of index.atoms.get(s.charCodeAt(pos)) ?? NONE) {
4185
+ if (s.startsWith(atom.code, pos)) out.push({ atom, len: atom.code.length });
4186
+ }
4187
+ for (const prefix of index.prefixes.get(s.charCodeAt(pos)) ?? NONE) {
4188
+ if (!s.startsWith(prefix.code, pos)) continue;
4189
+ const rem = pos + prefix.code.length;
4190
+ for (const atom of index.metricAtoms.get(s.charCodeAt(rem)) ?? NONE) {
4191
+ if (s.startsWith(atom.code, rem)) {
4192
+ out.push({ prefix, atom, len: prefix.code.length + atom.code.length });
4193
+ }
4194
+ }
4195
+ }
4196
+ out.sort(byPreference);
4197
+ return out;
4198
+ }
4199
+ function simpleMatchesByScan(essence, s, pos) {
3997
4200
  const out = [];
3998
4201
  for (const atom of essence.atoms) {
3999
4202
  if (s.startsWith(atom.code, pos)) out.push({ atom, len: atom.code.length });
@@ -4007,9 +4210,33 @@ function simpleMatches(essence, s, pos) {
4007
4210
  }
4008
4211
  }
4009
4212
  }
4010
- out.sort((a, b) => b.len - a.len || (a.prefix ? 1 : 0) - (b.prefix ? 1 : 0));
4213
+ out.sort(byPreference);
4214
+ return out;
4215
+ }
4216
+ var NONE = Object.freeze([]);
4217
+ var firstCharIndexes = /* @__PURE__ */ new WeakMap();
4218
+ function byFirstChar(items) {
4219
+ const out = /* @__PURE__ */ new Map();
4220
+ for (const item of items) {
4221
+ const first = item.code.charCodeAt(0);
4222
+ const list = out.get(first);
4223
+ if (list === void 0) out.set(first, [item]);
4224
+ else list.push(item);
4225
+ }
4011
4226
  return out;
4012
4227
  }
4228
+ function indexOf(essence) {
4229
+ let index = firstCharIndexes.get(essence);
4230
+ if (index === void 0) {
4231
+ index = {
4232
+ atoms: byFirstChar(essence.atoms),
4233
+ metricAtoms: byFirstChar(essence.atoms.filter((a) => a.metric)),
4234
+ prefixes: byFirstChar(essence.prefixes)
4235
+ };
4236
+ firstCharIndexes.set(essence, index);
4237
+ }
4238
+ return index;
4239
+ }
4013
4240
  var MAX_PAREN_DEPTH = 100;
4014
4241
  function parseExponent(cur) {
4015
4242
  const { s } = cur;
@@ -4042,8 +4269,8 @@ function parseAnnotation(cur) {
4042
4269
  return { ok: false, reason: "annotation is not closed with '}'" };
4043
4270
  }
4044
4271
  function parseSimpleUnit(cur) {
4045
- const candidates = simpleMatches(cur.essence, cur.s, cur.pos);
4046
- for (const cand of candidates) {
4272
+ const candidates2 = simpleMatches(cur.essence, cur.s, cur.pos);
4273
+ for (const cand of candidates2) {
4047
4274
  const save = cur.pos;
4048
4275
  cur.pos = save + cand.len;
4049
4276
  const exp = parseExponent(cur);
@@ -4269,57 +4496,62 @@ function isNoMapSentinel(target) {
4269
4496
  return target === NO_DX || target === NO_PCS;
4270
4497
  }
4271
4498
  function decodeFlags(raw) {
4272
- if (!/^[0-9]{5}$/.test(raw)) return void 0;
4273
4499
  return Object.freeze({
4274
- approximate: raw[0] === "1",
4275
- noMap: raw[1] === "1",
4276
- combination: raw[2] === "1",
4277
- scenario: Number(raw[3]),
4278
- choiceList: Number(raw[4]),
4500
+ approximate: raw.charCodeAt(0) === DIGIT_ONE,
4501
+ noMap: raw.charCodeAt(1) === DIGIT_ONE,
4502
+ combination: raw.charCodeAt(2) === DIGIT_ONE,
4503
+ scenario: raw.charCodeAt(3) - DIGIT_ZERO,
4504
+ choiceList: raw.charCodeAt(4) - DIGIT_ZERO,
4279
4505
  raw
4280
4506
  });
4281
4507
  }
4508
+ var DIGIT_ZERO = 48;
4509
+ var DIGIT_ONE = 49;
4510
+ var DIGIT_NINE = 57;
4511
+ var SPACE = 32;
4512
+ var NON_ASCII_SPACE = /^\s$/;
4513
+ function isSpace(c) {
4514
+ if (c < 128) return c === SPACE || c >= 9 && c <= 13;
4515
+ return NON_ASCII_SPACE.test(String.fromCharCode(c));
4516
+ }
4282
4517
  function loadGems(source) {
4283
4518
  const entries = /* @__PURE__ */ new Map();
4284
4519
  const warnings = [];
4285
4520
  let count = 0;
4286
- const lines = source.content.split(/\r?\n/);
4287
- for (let i = 0; i < lines.length; i++) {
4288
- const raw = lines[i];
4289
- if (raw === void 0) continue;
4290
- const trimmed = raw.trim();
4291
- if (trimmed === "") continue;
4292
- const fields = trimmed.split(/\s+/);
4293
- if (fields.length !== 3) {
4521
+ const sink = {
4522
+ flags: /* @__PURE__ */ new Map(),
4523
+ malformed: (lineNo, fieldCount) => {
4294
4524
  warnings.push(
4295
4525
  Object.freeze({
4296
4526
  code: "TERM_GEM_MALFORMED_ROW",
4297
- line: i + 1,
4298
- detail: `expected 3 whitespace-delimited fields, found ${String(fields.length)}`
4527
+ line: lineNo,
4528
+ detail: `expected 3 whitespace-delimited fields, found ${String(fieldCount)}`
4299
4529
  })
4300
4530
  );
4301
- continue;
4302
- }
4303
- const [src, tgt, flagStr] = fields;
4304
- const flags = decodeFlags(flagStr);
4305
- if (flags === void 0) {
4306
- warnings.push(
4307
- Object.freeze({
4308
- code: "TERM_GEM_MALFORMED_ROW",
4309
- line: i + 1,
4310
- detail: "flag field is not a 5-digit code"
4311
- })
4531
+ },
4532
+ line: (lineNo, src, tgt, flags) => {
4533
+ if (flags === void 0) {
4534
+ warnings.push(
4535
+ Object.freeze({
4536
+ code: "TERM_GEM_MALFORMED_ROW",
4537
+ line: lineNo,
4538
+ detail: "flag field is not a 5-digit code"
4539
+ })
4540
+ );
4541
+ return;
4542
+ }
4543
+ const frozen = Object.freeze(
4544
+ isNoMapSentinel(tgt) ? { source: src, flags } : { source: src, flags, target: tgt }
4312
4545
  );
4313
- continue;
4546
+ const list = entries.get(src);
4547
+ if (list) list.push(frozen);
4548
+ else entries.set(src, [frozen]);
4549
+ count++;
4314
4550
  }
4315
- const entry = { source: src, flags };
4316
- if (!isNoMapSentinel(tgt)) entry.target = tgt;
4317
- const frozen = Object.freeze(entry);
4318
- const list = entries.get(src);
4319
- if (list) list.push(frozen);
4320
- else entries.set(src, [frozen]);
4321
- count++;
4322
- }
4551
+ };
4552
+ const content = source.content;
4553
+ if (typeof content === "string") readLinesInPlace(content, sink);
4554
+ else readLinesBySplit(content, sink);
4323
4555
  for (const list of entries.values()) Object.freeze(list);
4324
4556
  return Object.freeze({
4325
4557
  direction: source.direction,
@@ -4329,6 +4561,90 @@ function loadGems(source) {
4329
4561
  warnings: Object.freeze(warnings)
4330
4562
  });
4331
4563
  }
4564
+ function sharedFlags(sink, text, from, to) {
4565
+ const key = fiveDigitsAt(text, from, to);
4566
+ if (key === -1) return void 0;
4567
+ let flags = sink.flags.get(key);
4568
+ if (flags === void 0) {
4569
+ flags = decodeFlags(text.slice(from, to));
4570
+ sink.flags.set(key, flags);
4571
+ }
4572
+ return flags;
4573
+ }
4574
+ function fiveDigitsAt(content, from, to) {
4575
+ if (to - from !== 5) return -1;
4576
+ let value = 0;
4577
+ for (let i = from; i < to; i++) {
4578
+ const c = content.charCodeAt(i);
4579
+ if (c < DIGIT_ZERO || c > DIGIT_NINE) return -1;
4580
+ value = value * 10 + (c - DIGIT_ZERO);
4581
+ }
4582
+ return value;
4583
+ }
4584
+ function readLinesInPlace(content, sink) {
4585
+ const n = content.length;
4586
+ let lineNo = 0;
4587
+ for (let start = 0; start <= n; ) {
4588
+ let end = content.indexOf("\n", start);
4589
+ if (end === -1) end = n;
4590
+ lineNo++;
4591
+ let fieldCount = 0;
4592
+ let srcStart = 0;
4593
+ let srcEnd = 0;
4594
+ let tgtStart = 0;
4595
+ let tgtEnd = 0;
4596
+ let flagStart = 0;
4597
+ let flagEnd = 0;
4598
+ for (let i = start; i < end; ) {
4599
+ if (isSpace(content.charCodeAt(i))) {
4600
+ i++;
4601
+ continue;
4602
+ }
4603
+ const from = i;
4604
+ while (i < end && !isSpace(content.charCodeAt(i))) i++;
4605
+ fieldCount++;
4606
+ if (fieldCount === 1) {
4607
+ srcStart = from;
4608
+ srcEnd = i;
4609
+ } else if (fieldCount === 2) {
4610
+ tgtStart = from;
4611
+ tgtEnd = i;
4612
+ } else if (fieldCount === 3) {
4613
+ flagStart = from;
4614
+ flagEnd = i;
4615
+ }
4616
+ }
4617
+ start = end + 1;
4618
+ if (fieldCount === 0) continue;
4619
+ if (fieldCount !== 3) {
4620
+ sink.malformed(lineNo, fieldCount);
4621
+ continue;
4622
+ }
4623
+ const flags = sharedFlags(sink, content, flagStart, flagEnd);
4624
+ sink.line(
4625
+ lineNo,
4626
+ ownString(content.slice(srcStart, srcEnd)),
4627
+ ownString(content.slice(tgtStart, tgtEnd)),
4628
+ flags
4629
+ );
4630
+ }
4631
+ }
4632
+ function readLinesBySplit(content, sink) {
4633
+ const lines = content.split(/\r?\n/);
4634
+ for (let i = 0; i < lines.length; i++) {
4635
+ const raw = lines[i];
4636
+ if (raw === void 0) continue;
4637
+ const trimmed = raw.trim();
4638
+ if (trimmed === "") continue;
4639
+ const fields = trimmed.split(/\s+/);
4640
+ if (fields.length !== 3) {
4641
+ sink.malformed(i + 1, fields.length);
4642
+ continue;
4643
+ }
4644
+ const [src, tgt, flagStr] = fields;
4645
+ sink.line(i + 1, src, tgt, sharedFlags(sink, flagStr, 0, flagStr.length));
4646
+ }
4647
+ }
4332
4648
  function buildCombinations(entries) {
4333
4649
  const combo = entries.filter((e) => e.flags.combination && e.target !== void 0);
4334
4650
  if (combo.length === 0) return void 0;
@@ -4412,7 +4728,7 @@ var MAP_CATEGORIES = {
4412
4728
  NO_MAP: "447638001",
4413
4729
  /** `447639009`: "Map of source concept is context dependent" (needs runtime patient context). */
4414
4730
  CONTEXT_DEPENDENT: "447639009",
4415
- /** `447640006`: "Source concept is ambiguous" (surface candidates, never pick one). */
4731
+ /** `447640006`: "Source concept is ambiguous" (carried through on the result; the rule decides). */
4416
4732
  AMBIGUOUS: "447640006"
4417
4733
  };
4418
4734
  var NO_MAP_CATEGORIES = /* @__PURE__ */ new Set([MAP_CATEGORIES.NO_MAP]);
@@ -4545,8 +4861,15 @@ function evaluateConjunct(clause, context) {
4545
4861
  const bound = Number(ageMatch[2]);
4546
4862
  if (context.ageYears === void 0 || !Number.isFinite(bound)) return "unknown";
4547
4863
  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";
4864
+ let ok;
4865
+ if (op === "<=") ok = age <= bound;
4866
+ else if (op === ">=") ok = age >= bound;
4867
+ else if (op === "<") ok = age < bound;
4868
+ else if (op === ">") ok = age > bound;
4869
+ else {
4870
+ if (op === "=") ok = age === bound;
4871
+ else return "unknown";
4872
+ }
4550
4873
  return ok ? "match" : "no-match";
4551
4874
  }
4552
4875
  return "unknown";
@@ -4965,7 +5288,8 @@ function jaccard(a, b) {
4965
5288
  let inter = 0;
4966
5289
  for (const t of a) if (b.has(t)) inter++;
4967
5290
  const union = a.size + b.size - inter;
4968
- return union === 0 ? 0 : inter / union;
5291
+ if (union === 0) return 0;
5292
+ return inter / union;
4969
5293
  }
4970
5294
  function approximateMatch(graph, query, options = {}) {
4971
5295
  const minScore = options.minScore ?? 0.34;
@@ -4982,7 +5306,7 @@ function approximateMatch(graph, query, options = {}) {
4982
5306
  }
4983
5307
 
4984
5308
  // src/index.ts
4985
- var VERSION = "0.1.5";
5309
+ var VERSION = "0.1.7";
4986
5310
 
4987
5311
  exports.COMPLEX_MAP_SOURCE_SYSTEM = COMPLEX_MAP_SOURCE_SYSTEM;
4988
5312
  exports.DIAGNOSTIC_CODES = DIAGNOSTIC_CODES;