fhirsmith 0.9.6 → 0.9.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/CHANGELOG.md CHANGED
@@ -6,6 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
 
9
+ ## [v0.9.7] - 2026-06-12
10
+
11
+ ### Added
12
+
13
+ - Support for tx modele to load resources directly
14
+ - Missing overwork protection processing ECL
15
+
16
+ ### Fixed
17
+
18
+ - Fix bug validating secondary displays across languages
19
+ - Fix CORS issue (double headers)
20
+ - Fix czech code in snomed import
21
+ - Many minor validation fixes
22
+ - ECL processing bugs
23
+ - Publishing: better logging, and fix timeout & restart error
24
+
25
+ ### Tx Conformance Statement
26
+
27
+ FHIRsmith passed all 2503 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed, tests v1.9.1, runner v6.9.10)
28
+
9
29
  ## [v0.9.6] - 2026-05-21
10
30
 
11
31
  ### Added
@@ -0,0 +1,91 @@
1
+ const fs = require('fs').promises;
2
+ const path = require('path');
3
+ const { PackageContentLoader } = require('./package-manager');
4
+
5
+ /**
6
+ * A content loader that scans a folder for JSON FHIR terminology resources
7
+ * (CodeSystem, ValueSet, ConceptMap). It synthesizes the package layout that
8
+ * PackageContentLoader expects so the existing PackageValueSetProvider /
9
+ * PackageConceptMapProvider machinery can be reused without modification.
10
+ *
11
+ * The user's source folder is treated as read-only. Any SQLite caches built
12
+ * by the Package*Provider classes are written to the cacheFolder passed in,
13
+ * not to the source folder.
14
+ */
15
+ class FolderContentLoader extends PackageContentLoader {
16
+ /**
17
+ * @param {string} sourceFolder - Folder to scan for JSON resources
18
+ * @param {string} cacheFolder - Folder where Package*Provider DBs may live
19
+ */
20
+ constructor(sourceFolder, cacheFolder) {
21
+ super(cacheFolder);
22
+ this.sourceFolder = sourceFolder;
23
+ // Resources are read directly from the source folder rather than from
24
+ // <packageFolder>/package as PackageContentLoader assumes.
25
+ this.packageSubfolder = sourceFolder;
26
+ }
27
+
28
+ /**
29
+ * Replaces the standard initialize: there is no package.json or .index.json
30
+ * to read, so we synthesize an index from the JSON files in the folder.
31
+ */
32
+ async initialize() {
33
+ if (this.loaded) {
34
+ return;
35
+ }
36
+
37
+ try {
38
+ const stat = await fs.stat(this.sourceFolder);
39
+ if (!stat.isDirectory()) {
40
+ throw new Error(`Folder source path is not a directory: ${this.sourceFolder}`);
41
+ }
42
+ } catch (err) {
43
+ if (err.code === 'ENOENT') {
44
+ throw new Error(`Folder source path does not exist: ${this.sourceFolder}`);
45
+ }
46
+ throw err;
47
+ }
48
+
49
+ // Synthesize package metadata so id()/version()/pid() return something usable.
50
+ this.package = {
51
+ name: 'folder.' + path.basename(this.sourceFolder),
52
+ version: '0.0.0',
53
+ fhirVersions: ['5.0.0']
54
+ };
55
+
56
+ const files = [];
57
+ const entries = await fs.readdir(this.sourceFolder, { withFileTypes: true });
58
+ for (const entry of entries) {
59
+ if (!entry.isFile()) continue;
60
+ if (!entry.name.toLowerCase().endsWith('.json')) continue;
61
+
62
+ const fullPath = path.join(this.sourceFolder, entry.name);
63
+ let json;
64
+ try {
65
+ const content = await fs.readFile(fullPath, 'utf8');
66
+ json = JSON.parse(content);
67
+ } catch {
68
+ // Skip unreadable / non-JSON files rather than failing the whole load.
69
+ continue;
70
+ }
71
+
72
+ if (!json || typeof json !== 'object') continue;
73
+ const rt = json.resourceType;
74
+ if (rt !== 'CodeSystem' && rt !== 'ValueSet' && rt !== 'ConceptMap') continue;
75
+
76
+ files.push({
77
+ filename: entry.name,
78
+ resourceType: rt,
79
+ id: json.id,
80
+ url: json.url,
81
+ version: json.version
82
+ });
83
+ }
84
+
85
+ this.index = { files, 'index-version': 2 };
86
+ this.buildIndexes();
87
+ this.loaded = true;
88
+ }
89
+ }
90
+
91
+ module.exports = { FolderContentLoader };
@@ -127,12 +127,8 @@ class NpmProjectorModule {
127
127
  * Set up Express routes
128
128
  */
129
129
  setupRoutes() {
130
- // CORS for browser access
131
- this.router.use((req, res, next) => {
132
- res.header('Access-Control-Allow-Origin', '*');
133
- res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
134
- next();
135
- });
130
+ // CORS is handled once at the app level (server.js, from config.server.cors).
131
+ // Do not set Access-Control-* headers here.
136
132
 
137
133
  // Root - module info
138
134
  this.router.get('/', (req, res) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fhirsmith",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
4
4
  "txVersion": "1.9.1",
5
5
  "description": "A Node.js server that provides a collection of tools to serve the FHIR ecosystem",
6
6
  "main": "server.js",
@@ -17,6 +17,7 @@ class PublisherModule {
17
17
  this.logger = null;
18
18
  this.taskProcessor = null;
19
19
  this.isProcessing = false;
20
+ this.activeTaskIds = new Set();
20
21
  this.shutdownRequested = false;
21
22
  this.stats = stats;
22
23
  }
@@ -308,13 +309,19 @@ class PublisherModule {
308
309
  if (this.shutdownRequested) return;
309
310
 
310
311
  if (this.isProcessing) {
312
+ // A publication can legitimately run for longer than an hour (go-publish on a
313
+ // large IG), so never reset isProcessing while a task is in flight — doing so
314
+ // lets the poller start a second, concurrent run of the same task, which then
315
+ // fails on 'ig-registry already exists' and stomps the real run's status.
316
+ // Recovery from a genuinely hung IG Publisher is handled by the
317
+ // igPublisherTimeoutMinutes kill in runIgPublisher/runPublisherGoPublish.
311
318
  const stuckMs = this.isProcessingStarted ? Date.now() - this.isProcessingStarted : 0;
312
- if (stuckMs > 60 * 60 * 1000) {
313
- this.logger.warn('Task processor appears stuck (' + Math.round(stuckMs / 60000) + ' min) — resetting');
314
- this.isProcessing = false;
315
- } else {
316
- return;
319
+ const warnAfterMs = ((this.config.igPublisherTimeoutMinutes || 60) + 30) * 60 * 1000;
320
+ if (stuckMs > warnAfterMs) {
321
+ this.logger.warn('Task processor still busy after ' + Math.round(stuckMs / 60000) + ' min (tasks: ' +
322
+ Array.from(this.activeTaskIds).join(', ') + ') — possible hang, not starting new work');
317
323
  }
324
+ return;
318
325
  }
319
326
 
320
327
  await this.processNextTask();
@@ -325,10 +332,14 @@ class PublisherModule {
325
332
  this.isProcessing = true;
326
333
  this.isProcessingStarted = Date.now();
327
334
 
335
+ let claimedTaskId = null;
328
336
  try {
329
337
  // Look for queued tasks first (draft builds)
330
338
  let task = await this.getNextQueuedTask();
331
339
  if (task) {
340
+ if (this.activeTaskIds.has(task.id)) return; // already being processed
341
+ claimedTaskId = task.id;
342
+ this.activeTaskIds.add(task.id);
332
343
  this.stats.task('Publisher', 'Building ' + task.npm_package_id + '#' + task.version);
333
344
  await this.processDraftBuild(task);
334
345
  this.stats.taskDone('Publisher', 'Built ' + task.npm_package_id + '#' + task.version);
@@ -338,6 +349,9 @@ class PublisherModule {
338
349
  // Then look for approved tasks (publishing)
339
350
  task = await this.getNextApprovedTask();
340
351
  if (task) {
352
+ if (this.activeTaskIds.has(task.id)) return; // already being processed
353
+ claimedTaskId = task.id;
354
+ this.activeTaskIds.add(task.id);
341
355
  this.stats.task('Publisher', 'Publishing ' + task.npm_package_id + '#' + task.version);
342
356
  await this.processPublication(task);
343
357
  this.stats.taskDone('Publisher', 'Published ' + task.npm_package_id + '#' + task.version);
@@ -349,6 +363,9 @@ class PublisherModule {
349
363
  this.logger.error('Error in task processor:', error);
350
364
  this.stats.taskError('Publisher', 'Error: ' + error.message);
351
365
  } finally {
366
+ if (claimedTaskId !== null) {
367
+ this.activeTaskIds.delete(claimedTaskId);
368
+ }
352
369
  this.isProcessing = false;
353
370
  }
354
371
  }
@@ -390,6 +407,9 @@ class PublisherModule {
390
407
  fields.push('waiting_approval_at = CURRENT_TIMESTAMP');
391
408
  } else if (status === 'complete') {
392
409
  fields.push('completed_at = CURRENT_TIMESTAMP');
410
+ // A successful completion invalidates any earlier failure (e.g. from a
411
+ // superseded duplicate run) — don't let a stale message outlive success.
412
+ fields.push('failure_reason = NULL');
393
413
  } else if (status === 'failed') {
394
414
  fields.push('failed_at = CURRENT_TIMESTAMP');
395
415
  }
@@ -414,6 +434,28 @@ class PublisherModule {
414
434
  });
415
435
  }
416
436
 
437
+ // Update arbitrary task fields WITHOUT touching status. Use this from long-running
438
+ // work (e.g. saving the announcement mid-publication) — writing back the in-memory
439
+ // task.status can resurrect a stale status and cause the poller to re-run the task.
440
+ async updateTaskFields(taskId, fields) {
441
+ const keys = Object.keys(fields);
442
+ if (keys.length === 0) return;
443
+ const assignments = keys.map(key => key + ' = ?');
444
+ const values = keys.map(key => fields[key]);
445
+ values.push(taskId);
446
+
447
+ return new Promise((resolve, reject) => {
448
+ this.db.run(
449
+ 'UPDATE tasks SET ' + assignments.join(', ') + ' WHERE id = ?',
450
+ values,
451
+ (err) => {
452
+ if (err) reject(err);
453
+ else resolve();
454
+ }
455
+ );
456
+ });
457
+ }
458
+
417
459
  async logTaskMessage(taskId, level, message) {
418
460
  return new Promise((resolve) => {
419
461
  this.db.run(
@@ -622,14 +664,38 @@ class PublisherModule {
622
664
  // Create log file stream
623
665
  const logStream = fs.createWriteStream(logFile);
624
666
 
667
+ const buildStart = Date.now();
668
+ let lastDataAt = Date.now();
669
+
625
670
  java.stdout.on('data', (data) => {
671
+ lastDataAt = Date.now();
626
672
  logStream.write(data);
627
673
  });
628
674
 
629
675
  java.stderr.on('data', (data) => {
676
+ lastDataAt = Date.now();
630
677
  logStream.write(data);
631
678
  });
632
679
 
680
+ // Heartbeat: emit a status line every 60s regardless of stdout activity,
681
+ // so silent phases of the Publisher (e.g. "Validating Resources") still
682
+ // surface a signal-of-life in the task log.
683
+ const heartbeat = setInterval(async () => {
684
+ const elapsedMs = Date.now() - buildStart;
685
+ const sinceDataMs = Date.now() - lastDataAt;
686
+ let logKb = 0;
687
+ try { logKb = Math.round(fs.statSync(logFile).size / 1024); } catch (_) {}
688
+ const elapsedMin = Math.floor(elapsedMs / 60000);
689
+ const elapsedSec = Math.floor(elapsedMs / 1000) % 60;
690
+ const idleSec = Math.floor(sinceDataMs / 1000);
691
+ await this.logTaskMessage(
692
+ taskId,
693
+ 'info',
694
+ 'IG Publisher heartbeat: elapsed ' + elapsedMin + 'm' + elapsedSec + 's, ' +
695
+ 'log ' + logKb + ' KB, last output ' + idleSec + 's ago'
696
+ );
697
+ }, 60 * 1000);
698
+
633
699
  java.on('close', async (code) => {
634
700
  logStream.end();
635
701
 
@@ -660,6 +726,7 @@ class PublisherModule {
660
726
 
661
727
  java.on('close', () => {
662
728
  clearTimeout(timeout);
729
+ clearInterval(heartbeat);
663
730
  });
664
731
  });
665
732
  }
@@ -789,7 +856,7 @@ class PublisherModule {
789
856
  if (fs.existsSync(announcementPath)) {
790
857
  try {
791
858
  const announcement = fs.readFileSync(announcementPath, 'utf8');
792
- await this.updateTaskStatus(task.id, task.status, { announcement: announcement });
859
+ await this.updateTaskFields(task.id, { announcement: announcement });
793
860
  await this.logTaskMessage(task.id, 'info', 'Announcement text saved (' + announcement.length + ' chars)');
794
861
  } catch (err) {
795
862
  await this.logTaskMessage(task.id, 'warn', 'Failed to read announcement file: ' + err.message);
@@ -830,14 +897,37 @@ class PublisherModule {
830
897
 
831
898
  const logStream = fs.createWriteStream(logFile);
832
899
 
900
+ const buildStart = Date.now();
901
+ let lastDataAt = Date.now();
902
+
833
903
  java.stdout.on('data', (data) => {
904
+ lastDataAt = Date.now();
834
905
  logStream.write(data);
835
906
  });
836
907
 
837
908
  java.stderr.on('data', (data) => {
909
+ lastDataAt = Date.now();
838
910
  logStream.write(data);
839
911
  });
840
912
 
913
+ // Heartbeat: emit a status line every 60s regardless of stdout activity,
914
+ // so silent phases of the Publisher still surface a signal-of-life.
915
+ const heartbeat = setInterval(async () => {
916
+ const elapsedMs = Date.now() - buildStart;
917
+ const sinceDataMs = Date.now() - lastDataAt;
918
+ let logKb = 0;
919
+ try { logKb = Math.round(fs.statSync(logFile).size / 1024); } catch (_) {}
920
+ const elapsedMin = Math.floor(elapsedMs / 60000);
921
+ const elapsedSec = Math.floor(elapsedMs / 1000) % 60;
922
+ const idleSec = Math.floor(sinceDataMs / 1000);
923
+ await this.logTaskMessage(
924
+ taskId,
925
+ 'info',
926
+ 'IG Publisher go-publish heartbeat: elapsed ' + elapsedMin + 'm' + elapsedSec + 's, ' +
927
+ 'log ' + logKb + ' KB, last output ' + idleSec + 's ago'
928
+ );
929
+ }, 60 * 1000);
930
+
841
931
  java.on('close', async (code) => {
842
932
  logStream.end();
843
933
  if (code === 0) {
@@ -856,16 +946,18 @@ class PublisherModule {
856
946
  reject(error);
857
947
  });
858
948
 
859
- // Timeout after 60 minutes for publication (longer than draft build)
949
+ // Timeout configurable via publisher.igPublisherTimeoutMinutes (default: 60 minutes)
950
+ const timeoutMinutes = this.config.igPublisherTimeoutMinutes || 60;
860
951
  const timeout = setTimeout(async () => {
861
952
  java.kill();
862
953
  logStream.end();
863
- await this.logTaskMessage(taskId, 'error', 'IG Publisher go-publish timed out after 60 minutes');
954
+ await this.logTaskMessage(taskId, 'error', 'IG Publisher go-publish timed out after ' + timeoutMinutes + ' minutes');
864
955
  reject(new Error('IG Publisher go-publish timed out'));
865
- }, 60 * 60 * 1000);
956
+ }, timeoutMinutes * 60 * 1000);
866
957
 
867
958
  java.on('close', () => {
868
959
  clearTimeout(timeout);
960
+ clearInterval(heartbeat);
869
961
  });
870
962
  });
871
963
  }
@@ -758,7 +758,7 @@ class RegistryModule {
758
758
  case '11000267109': edition = 'KR'; break;
759
759
  case '900000001000122104': edition = 'ES-ES'; break;
760
760
  case '2011000195101': edition = 'CH'; break;
761
- case '11000279109': edition = 'CX'; break;
761
+ case '11000279109': edition = 'CZ'; break;
762
762
  case '999000021000000109': edition = 'UK+Clinical'; break;
763
763
  case '5631000179106': edition = 'UY'; break;
764
764
  case '21000325107': edition = 'CL'; break;
@@ -1233,7 +1233,7 @@ class RegistryModule {
1233
1233
  // FHIR Version field
1234
1234
  html += '<p>';
1235
1235
  html += '<label for="fhirVersion" class="form-label fw-bold">FHIR Version <span class="text-danger">*</span></label>';
1236
- html += `<input type="text" class="form-control" id="fhirVersion" name="fhirVersion" size="8"
1236
+ html += `<input type="text" class="form-control" id="fhirVersion" name="fhirVersion" size="8"
1237
1237
  value="${escape(fhirVersion)}" required>`;
1238
1238
  html += '</p>';
1239
1239
  html += '<p class="text-muted small">Examples: R4, 4.0.1, 5.0.0, etc.</p>';
@@ -1241,7 +1241,7 @@ class RegistryModule {
1241
1241
  html += '<div class="alert alert-info">Either Code System URL or Value Set URL must be provided:</div>';
1242
1242
  html += '<p>';
1243
1243
  html += '<label for="url" class="form-label fw-bold">Code System URL</label>';
1244
- html += `<input type="url" class="form-control" id="url" name="url"
1244
+ html += `<input type="url" class="form-control" id="url" name="url"
1245
1245
  value="${escape(url)}">`;
1246
1246
  html += '</p>';
1247
1247
  html += '<p class="text-muted small">Example: http://loinc.org</p>';
@@ -1249,7 +1249,7 @@ class RegistryModule {
1249
1249
  // ValueSet URL field - now vertical
1250
1250
  html += '<p>';
1251
1251
  html += '<label for="valueSet" class="form-label fw-bold">Value Set URL</label>';
1252
- html += `<input type="url" class="form-control" id="valueSet" name="valueSet"
1252
+ html += `<input type="url" class="form-control" id="valueSet" name="valueSet"
1253
1253
  value="${escape(valueSet)}">`;
1254
1254
  html += '</p>';
1255
1255
  html += '<p class="text-muted small">Example: http://hl7.org/fhir/ValueSet/observation-codes</p>';
@@ -1257,7 +1257,7 @@ class RegistryModule {
1257
1257
 
1258
1258
  // Authoritative Only checkbox
1259
1259
  html += '<p>';
1260
- html += `<input type="checkbox" class="form-check-input" id="authoritativeOnly"
1260
+ html += `<input type="checkbox" class="form-check-input" id="authoritativeOnly"
1261
1261
  name="authoritativeOnly" value="true" ${authoritativeOnly ? 'checked' : ''}>`;
1262
1262
  html += '<label class="form-check-label" for="authoritativeOnly">&nbsp;Show only authoritative servers</label>';
1263
1263
  html += '</p>';
@@ -1276,7 +1276,7 @@ class RegistryModule {
1276
1276
  document.querySelector('form').addEventListener('submit', function(e) {
1277
1277
  const url = document.getElementById('url').value.trim();
1278
1278
  const valueSet = document.getElementById('valueSet').value.trim();
1279
-
1279
+
1280
1280
  if (!url && !valueSet) {
1281
1281
  e.preventDefault();
1282
1282
  alert('You must provide either a Code System URL or a Value Set URL');
package/server.js CHANGED
@@ -7,7 +7,7 @@
7
7
  //
8
8
 
9
9
  const express = require('express');
10
- // const cors = require('cors');
10
+ const cors = require('cors');
11
11
  const path = require('path');
12
12
  const fs = require('fs');
13
13
  const os = require('os');
@@ -82,7 +82,11 @@ app.use((req, res, next) => {
82
82
  next();
83
83
  });
84
84
 
85
- // app.use(cors(config.server.cors));
85
+ // Single, app-level CORS policy for all modules, driven by config.server.cors.
86
+ // This also handles OPTIONS preflight, so modules must NOT set their own
87
+ // Access-Control-* headers (doing so stacks a second CORS layer and produces
88
+ // duplicate, conflicting headers that browsers reject).
89
+ app.use(cors(config.server.cors));
86
90
 
87
91
  // Module instances
88
92
  const modules = {};
@@ -544,7 +544,7 @@ NO_VALID_DISPLAY_FOUND_LANG_NONE_other = ''{5}'' is the default display; the cod
544
544
  NO_VALID_DISPLAY_FOUND_LANG_SOME_one = ''{5}'' is the default display; no valid Display Names found for {1}#{2} in the language {4}
545
545
  NO_VALID_DISPLAY_FOUND_LANG_SOME_other = ''{5}'' is the default display; no valid Display Names found for {1}#{2} in the language {4}
546
546
  NO_VALID_DISPLAY_FOUND_NONE_FOR_LANG_ERR = Wrong Display Name ''{0}'' for {1}#{2}. There are no valid display names found for language(s) ''{3}''. Default display is ''{4}''
547
- NO_VALID_DISPLAY_FOUND_NONE_FOR_LANG_OK = There are no valid display names found for the code {1}#{2} for language(s) ''{3}''. The display is ''{4}'' which is the default language display
547
+ NO_VALID_DISPLAY_FOUND_NONE_FOR_LANG_OK = There are no valid display names found for the code {1}#{2} for language(s) ''{3}''. The display is ''{0}'' which is a valid display for the default language
548
548
  NO_VALID_DISPLAY_FOUND_one = No valid Display Names found for {1}#{2} in the language {4}
549
549
  NO_VALID_DISPLAY_FOUND_other = No valid Display Names found for {1}#{2} in the languages {4}
550
550
  No_validator_configured = No validator configured
package/tx/cs/cs-cs.js CHANGED
@@ -1140,6 +1140,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1140
1140
  const allConcepts = this.codeSystem.getAllConcepts();
1141
1141
 
1142
1142
  for (const concept of allConcepts) {
1143
+ this.opContext.deadCheck('cs:searchFilter');
1143
1144
  const rating = this._calculateSearchRating(concept, searchTerm);
1144
1145
  if (rating > 0) {
1145
1146
  results.add(concept, rating);
@@ -1285,6 +1286,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1285
1286
 
1286
1287
  const allCodes = this.codeSystem.getAllCodes();
1287
1288
  for (const code of allCodes) {
1289
+ this.opContext.deadCheck('cs:conceptFilter:is-not-a');
1288
1290
  if (!excludeSet.has(code)) {
1289
1291
  const concept = this.codeSystem.getConceptByCode(code);
1290
1292
  if (concept) {
@@ -1316,6 +1318,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1316
1318
  const regex = regexUtilities.compile('^' + value + '$');
1317
1319
  const allCodes = this.codeSystem.getAllCodes();
1318
1320
  for (const code of allCodes) {
1321
+ this.opContext.deadCheck('cs:conceptFilter:regex');
1319
1322
  if (regex.test(code)) {
1320
1323
  const concept = this.codeSystem.getConceptByCode(code);
1321
1324
  if (concept) {
@@ -1349,6 +1352,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1349
1352
  }
1350
1353
  const descendants = this.codeSystem.getDescendants(ancestorCode);
1351
1354
  for (const code of descendants) {
1355
+ this.opContext.deadCheck('cs:addDescendants');
1352
1356
  if (code !== ancestorCode) {
1353
1357
  const concept = this.codeSystem.getConceptByCode(code);
1354
1358
  if (concept) {
@@ -1370,6 +1374,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1370
1374
  if (concept) {
1371
1375
  const descendants = this.codeSystem.getChildren(parentCode);
1372
1376
  for (const code of descendants) {
1377
+ this.opContext.deadCheck('cs:addChildren');
1373
1378
  if (code !== parentCode) { // should not be
1374
1379
  const concept = this.codeSystem.getConceptByCode(code);
1375
1380
  if (concept) {
@@ -1393,6 +1398,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1393
1398
 
1394
1399
  const allCodes = this.codeSystem.getAllCodes();
1395
1400
  for (const code of allCodes) {
1401
+ this.opContext.deadCheck('cs:childExistsFilter');
1396
1402
  const hasChildren = this.codeSystem.getChildren(code).length > 0;
1397
1403
  if (hasChildren === wantChildren) {
1398
1404
  const concept = this.codeSystem.getConceptByCode(code);
@@ -1419,6 +1425,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1419
1425
  const allConcepts = this.codeSystem.getAllConcepts();
1420
1426
 
1421
1427
  for (const concept of allConcepts) {
1428
+ this.opContext.deadCheck('cs:propertyFilter');
1422
1429
  if (this._conceptMatchesPropertyFilter(concept, propertyDef, op, value)) {
1423
1430
  results.add(concept, 0);
1424
1431
  }
@@ -1498,6 +1505,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1498
1505
  const allConcepts = this.codeSystem.getAllConcepts();
1499
1506
 
1500
1507
  for (const concept of allConcepts) {
1508
+ this.opContext.deadCheck('cs:knownPropertyFilter');
1501
1509
  let matches = false;
1502
1510
 
1503
1511
  if (prop === 'notSelectable') {
package/tx/cs/cs-loinc.js CHANGED
@@ -947,6 +947,7 @@ class LoincServices extends BaseCSServices {
947
947
  reject(err);
948
948
  } else {
949
949
  for (const row of rows) {
950
+ if (this.opContext) this.opContext.deadCheck('loinc:findRegexMatches');
950
951
  if (regex.test(row[valueColumn])) {
951
952
  matchingKeys.push(row[keyColumn]);
952
953
  }
@@ -5,7 +5,8 @@ const { AbstractCodeSystemProvider } = require('./cs-provider-api');
5
5
  */
6
6
  class ListCodeSystemProvider extends AbstractCodeSystemProvider {
7
7
  /**
8
- * {Map<String, CodeSystem>} A list of code system factories that contains all the preloaded native code systems
8
+ * {CodeSystem[]} The preloaded FHIR code systems in this list. This is an
9
+ * array — append with .push(), not Map-style .set().
9
10
  */
10
11
  codeSystems = [];
11
12