fhirsmith 0.10.1 → 0.11.0

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/changelog-template.md +22 -0
  3. package/library/logger.js +8 -1
  4. package/library/package-manager.js +40 -2
  5. package/npmprojector/watcher.js +1 -1
  6. package/package.json +3 -2
  7. package/packages/packages.js +28 -15
  8. package/publisher/publisher.js +8 -8
  9. package/registry/api.js +92 -31
  10. package/registry/crawler.js +15 -1
  11. package/registry/model.js +158 -8
  12. package/registry/readme.md +5 -13
  13. package/registry/registry.js +87 -46
  14. package/registry/test-runner.js +1 -1
  15. package/server.js +8 -1
  16. package/token/token.js +1 -1
  17. package/translations/Messages.properties +3 -1
  18. package/translations/rendering-phrases.properties +1227 -1218
  19. package/translations/rendering-phrases_ar.properties +1097 -1091
  20. package/translations/rendering-phrases_fr.properties +626 -624
  21. package/translations/rendering-phrases_nl.properties +976 -970
  22. package/translations/rendering-phrases_pt.properties +1026 -1020
  23. package/translations/rendering-phrases_ru.properties +1100 -1094
  24. package/tsconfig.eslint.json +9 -0
  25. package/tx/cs/cs-api.js +57 -4
  26. package/tx/cs/cs-cs.js +98 -25
  27. package/tx/cs/cs-hgvs.js +4 -0
  28. package/tx/cs/cs-lang.js +8 -0
  29. package/tx/cs/cs-mimetypes.js +4 -0
  30. package/tx/cs/cs-snomed.js +530 -75
  31. package/tx/cs/cs-uri.js +4 -0
  32. package/tx/data/snomed-subset.txt +40 -3
  33. package/tx/importers/import-sct.module.js +18 -0
  34. package/tx/library/designations.js +3 -5
  35. package/tx/library/renderer.js +227 -30
  36. package/tx/library.js +5 -1
  37. package/tx/operation-context.js +40 -3
  38. package/tx/provider.js +38 -9
  39. package/tx/sct/ecl.js +30 -11
  40. package/tx/sct/expressions.js +28 -1
  41. package/tx/sct/structures.js +5 -5
  42. package/tx/tx-html.js +24 -15
  43. package/tx/tx.fhir.org.yml +1 -4
  44. package/tx/vs/vs-package.js +52 -0
  45. package/tx/workers/batch-validate.js +125 -18
  46. package/tx/workers/cache-control.js +31 -2
  47. package/tx/workers/expand.js +50 -32
  48. package/tx/workers/metadata.js +27 -6
  49. package/tx/workers/search.js +7 -1
  50. package/tx/workers/validate.js +30 -18
  51. package/tx/workers/worker.js +30 -7
  52. package/tx/xversion/xv-valueset.js +5 -2
  53. package/xig/xig.js +3 -3
package/tx/cs/cs-uri.js CHANGED
@@ -162,6 +162,10 @@ class UriServices extends CodeSystemProvider {
162
162
  return true;
163
163
  }
164
164
 
165
+ canBeExpanded() {
166
+ return false; // URIs cannot be iterated / enumerated
167
+ }
168
+
165
169
  // ============================================================================
166
170
  // Filtering (not supported for URIs)
167
171
  // ============================================================================
@@ -22,6 +22,8 @@
22
22
  74400008 |Appendicitis (disorder)|
23
23
  155729003 |Appendicitis (disorder)|
24
24
  307530000 |Appendicitis NOS (disorder)|
25
+ 42463004 |Genus Opisthorchis (organism)|
26
+ 71650008 |Opisthorchis viverrini (organism)|
25
27
  116676008 |Associated morphology (attribute)|
26
28
  128045006 |Cellulitis (disorder)|
27
29
  20946005 |Fracture, closed (morphologic abnormality)|
@@ -34,14 +36,49 @@
34
36
  6990005 |Fracture of shaft of tibia (disorder)|
35
37
  <<128241005 |Inflammatory disease of liver (disorder)|
36
38
  <<10200004 |Liver structure (body structure)|
39
+ <<90140006 |Structure of intrahepatic biliary tree (body structure)|
37
40
  <<364159005 |Liver observable (observable entity)|
38
41
  <<227014008 |Liver pate (substance)|
39
42
  <<249565005 |Liver finding (finding)|
40
43
  <<406459008 |Product containing halibut liver oil (medicinal product)|
41
44
  <<776168003 |Product containing only halibut liver oil (medicinal product)|
42
- <<776168003 |Product containing only halibut liver oil (medicinal product)|
43
45
  <<309399009 |Hospital-based dietitian (occupation)|
44
46
  <<421813008 |Class Hepaticopsida (organism)|
45
- <<441802002 |Imaging of liver (procedure)|
47
+ <<441802002 |Computed tomography of liver (procedure)|
46
48
  <<119383005 |Specimen from liver (specimen)|
47
- 129433002 |Inspection|
49
+ 129433002 |Inspection|
50
+ 367430006 |Repair of tendon of hand (procedure)|
51
+ 119657005 |Hand repair (procedure)|
52
+ 274059009 |Hand tendon operation (procedure)|
53
+ 281760001 |Repair of tendon of upper limb (procedure)|
54
+ 18701002 |Repair of tendon of hand with graft (procedure)|
55
+ 214433003 |Repair of ruptured tendon of hand (procedure)|
56
+ 243234005 |Repair of tendon of hand by transfer or transplantation (procedure)|
57
+ 26731003 |Repair of tendon of hand by suture (procedure)|
58
+ 27106001 |Reconstruction of tendon pulley of hand (procedure)|
59
+ 45810006 |Reattachment of tendon of hand (procedure)|
60
+ 709291000 |Repair of flexor tendon of hand (procedure)|
61
+ 712638006 |Repair of extensor tendon of hand (procedure)|
62
+ 76340004 |Plication of tendon of hand (procedure)|
63
+ 90650008 |Lengthening of tendon of hand (procedure)|
64
+ 90907001 |Shortening of tendon of hand (procedure)|
65
+ 91092007 |Myotenontoplasty of hand (procedure)|
66
+ 118632007 |Structure of tendon within hand (body structure)|
67
+ 13024002 |Tendon structure (body structure)|
68
+ 245098002 |Structure of tendon within upper limb (body structure)|
69
+ 85562004 |Hand structure (body structure)|
70
+ #Referenced in request
71
+ 734139008 |Anatomy structure and part association reference set (foundation metadata concept)|
72
+ #For snomed-procedure-in-display test
73
+ #Required by: validate-code-isa-in-response.json
74
+ 41271000119108 |Infective arthritis caused by virus and co-occurrent with hepatitis (disorder)|
75
+ #Required by: expand-ecl-ancOrSelf-response.json, expand-ecl-ancestors-response.json, expand-ecl-parents-response.json, expand-ecl-parentsOrSelf-response.json
76
+ 303270005 |Liver and biliary structure (body structure)|
77
+ 699602007 |Structure of viscus of abdomen proper (body structure)|
78
+ #Required by: expand-ecl-ancOrSelf-response.json, expand-ecl-ancestors-response.json
79
+ 420473002 |Structure of subregion of trunk (body structure)|
80
+ 49596003 |Digestive organ (body structure)|
81
+ 86762007 |Digestive system (body structure)|
82
+ 1197039003 |Structure of lymphatic vessel of liver (body structure)|
83
+ #Required by: lookup-procedure-pc-response.json
84
+ 24028007 |Right (qualifier value)|
@@ -2407,6 +2407,16 @@ class SnomedImporter {
2407
2407
  const refSet = new RefSet(refSetId);
2408
2408
  refSet.title = displayName;
2409
2409
  refSet.isLangRefset = isLangRefset;
2410
+ // Set the reference set's defining-concept index at creation time, using
2411
+ // this refset's own id. Doing it here (rather than once after the member
2412
+ // loop) is essential when a single RF2 file contains members for more than
2413
+ // one reference set (e.g. the association file carries both 734139008 and
2414
+ // 900000000000526001) — otherwise only the last refset in the file gets a
2415
+ // valid index and the others collapse to index 0.
2416
+ const refSetConcept = this.conceptMap.get(BigInt(refSetId));
2417
+ if (refSetConcept) {
2418
+ refSet.index = refSetConcept.index;
2419
+ }
2410
2420
  this.refSets.set(refSetId, refSet);
2411
2421
  }
2412
2422
  return this.refSets.get(refSetId);
@@ -2585,6 +2595,14 @@ class SnomedImporter {
2585
2595
 
2586
2596
  // Index concepts - optimized version
2587
2597
  for (const concept of this.conceptList) {
2598
+ // Store the concept's module (resolved to its concept index) so that
2599
+ // $lookup can report the 'module' property. Modules whose concept is not
2600
+ // part of the subset are skipped (moduleId stays 0 / property omitted).
2601
+ const moduleConcept = this.conceptMap.get(concept.moduleId);
2602
+ if (moduleConcept && moduleConcept.index != null) {
2603
+ this.concepts.setModuleId(concept.index, moduleConcept.index);
2604
+ }
2605
+
2588
2606
  const refSetRefs = [];
2589
2607
  const refSetValues = [];
2590
2608
 
@@ -214,7 +214,8 @@ const DisplayDifference = {
214
214
  const DesignationUse = {
215
215
  DISPLAY: {
216
216
  system: 'http://terminology.hl7.org/CodeSystem/hl7TermMaintInfra', // http://terminology.hl7.org/CodeSystem/designation-use',
217
- code: 'preferredForLanguage' // 'display'
217
+ code: 'preferredForLanguage', // 'display'
218
+ display: 'Preferred For Language'
218
219
  },
219
220
  FSN: {
220
221
  system: 'http://snomed.info/sct',
@@ -356,10 +357,7 @@ class Designations {
356
357
  designation.status = status;
357
358
 
358
359
  if (isDisplay) {
359
- designation.use = {
360
- system: DesignationUse.DISPLAY.system,
361
- code: DesignationUse.DISPLAY.code
362
- };
360
+ designation.use = { ...DesignationUse.DISPLAY };
363
361
  } else if (use) {
364
362
  designation.use = use;
365
363
  }
@@ -157,8 +157,8 @@ class Renderer {
157
157
  async renderMetadataTable(res, tbl, sourcePackage) {
158
158
  this.renderMetadataVersion(res, tbl);
159
159
  await this.renderMetadataProfiles(res, tbl);
160
- this.renderMetadataTags(res, tbl);
161
- this.renderMetadataLabels(res, tbl);
160
+ await this.renderMetadataTags(res, tbl);
161
+ await this.renderMetadataLabels(res, tbl);
162
162
  this.renderMetadataLastUpdated(res, tbl);
163
163
  this.renderMetadataSource(res, tbl);
164
164
  this.renderProperty(tbl, 'TEST_PLAN_LANG', res.language);
@@ -173,6 +173,7 @@ class Renderer {
173
173
  this.renderProperty(tbl, 'CANON_REND_COMMITTEE', Extensions.readString(res, 'http://hl7.org/fhir/StructureDefinition/structuredefinition-wg'));
174
174
  this.renderPropertyCopy(tbl, 'GENERAL_COPYRIGHT', res.copyright);
175
175
  this.renderProperty(tbl, 'EXT_FMM_LEVEL', Extensions.readString(res, 'http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm'));
176
+ this.renderProperty(tbl, 'EXT_STANDARDS_STATUS', Extensions.readString(res, 'http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status'));
176
177
  this.renderProperty(tbl, 'PAT_PERIOD', res.effectivePeriod);
177
178
  this.renderPropertyLink(tbl, 'WEB_SOURCE', Extensions.readString(res, 'http://hl7.org/fhir/StructureDefinition/web-source'));
178
179
  this.renderProperty(tbl, 'GENERAL_SOURCE', sourcePackage);
@@ -221,41 +222,45 @@ class Renderer {
221
222
  }
222
223
  }
223
224
 
224
- renderMetadataTags(res, tbl) {
225
+ async renderMetadataTags(res, tbl) {
225
226
  if (res.meta?.tag) {
226
227
  let tr = tbl.tr();
227
228
  tr.td().b().tx(this.translate('GENERAL_PROF'));
228
229
  if (res.meta.tag.length > 1) {
229
230
  let ul = tr.td();
230
231
  for (let u of res.meta.tag) {
231
- this.renderCoding(ul.li(), u);
232
+ await this.renderCoding(ul.li(), u);
232
233
  }
233
234
  } else {
234
- this.renderCoding(tr.td(), res.meta.tag[0]);
235
+ await this.renderCoding(tr.td(), res.meta.tag[0]);
235
236
  }
236
237
  }
237
238
  }
238
239
 
239
- renderMetadataLabels(res, tbl) {
240
+ async renderMetadataLabels(res, tbl) {
240
241
  if (res.meta?.label) {
241
242
  let tr = tbl.tr();
242
243
  tr.td().b().tx(this.translate('GENERAL_PROF'));
243
244
  if (res.meta.label.length > 1) {
244
245
  let ul = tr.td();
245
246
  for (let u of res.meta.label) {
246
- this.renderCodin(ul.li(), u);
247
+ await this.renderCoding(ul.li(), u);
247
248
  }
248
249
  } else {
249
- this.renderCoding(tr.td(), res.meta.label[0]);
250
+ await this.renderCoding(tr.td(), res.meta.label[0]);
250
251
  }
251
252
  }
252
253
  }
253
254
 
255
+ // Inline phrases inherited from the Java core translations (RES_REND_VER = "version: {0}"
256
+ // etc) are not table labels; when rendering into a name/value table we use the dedicated
257
+ // *_NAME / *_VALUE phrases instead, and leave the inline phrases to the Java code.
258
+
254
259
  renderMetadataVersion(res, tbl) {
255
260
  if (res.meta?.version) {
256
261
  let tr = tbl.tr();
257
- tr.td().b().tx(this.translate('RES_REND_VER'));
258
- tr.td().tx(res.meta.version);
262
+ tr.td().b().tx(this.translate('RES_REND_VER_NAME'));
263
+ tr.td().tx(this.translate('RES_REND_VER_VALUE', [res.meta.version]));
259
264
  }
260
265
  }
261
266
 
@@ -263,9 +268,8 @@ class Renderer {
263
268
  if (res.meta?.lastUpdated) {
264
269
  this._requireValidDate(res.meta.lastUpdated, 'meta.lastUpdated');
265
270
  let tr = tbl.tr();
266
- // RES_REND_UPDATED is "Last updated: {0}" — supply the formatted date as
267
- // the {0} parameter so the placeholder is substituted.
268
- tr.td().b().tx(this.translate('RES_REND_UPDATED', [this.displayDate(res.meta.lastUpdated)]));
271
+ tr.td().b().tx(this.translate('RES_REND_UPDATED_NAME'));
272
+ tr.td().tx(this.translate('RES_REND_UPDATED_VALUE', [this.displayDate(res.meta.lastUpdated)]));
269
273
  }
270
274
  }
271
275
 
@@ -326,8 +330,8 @@ class Renderer {
326
330
  renderMetadataSource(res, tbl) {
327
331
  if (res.meta?.source) {
328
332
  let tr = tbl.tr();
329
- tr.td().b().tx(this.translate('RES_REND_INFO_SOURCE'));
330
- tr.td().tx(res.meta.source);
333
+ tr.td().b().tx(this.translate('RES_REND_INFO_SOURCE_NAME'));
334
+ tr.td().tx(this.translate('RES_REND_INFO_SOURCE_VALUE', [res.meta.source]));
331
335
  }
332
336
  }
333
337
 
@@ -654,7 +658,7 @@ class Renderer {
654
658
  await this.renderMetadataTable(cs, div_.table("grid"), sourcePackage);
655
659
 
656
660
  // Code system properties
657
- const hasProps = this.generateProperties(div_, cs);
661
+ const hasProps = await this.generateProperties(div_, cs);
658
662
 
659
663
  // Filters
660
664
  this.generateFilters(div_, cs);
@@ -773,18 +777,22 @@ class Renderer {
773
777
  }
774
778
  }
775
779
 
776
- generateProperties(x, cs) {
780
+ async generateProperties(x, cs) {
777
781
  if (!cs.property || cs.property.length === 0) {
778
782
  return false;
779
783
  }
780
784
 
785
+ const EXT_PROPERTY_VALUESET = 'http://hl7.org/fhir/StructureDefinition/codesystem-property-valueset';
786
+
781
787
  // Check what columns we need
782
788
  let hasURI = false;
783
789
  let hasDescription = false;
790
+ let hasValueSet = false;
784
791
 
785
792
  for (const p of cs.property) {
786
793
  hasURI = hasURI || !!p.uri;
787
794
  hasDescription = hasDescription || !!p.description;
795
+ hasValueSet = hasValueSet || !!Extensions.has(p, EXT_PROPERTY_VALUESET);
788
796
  }
789
797
 
790
798
  x.para().b().tx(this.translate('GENERAL_PROPS'));
@@ -800,6 +808,9 @@ class Renderer {
800
808
  if (hasDescription) {
801
809
  tr.th().tx(this.translate('GENERAL_DESC'));
802
810
  }
811
+ if (hasValueSet) {
812
+ tr.th().tx(this.translate('GENERAL_VALUESET'));
813
+ }
803
814
 
804
815
  for (const p of cs.property) {
805
816
  const row = tbl.tr();
@@ -811,6 +822,13 @@ class Renderer {
811
822
  if (hasDescription) {
812
823
  row.td().tx(p.description || '');
813
824
  }
825
+ if (hasValueSet) {
826
+ const td = row.td();
827
+ const vsUrl = Extensions.readString(p, EXT_PROPERTY_VALUESET);
828
+ if (vsUrl) {
829
+ await this.renderLink(td, vsUrl);
830
+ }
831
+ }
814
832
  }
815
833
 
816
834
  return true;
@@ -879,6 +897,9 @@ class Renderer {
879
897
  if (columnInfo.hasDefinition) {
880
898
  headerRow.th().tx(this.translate('GENERAL_DEFINITION'));
881
899
  }
900
+ if (columnInfo.hasComment) {
901
+ headerRow.th().tx(this.translate('GENERAL_COMMENTS'));
902
+ }
882
903
  if (columnInfo.hasDeprecated) {
883
904
  headerRow.th().tx(this.translate('CODESYSTEM_DEPRECATED'));
884
905
  }
@@ -888,6 +909,11 @@ class Renderer {
888
909
  headerRow.th().tx(this.getDisplayForProperty(prop) || prop.code);
889
910
  }
890
911
 
912
+ // Designation columns (by use + language)
913
+ for (const desig of columnInfo.designations) {
914
+ headerRow.th().tx(this.formatDesignationHeader(desig));
915
+ }
916
+
891
917
  // Render concepts recursively
892
918
  for (const concept of cs.concept) {
893
919
  await this.addConceptRow(tbl, concept, 0, cs, columnInfo);
@@ -913,17 +939,23 @@ class Renderer {
913
939
  }
914
940
 
915
941
  analyzeConceptColumns(cs) {
942
+ const EXT_CS_COMMENT = 'http://hl7.org/fhir/StructureDefinition/codesystem-concept-comments';
943
+ const EXT_DISPLAY_HINT = 'http://hl7.org/fhir/StructureDefinition/structuredefinition-display-hint';
916
944
  const info = {
917
945
  hasHierarchy: false,
918
946
  hasDisplay: false,
919
947
  hasDefinition: false,
920
948
  hasDeprecated: false,
921
949
  hasComment: false,
922
- properties: []
950
+ properties: [],
951
+ designations: [],
952
+ useMarkdown: this.hasMarkdownInDefinitions(cs)
923
953
  };
924
954
 
925
955
  // Check which properties are actually used
926
956
  const usedProperties = new Set();
957
+ // Track distinct designations, keyed by use + language.
958
+ const usedDesignations = new Map();
927
959
 
928
960
  const analyzeConceptList = (concepts) => {
929
961
  for (const c of concepts) {
@@ -943,30 +975,59 @@ class Renderer {
943
975
  info.hasDeprecated = true;
944
976
  }
945
977
 
978
+ // Check for concept comments
979
+ if (Extensions.has(c, EXT_CS_COMMENT)) {
980
+ info.hasComment = true;
981
+ }
982
+
946
983
  // Track used properties
947
984
  if (c.property) {
948
985
  for (const prop of c.property) {
949
986
  usedProperties.add(prop.code);
950
987
  }
951
988
  }
989
+
990
+ // Track distinct designations (by use + language)
991
+ if (c.designation) {
992
+ for (const desig of c.designation) {
993
+ const key = this.getDesignationKey(desig);
994
+ if (!usedDesignations.has(key)) {
995
+ usedDesignations.set(key, { use: desig.use, language: desig.language });
996
+ }
997
+ }
998
+ }
952
999
  }
953
1000
  };
954
1001
 
955
1002
  analyzeConceptList(cs.concept || []);
956
1003
 
1004
+ // If any property carries a display-hint extension, switch to "manual"
1005
+ // column selection: only properties explicitly hinted display/no-link show.
1006
+ const isManual = (cs.property || []).some(p => Extensions.has(p, EXT_DISPLAY_HINT));
1007
+
957
1008
  // Filter to properties that are actually used
958
1009
  if (cs.property) {
959
1010
  for (const prop of cs.property) {
960
- if (usedProperties.has(prop.code) && this.showPropertyInTable(prop)) {
1011
+ if (usedProperties.has(prop.code) && this.showPropertyInTable(prop, isManual)) {
961
1012
  info.properties.push(prop);
962
1013
  }
963
1014
  }
964
1015
  }
965
1016
 
1017
+ // Convert designation map to array, sorted for stable ordering
1018
+ info.designations = Array.from(usedDesignations.values()).sort((a, b) => {
1019
+ return this.getDesignationKey(a).localeCompare(this.getDesignationKey(b));
1020
+ });
1021
+
966
1022
  return info;
967
1023
  }
968
1024
 
969
- showPropertyInTable(prop) {
1025
+ showPropertyInTable(prop, isManual = false) {
1026
+ if (isManual) {
1027
+ // Manual mode: only show properties explicitly hinted display/no-link.
1028
+ const hint = Extensions.readString(prop, 'http://hl7.org/fhir/StructureDefinition/structuredefinition-display-hint');
1029
+ return hint === 'display' || hint === 'no-link';
1030
+ }
970
1031
  // Skip certain internal properties
971
1032
  const skipCodes = ['status', 'inactive', 'deprecated', 'notSelectable'];
972
1033
  return !skipCodes.includes(prop.code);
@@ -977,6 +1038,60 @@ class Renderer {
977
1038
  return prop.description || prop.code;
978
1039
  }
979
1040
 
1041
+ /**
1042
+ * Translations of a concept's definition, carried as translation extensions
1043
+ * on the definition primitive (the sibling _definition element in JSON).
1044
+ * Returns [{lang, value}]. Display translations are intentionally NOT read
1045
+ * here - display variants belong in designations.
1046
+ */
1047
+ definitionTranslations(concept) {
1048
+ const el = concept._definition;
1049
+ if (!el || !el.extension) {
1050
+ return [];
1051
+ }
1052
+ const out = [];
1053
+ for (const ext of el.extension) {
1054
+ if (ext.url === 'http://hl7.org/fhir/StructureDefinition/translation') {
1055
+ const lang = Extensions.readString(ext, 'lang');
1056
+ const value = Extensions.readString(ext, 'content');
1057
+ if (value) {
1058
+ out.push({ lang: lang || '', value });
1059
+ }
1060
+ }
1061
+ }
1062
+ return out;
1063
+ }
1064
+
1065
+ /**
1066
+ * Whether concept definitions should be rendered as markdown. Honours the
1067
+ * codesystem-use-markdown extension; otherwise auto-detects common markdown
1068
+ * constructs in any definition.
1069
+ */
1070
+ hasMarkdownInDefinitions(cs) {
1071
+ const flag = Extensions.readValue(cs, 'http://hl7.org/fhir/StructureDefinition/codesystem-use-markdown');
1072
+ if (flag && typeof flag.valueBoolean === 'boolean') {
1073
+ return flag.valueBoolean;
1074
+ }
1075
+ const looksMarkdown = (str) => {
1076
+ if (!str) {
1077
+ return false;
1078
+ }
1079
+ return /\]\([^)]*\)|^\s*#{1,6}\s+|^\s*[-*+]\s+|^\s*\d+\.\s+/m.test(str);
1080
+ };
1081
+ const walk = (concepts) => {
1082
+ for (const c of concepts || []) {
1083
+ if (looksMarkdown(c.definition)) {
1084
+ return true;
1085
+ }
1086
+ if (c.concept && walk(c.concept)) {
1087
+ return true;
1088
+ }
1089
+ }
1090
+ return false;
1091
+ };
1092
+ return walk(cs.concept || []);
1093
+ }
1094
+
980
1095
  isDeprecated(concept) {
981
1096
  if (concept.property) {
982
1097
  for (const prop of concept.property) {
@@ -987,6 +1102,12 @@ class Renderer {
987
1102
  }
988
1103
  }
989
1104
  }
1105
+ // The valueset-deprecated extension (on compose.include.concept or
1106
+ // expansion.contains) marks a concept as deprecated within this value set.
1107
+ const ext = Extensions.has(concept, 'http://hl7.org/fhir/StructureDefinition/valueset-deprecated');
1108
+ if (ext && ext.valueBoolean === true) {
1109
+ return true;
1110
+ }
990
1111
  return false;
991
1112
  }
992
1113
 
@@ -1031,7 +1152,35 @@ class Renderer {
1031
1152
 
1032
1153
  // Definition column
1033
1154
  if (columnInfo.hasDefinition) {
1034
- tr.td().tx(concept.definition || '');
1155
+ const td = tr.td();
1156
+ const useMarkdown = columnInfo.useMarkdown;
1157
+ if (concept.definition) {
1158
+ if (useMarkdown) {
1159
+ td.markdown(concept.definition);
1160
+ } else {
1161
+ td.tx(concept.definition);
1162
+ }
1163
+ }
1164
+ // Translations of the definition (translation extension on the
1165
+ // definition primitive) are rendered below the definition, same cell.
1166
+ for (const t of this.definitionTranslations(concept)) {
1167
+ td.br();
1168
+ td.i().tx('(' + t.lang + ') ');
1169
+ if (useMarkdown) {
1170
+ td.markdown(t.value);
1171
+ } else {
1172
+ td.tx(t.value);
1173
+ }
1174
+ }
1175
+ }
1176
+
1177
+ // Comment column
1178
+ if (columnInfo.hasComment) {
1179
+ const td = tr.td();
1180
+ const comment = Extensions.readString(concept, 'http://hl7.org/fhir/StructureDefinition/codesystem-concept-comments');
1181
+ if (comment) {
1182
+ td.tx(comment);
1183
+ }
1035
1184
  }
1036
1185
 
1037
1186
  // Deprecated column
@@ -1040,11 +1189,36 @@ class Renderer {
1040
1189
  if (this.isDeprecated(concept)) {
1041
1190
  td.tx(this.translate('CODESYSTEM_DEPRECATED_TRUE'));
1042
1191
 
1043
- // Check for replacement
1044
- const replacedBy = this.getPropertyValue(concept, 'replacedBy');
1045
- if (replacedBy) {
1192
+ // Replacement: prefer the codesystem-replacedby extension (a Coding),
1193
+ // then fall back to a 'replacedBy' property value.
1194
+ const replacedByExt = Extensions.readValue(concept, 'http://hl7.org/fhir/StructureDefinition/codesystem-replacedby');
1195
+ const replacedByCoding = replacedByExt && replacedByExt.valueCoding;
1196
+ const replacedByProp = this.getPropertyValue(concept, 'replacedBy');
1197
+ if (replacedByCoding) {
1198
+ td.tx(' ' + this.translate('CODESYSTEM_REPLACED_BY') + ' ');
1199
+ const link = this.linkResolver ?
1200
+ await this.linkResolver.resolveCode(this.opContext, replacedByCoding.system, replacedByCoding.version, replacedByCoding.code) : null;
1201
+ if (link) {
1202
+ td.ah(link.link).tx(replacedByCoding.code);
1203
+ } else {
1204
+ td.code().tx(replacedByCoding.code);
1205
+ }
1206
+ if (replacedByCoding.display) {
1207
+ td.tx(' "' + replacedByCoding.display + '"');
1208
+ }
1209
+ } else if (replacedByProp) {
1046
1210
  td.tx(' ' + this.translate('CODESYSTEM_REPLACED_BY') + ' ');
1047
- td.code().tx(replacedBy);
1211
+ td.code().tx(replacedByProp.value);
1212
+ } else {
1213
+ // No replacement: show the concept-level standards-status reason.
1214
+ const ssExt = Extensions.readValue(concept, 'http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status');
1215
+ if (ssExt) {
1216
+ const reason = Extensions.readString(ssExt, 'http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status-reason');
1217
+ if (reason) {
1218
+ td.tx(' ');
1219
+ td.markdown(reason);
1220
+ }
1221
+ }
1048
1222
  }
1049
1223
  }
1050
1224
  }
@@ -1052,6 +1226,7 @@ class Renderer {
1052
1226
  // Property columns
1053
1227
  for (const prop of columnInfo.properties) {
1054
1228
  const td = tr.td();
1229
+ const noLink = Extensions.readString(prop, 'http://hl7.org/fhir/StructureDefinition/structuredefinition-display-hint') === 'no-link';
1055
1230
  const values = this.getPropertyValues(concept, prop.code);
1056
1231
 
1057
1232
  let first = true;
@@ -1061,7 +1236,16 @@ class Renderer {
1061
1236
  }
1062
1237
  first = false;
1063
1238
 
1064
- await this.renderPropertyValue(td, val, prop, cs);
1239
+ await this.renderPropertyValue(td, val, prop, cs, noLink);
1240
+ }
1241
+ }
1242
+
1243
+ // Designation columns (by use + language)
1244
+ for (const desigDef of columnInfo.designations) {
1245
+ const td = tr.td();
1246
+ const value = this.getDesignationValue(concept, desigDef);
1247
+ if (value) {
1248
+ td.tx(value);
1065
1249
  }
1066
1250
  }
1067
1251
 
@@ -1098,13 +1282,13 @@ class Renderer {
1098
1282
  return null;
1099
1283
  }
1100
1284
 
1101
- async renderPropertyValue(td, val, propDef, cs) {
1285
+ async renderPropertyValue(td, val, propDef, cs, noLink = false) {
1102
1286
  if (!val) return;
1103
1287
 
1104
1288
  switch (val.type) {
1105
1289
  case 'code': {
1106
1290
  // If it's a parent reference, link to it
1107
- if (propDef.code === 'parent' || propDef.code === 'child') {
1291
+ if ((propDef.code === 'parent' || propDef.code === 'child') && !noLink) {
1108
1292
  td.ah('#' + cs.id + '-' + val.value).tx(val.value);
1109
1293
  } else {
1110
1294
  td.code().tx(val.value);
@@ -1113,7 +1297,7 @@ class Renderer {
1113
1297
  }
1114
1298
  case 'coding': {
1115
1299
  const coding = val.value;
1116
- const link = this.linkResolver ?
1300
+ const link = (!noLink && this.linkResolver) ?
1117
1301
  await this.linkResolver.resolveCode(this.opContext, coding.system, coding.version, coding.code) : null;
1118
1302
  if (link) {
1119
1303
  td.ah(link.link).tx(coding.code);
@@ -1131,7 +1315,7 @@ class Renderer {
1131
1315
  }
1132
1316
  case 'string': {
1133
1317
  // Check if it's a URL
1134
- if (val.value.startsWith('http://') || val.value.startsWith('https://')) {
1318
+ if (!noLink && (val.value.startsWith('http://') || val.value.startsWith('https://'))) {
1135
1319
  td.ah(val.value).tx(val.value);
1136
1320
  } else {
1137
1321
  td.tx(val.value);
@@ -1215,6 +1399,19 @@ class Renderer {
1215
1399
  }
1216
1400
 
1217
1401
  async renderExpansion(x, vs, tbl) {
1402
+ // Surface expansion-level status extensions as prominent warnings: the
1403
+ // expansion may not contain every code in the value set.
1404
+ if (Extensions.has(vs.expansion, 'http://hl7.org/fhir/StructureDefinition/valueset-unclosed')) {
1405
+ const reason = Extensions.readString(vs.expansion, 'http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason');
1406
+ const p = x.para();
1407
+ p.b().tx('Warning: ');
1408
+ p.tx('This expansion is not closed - it may not contain all the codes in the value set.' + (reason ? ' ' + reason : ''));
1409
+ }
1410
+ if (Extensions.has(vs.expansion, 'http://hl7.org/fhir/StructureDefinition/valueset-toocostly')) {
1411
+ const p = x.para();
1412
+ p.b().tx('Warning: ');
1413
+ p.tx('This expansion is incomplete - a complete expansion was too costly to produce.');
1414
+ }
1218
1415
  this.renderProperty(tbl, 'Expansion Identifier', vs.expansion.identifier);
1219
1416
  this.renderProperty(tbl, 'Expansion Timestamp', vs.expansion.timestamp);
1220
1417
  this.renderProperty(tbl, 'Expansion Total', vs.expansion.total);
package/tx/library.js CHANGED
@@ -481,7 +481,11 @@ class Library {
481
481
  throw new Error("Unable to load VSAC provider unless vsacCfg is provided in the configuration");
482
482
  }
483
483
  let vsac = new VSACValueSetProvider(this.vsacCfg, this.stats);
484
- vsac.initialize();
484
+ // Intentionally not awaited: initialize() might run a full VSAC sync on first load,
485
+ // which can take minutes - we don't want to block startup. But attach a handler
486
+ // so a failed background sync surfaces as a log error rather than an unhandled
487
+ // promise rejection.
488
+ vsac.initialize().catch((err) => this.log.error(`VSAC initialization failed: ${err.message}`));
485
489
  this.valueSetProviders.push(vsac);
486
490
  this.externalSources.push(vsac);
487
491
  //const mem = process.memoryUsage();