fhirsmith 0.7.6 → 0.8.2

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 (57) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +5 -1
  3. package/library/languages.js +10 -0
  4. package/package.json +1 -1
  5. package/packages/package-crawler.js +2 -2
  6. package/publisher/publisher.js +1 -1
  7. package/registry/registry.js +2 -2
  8. package/root-bare-template.html +1 -2
  9. package/security.md +3 -0
  10. package/server.js +100 -70
  11. package/stats.js +37 -6
  12. package/tx/cs/cs-api.js +8 -4
  13. package/tx/cs/cs-loinc.js +14 -2
  14. package/tx/cs/cs-omop.js +5 -3
  15. package/tx/cs/cs-rxnorm.js +18 -16
  16. package/tx/cs/cs-snomed.js +279 -6
  17. package/tx/data/cpt-fragment.db +0 -0
  18. package/tx/data/cs-de.json +186 -0
  19. package/tx/data/cs-extensions.json +92 -0
  20. package/tx/data/cs-simple.json +130 -0
  21. package/tx/data/cs-supplement.json +78 -0
  22. package/tx/data/lang.dat +49180 -0
  23. package/tx/data/languages.csv +191 -0
  24. package/tx/data/loinc-subset.txt +75 -0
  25. package/tx/data/omop-fragment.db +0 -0
  26. package/tx/data/readme.md +43 -0
  27. package/tx/data/regions.csv +273 -0
  28. package/tx/data/rxnorm-subset.txt +22 -0
  29. package/tx/data/snomed-subset.txt +47 -0
  30. package/tx/data/ucum-essence.xml +2059 -0
  31. package/tx/html/dash-metrics.liquid +147 -0
  32. package/tx/importers/import-rxnorm.module.js +4 -30
  33. package/tx/library/canonical-resource.js +8 -0
  34. package/tx/library/conceptmap.js +29 -1
  35. package/tx/library/designations.js +4 -8
  36. package/tx/library/extensions.js +4 -3
  37. package/tx/library/renderer.js +9 -9
  38. package/tx/ocl/cm-ocl.cjs +185 -65
  39. package/tx/ocl/cs-ocl.cjs +69 -50
  40. package/tx/ocl/jobs/background-queue.cjs +0 -8
  41. package/tx/ocl/mappers/concept-mapper.cjs +13 -3
  42. package/tx/ocl/shared/patches.cjs +1 -0
  43. package/tx/ocl/vs-ocl.cjs +137 -157
  44. package/tx/operation-context.js +3 -3
  45. package/tx/params.js +2 -2
  46. package/tx/provider.js +6 -3
  47. package/tx/sct/structures.js +6 -1
  48. package/tx/tx.fhir.org.yml +1 -1
  49. package/tx/vs/vs-database.js +107 -23
  50. package/tx/vs/vs-vsac.js +66 -19
  51. package/tx/workers/expand.js +10 -10
  52. package/tx/workers/related.js +2 -2
  53. package/tx/workers/search.js +2 -1
  54. package/tx/workers/translate.js +222 -33
  55. package/tx/workers/validate.js +13 -13
  56. package/tx/xversion/xv-parameters.js +54 -1
  57. package/xig/xig.js +171 -9
package/tx/ocl/cm-ocl.cjs CHANGED
@@ -59,15 +59,19 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider {
59
59
  return await this.fetchConceptMapById(mappingId);
60
60
  }
61
61
 
62
- const mappings = await this.#searchMappings({ from_source_url: url }, this.maxSearchPages);
63
- for (const mapping of mappings) {
64
- const cm = this.#toConceptMap(mapping);
65
- if (cm) {
66
- this.#indexConceptMap(cm);
67
- if (cm.url === url && (!version || cm.version === version)) {
68
- return cm;
62
+ try {
63
+ const mappings = await this.#searchMappings({ from_source_url: url }, this.maxSearchPages);
64
+ for (const mapping of mappings) {
65
+ const cm = this.#toConceptMap(mapping);
66
+ if (cm) {
67
+ this.#indexConceptMap(cm);
68
+ if (cm.url === url && (!version || cm.version === version)) {
69
+ return cm;
70
+ }
69
71
  }
70
72
  }
73
+ } catch (_err) {
74
+ // OCL API unreachable or returned error — treat as not found
71
75
  }
72
76
 
73
77
  return null;
@@ -87,45 +91,194 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider {
87
91
  return this._idMap.get(rawId);
88
92
  }
89
93
 
90
- const response = await this.httpClient.get(`/mappings/${encodeURIComponent(rawId)}/`);
91
- const cm = this.#toConceptMap(response.data);
92
- if (!cm) {
94
+ try {
95
+ const response = await this.httpClient.get(`/mappings/${encodeURIComponent(rawId)}/`);
96
+ const cm = this.#toConceptMap(response.data);
97
+ if (!cm) {
98
+ return null;
99
+ }
100
+ this.#indexConceptMap(cm);
101
+ return cm;
102
+ } catch (_err) {
93
103
  return null;
94
104
  }
95
- this.#indexConceptMap(cm);
96
- return cm;
97
105
  }
98
106
 
99
- // eslint-disable-next-line no-unused-vars
100
107
  async searchConceptMaps(searchParams, _elements) {
101
108
  this._validateSearchParams(searchParams);
102
109
 
103
- const params = Object.fromEntries(searchParams.map(({ name, value }) => [name, String(value).toLowerCase()]));
104
- const oclParams = {};
110
+ const params = Object.fromEntries(
111
+ searchParams.map(({ name, value }) => [name, String(value).toLowerCase()])
112
+ );
113
+ const sourceSystem = params['source-system'] || params.source || null;
114
+ const targetSystem = params['target-system'] || params.target || null;
105
115
 
106
- if (params.source) {
107
- oclParams.from_source_url = params.source;
116
+ // Without a source or target filter the search would have to fetch every
117
+ // mapping in the organisation — too expensive. Return empty so the
118
+ // Package providers can still answer generic ConceptMap listings.
119
+ if (!sourceSystem && !targetSystem) {
120
+ return [];
121
+ }
122
+
123
+ try {
124
+ const allMappings = await this.#collectMappingsForSearch(sourceSystem, targetSystem);
125
+ return this.#aggregateMappingsToConceptMaps(allMappings);
126
+ } catch (_err) {
127
+ return [];
108
128
  }
109
- if (params.target) {
110
- oclParams.to_source_url = params.target;
129
+ }
130
+
131
+ async #collectMappingsForSearch(sourceSystem, targetSystem) {
132
+ const systemUrl = sourceSystem || targetSystem;
133
+ const candidates = await this.#candidateSourceUrls(systemUrl);
134
+ const sourcePaths = candidates.filter(s => String(s || '').startsWith('/orgs/'));
135
+
136
+ if (sourcePaths.length === 0) {
137
+ return [];
111
138
  }
112
139
 
113
- const mappings = await this.#searchMappings(oclParams, this.maxSearchPages);
114
- const results = [];
140
+ const allMappings = [];
141
+ for (const sourcePath of sourcePaths) {
142
+ const normalizedPath = this.#normalizeSourcePath(sourcePath);
143
+ let concepts;
144
+ try {
145
+ concepts = await this.#fetchAllPages(
146
+ `${normalizedPath}concepts/`, { limit: PAGE_SIZE }, this.maxSearchPages
147
+ );
148
+ } catch (_err) {
149
+ continue;
150
+ }
151
+
152
+ for (const concept of concepts) {
153
+ const code = concept.id || concept.mnemonic;
154
+ if (!code) {
155
+ continue;
156
+ }
157
+ try {
158
+ const mappings = await this.#fetchAllPages(
159
+ `${normalizedPath}concepts/${encodeURIComponent(code)}/mappings/`,
160
+ { limit: PAGE_SIZE }, 2
161
+ );
162
+ allMappings.push(...mappings);
163
+ } catch (_err) {
164
+ // concept has no mappings or endpoint inaccessible — skip
165
+ }
166
+ }
167
+ }
168
+
169
+ const sourceUrlsToResolve = new Set();
170
+ for (const m of allMappings) {
171
+ const from = m?.from_source_url || m?.fromSourceUrl;
172
+ const to = m?.to_source_url || m?.toSourceUrl;
173
+ if (from) sourceUrlsToResolve.add(from);
174
+ if (to) sourceUrlsToResolve.add(to);
175
+ }
176
+ await this.#ensureCanonicalForSourceUrls(sourceUrlsToResolve);
177
+
178
+ return allMappings;
179
+ }
180
+
181
+ #aggregateMappingsToConceptMaps(mappings) {
182
+ const groups = new Map();
183
+
115
184
  for (const mapping of mappings) {
116
- const cm = this.#toConceptMap(mapping);
117
- if (!cm) {
185
+ const fromSource = mapping.from_source_url || mapping.fromSourceUrl || null;
186
+ const toSource = mapping.to_source_url || mapping.toSourceUrl || null;
187
+ const sourceCode = mapping.from_concept_code || mapping.fromConceptCode;
188
+ const targetCode = mapping.to_concept_code || mapping.toConceptCode;
189
+
190
+ if (!fromSource || !toSource || !sourceCode || !targetCode) {
118
191
  continue;
119
192
  }
120
- this.#indexConceptMap(cm);
121
- if (this.#matches(cm.jsonObj, params)) {
122
- results.push(cm);
193
+
194
+ const sourceCanonical = this.#canonicalForSourceUrl(fromSource) || fromSource;
195
+ const targetCanonical = this.#canonicalForSourceUrl(toSource) || toSource;
196
+ const groupKey = `${this.#norm(sourceCanonical)}|${this.#norm(targetCanonical)}`;
197
+
198
+ if (!groups.has(groupKey)) {
199
+ groups.set(groupKey, {
200
+ sourceCanonical, targetCanonical, elements: new Map(), lastUpdated: null
201
+ });
202
+ }
203
+
204
+ const group = groups.get(groupKey);
205
+ const ts = this.#toIsoDate(
206
+ mapping.updated_on || mapping.updatedOn || mapping.updated_at || mapping.updatedAt
207
+ );
208
+ if (ts && (!group.lastUpdated || ts > group.lastUpdated)) {
209
+ group.lastUpdated = ts;
210
+ }
211
+
212
+ if (!group.elements.has(sourceCode)) {
213
+ group.elements.set(sourceCode, {
214
+ code: sourceCode,
215
+ display: mapping.from_concept_name_resolved || mapping.fromConceptNameResolved
216
+ || mapping.from_concept_name || mapping.fromConceptName || null,
217
+ targets: []
218
+ });
219
+ }
220
+
221
+ group.elements.get(sourceCode).targets.push({
222
+ code: targetCode,
223
+ display: mapping.to_concept_name_resolved || mapping.toConceptNameResolved
224
+ || mapping.to_concept_name || mapping.toConceptName || null,
225
+ relationship: this.#toRelationship(mapping.map_type || mapping.mapType),
226
+ comment: mapping.comment || null
227
+ });
228
+ }
229
+
230
+ const results = [];
231
+ for (const [, group] of groups) {
232
+ const sourceId = this.#lastSegment(group.sourceCanonical);
233
+ const targetId = this.#lastSegment(group.targetCanonical);
234
+ const id = `${sourceId}-to-${targetId}`;
235
+
236
+ const elements = [];
237
+ for (const [, el] of group.elements) {
238
+ elements.push({ code: el.code, display: el.display, target: el.targets });
239
+ }
240
+
241
+ const json = {
242
+ resourceType: 'ConceptMap',
243
+ id,
244
+ url: `${this.baseUrl}/ConceptMap/${id}`,
245
+ name: id,
246
+ title: `${sourceId} to ${targetId}`,
247
+ status: 'active',
248
+ sourceScopeUri: group.sourceCanonical,
249
+ targetScopeUri: group.targetCanonical,
250
+ group: [{
251
+ source: group.sourceCanonical,
252
+ target: group.targetCanonical,
253
+ element: elements
254
+ }]
255
+ };
256
+ if (group.lastUpdated) {
257
+ json.meta = { lastUpdated: group.lastUpdated };
123
258
  }
259
+
260
+ const cm = new ConceptMap(json, 'R5');
261
+ this.#indexConceptMap(cm);
262
+ results.push(cm);
124
263
  }
125
264
  return results;
126
265
  }
127
266
 
267
+ #lastSegment(canonical) {
268
+ const raw = String(canonical || '').trim().replace(/\/+$/, '');
269
+ const slash = raw.lastIndexOf('/');
270
+ return slash >= 0 && slash < raw.length - 1 ? raw.substring(slash + 1) : raw;
271
+ }
272
+
128
273
  async findConceptMapForTranslation(opContext, conceptMaps, sourceSystem, sourceScope, targetScope, targetSystem, sourceCode = null) {
274
+ try {
275
+ await this.#doFindConceptMapForTranslation(opContext, conceptMaps, sourceSystem, sourceScope, targetScope, targetSystem, sourceCode);
276
+ } catch (_err) {
277
+ // OCL API errors must not break $translate for other providers
278
+ }
279
+ }
280
+
281
+ async #doFindConceptMapForTranslation(opContext, conceptMaps, sourceSystem, sourceScope, targetScope, targetSystem, sourceCode) {
129
282
  const sourceCandidates = await this.#candidateSourceUrls(sourceSystem);
130
283
  const targetCandidates = await this.#candidateSourceUrls(targetSystem);
131
284
 
@@ -135,8 +288,12 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider {
135
288
  if (sourceCode && sourcePaths.length > 0) {
136
289
  for (const sourcePath of sourcePaths) {
137
290
  const conceptPath = `${this.#normalizeSourcePath(sourcePath)}concepts/${encodeURIComponent(sourceCode)}/mappings/`;
138
- const found = await this.#fetchAllPages(conceptPath, { limit: PAGE_SIZE }, Math.min(2, this.maxSearchPages));
139
- mappings.push(...found);
291
+ try {
292
+ const found = await this.#fetchAllPages(conceptPath, { limit: PAGE_SIZE }, Math.min(2, this.maxSearchPages));
293
+ mappings.push(...found);
294
+ } catch (_err) {
295
+ // concept not found or mappings endpoint unavailable
296
+ }
140
297
  }
141
298
  }
142
299
 
@@ -311,43 +468,6 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider {
311
468
  }
312
469
  }
313
470
 
314
- #matches(json, params) {
315
- for (const [name, value] of Object.entries(params)) {
316
- if (!value) {
317
- continue;
318
- }
319
-
320
- if (name === 'url') {
321
- if ((json.url || '').toLowerCase() !== value) {
322
- return false;
323
- }
324
- continue;
325
- }
326
-
327
- if (name === 'source') {
328
- const src = json.group?.[0]?.source || '';
329
- if (!src.toLowerCase().includes(value)) {
330
- return false;
331
- }
332
- continue;
333
- }
334
-
335
- if (name === 'target') {
336
- const tgt = json.group?.[0]?.target || '';
337
- if (!tgt.toLowerCase().includes(value)) {
338
- return false;
339
- }
340
- continue;
341
- }
342
-
343
- const field = json[name];
344
- if (field == null || !String(field).toLowerCase().includes(value)) {
345
- return false;
346
- }
347
- }
348
- return true;
349
- }
350
-
351
471
  async #searchMappings(params = {}, maxPages = this.maxSearchPages) {
352
472
  const endpoint = this.org ? `/orgs/${encodeURIComponent(this.org)}/mappings/` : '/mappings/';
353
473
  return await this.#fetchAllPages(endpoint, params, maxPages);
package/tx/ocl/cs-ocl.cjs CHANGED
@@ -48,6 +48,8 @@ class OCLCodeSystemProvider extends AbstractCodeSystemProvider {
48
48
  this._pendingChanges = null;
49
49
  this._initialized = false;
50
50
  this._initializePromise = null;
51
+ this._organizationIdsCache = null;
52
+ this._organizationIdsFetchPromise = null;
51
53
  }
52
54
 
53
55
  async initialize() {
@@ -206,35 +208,54 @@ class OCLCodeSystemProvider extends AbstractCodeSystemProvider {
206
208
  }
207
209
 
208
210
  async #fetchOrganizationIds() {
209
- const endpoint = '/orgs/';
210
- const orgs = await this.#fetchAllPages(endpoint);
211
+ // Return cached result if available
212
+ if (this._organizationIdsCache) {
213
+ return this._organizationIdsCache;
214
+ }
211
215
 
212
- const ids = [];
213
- const seen = new Set();
214
- for (const org of orgs || []) {
215
- if (!org || typeof org !== 'object') {
216
- continue;
217
- }
216
+ // Deduplicate concurrent requests
217
+ if (this._organizationIdsFetchPromise) {
218
+ return this._organizationIdsFetchPromise;
219
+ }
218
220
 
219
- const id = org.id || org.mnemonic || org.short_code || org.shortCode || org.name || null;
220
- if (!id) {
221
- continue;
221
+ this._organizationIdsFetchPromise = (async () => {
222
+ const endpoint = '/orgs/';
223
+ const orgs = await this.#fetchAllPages(endpoint);
224
+
225
+ const ids = [];
226
+ const seen = new Set();
227
+ for (const org of orgs || []) {
228
+ if (!org || typeof org !== 'object') {
229
+ continue;
230
+ }
231
+
232
+ const id = org.id || org.mnemonic || org.short_code || org.shortCode || org.name || null;
233
+ if (!id) {
234
+ continue;
235
+ }
236
+
237
+ const normalized = String(id).trim();
238
+ if (!normalized || seen.has(normalized)) {
239
+ continue;
240
+ }
241
+
242
+ seen.add(normalized);
243
+ ids.push(normalized);
222
244
  }
223
245
 
224
- const normalized = String(id).trim();
225
- if (!normalized || seen.has(normalized)) {
226
- continue;
246
+ if (ids.length === 0 && this.org) {
247
+ ids.push(this.org);
227
248
  }
228
249
 
229
- seen.add(normalized);
230
- ids.push(normalized);
231
- }
250
+ this._organizationIdsCache = ids;
251
+ return ids;
252
+ })();
232
253
 
233
- if (ids.length === 0 && this.org) {
234
- ids.push(this.org);
254
+ try {
255
+ return await this._organizationIdsFetchPromise;
256
+ } finally {
257
+ this._organizationIdsFetchPromise = null;
235
258
  }
236
-
237
- return ids;
238
259
  }
239
260
 
240
261
  #sourceIdentity(source) {
@@ -673,16 +694,33 @@ class OCLSourceCodeSystemProvider extends CodeSystemProvider {
673
694
  async designations(code, displays) {
674
695
  const ctxt = await this.#ensureContext(code);
675
696
  if (ctxt && ctxt.display) {
676
- const hasConceptDesignations = Array.isArray(ctxt.designations) && ctxt.designations.length > 0;
697
+ const hasConceptDesignations = Array.isArray(ctxt.designation) && ctxt.designation.length > 0;
677
698
  if (hasConceptDesignations) {
678
- for (const d of ctxt.designations) {
699
+ let hasNoLanguageEntry = false;
700
+ for (const d of ctxt.designation) {
679
701
  if (!d || !d.value) {
680
702
  continue;
681
703
  }
682
704
  displays.addDesignation(true, 'active', d.language || '', CodeSystem.makeUseForDisplay(), d.value);
705
+ if (!d.language) {
706
+ hasNoLanguageEntry = true;
707
+ }
708
+ }
709
+ // Guarantee a language-neutral fallback so preferredDesignation() always returns
710
+ // a display value when the requested language has no matching designation.
711
+ // This implements the FHIR graceful-fallback rule for displayLanguage.
712
+ if (!hasNoLanguageEntry) {
713
+ displays.addDesignation(true, 'active', '', CodeSystem.makeUseForDisplay(), ctxt.display);
683
714
  }
684
715
  } else {
685
- displays.addDesignation(true, 'active', 'en', CodeSystem.makeUseForDisplay(), ctxt.display);
716
+ // No structured designations available. Use the source's configured language
717
+ // rather than hard-coding 'en' to avoid mislabeling non-English displays as English.
718
+ const defaultLang = this.meta?.codeSystem?.jsonObj?.language || '';
719
+ displays.addDesignation(true, 'active', defaultLang, CodeSystem.makeUseForDisplay(), ctxt.display);
720
+ // Also provide a no-language fallback for graceful language resolution.
721
+ if (defaultLang) {
722
+ displays.addDesignation(true, 'active', '', CodeSystem.makeUseForDisplay(), ctxt.display);
723
+ }
686
724
  }
687
725
  this._listSupplementDesignations(ctxt.code, displays);
688
726
  }
@@ -914,7 +952,7 @@ class OCLSourceCodeSystemProvider extends CodeSystemProvider {
914
952
  if (!this.meta.conceptsUrl) {
915
953
  return [];
916
954
  }
917
- const cacheKey = `${this.meta.conceptsUrl}|p=${page}|l=${CONCEPT_PAGE_SIZE}`;
955
+ const cacheKey = `${this.meta.conceptsUrl}|p=${page}|l=${CONCEPT_PAGE_SIZE}|verbose=1`;
918
956
  if (this.pageCache.has(cacheKey)) {
919
957
  const cached = this.pageCache.get(cacheKey);
920
958
  return Array.isArray(cached)
@@ -939,7 +977,7 @@ class OCLSourceCodeSystemProvider extends CodeSystemProvider {
939
977
  const pending = (async () => {
940
978
  let response;
941
979
  try {
942
- response = await this.httpClient.get(this.meta.conceptsUrl, { params: { page, limit: CONCEPT_PAGE_SIZE } });
980
+ response = await this.httpClient.get(this.meta.conceptsUrl, { params: { page, limit: CONCEPT_PAGE_SIZE, verbose: true } });
943
981
  } catch (error) {
944
982
  // Some OCL instances return 404 for sources without concept listing endpoints.
945
983
  // Treat this as an empty page so terminology operations degrade gracefully.
@@ -998,7 +1036,7 @@ class OCLSourceCodeSystemProvider extends CodeSystemProvider {
998
1036
  const pending = (async () => {
999
1037
  let response;
1000
1038
  try {
1001
- response = await this.httpClient.get(url);
1039
+ response = await this.httpClient.get(url, { params: { verbose: true } });
1002
1040
  } catch (error) {
1003
1041
  // Missing concept should be treated as not-found, not as an internal server failure.
1004
1042
  if (error && error.response && error.response.status === 404) {
@@ -1302,7 +1340,6 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
1302
1340
 
1303
1341
  this.#applyConceptsToCodeSystemResource(this.meta?.codeSystem || null);
1304
1342
 
1305
- console.log(`[OCL] Loaded CodeSystem from cold cache: ${canonicalUrl} (${cached.concepts.length} concepts, fingerprint=${this.customFingerprint?.substring(0, 8)})`);
1306
1343
  } catch (error) {
1307
1344
  if (error.code !== 'ENOENT') {
1308
1345
  console.error(`[OCL] Failed to load cold cache for CodeSystem ${canonicalUrl}:`, error.message);
@@ -1329,7 +1366,6 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
1329
1366
  };
1330
1367
 
1331
1368
  await fs.writeFile(cacheFilePath, JSON.stringify(cacheData, null, 2), 'utf-8');
1332
- console.log(`[OCL] Saved CodeSystem to cold cache: ${canonicalUrl} (${concepts.length} concepts, fingerprint=${fingerprint?.substring(0, 8)})`);
1333
1369
 
1334
1370
  return fingerprint;
1335
1371
  } catch (error) {
@@ -1343,7 +1379,6 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
1343
1379
  const key = `${normalizedSystem}|${version || ''}`;
1344
1380
  const factory = OCLSourceCodeSystemFactory.#findFactory(normalizedSystem, version);
1345
1381
  if (!factory) {
1346
- console.log(`[OCL] CodeSystem load not scheduled (factory unavailable): ${key}`);
1347
1382
  return false;
1348
1383
  }
1349
1384
  factory.scheduleBackgroundLoad(reason);
@@ -1412,7 +1447,6 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
1412
1447
  }
1413
1448
 
1414
1449
  if (cacheAgeMs != null && cacheAgeMs < COLD_CACHE_FRESHNESS_MS) {
1415
- console.log(`[OCL] Skipping warm-up for CodeSystem ${this.system()} (cold cache age: ${formatCacheAgeMinutes(cacheAgeMs)})`);
1416
1450
  return;
1417
1451
  }
1418
1452
 
@@ -1420,12 +1454,10 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
1420
1454
  const jobKey = `cs:${key}`;
1421
1455
 
1422
1456
  if (OCLBackgroundJobQueue.isQueuedOrRunning(jobKey)) {
1423
- console.log(`[OCL] CodeSystem load already queued or running: ${key}`);
1424
1457
  return;
1425
1458
  }
1426
1459
 
1427
1460
  let queuedJobSize = null;
1428
- console.log(`[OCL] CodeSystem load enqueued: ${key} (${reason})`);
1429
1461
  OCLBackgroundJobQueue.enqueue(
1430
1462
  jobKey,
1431
1463
  'CodeSystem load',
@@ -1444,7 +1476,6 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
1444
1476
  }
1445
1477
 
1446
1478
  async #runBackgroundLoad(key, knownConceptCount = null) {
1447
- console.log(`[OCL] CodeSystem load started: ${key}`);
1448
1479
  try {
1449
1480
  this.backgroundLoadProgress = { processed: 0, total: null };
1450
1481
  const resolvedTotal = Number.isFinite(knownConceptCount) && knownConceptCount >= 0
@@ -1470,16 +1501,7 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
1470
1501
  const allConcepts = Array.from(this.sharedConceptCache.values());
1471
1502
  const newFingerprint = computeCodeSystemFingerprint(allConcepts);
1472
1503
 
1473
- if (this.customFingerprint && newFingerprint === this.customFingerprint) {
1474
- console.log(`[OCL] CodeSystem fingerprint unchanged: ${key} (fingerprint=${newFingerprint?.substring(0, 8)})`);
1475
- } else {
1476
- if (this.customFingerprint) {
1477
- console.log(`[OCL] CodeSystem fingerprint changed: ${key} (${this.customFingerprint?.substring(0, 8)} -> ${newFingerprint?.substring(0, 8)})`);
1478
- console.log(`[OCL] Replacing cold cache with new hot cache: ${key}`);
1479
- } else {
1480
- console.log(`[OCL] Computed fingerprint for CodeSystem: ${key} (fingerprint=${newFingerprint?.substring(0, 8)})`);
1481
- }
1482
-
1504
+ if (!this.customFingerprint || newFingerprint !== this.customFingerprint) {
1483
1505
  // Save to cold cache
1484
1506
  const savedFingerprint = await this.#saveColdCache(allConcepts);
1485
1507
  if (savedFingerprint) {
@@ -1487,10 +1509,7 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
1487
1509
  }
1488
1510
  }
1489
1511
 
1490
- console.log(`[OCL] CodeSystem load completed, marked content=complete: ${key}`);
1491
- const progress = OCLSourceCodeSystemFactory.loadProgress();
1492
- console.log(`[OCL] CodeSystem load completed: ${this.system()}. Loaded ${progress.loaded}/${progress.total} CodeSystems (${progress.percentage.toFixed(2)}%)`);
1493
- console.log(`[OCL] CodeSystem now available in cache: ${key} (${count} concepts)`);
1512
+ console.log(`[OCL] CodeSystem loaded: ${this.system()} (${count} concepts)`);
1494
1513
  } catch (error) {
1495
1514
  console.error(`[OCL] CodeSystem background load failed: ${key}: ${error.message}`);
1496
1515
  }
@@ -1525,7 +1544,7 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
1525
1544
  }
1526
1545
 
1527
1546
  async #fetchAndCacheConceptPage(page) {
1528
- const cacheKey = `${this.meta.conceptsUrl}|p=${page}|l=${CONCEPT_PAGE_SIZE}`;
1547
+ const cacheKey = `${this.meta.conceptsUrl}|p=${page}|l=${CONCEPT_PAGE_SIZE}|verbose=1`;
1529
1548
  if (this.sharedPageCache.has(cacheKey)) {
1530
1549
  const cached = this.sharedPageCache.get(cacheKey);
1531
1550
  const concepts = Array.isArray(cached)
@@ -1544,7 +1563,7 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
1544
1563
  const pending = (async () => {
1545
1564
  let response;
1546
1565
  try {
1547
- response = await this.httpClient.get(this.meta.conceptsUrl, { params: { page, limit: CONCEPT_PAGE_SIZE } });
1566
+ response = await this.httpClient.get(this.meta.conceptsUrl, { params: { page, limit: CONCEPT_PAGE_SIZE, verbose: true } });
1548
1567
  } catch (error) {
1549
1568
  if (error && error.response && error.response.status === 404) {
1550
1569
  this.sharedPageCache.set(cacheKey, []);
@@ -34,7 +34,6 @@ class OCLBackgroundJobQueue {
34
34
  };
35
35
  this.#insertPendingJobOrdered(job);
36
36
  this.ensureHeartbeatRunning();
37
- console.log(`[OCL] ${jobType || 'Background job'} enqueued: ${jobKey} (size=${normalizedSize}, queue=${this.pendingJobs.length}, active=${this.activeCount})`);
38
37
  this.processNext();
39
38
  };
40
39
 
@@ -77,7 +76,6 @@ class OCLBackgroundJobQueue {
77
76
  // Prioridade máxima para userRequested
78
77
  if (job.userRequested) {
79
78
  this.pendingJobs.unshift(job);
80
- console.log(`[OCL] User-requested job prioritized: ${job.jobKey}`);
81
79
  return;
82
80
  }
83
81
  let index = this.pendingJobs.findIndex(existing => {
@@ -179,13 +177,8 @@ class OCLBackgroundJobQueue {
179
177
  getProgress: job.getProgress || null,
180
178
  startedAt: Date.now()
181
179
  });
182
- console.log(`[OCL] Background job started: ${job.jobType} ${job.jobKey} (size=${job.jobSize}, queue=${this.pendingJobs.length}, active=${this.activeCount})`);
183
-
184
180
  Promise.resolve()
185
181
  .then(() => job.runJob())
186
- .then(() => {
187
- console.log(`[OCL] Background job completed: ${job.jobType} ${job.jobKey}`);
188
- })
189
182
  .catch((error) => {
190
183
  const message = error && error.message ? error.message : String(error);
191
184
  console.error(`[OCL] Background job failed: ${job.jobType} ${job.jobKey}: ${message}`);
@@ -194,7 +187,6 @@ class OCLBackgroundJobQueue {
194
187
  this.activeCount -= 1;
195
188
  this.queuedOrRunningKeys.delete(job.jobKey);
196
189
  this.activeJobs.delete(job.jobKey);
197
- console.log(`[OCL] Background queue status: queue=${this.pendingJobs.length}, active=${this.activeCount}`);
198
190
  this.processNext();
199
191
  });
200
192
  }
@@ -13,13 +13,14 @@ function toConceptContext(concept) {
13
13
  display: concept.display_name || concept.display || concept.name || null,
14
14
  definition: concept.description || concept.definition || null,
15
15
  retired: concept.retired === true,
16
- designations: extractDesignations(concept)
16
+ designation: extractDesignations(concept)
17
17
  };
18
18
  }
19
19
 
20
20
  function extractDesignations(concept) {
21
21
  const result = [];
22
22
  const seen = new Set();
23
+ const seenValues = new Set();
23
24
 
24
25
  const add = (language, value) => {
25
26
  const text = typeof value === 'string' ? value.trim() : '';
@@ -28,12 +29,19 @@ function extractDesignations(concept) {
28
29
  }
29
30
 
30
31
  const lang = typeof language === 'string' ? language.trim() : '';
32
+
33
+ // Skip empty-language entries whose value already appears under any language
34
+ if (!lang && seenValues.has(text)) {
35
+ return;
36
+ }
37
+
31
38
  const key = `${lang}|${text}`;
32
39
  if (seen.has(key)) {
33
40
  return;
34
41
  }
35
42
 
36
43
  seen.add(key);
44
+ seenValues.add(text);
37
45
  result.push({ language: lang, value: text });
38
46
  };
39
47
 
@@ -42,8 +50,10 @@ function extractDesignations(concept) {
42
50
  if (!entry || typeof entry !== 'object') {
43
51
  continue;
44
52
  }
45
-
46
- add(entry.locale || entry.language || entry.lang || '', entry.name || entry.display_name || entry.display || entry.value || entry.term);
53
+ // Prioriza locale como language
54
+ const lang = entry.locale;
55
+ const value = entry.name;
56
+ add(lang, value);
47
57
  }
48
58
  }
49
59
 
@@ -253,6 +253,7 @@ function patchValueSetExpandWholeSystemForOcl() {
253
253
  });
254
254
  }
255
255
 
256
+
256
257
  module.exports = {
257
258
  patchSearchWorkerForOCLCodeFiltering,
258
259
  ensureTxParametersHashIncludesFilter,