@peopl-health/nexus 5.9.0-dev.921 → 6.0.0-dev.611

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 (126) hide show
  1. package/lib/clinical/AssistantProcessor.js +5 -34
  2. package/lib/clinical/config/divergenceConfig.js +1 -18
  3. package/lib/clinical/config/llmConfig.js +1 -9
  4. package/lib/clinical/context/ToolRuntimeContext.js +2 -1
  5. package/lib/clinical/helpers/divergenceHelper.js +0 -12
  6. package/lib/clinical/index.js +0 -2
  7. package/lib/clinical/memory/DefaultMemoryManager.js +9 -19
  8. package/lib/clinical/models/turnTraceModel.js +5 -18
  9. package/lib/clinical/providers/AnthropicProvider.js +29 -291
  10. package/lib/clinical/providers/BaseLLMProvider.js +24 -113
  11. package/lib/clinical/providers/GatewayProvider.js +3 -3
  12. package/lib/clinical/providers/OpenAIResponsesProvider.js +4 -46
  13. package/lib/clinical/services/divergenceService.js +5 -22
  14. package/lib/clinical/services/promptComposerService.js +15 -46
  15. package/lib/clinical/services/shadowMessageService.js +12 -28
  16. package/lib/clinical/services/shadowService.js +3 -12
  17. package/lib/clinical/stores/CtcaeCatalog.js +0 -20
  18. package/lib/config/airtableConfig.js +0 -3
  19. package/lib/core/BatchingManager.js +3 -10
  20. package/lib/core/NexusMessaging.js +13 -31
  21. package/lib/eval/EvalProvider.js +2 -3
  22. package/lib/fhir/config/fhirConfig.js +3 -34
  23. package/lib/fhir/constants/projectionSlugs.js +1 -41
  24. package/lib/fhir/helpers/elementHelper.js +0 -21
  25. package/lib/fhir/helpers/fhirHelper.js +2 -5
  26. package/lib/fhir/index.js +0 -24
  27. package/lib/fhir/models/envelopeModel.js +0 -5
  28. package/lib/fhir/projections/registerProjectors.js +0 -16
  29. package/lib/fhir/resources/Condition.js +2 -107
  30. package/lib/fhir/resources/Observation.js +1 -73
  31. package/lib/fhir/resources/Procedure.js +2 -34
  32. package/lib/fhir/resources/Provenance.js +2 -93
  33. package/lib/fhir/services/fhirService.js +2 -2
  34. package/lib/helpers/twilioHelper.js +1 -9
  35. package/lib/index.d.ts +0 -16
  36. package/lib/index.js +0 -9
  37. package/lib/models/messageModel.js +0 -6
  38. package/lib/routes/index.js +1 -21
  39. package/lib/services/airtableService.js +1 -2
  40. package/lib/shared/dtos/ContingencySafetyNet.js +3 -10
  41. package/lib/shared/dtos/DispatchedEscalation.js +8 -3
  42. package/lib/shared/dtos/Reminder.js +2 -2
  43. package/lib/storage/MongoStorage.js +0 -2
  44. package/package.json +1 -1
  45. package/lib/clinical/config/cohortConfig.js +0 -68
  46. package/lib/clinical/config/composerConfig.js +0 -42
  47. package/lib/clinical/config/subAgentsConfig.js +0 -44
  48. package/lib/clinical/flags/bridgeConfig.js +0 -14
  49. package/lib/clinical/flags/composerFlags.js +0 -14
  50. package/lib/clinical/helpers/clinicalFactsHelper.js +0 -108
  51. package/lib/clinical/helpers/clinicalMentionHelper.js +0 -68
  52. package/lib/clinical/helpers/cohortHelper.js +0 -22
  53. package/lib/clinical/helpers/gradeEstimateHelper.js +0 -29
  54. package/lib/clinical/helpers/resultsHelper.js +0 -34
  55. package/lib/clinical/helpers/safetyFlagsHelper.js +0 -10
  56. package/lib/clinical/services/bridgeService.js +0 -55
  57. package/lib/clinical/services/clinicalAirtableService.js +0 -162
  58. package/lib/clinical/services/clinicalExtractionService.js +0 -112
  59. package/lib/clinical/services/composerService.js +0 -59
  60. package/lib/clinical/services/dispatcherService.js +0 -137
  61. package/lib/clinical/services/reminderDispatchService.js +0 -36
  62. package/lib/clinical/services/skillService.js +0 -44
  63. package/lib/clinical/tools/analyzeSymptomPatternsTool.js +0 -279
  64. package/lib/clinical/tools/deliverPatientMessageTool.js +0 -202
  65. package/lib/clinical/tools/extractClinicalInfoTool.js +0 -167
  66. package/lib/clinical/tools/getActiveSymptomLandscapeTool.js +0 -141
  67. package/lib/clinical/tools/getPatientHistoryTool.js +0 -235
  68. package/lib/clinical/tools/getPatientRiskProfileTool.js +0 -71
  69. package/lib/clinical/tools/getRouterContextBundleTool.js +0 -93
  70. package/lib/clinical/tools/lookupCtcaeEntryTool.js +0 -35
  71. package/lib/clinical/tools/openClusterHypothesisTool.js +0 -160
  72. package/lib/clinical/tools/openConditionTool.js +0 -136
  73. package/lib/clinical/tools/recordClinicalImpressionTool.js +0 -251
  74. package/lib/clinical/tools/recordInterventionTool.js +0 -140
  75. package/lib/clinical/tools/registerClinicalTools.js +0 -39
  76. package/lib/clinical/tools/reportCrisisTool.js +0 -211
  77. package/lib/clinical/tools/reportMedicalEscalationTool.js +0 -187
  78. package/lib/clinical/tools/reportResultsReceivedTool.js +0 -153
  79. package/lib/clinical/tools/reportUnresolvedRequestv2Tool.js +0 -177
  80. package/lib/clinical/tools/schedulePatientReminderTool.js +0 -149
  81. package/lib/clinical/tools/setContingencyPlanTool.js +0 -176
  82. package/lib/clinical/tools/skillTool.js +0 -67
  83. package/lib/clinical/tools/submitRoutingDecisionTool.js +0 -291
  84. package/lib/clinical/tools/updateClusterHypothesisTool.js +0 -159
  85. package/lib/controllers/cohortController.js +0 -16
  86. package/lib/controllers/traceController.js +0 -26
  87. package/lib/core/workflowRunner.js +0 -35
  88. package/lib/fhir/config/fhirTablesConfig.js +0 -9
  89. package/lib/fhir/constants/severityPriority.js +0 -8
  90. package/lib/fhir/helpers/compositeHelper.js +0 -88
  91. package/lib/fhir/helpers/fhirReadHelper.js +0 -42
  92. package/lib/fhir/helpers/terminologyHelper.js +0 -260
  93. package/lib/fhir/projections/clusterProjection.js +0 -24
  94. package/lib/fhir/projections/contingencyProjection.js +0 -36
  95. package/lib/fhir/projections/dispatchedEscalationProjection.js +0 -25
  96. package/lib/fhir/projections/interventionProjection.js +0 -42
  97. package/lib/fhir/projections/proctcaeProjection.js +0 -179
  98. package/lib/fhir/projections/reminderProjection.js +0 -20
  99. package/lib/fhir/projections/riskProjection.js +0 -16
  100. package/lib/fhir/projections/routingProjection.js +0 -48
  101. package/lib/fhir/projections/symptomCaseProjection.js +0 -65
  102. package/lib/fhir/resources/CarePlan.js +0 -93
  103. package/lib/fhir/resources/ClinicalImpression.js +0 -118
  104. package/lib/fhir/resources/Communication.js +0 -65
  105. package/lib/fhir/resources/CommunicationRequest.js +0 -182
  106. package/lib/fhir/resources/Device.js +0 -24
  107. package/lib/fhir/resources/Patient.js +0 -18
  108. package/lib/fhir/resources/Questionnaire.js +0 -23
  109. package/lib/fhir/resources/QuestionnaireResponse.js +0 -29
  110. package/lib/fhir/resources/RequestOrchestration.js +0 -46
  111. package/lib/fhir/resources/RiskAssessment.js +0 -99
  112. package/lib/fhir/resources/Task.js +0 -106
  113. package/lib/fhir/resources/interventionExtensions.js +0 -28
  114. package/lib/fhir/services/clusterService.js +0 -77
  115. package/lib/fhir/services/contingencyService.js +0 -117
  116. package/lib/fhir/services/interventionService.js +0 -149
  117. package/lib/fhir/services/resultObservationService.js +0 -52
  118. package/lib/fhir/services/riskService.js +0 -101
  119. package/lib/fhir/services/routingService.js +0 -147
  120. package/lib/fhir/services/snapshotService.js +0 -28
  121. package/lib/fhir/services/symptomCaseService.js +0 -139
  122. package/lib/fhir/services/terminologyService.js +0 -58
  123. package/lib/fhir/services/triageService.js +0 -57
  124. package/lib/fhir/utils/normalizeUtils.js +0 -33
  125. package/lib/shared/dtos/GradedSymptom.js +0 -23
  126. package/lib/shared/dtos/TriageResponse.js +0 -35
@@ -15,28 +15,22 @@ async function runShadowTurn({
15
15
  resolveAssistant,
16
16
  executeLLM,
17
17
  }) {
18
- if (!shouldContinue()) {
19
- logger.debug('[shadow] superseded pre-run', { code: thread?.code, runId });
20
- return null;
21
- }
18
+ if (!shouldContinue()) return null;
22
19
  if (!runId) throw new Error('runShadowTurn requires a runId');
23
20
 
24
21
  const code = thread.code;
25
- logger.debug('[shadow] start', { code, runId, sessionId });
26
22
  const shadowThread = cloneShadowThread(thread);
27
23
  const shadowAssistant = resolveAssistant(shadowThread);
28
24
 
29
- const context = new ToolRuntimeContext({ patientCode: code, turnId: runId, sessionId });
25
+ const context = new ToolRuntimeContext({ patientCode: code, turnId: runId, sessionId, thread: shadowThread });
30
26
 
31
27
  const runOptions = {
32
28
  toolRuntimeContext: context, shadow: true, shouldContinue, additionalInstructions,
33
29
  };
34
30
 
35
- const startedAt = Date.now();
36
31
  const result = await runInShadowScope(() =>
37
32
  executeLLM(shadowThread, shadowAssistant, runOptions, message),
38
33
  );
39
- logger.debug('[shadow] done', { code, runId, completed: !!result?.completed, steps: context.trace?.steps?.length || 0, durationMs: Date.now() - startedAt });
40
34
 
41
35
  return { turnId: runId, context, result };
42
36
  }
@@ -46,10 +40,7 @@ function forkShadowTurn({
46
40
  }) {
47
41
  Promise.resolve()
48
42
  .then(() => {
49
- if (!isShadowEnabled()) {
50
- logger.debug('[shadow] disabled', { code: thread?.code });
51
- return null;
52
- }
43
+ if (!isShadowEnabled()) return null;
53
44
  return runShadowTurn({
54
45
  thread,
55
46
  message,
@@ -32,26 +32,6 @@ class CtcaeCatalog {
32
32
  return grades && Object.keys(grades).length ? grades : null;
33
33
  }
34
34
 
35
- lookupEntry(ctcaeTerm) {
36
- this._assertLoaded();
37
- const key = (ctcaeTerm || '').trim().toLowerCase();
38
- const symptom = key ? this.symptomsByTerm.get(key) : null;
39
- if (symptom) {
40
- return {
41
- entry: {
42
- ctcae_term: symptom.ctcaeTerm,
43
- spanish_label: symptom.spanishLabel,
44
- patient_phrases_validated: symptom.patientPhrasesValidated,
45
- grades: symptom.grades,
46
- },
47
- found: true,
48
- };
49
- }
50
- const nearestTerms = [...this.symptomsByTerm.keys()]
51
- .filter((k) => key && (key.includes(k) || k.includes(key) || k.split('_')[0] === key.split('_')[0]));
52
- return { entry: {}, found: false, nearest_terms: nearestTerms };
53
- }
54
-
55
35
  _assertLoaded() {
56
36
  if (this.symptomsByTerm === null) throw new Error('ctcae catalog not loaded: call initialize() at startup');
57
37
  }
@@ -13,7 +13,6 @@ const Logging_ID = runtimeConfig.get('AIRTABLE_LOGGING_ID') || 'appQ7YhzfebRDbSP
13
13
  const Monitoreo_ID = runtimeConfig.get('AIRTABLE_MONITOREO_ID') || 'appdvraKSdp0XVn5n';
14
14
  const Programa_Juntas_ID = runtimeConfig.get('AIRTABLE_PROGRAMA_JUNTAS_ID') || 'appKFWzkcDEWlrXBE';
15
15
  const Symptoms_ID = runtimeConfig.get('AIRTABLE_SYMPTOMS_ID') || 'appQRhZlQ9tMfYZWJ';
16
- const Ctcae_ID = runtimeConfig.get('AIRTABLE_CTCAE_ID') || 'appEr4ATOs0Xc8ymP';
17
16
  const Follow_Up_ID = runtimeConfig.get('AIRTABLE_FOLLOW_UP_ID') || 'appBjKw1Ub0KkbZf0';
18
17
  const Webinars_Leads_ID = runtimeConfig.get('AIRTABLE_WEBINARS_LEADS_ID') || 'appzjpVXTI0TgqGPq';
19
18
  const Product_ID = runtimeConfig.get('AIRTABLE_PRODUCT_ID') || 'appu2YDW2pKDYLL5H';
@@ -33,7 +32,6 @@ const BASE_MAP = {
33
32
  monitoreo: Monitoreo_ID,
34
33
  programa: Programa_Juntas_ID,
35
34
  symptoms: Symptoms_ID,
36
- ctcae: Ctcae_ID,
37
35
  followup: Follow_Up_ID,
38
36
  webinars: Webinars_Leads_ID,
39
37
  product: Product_ID,
@@ -51,7 +49,6 @@ module.exports = {
51
49
  Monitoreo_ID,
52
50
  Programa_Juntas_ID,
53
51
  Symptoms_ID,
54
- Ctcae_ID,
55
52
  Follow_Up_ID,
56
53
  Webinars_Leads_ID,
57
54
  Product_ID,
@@ -53,10 +53,10 @@ class BatchingManager {
53
53
  }
54
54
 
55
55
  async enqueueProcessing(chatId, processingFn, sendResponseFn) {
56
- await this._runWithLock(chatId, processingFn, sendResponseFn, { rethrow: true });
56
+ await this._runWithLock(chatId, processingFn, sendResponseFn);
57
57
  }
58
58
 
59
- async _runWithLock(chatId, processingFn, sendResponseFn, { rethrow = false } = {}) {
59
+ async _runWithLock(chatId, processingFn, sendResponseFn) {
60
60
  const prev = this.chatQueues.get(chatId) || Promise.resolve();
61
61
  let resolveGate;
62
62
  const gate = new Promise(r => { resolveGate = r; });
@@ -74,16 +74,9 @@ class BatchingManager {
74
74
  const result = await processingFn(runId);
75
75
  if (this._checkAbandoned(runId)) return;
76
76
 
77
- if (sendResponseFn && result) {
78
- try {
79
- await sendResponseFn(result);
80
- } catch (error) {
81
- logger.error('[BatchingManager] Error sending response', { chatId, error: error.message });
82
- }
83
- }
77
+ if (sendResponseFn && result) await sendResponseFn(result);
84
78
  } catch (error) {
85
79
  logger.error('[BatchingManager] Error processing messages', { chatId, error: error.message });
86
- if (rethrow) throw error;
87
80
  } finally {
88
81
  if (this.activeRequests.get(chatId) === runId) {
89
82
  this._clearProcessingState(chatId);
@@ -525,7 +525,7 @@ class NexusMessaging {
525
525
  body: instruction,
526
526
  messageId: `instruction_${Date.now()}_${Math.random().toString(36).substring(7)}`,
527
527
  fromMe: true,
528
- processed: false,
528
+ processed: true,
529
529
  origin: 'instruction',
530
530
  raw: { role },
531
531
  triggeredBy: triggeredBy || null,
@@ -535,27 +535,17 @@ class NexusMessaging {
535
535
  const result = await this.phiProcessor.inData(
536
536
  messageData,
537
537
  this._saveMessageFn,
538
- async ({ body, _id }) => {
539
- const runResult = await this._executeWithPipeline(code, 'instruction', 'queue',
540
- async (preProcessResult, _shouldContinue, runId) => this.assistantProcessor.process({
541
- code,
542
- runOptions: {
543
- prePromptResult: preProcessResult,
544
- additionalInstructions: body,
545
- toolChoice: 'none',
546
- runId,
547
- }
548
- }),
549
- );
550
- if (_id) {
551
- try {
552
- await updateMessage({ _id }, { $set: { processed: true } });
553
- } catch (err) {
554
- logger.error('[processInstruction] Could not mark instruction processed', { error: err.message, code });
538
+ ({ body }) => this._executeWithPipeline(code, 'instruction', 'queue',
539
+ async (preProcessResult, _shouldContinue, runId) => this.assistantProcessor.process({
540
+ code,
541
+ runOptions: {
542
+ prePromptResult: preProcessResult,
543
+ additionalInstructions: body,
544
+ toolChoice: 'none',
545
+ runId,
555
546
  }
556
- }
557
- return runResult;
558
- },
547
+ }),
548
+ ),
559
549
  );
560
550
 
561
551
  return result?.output || null;
@@ -565,15 +555,7 @@ class NexusMessaging {
565
555
  const thread = await Thread.findOne({ code }).lean();
566
556
  if (!thread) return null;
567
557
 
568
- // Dropped rather than stored: these now emit message:new and would render as an empty bubble.
569
- const requestedMessages = Array.isArray(messages) ? messages : [messages];
570
- const normalizedMessages = requestedMessages.filter(body => typeof body === 'string' && body.trim());
571
- if (normalizedMessages.length !== requestedMessages.length) {
572
- logger.warn('[processSystemMessage] Ignoring blank message entries', {
573
- code,
574
- ignored: requestedMessages.length - normalizedMessages.length,
575
- });
576
- }
558
+ const normalizedMessages = Array.isArray(messages) ? messages : [messages];
577
559
 
578
560
  for (let i = 0; i < normalizedMessages.length; i++) {
579
561
  try {
@@ -587,7 +569,7 @@ class NexusMessaging {
587
569
  origin: 'system',
588
570
  raw: { role },
589
571
  triggeredBy: triggeredBy || null,
590
- // No `silent`: it suppresses the message:new event an interface renders from. Instructions keep it.
572
+ silent: true,
591
573
  };
592
574
  await this.phiProcessor.inData(messageData, this._saveMessageFn, () => {});
593
575
  } catch (err) {
@@ -163,9 +163,8 @@ class EvalProvider {
163
163
  devContent += `\n\nYou only have access to these tools: ${activeToolSchemas.map(s => s.function?.name).join(', ')}. Do not call or reference any tools not listed here.`;
164
164
  }
165
165
  }
166
- } catch (error) {
167
- toolSchemas = [];
168
- logger.error('[NexusEvalProvider] Failed to resolve assistant — running with no tools', { assistantId, error: error.message });
166
+ } catch {
167
+ logger.warn('[NexusEvalProvider] Failed to resolve assistant', { assistantId });
169
168
  }
170
169
  }
171
170
 
@@ -2,32 +2,12 @@ const { Config_ID } = require('../../config/airtableConfig');
2
2
 
3
3
  const { getRecordByFilter } = require('../../services/airtableService');
4
4
 
5
- const { safeParse, safeParseArray } = require('../../utils/jsonUtils');
5
+ const { safeParse } = require('../../utils/jsonUtils');
6
6
 
7
7
  const FHIR_CONFIG_TABLE = 'fhir';
8
- const REQUIRED_KEYS = [
9
- 'IDENT_BASE',
10
- 'EXT_BASE',
11
- 'CODESYSTEM_BASE',
12
- 'SYSTEMS',
13
- 'SUSPECTED_GRADE_DISPLAYS',
14
- 'QUESTIONNAIRE_CANONICAL_URL',
15
- 'PROCTCAE_VERSION',
16
- 'DEVICE_ID',
17
- 'ORGANIZATION_ID',
18
- ];
8
+ const REQUIRED_KEYS = ['IDENT_BASE', 'EXT_BASE', 'SYSTEMS', 'DEVICE_ID', 'ORGANIZATION_ID'];
19
9
 
20
- const state = {
21
- identBase: null,
22
- extBase: null,
23
- codesystemBase: null,
24
- systems: null,
25
- suspectedGradeDisplays: null,
26
- questionnaireCanonicalUrl: null,
27
- proctcaeVersion: null,
28
- deviceId: null,
29
- organizationId: null,
30
- };
10
+ const state = { identBase: null, extBase: null, systems: null, deviceId: null, organizationId: null };
31
11
 
32
12
  async function initialize() {
33
13
  const records = await getRecordByFilter(Config_ID, FHIR_CONFIG_TABLE, 'TRUE()');
@@ -40,16 +20,9 @@ async function initialize() {
40
20
  const systems = safeParse(byKey.SYSTEMS, null);
41
21
  if (!systems) throw new Error('fhir config SYSTEMS is not valid JSON');
42
22
 
43
- const suspectedGradeDisplays = safeParseArray(byKey.SUSPECTED_GRADE_DISPLAYS, null);
44
- if (!suspectedGradeDisplays) throw new Error('fhir config SUSPECTED_GRADE_DISPLAYS is not a JSON array');
45
-
46
23
  state.identBase = byKey.IDENT_BASE;
47
24
  state.extBase = byKey.EXT_BASE;
48
- state.codesystemBase = byKey.CODESYSTEM_BASE;
49
25
  state.systems = systems;
50
- state.suspectedGradeDisplays = suspectedGradeDisplays;
51
- state.questionnaireCanonicalUrl = byKey.QUESTIONNAIRE_CANONICAL_URL;
52
- state.proctcaeVersion = byKey.PROCTCAE_VERSION;
53
26
  state.deviceId = byKey.DEVICE_ID;
54
27
  state.organizationId = byKey.ORGANIZATION_ID;
55
28
  }
@@ -62,11 +35,7 @@ module.exports = {
62
35
  initialize,
63
36
  getIdentBase: () => { assertLoaded(); return state.identBase; },
64
37
  getExtBase: () => { assertLoaded(); return state.extBase; },
65
- getCodesystemBase: () => { assertLoaded(); return state.codesystemBase; },
66
38
  getSystems: () => { assertLoaded(); return state.systems; },
67
- getSuspectedGradeDisplays: () => { assertLoaded(); return state.suspectedGradeDisplays; },
68
- getQuestionnaireCanonicalUrl: () => { assertLoaded(); return state.questionnaireCanonicalUrl; },
69
- getProctcaeVersion: () => { assertLoaded(); return state.proctcaeVersion; },
70
39
  getDeviceId: () => { assertLoaded(); return state.deviceId; },
71
40
  getOrganizationId: () => { assertLoaded(); return state.organizationId; },
72
41
  };
@@ -1,43 +1,3 @@
1
1
  const MENTION_SLUG = 'clinical-mention';
2
- const RESULT_SLUG = 'patient-reported-result';
3
- const ESCALATION_SLUG = 'dispatched-escalation';
4
- const ESCALATION_KIND_SLUG = 'escalation-kind';
5
- const REMINDER_SLUG = 'reminder';
6
- const REMINDER_KIND_SLUG = 'reminder-kind';
7
- const COMMUNICATION_CHANNEL_SLUG = 'communication-channel';
8
- const SYMPTOM_CASE_SLUG = 'symptom-condition';
9
- const SYMPTOM_ASSESSMENT_SLUG = 'symptom-assessment';
10
- const SYMPTOM_GRADE_SLUG = 'symptom-grade';
11
- const PATIENT_RISK_SLUG = 'patient-risk-assessment';
12
- const CONTINGENCY_PLAN_SLUG = 'contingency-plan';
13
- const CONTINGENCY_STEP_SLUG = 'contingency-step';
14
- const INTERVENTION_SLUG = 'intervention';
15
- const INTERVENTION_OUTCOME_SLUG = 'intervention-outcome';
16
- const INTERVENTION_KIND_SLUG = 'intervention-kind';
17
- const CLUSTER_IMPRESSION_SLUG = 'cluster-impression';
18
- const CLUSTER_LABEL_SLUG = 'cluster-label';
19
- const ROUTING_ASSESSMENT_SLUG = 'routing-assessment';
20
- const ROUTING_DISPOSITION_SLUG = 'routing-disposition';
21
2
 
22
- module.exports = {
23
- MENTION_SLUG,
24
- RESULT_SLUG,
25
- ESCALATION_SLUG,
26
- ESCALATION_KIND_SLUG,
27
- REMINDER_SLUG,
28
- REMINDER_KIND_SLUG,
29
- COMMUNICATION_CHANNEL_SLUG,
30
- SYMPTOM_CASE_SLUG,
31
- SYMPTOM_ASSESSMENT_SLUG,
32
- SYMPTOM_GRADE_SLUG,
33
- PATIENT_RISK_SLUG,
34
- CONTINGENCY_PLAN_SLUG,
35
- CONTINGENCY_STEP_SLUG,
36
- INTERVENTION_SLUG,
37
- INTERVENTION_OUTCOME_SLUG,
38
- INTERVENTION_KIND_SLUG,
39
- CLUSTER_IMPRESSION_SLUG,
40
- CLUSTER_LABEL_SLUG,
41
- ROUTING_ASSESSMENT_SLUG,
42
- ROUTING_DISPOSITION_SLUG,
43
- };
3
+ module.exports = { MENTION_SLUG };
@@ -12,11 +12,6 @@ function patientReference(patientId) {
12
12
  return reference('Patient', patientId);
13
13
  }
14
14
 
15
- function recipientReference({ patientId, recipientKind = 'patient', recipientRefId = null }) {
16
- if (recipientKind === 'related_person' && recipientRefId) return reference('RelatedPerson', recipientRefId);
17
- return reference('Patient', patientId);
18
- }
19
-
20
15
  function codeableConcept(text, { code = null, system = null, display = null } = {}) {
21
16
  const out = { text };
22
17
  if (code !== null && system !== null) {
@@ -29,26 +24,10 @@ function extension(name, value) {
29
24
  return { url: extensionUrl(name), ...value };
30
25
  }
31
26
 
32
- function notes(...texts) {
33
- const present = texts.filter(Boolean).map((text) => ({ text }));
34
- return present.length ? present : null;
35
- }
36
-
37
- function toIso(value) {
38
- if (value instanceof Date) return value.toISOString().replace(/\.000Z$/, 'Z');
39
- const s = String(value);
40
- if (/\+00:00$/.test(s)) return s.replace(/\+00:00$/, 'Z');
41
- if (/(Z|[+-]\d{2}:\d{2})$/.test(s)) return s;
42
- return `${s}Z`;
43
- }
44
-
45
27
  module.exports = {
46
28
  identifier,
47
29
  reference,
48
30
  patientReference,
49
- recipientReference,
50
31
  codeableConcept,
51
32
  extension,
52
- notes,
53
- toIso,
54
33
  };
@@ -1,17 +1,14 @@
1
- const { getIdentBase, getExtBase, getCodesystemBase } = require('../config/fhirConfig');
1
+ const { getIdentBase, getExtBase } = require('../config/fhirConfig');
2
2
 
3
3
  function fhirId(businessId) {
4
- const id = String(businessId).replace(/[^A-Za-z0-9.-]/g, '-').slice(0, 64);
5
- return id || 'x';
4
+ return String(businessId).replace(/_/g, '-').replace(/[^A-Za-z0-9.-]/g, '').slice(0, 64);
6
5
  }
7
6
 
8
7
  const identifierSystem = (slug) => `${getIdentBase()}/${slug}`;
9
8
  const extensionUrl = (name) => `${getExtBase()}/${name}`;
10
- const codeSystemUrl = (name) => `${getCodesystemBase()}/${name}`;
11
9
 
12
10
  module.exports = {
13
11
  fhirId,
14
12
  identifierSystem,
15
13
  extensionUrl,
16
- codeSystemUrl,
17
14
  };
package/lib/fhir/index.js CHANGED
@@ -1,14 +1,5 @@
1
1
  const { registerProjector, project, storeResources } = require('./services/fhirService');
2
2
  const { storeClinicalMentions } = require('./services/clinicalMentionService');
3
- const { storeResultObservations } = require('./services/resultObservationService');
4
- const { storeSymptomCase, readSymptomCases } = require('./services/symptomCaseService');
5
- const { storePatientRisk, readPatientRisk } = require('./services/riskService');
6
- const { storeContingency, readContingencies } = require('./services/contingencyService');
7
- const { storeIntervention, readInterventions } = require('./services/interventionService');
8
- const { storeCluster, readClusters } = require('./services/clusterService');
9
- const { storeRoutingDecision, readRoutingByTurn } = require('./services/routingService');
10
- const { storeTriage } = require('./services/triageService');
11
- const { patientSnapshot } = require('./services/snapshotService');
12
3
  const { registerAllProjectors } = require('./projections/registerProjectors');
13
4
  const { getFhirStore, ensureFhirIndexes } = require('./stores/fhirStore');
14
5
  const fhirConfig = require('./config/fhirConfig');
@@ -25,21 +16,6 @@ module.exports = {
25
16
  project,
26
17
  storeResources,
27
18
  storeClinicalMentions,
28
- storeResultObservations,
29
- storeSymptomCase,
30
- readSymptomCases,
31
- storePatientRisk,
32
- readPatientRisk,
33
- storeContingency,
34
- readContingencies,
35
- storeIntervention,
36
- readInterventions,
37
- storeCluster,
38
- readClusters,
39
- storeRoutingDecision,
40
- readRoutingByTurn,
41
- storeTriage,
42
- patientSnapshot,
43
19
  getFhirStore,
44
20
  ensureFhirIndexes,
45
21
  };
@@ -48,14 +48,10 @@ envelopeSchema.statics.deriveIndex = function deriveIndex(resource) {
48
48
  resource.date ||
49
49
  resource.recorded ||
50
50
  resource.recordedDate ||
51
- resource.onsetDateTime ||
52
- resource.occurrenceDateTime ||
53
- resource.sent ||
54
51
  resource.start ||
55
52
  (resource.effectivePeriod || {}).start ||
56
53
  (resource.period || {}).start ||
57
54
  (resource.actualPeriod || {}).start ||
58
- ((resource.requestedPeriod || [])[0] || {}).start ||
59
55
  null;
60
56
  return {
61
57
  resourceType: resource.resourceType || null,
@@ -123,7 +119,6 @@ function patientReferenceOf(resource) {
123
119
  return (
124
120
  patientRef(resource.subject) ||
125
121
  patientRef(resource.patient) ||
126
- patientRef(resource.for) ||
127
122
  patientRef(resource.performer) ||
128
123
  participantRef()
129
124
  );
@@ -1,24 +1,8 @@
1
1
  const { registerProjector } = require('../services/fhirService');
2
2
  const { projectClinicalMention, PROJECTOR_NAME: MENTION_PROJECTOR_NAME } = require('./clinicalMentionProjection');
3
- const { projectDispatchedEscalation, PROJECTOR_NAME: ESCALATION_PROJECTOR_NAME } = require('./dispatchedEscalationProjection');
4
- const { projectReminder, PROJECTOR_NAME: REMINDER_PROJECTOR_NAME } = require('./reminderProjection');
5
- const { projectSymptomCase, PROJECTOR_NAME: SYMPTOM_CASE_PROJECTOR_NAME } = require('./symptomCaseProjection');
6
- const { projectPatientRisk, PROJECTOR_NAME: RISK_PROJECTOR_NAME } = require('./riskProjection');
7
- const { projectContingency, PROJECTOR_NAME: CONTINGENCY_PROJECTOR_NAME } = require('./contingencyProjection');
8
- const { projectIntervention, PROJECTOR_NAME: INTERVENTION_PROJECTOR_NAME } = require('./interventionProjection');
9
- const { projectCluster, PROJECTOR_NAME: CLUSTER_PROJECTOR_NAME } = require('./clusterProjection');
10
- const { projectRoutingDecision, PROJECTOR_NAME: ROUTING_PROJECTOR_NAME } = require('./routingProjection');
11
3
 
12
4
  const PROJECTORS = [
13
5
  { name: MENTION_PROJECTOR_NAME, project: projectClinicalMention },
14
- { name: ESCALATION_PROJECTOR_NAME, project: projectDispatchedEscalation },
15
- { name: REMINDER_PROJECTOR_NAME, project: projectReminder },
16
- { name: SYMPTOM_CASE_PROJECTOR_NAME, project: projectSymptomCase },
17
- { name: RISK_PROJECTOR_NAME, project: projectPatientRisk },
18
- { name: CONTINGENCY_PROJECTOR_NAME, project: projectContingency },
19
- { name: INTERVENTION_PROJECTOR_NAME, project: projectIntervention },
20
- { name: CLUSTER_PROJECTOR_NAME, project: projectCluster },
21
- { name: ROUTING_PROJECTOR_NAME, project: projectRoutingDecision },
22
6
  ];
23
7
 
24
8
  function registerAllProjectors() {
@@ -1,95 +1,12 @@
1
1
  const { getSystems } = require('../config/fhirConfig');
2
- const { fhirId, codeSystemUrl } = require('../helpers/fhirHelper');
3
- const { identifier, reference, patientReference, codeableConcept, extension, toIso } = require('../helpers/elementHelper');
4
- const { SYMPTOM_CASE_SLUG } = require('../constants/projectionSlugs');
5
-
6
- const CLINICAL_STATUS_TO_FHIR = {
7
- resolved: 'resolved',
8
- inactive: 'inactive',
9
- };
10
-
11
- const RESOLUTION_CLOSE_REASONS = new Set([
12
- 'resolved', 'team_resolved', 'resolved_spontaneously', 'resolved_by_intervention',
13
- ]);
14
-
15
- const VALID_VERIFICATION = new Set(['confirmed', 'provisional', 'unconfirmed']);
16
-
17
- const PROVENANCE_PARTICIPANT_TYPE_SYSTEM = 'http://terminology.hl7.org/CodeSystem/provenance-participant-type';
18
-
19
- function fhirClinicalStatusFor(condition) {
20
- const status = condition.clinicalStatus;
21
- if (status === 'characterizing' || status === 'monitoring') {
22
- return condition.recurrenceCount > 0 ? 'recurrence' : 'active';
23
- }
24
- if (status === 'closed') {
25
- return RESOLUTION_CLOSE_REASONS.has((condition.closeReason || '').trim().toLowerCase()) ? 'resolved' : 'inactive';
26
- }
27
- return CLINICAL_STATUS_TO_FHIR[status];
28
- }
2
+ const { fhirId } = require('../helpers/fhirHelper');
3
+ const { identifier, patientReference, codeableConcept } = require('../helpers/elementHelper');
29
4
 
30
5
  class Condition {
31
6
  constructor(payload) {
32
7
  Object.assign(this, payload);
33
8
  }
34
9
 
35
- static fromSymptomCase({ condition, verification, headAssessmentId = null }) {
36
- const systems = getSystems();
37
- const clinicalStatusCode = fhirClinicalStatusFor(condition);
38
- const verificationCode = VALID_VERIFICATION.has(verification) ? verification : 'provisional';
39
- const code = condition.ctcaeTerm && condition.ctcaeTerm !== 'other'
40
- ? codeableConcept(condition.ctcaeTerm, { code: condition.ctcaeTerm, system: codeSystemUrl('pro-ctcae'), display: condition.ctcaeTerm })
41
- : codeableConcept(condition.ctcaeTerm || 'other');
42
- const extensions = [extension('symptom-case-status', { valueString: condition.clinicalStatus })];
43
- const payload = {
44
- resourceType: 'Condition',
45
- id: fhirId(condition.conditionId),
46
- identifier: [identifier(SYMPTOM_CASE_SLUG, condition.conditionId)],
47
- clinicalStatus: codeableConcept(clinicalStatusCode, { code: clinicalStatusCode, system: systems.conditionClinical }),
48
- verificationStatus: codeableConcept(verificationCode, { code: verificationCode, system: systems.conditionVerStatus }),
49
- category: [
50
- codeableConcept('problem-list-item', { code: 'problem-list-item', system: systems.conditionCategory }),
51
- codeableConcept('PRO-CTCAE patient-reported symptom (agent-tracked case)', { code: 'proctcae-symptom', system: codeSystemUrl('condition-category') }),
52
- ],
53
- code,
54
- subject: patientReference(condition.patientId),
55
- participant: [{
56
- function: codeableConcept('Informant', { code: 'informant', system: PROVENANCE_PARTICIPANT_TYPE_SYSTEM }),
57
- actor: patientReference(condition.patientId),
58
- }],
59
- onsetDateTime: toIso(condition.onsetAt),
60
- extension: extensions,
61
- };
62
- if (condition.abatementAt) {
63
- if (clinicalStatusCode === 'resolved') {
64
- payload.abatementDateTime = toIso(condition.abatementAt);
65
- } else {
66
- extensions.push(extension('legacy-abatement', { valueDateTime: toIso(condition.abatementAt) }));
67
- }
68
- }
69
- if (condition.closeReason) {
70
- extensions.push(extension('symptom-close-reason', { valueString: condition.closeReason }));
71
- if (clinicalStatusCode === 'inactive' && condition.closeReason === 'stale_no_activity') {
72
- extensions.push(extension('presumed-inactive-by-gap', { valueBoolean: true }));
73
- }
74
- }
75
- if (condition.temporality) extensions.push(extension('symptom-temporality', { valueString: condition.temporality }));
76
- if (condition.currentEpisodeId) extensions.push(extension('symptom-current-episode-id', { valueString: condition.currentEpisodeId }));
77
- for (const id of condition.evidenceEpisodeIds) extensions.push(extension('symptom-evidence-episode-ids', { valueString: id }));
78
- for (const id of condition.evidenceMentionIds) extensions.push(extension('symptom-evidence-mention-ids', { valueString: id }));
79
- extensions.push(extension('symptom-recurrence-count', { valueInteger: condition.recurrenceCount }));
80
- if (condition.reactivatedAt) extensions.push(extension('symptom-reactivated-at', { valueDateTime: toIso(condition.reactivatedAt) }));
81
- if (condition.lastTransitionReason) payload.note = [{ text: condition.lastTransitionReason }];
82
- const evidence = [
83
- ...condition.evidenceEpisodeIds.map((id) => ({ reference: reference('Observation', id) })),
84
- ...condition.evidenceMentionIds.map((id) => ({ reference: reference('Observation', id) })),
85
- ];
86
- if (evidence.length) payload.evidence = evidence;
87
- if (headAssessmentId) {
88
- payload.stage = [{ assessment: [reference('ClinicalImpression', headAssessmentId)] }];
89
- }
90
- return new Condition(payload);
91
- }
92
-
93
10
  static fromMentionProjection({ fact, provenance, code, extensions, slug }) {
94
11
  const assertion = fact.assertion;
95
12
  const notes = provenance.verbatimQuotes && provenance.verbatimQuotes.length
@@ -112,28 +29,6 @@ class Condition {
112
29
  if (notes) payload.note = notes;
113
30
  return new Condition(payload);
114
31
  }
115
-
116
- static fromTriageProjection({ sym, code }) {
117
- const caseId = `${sym.patientId}-${sym.term}`;
118
- const systems = getSystems();
119
- const payload = {
120
- resourceType: 'Condition',
121
- id: fhirId(`symptom-${caseId}`),
122
- identifier: [identifier('symptom-condition', caseId)],
123
- clinicalStatus: codeableConcept('active', { code: 'active', system: systems.conditionClinical }),
124
- verificationStatus: codeableConcept('provisional', { code: 'provisional', system: systems.conditionVerStatus }),
125
- category: [
126
- codeableConcept('problem-list-item', { code: 'problem-list-item', system: systems.conditionCategory }),
127
- codeableConcept('PRO-CTCAE patient-reported symptom (triage-tracked case)', { code: 'proctcae-symptom', system: codeSystemUrl('condition-category') }),
128
- ],
129
- code,
130
- subject: patientReference(sym.patientId),
131
- evidence: [{ reference: reference('Observation', String(sym.symptomId)) }],
132
- extension: [extension('symptom-source', { valueString: String(sym.source || 'triage') })],
133
- };
134
- if (sym.effective) payload.onsetDateTime = toIso(sym.effective);
135
- return new Condition(payload);
136
- }
137
32
  }
138
33
 
139
34
  module.exports = { Condition };