@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
@@ -1,46 +0,0 @@
1
- const { fhirId } = require('../helpers/fhirHelper');
2
- const {
3
- identifier,
4
- patientReference,
5
- reference,
6
- codeableConcept,
7
- toIso,
8
- } = require('../helpers/elementHelper');
9
- const { ROUTING_DISPOSITION_SLUG } = require('../constants/projectionSlugs');
10
-
11
- class RequestOrchestration {
12
- constructor(payload) {
13
- Object.assign(this, payload);
14
- }
15
-
16
- static fromRoutingDisposition({ disposition }) {
17
- const action = [{
18
- title: disposition.primaryAction,
19
- description: disposition.primaryReason,
20
- code: [codeableConcept(disposition.primaryAction)],
21
- }];
22
- for (const secondary of disposition.secondaryActions) {
23
- action.push({
24
- title: secondary.action,
25
- description: `compose_as: ${secondary.composeAs}`,
26
- code: [codeableConcept(secondary.action)],
27
- });
28
- }
29
- const payload = {
30
- resourceType: 'RequestOrchestration',
31
- id: fhirId(disposition.dispositionId),
32
- identifier: [identifier(ROUTING_DISPOSITION_SLUG, disposition.dispositionId)],
33
- status: 'active',
34
- intent: 'plan',
35
- subject: patientReference(disposition.patientId),
36
- authoredOn: toIso(disposition.authoredAt),
37
- action,
38
- };
39
- if (disposition.primaryCaseId) {
40
- payload.reason = [{ reference: reference('Condition', disposition.primaryCaseId) }];
41
- }
42
- return new RequestOrchestration(payload);
43
- }
44
- }
45
-
46
- module.exports = { RequestOrchestration };
@@ -1,99 +0,0 @@
1
- const { getDeviceId } = require('../config/fhirConfig');
2
- const { fhirId, codeSystemUrl } = require('../helpers/fhirHelper');
3
- const {
4
- identifier,
5
- patientReference,
6
- reference,
7
- codeableConcept,
8
- extension,
9
- toIso,
10
- } = require('../helpers/elementHelper');
11
- const { PATIENT_RISK_SLUG } = require('../constants/projectionSlugs');
12
-
13
- const RISK_SEVERITY_CODESYSTEM = 'risk-severity';
14
- const PREDICTION_KIND_AE = 'ae';
15
- const PREDICTION_KIND_DISEASE = 'disease';
16
- const PREDICTION_KIND_COMORBIDITY = 'comorbidity';
17
-
18
- function aePrediction(ae) {
19
- const prediction = {
20
- outcome: codeableConcept(ae.aeName),
21
- qualitativeRisk: codeableConcept(ae.severityIfConfirmed, {
22
- code: ae.severityIfConfirmed,
23
- system: codeSystemUrl(RISK_SEVERITY_CODESYSTEM),
24
- }),
25
- extension: [extension('risk-prediction-kind', { valueString: PREDICTION_KIND_AE })],
26
- };
27
- if (ae.why) prediction.rationale = ae.why;
28
- for (const symptom of ae.minimumSymptomSet) {
29
- prediction.extension.push(extension('risk-minimum-symptom-set', { valueString: symptom }));
30
- }
31
- return prediction;
32
- }
33
-
34
- function diseasePrediction(risk) {
35
- const prediction = {
36
- outcome: codeableConcept(risk.bodySite),
37
- extension: [extension('risk-prediction-kind', { valueString: PREDICTION_KIND_DISEASE })],
38
- };
39
- for (const complication of risk.complications) {
40
- prediction.extension.push(extension('risk-disease-complication', { valueString: complication }));
41
- }
42
- for (const symptom of risk.warningSymptoms) {
43
- prediction.extension.push(extension('risk-disease-warning-symptom', { valueString: symptom }));
44
- }
45
- for (const comorbidity of risk.amplifierComorbidities) {
46
- prediction.extension.push(extension('risk-disease-amplifier', { valueString: comorbidity }));
47
- }
48
- return prediction;
49
- }
50
-
51
- function comorbidityPrediction(amplifier) {
52
- const prediction = {
53
- outcome: codeableConcept(amplifier.comorbidity),
54
- rationale: amplifier.interaction,
55
- extension: [extension('risk-prediction-kind', { valueString: PREDICTION_KIND_COMORBIDITY })],
56
- };
57
- if (amplifier.clinicalNote) {
58
- prediction.extension.push(extension('risk-comorbidity-clinical-note', { valueString: amplifier.clinicalNote }));
59
- }
60
- return prediction;
61
- }
62
-
63
- class RiskAssessment {
64
- constructor(payload) {
65
- Object.assign(this, payload);
66
- }
67
-
68
- static fromPatientRisk({ assessment }) {
69
- const payload = {
70
- resourceType: 'RiskAssessment',
71
- id: fhirId(assessment.assessmentId),
72
- identifier: [identifier(PATIENT_RISK_SLUG, assessment.assessmentId)],
73
- status: assessment.status,
74
- subject: patientReference(assessment.patientId),
75
- performer: reference('Device', getDeviceId()),
76
- occurrenceDateTime: toIso(assessment.assessedAt),
77
- };
78
- const prediction = [
79
- ...assessment.predictedAes.map(aePrediction),
80
- ...assessment.diseaseRisks.map(diseasePrediction),
81
- ...assessment.comorbidityAmplifiers.map(comorbidityPrediction),
82
- ];
83
- if (prediction.length) payload.prediction = prediction;
84
- const extensions = [];
85
- if (assessment.sourceRegimen) extensions.push(extension('assessed-source-regimen', { valueString: assessment.sourceRegimen }));
86
- if (assessment.sourceDiagnosis) extensions.push(extension('assessed-source-diagnosis', { valueString: assessment.sourceDiagnosis }));
87
- if (assessment.sourcePhase) extensions.push(extension('assessed-source-phase', { valueString: assessment.sourcePhase }));
88
- if (assessment.assessedByModel) extensions.push(extension('assessed-by-model', { valueString: assessment.assessedByModel }));
89
- if (extensions.length) payload.extension = extensions;
90
- return new RiskAssessment(payload);
91
- }
92
- }
93
-
94
- module.exports = {
95
- RiskAssessment,
96
- PREDICTION_KIND_AE,
97
- PREDICTION_KIND_DISEASE,
98
- PREDICTION_KIND_COMORBIDITY,
99
- };
@@ -1,106 +0,0 @@
1
- const { getDeviceId } = require('../config/fhirConfig');
2
- const { fhirId, codeSystemUrl } = require('../helpers/fhirHelper');
3
- const {
4
- identifier,
5
- reference,
6
- patientReference,
7
- codeableConcept,
8
- extension,
9
- notes,
10
- toIso,
11
- } = require('../helpers/elementHelper');
12
- const {
13
- ESCALATION_SLUG,
14
- ESCALATION_KIND_SLUG,
15
- INTERVENTION_SLUG,
16
- INTERVENTION_KIND_SLUG,
17
- INTERVENTION_OUTCOME_SLUG,
18
- ROUTING_DISPOSITION_SLUG,
19
- } = require('../constants/projectionSlugs');
20
- const { SEVERITY_TO_PRIORITY } = require('../constants/severityPriority');
21
- const { interventionExtensions } = require('./interventionExtensions');
22
-
23
- const TICKET_STATUS_TO_TASK_STATUS = {
24
- queued: 'requested',
25
- sent: 'requested',
26
- acknowledged: 'in-progress',
27
- failed: 'failed',
28
- };
29
-
30
- class Task {
31
- constructor(payload) {
32
- Object.assign(this, payload);
33
- }
34
-
35
- static fromTicketEscalation({ ticket, deviceId }) {
36
- const payload = {
37
- resourceType: 'Task',
38
- id: fhirId(ticket.eventId),
39
- identifier: [identifier(ESCALATION_SLUG, ticket.eventId)],
40
- status: TICKET_STATUS_TO_TASK_STATUS[ticket.status],
41
- intent: 'order',
42
- priority: SEVERITY_TO_PRIORITY[ticket.severity] || 'routine',
43
- code: codeableConcept(ticket.kind, { code: ticket.kind, system: codeSystemUrl(ESCALATION_KIND_SLUG) }),
44
- for: patientReference(ticket.patientId),
45
- authoredOn: toIso(ticket.createdAt),
46
- requester: reference('Device', deviceId),
47
- };
48
- if (ticket.title) payload.description = ticket.title;
49
- if (ticket.reason) payload.note = [{ text: ticket.reason }];
50
- if (typeof ticket.slaMinutes === 'number' && ticket.slaMinutes > 0) {
51
- const start = new Date(ticket.createdAt);
52
- const end = new Date(start.getTime() + ticket.slaMinutes * 60000);
53
- payload.restriction = { period: { start: toIso(start), end: toIso(end) } };
54
- payload.extension = [extension('sla-budget-minutes', { valueInteger: ticket.slaMinutes })];
55
- }
56
- return new Task(payload);
57
- }
58
-
59
- static fromIntervention({ intervention }) {
60
- const payload = {
61
- resourceType: 'Task',
62
- id: fhirId(intervention.interventionId),
63
- identifier: [identifier(INTERVENTION_SLUG, intervention.interventionId)],
64
- status: intervention.outcome === 'pending' ? 'requested' : 'completed',
65
- intent: 'order',
66
- code: codeableConcept(intervention.interventionLabel, {
67
- code: intervention.kind,
68
- system: codeSystemUrl(INTERVENTION_KIND_SLUG),
69
- display: intervention.interventionLabel,
70
- }),
71
- authoredOn: toIso(intervention.deliveredAt),
72
- for: patientReference(intervention.patientId),
73
- requester: reference('Device', getDeviceId()),
74
- reason: [{ reference: reference('Condition', intervention.caseId) }],
75
- extension: interventionExtensions(intervention, { withRecipient: true }),
76
- };
77
- if (intervention.outcome !== 'pending') {
78
- const outcomeCc = codeableConcept(intervention.outcome, { code: intervention.outcome, system: codeSystemUrl(INTERVENTION_OUTCOME_SLUG) });
79
- payload.output = [{ type: codeableConcept(INTERVENTION_OUTCOME_SLUG), valueCodeableConcept: outcomeCc }];
80
- }
81
- const note = notes(intervention.rationale, intervention.outcomeNotes);
82
- if (note) payload.note = note;
83
- return new Task(payload);
84
- }
85
-
86
- static fromRoutingDisposition({ disposition }) {
87
- const payload = {
88
- resourceType: 'Task',
89
- id: fhirId(disposition.dispositionId),
90
- identifier: [identifier(ROUTING_DISPOSITION_SLUG, disposition.dispositionId)],
91
- status: 'requested',
92
- intent: 'order',
93
- code: codeableConcept(disposition.primaryAction),
94
- description: disposition.primaryReason,
95
- for: patientReference(disposition.patientId),
96
- authoredOn: toIso(disposition.authoredAt),
97
- requester: reference('Device', getDeviceId()),
98
- };
99
- if (disposition.primaryCaseId) {
100
- payload.reason = [{ reference: reference('Condition', disposition.primaryCaseId) }];
101
- }
102
- return new Task(payload);
103
- }
104
- }
105
-
106
- module.exports = { Task };
@@ -1,28 +0,0 @@
1
- const { extension } = require('../helpers/elementHelper');
2
-
3
- const RATIONALE_EXT = 'intervention-rationale';
4
- const OUTCOME_NOTES_EXT = 'intervention-outcome-notes';
5
- const OUTCOME_EXT = 'intervention-outcome';
6
- const RECIPIENT_KIND_EXT = 'intervention-recipient-kind';
7
- const RECIPIENT_REF_EXT = 'intervention-recipient-ref';
8
-
9
- function interventionExtensions(intervention, { withRecipient = false } = {}) {
10
- const out = [];
11
- if (intervention.rationale) out.push(extension(RATIONALE_EXT, { valueString: intervention.rationale }));
12
- if (intervention.outcomeNotes) out.push(extension(OUTCOME_NOTES_EXT, { valueString: intervention.outcomeNotes }));
13
- if (intervention.outcome) out.push(extension(OUTCOME_EXT, { valueString: intervention.outcome }));
14
- if (withRecipient && intervention.recipientKind === 'related_person' && intervention.recipientRefId) {
15
- out.push(extension(RECIPIENT_KIND_EXT, { valueString: 'related_person' }));
16
- out.push(extension(RECIPIENT_REF_EXT, { valueString: intervention.recipientRefId }));
17
- }
18
- return out;
19
- }
20
-
21
- module.exports = {
22
- interventionExtensions,
23
- RATIONALE_EXT,
24
- OUTCOME_NOTES_EXT,
25
- OUTCOME_EXT,
26
- RECIPIENT_KIND_EXT,
27
- RECIPIENT_REF_EXT,
28
- };
@@ -1,77 +0,0 @@
1
- const { ZodError } = require('zod');
2
-
3
- const { fhirId, identifierSystem } = require('../helpers/fhirHelper');
4
- const { extMap, identifierValue, refToId } = require('../helpers/fhirReadHelper');
5
- const { CLUSTER_IMPRESSION_SLUG, CLUSTER_LABEL_SLUG } = require('../constants/projectionSlugs');
6
- const { CLUSTER_LIST_SEPARATOR } = require('../resources/ClinicalImpression');
7
- const { PROJECTOR_NAME } = require('../projections/clusterProjection');
8
- const { getFhirStore } = require('../stores/fhirStore');
9
- const { ClusterImpression } = require('../../shared/dtos/ClusterImpression');
10
- const { logger } = require('../../utils/logger');
11
- const { project, storeResources } = require('./fhirService');
12
-
13
- async function storeCluster({ patientId, cluster, history = [] }) {
14
- if (cluster.patientId !== patientId) {
15
- throw new Error(`storeCluster patientId mismatch: expected ${patientId}`);
16
- }
17
- const bundle = project([{ name: PROJECTOR_NAME, aggregate: { cluster, history } }]);
18
- const resources = bundle.entry.map((entry) => entry.resource);
19
- const { stored } = await storeResources(resources);
20
- return { count: stored.length, ids: stored };
21
- }
22
-
23
- async function readClusters({ patientId, label = null, status = null }) {
24
- const store = getFhirStore();
25
- const patient = `Patient/${fhirId(patientId)}`;
26
- const clusterSystem = identifierSystem(CLUSTER_IMPRESSION_SLUG);
27
- const impressions = (await store.find({ patient, resourceType: 'ClinicalImpression' }))
28
- .filter((resource) => (resource.identifier || []).some((id) => id.system === clusterSystem))
29
- .filter((resource) => identifierValue(resource, CLUSTER_IMPRESSION_SLUG));
30
- const clusters = [];
31
- for (const impression of impressions) {
32
- let cluster;
33
- try {
34
- cluster = new ClusterImpression(reconstructCluster(impression, patientId));
35
- } catch (error) {
36
- if (error instanceof ZodError) {
37
- logger.warn({ clusterId: impression.id }, 'skipping malformed cluster ClinicalImpression');
38
- continue;
39
- }
40
- throw error;
41
- }
42
- if (label && cluster.label !== label) continue;
43
- if (status && cluster.status !== status) continue;
44
- clusters.push(cluster);
45
- }
46
- return clusters;
47
- }
48
-
49
- function reconstructCluster(resource, patientId) {
50
- const ext = extMap(resource);
51
- const period = resource.effectivePeriod || {};
52
- return {
53
- schemaVersion: '1',
54
- clusterId: identifierValue(resource, CLUSTER_IMPRESSION_SLUG),
55
- patientId,
56
- label: identifierValue(resource, CLUSTER_LABEL_SLUG) || '',
57
- hypothesisType: ext.clusterHypothesisType,
58
- status: ext['cluster-status'],
59
- confidence: ext.clusterHypothesisConfidence,
60
- clinicalSignificance: ext.clusterClinicalSignificance,
61
- memberCaseIds: (resource.problem || []).map((problemRef) => refToId(problemRef.reference)).filter(Boolean),
62
- consultationId: ext['cluster-consultation-id'] || null,
63
- relationshipModel: resource.summary || '',
64
- strengthensIf: ext.clusterStrengthensIf ? ext.clusterStrengthensIf.split(CLUSTER_LIST_SEPARATOR) : [],
65
- weakensIf: ext.clusterWeakensIf ? ext.clusterWeakensIf.split(CLUSTER_LIST_SEPARATOR) : [],
66
- openingReasoning: resource.description || '',
67
- openedAt: resource.effectiveDateTime || period.start,
68
- dissolvedAt: period.end || null,
69
- dissolveReason: ext['cluster-dissolve-reason'] || null,
70
- dissolveCategory: (resource.statusReason && resource.statusReason.text) || null,
71
- };
72
- }
73
-
74
- module.exports = {
75
- storeCluster,
76
- readClusters,
77
- };
@@ -1,117 +0,0 @@
1
- const { ZodError } = require('zod');
2
-
3
- const { fhirId } = require('../helpers/fhirHelper');
4
- const { extMap, identifierValue, refToId } = require('../helpers/fhirReadHelper');
5
- const {
6
- CONTINGENCY_PLAN_SLUG,
7
- CONTINGENCY_STEP_SLUG,
8
- } = require('../constants/projectionSlugs');
9
- const { CONTINGENCY_STATUS_TO_FHIR } = require('../resources/CarePlan');
10
- const { PROJECTOR_NAME } = require('../projections/contingencyProjection');
11
- const { getFhirStore } = require('../stores/fhirStore');
12
- const { ContingencySafetyNet } = require('../../shared/dtos/ContingencySafetyNet');
13
- const { logger } = require('../../utils/logger');
14
- const { project, storeResources } = require('./fhirService');
15
-
16
- const FHIR_TO_CONTINGENCY_STATUS = invert(CONTINGENCY_STATUS_TO_FHIR);
17
-
18
- async function storeContingency({ patientId, contingency }) {
19
- if (contingency.carePlan.patientId !== patientId) {
20
- throw new Error(`storeContingency patientId mismatch: expected ${patientId}`);
21
- }
22
- const bundle = project([{ name: PROJECTOR_NAME, aggregate: contingency }]);
23
- const resources = bundle.entry.map((entry) => entry.resource);
24
- const { stored } = await storeResources(resources);
25
- return { count: stored.length, ids: stored };
26
- }
27
-
28
- async function readContingencies({ patientId, caseId = null, clusterId = null }) {
29
- const store = getFhirStore();
30
- const patient = `Patient/${fhirId(patientId)}`;
31
- const plans = (await store.find({ patient, resourceType: 'CarePlan' }))
32
- .filter((plan) => identifierValue(plan, CONTINGENCY_PLAN_SLUG));
33
- const requests = (await store.find({ patient, resourceType: 'CommunicationRequest' }))
34
- .filter((request) => identifierValue(request, CONTINGENCY_STEP_SLUG));
35
-
36
- const nets = [];
37
- for (const plan of plans) {
38
- const net = reconstructContingency(plan, requests, patientId);
39
- if (caseId && net.carePlan.relatedCaseId !== caseId) continue;
40
- if (clusterId && net.carePlan.relatedClusterId !== clusterId) continue;
41
- try {
42
- nets.push(new ContingencySafetyNet(net));
43
- } catch (error) {
44
- if (error instanceof ZodError) {
45
- logger.warn({ planId: net.carePlan.planId }, 'skipping malformed contingency CarePlan');
46
- continue;
47
- }
48
- throw error;
49
- }
50
- }
51
- return nets;
52
- }
53
-
54
- function reconstructContingency(plan, requests, patientId) {
55
- const planId = identifierValue(plan, CONTINGENCY_PLAN_SLUG);
56
- const ext = extMap(plan);
57
- const stepIds = new Set([`${planId}-care_team`, `${planId}-patient`]);
58
- const planSteps = requests
59
- .filter((resource) => stepIds.has(identifierValue(resource, CONTINGENCY_STEP_SLUG)))
60
- .map(reconstructStep);
61
- const teamEscalation = planSteps.find((step) => step.recipient === 'care_team') || null;
62
- const patientReminder = planSteps.find((step) => step.recipient === 'patient') || null;
63
- const address = plan.addresses && plan.addresses[0] && plan.addresses[0].reference;
64
- return {
65
- schemaVersion: '1',
66
- carePlan: {
67
- planId,
68
- patientId,
69
- relatedCaseId: refToId(address && address.reference),
70
- relatedClusterId: refToId(plan.supportingInfo && plan.supportingInfo[0] && plan.supportingInfo[0].reference),
71
- concern: plan.description || '',
72
- awaitingCharacterization: ext['contingency-awaiting-characterization'] || '',
73
- whyThisMatters: ext['contingency-why-this-matters'] || '',
74
- creationReasoning: ext['contingency-creation-reasoning'] || '',
75
- status: ext['contingency-status'] || FHIR_TO_CONTINGENCY_STATUS[plan.status] || 'armed',
76
- createdAt: plan.created,
77
- resolvedAt: (plan.period && plan.period.end) || null,
78
- resolvedReason: ext['contingency-resolved-reason'] || null,
79
- },
80
- teamEscalation,
81
- patientReminder,
82
- };
83
- }
84
-
85
- function reconstructStep(resource) {
86
- const ext = extMap(resource);
87
- const silence = ext['fires-after-silence-minutes'];
88
- const firstPayload = resource.payload && resource.payload[0];
89
- return {
90
- recipient: recipientOf(resource),
91
- message: (firstPayload && firstPayload.contentCodeableConcept && firstPayload.contentCodeableConcept.text) || null,
92
- priority: ext['contingency-step-priority'] || resource.priority || null,
93
- actionRequested: ext['contingency-action-requested'] || null,
94
- firesAfterSilenceMinutes: typeof silence === 'number' ? silence : null,
95
- };
96
- }
97
-
98
- function recipientOf(resource) {
99
- const category = resource.category && resource.category[0] && resource.category[0].text;
100
- if (category === 'contingency-patient') return 'patient';
101
- if (category === 'contingency-care_team') return 'care_team';
102
- const ref = resource.recipient && resource.recipient[0] && resource.recipient[0].reference;
103
- return ref && ref.startsWith('Organization/') ? 'care_team' : 'patient';
104
- }
105
-
106
- function invert(map) {
107
- const out = {};
108
- for (const [key, value] of Object.entries(map)) {
109
- if (!(value in out)) out[value] = key;
110
- }
111
- return out;
112
- }
113
-
114
- module.exports = {
115
- storeContingency,
116
- readContingencies,
117
- };
@@ -1,149 +0,0 @@
1
- const { ZodError } = require('zod');
2
-
3
- const { fhirId, identifierSystem } = require('../helpers/fhirHelper');
4
- const { identifierValue, refToId, extValues } = require('../helpers/fhirReadHelper');
5
- const { INTERVENTION_SLUG } = require('../constants/projectionSlugs');
6
- const {
7
- RATIONALE_EXT,
8
- OUTCOME_NOTES_EXT,
9
- OUTCOME_EXT,
10
- RECIPIENT_KIND_EXT,
11
- RECIPIENT_REF_EXT,
12
- } = require('../resources/interventionExtensions');
13
- const { PROJECTOR_NAME } = require('../projections/interventionProjection');
14
- const { getFhirStore } = require('../stores/fhirStore');
15
- const { Intervention } = require('../../shared/dtos/Intervention');
16
- const { logger } = require('../../utils/logger');
17
- const { project, storeResources } = require('./fhirService');
18
-
19
- const VARIANT_TYPES = new Set(['Procedure', 'Communication', 'Task', 'CommunicationRequest']);
20
-
21
- async function storeIntervention({ patientId, intervention }) {
22
- if (intervention.patientId !== patientId) {
23
- throw new Error(`storeIntervention patientId mismatch: expected ${patientId}`);
24
- }
25
- const bundle = project([{ name: PROJECTOR_NAME, aggregate: intervention }]);
26
- const resources = bundle.entry.map((entry) => entry.resource);
27
- const { stored } = await storeResources(resources);
28
- return { count: stored.length, ids: stored };
29
- }
30
-
31
- async function readInterventions({ patientId, caseId = null }) {
32
- const store = getFhirStore();
33
- const patient = `Patient/${fhirId(patientId)}`;
34
- const interventionSystem = identifierSystem(INTERVENTION_SLUG);
35
- const resources = (await store.find({ patient }))
36
- .filter((resource) => VARIANT_TYPES.has(resource.resourceType)
37
- && (resource.identifier || []).some((id) => id.system === interventionSystem));
38
- const interventions = [];
39
- for (const resource of resources) {
40
- if (!identifierValue(resource, INTERVENTION_SLUG)) continue;
41
- const reconstructed = reconstructIntervention(resource, patientId);
42
- if (!reconstructed.caseId) continue;
43
- if (caseId && reconstructed.caseId !== caseId) continue;
44
- try {
45
- interventions.push(new Intervention(reconstructed));
46
- } catch (error) {
47
- if (error instanceof ZodError) {
48
- logger.warn({ interventionId: reconstructed.interventionId }, 'skipping malformed intervention resource');
49
- continue;
50
- }
51
- throw error;
52
- }
53
- }
54
- return interventions;
55
- }
56
-
57
- function reconstructIntervention(resource, patientId) {
58
- const reasonRef = resource.reason && resource.reason[0] && resource.reason[0].reference;
59
- const base = {
60
- interventionId: identifierValue(resource, INTERVENTION_SLUG),
61
- patientId,
62
- caseId: refToId(reasonRef && reasonRef.reference),
63
- rationale: extValues(resource, RATIONALE_EXT)[0] ?? null,
64
- outcomeNotes: extValues(resource, OUTCOME_NOTES_EXT)[0] ?? null,
65
- outcomeExt: extValues(resource, OUTCOME_EXT)[0] ?? null,
66
- recipientKind: 'patient',
67
- recipientRefId: null,
68
- };
69
- if (resource.resourceType === 'Procedure') return reconstructProcedure(resource, base);
70
- if (resource.resourceType === 'Communication') return reconstructCommunication(resource, base);
71
- if (resource.resourceType === 'Task') return reconstructTask(resource, base);
72
- return reconstructCommunicationRequest(resource, base);
73
- }
74
-
75
- function reconstructProcedure(resource, base) {
76
- const { outcomeExt, ...dto } = base;
77
- return {
78
- ...dto,
79
- kind: categoryCode(resource),
80
- interventionLabel: (resource.code && resource.code.text) || null,
81
- deliveredAt: resource.occurrenceDateTime,
82
- outcome: outcomeExt ?? codeableConceptCode(resource.outcome),
83
- ...recipientFromExtension(resource),
84
- };
85
- }
86
-
87
- function reconstructCommunication(resource, base) {
88
- const { outcomeExt, ...dto } = base;
89
- const content = resource.payload && resource.payload[0] && resource.payload[0].contentCodeableConcept;
90
- const outcome = outcomeExt ?? undefined;
91
- return {
92
- ...dto,
93
- kind: categoryCode(resource),
94
- interventionLabel: (content && content.text) || null,
95
- deliveredAt: resource.sent,
96
- ...(outcome ? { outcome } : {}),
97
- ...recipientFromReference(resource),
98
- };
99
- }
100
-
101
- function reconstructTask(resource, base) {
102
- const { outcomeExt, ...dto } = base;
103
- const output = resource.output && resource.output[0];
104
- return {
105
- ...dto,
106
- kind: codingCode(resource.code),
107
- interventionLabel: (resource.code && resource.code.text) || null,
108
- deliveredAt: resource.authoredOn,
109
- outcome: outcomeExt ?? (output ? codeableConceptCode(output.valueCodeableConcept) : 'pending'),
110
- ...recipientFromExtension(resource),
111
- };
112
- }
113
-
114
- function reconstructCommunicationRequest(resource, base) {
115
- const { outcomeExt, ...dto } = base;
116
- const content = resource.payload && resource.payload[0] && resource.payload[0].contentCodeableConcept;
117
- return {
118
- ...dto,
119
- kind: categoryCode(resource),
120
- interventionLabel: (content && content.text) || null,
121
- deliveredAt: resource.occurrenceDateTime,
122
- outcome: outcomeExt ?? (resource.status === 'active' ? 'pending' : 'unknown'),
123
- ...recipientFromReference(resource),
124
- };
125
- }
126
-
127
- function recipientFromExtension(resource) {
128
- const kind = extValues(resource, RECIPIENT_KIND_EXT)[0];
129
- const refId = extValues(resource, RECIPIENT_REF_EXT)[0];
130
- if (kind === 'related_person' && refId) return { recipientKind: 'related_person', recipientRefId: refId };
131
- return { recipientKind: 'patient', recipientRefId: null };
132
- }
133
-
134
- function recipientFromReference(resource) {
135
- const recipientRef = resource.recipient && resource.recipient[0] && resource.recipient[0].reference;
136
- if (recipientRef && recipientRef.startsWith('RelatedPerson/')) {
137
- return { recipientKind: 'related_person', recipientRefId: refToId(recipientRef) };
138
- }
139
- return { recipientKind: 'patient', recipientRefId: null };
140
- }
141
-
142
- const codingCode = (concept) => (concept && concept.coding && concept.coding[0] && concept.coding[0].code) || null;
143
- const categoryCode = (resource) => codingCode(resource.category && resource.category[0]);
144
- const codeableConceptCode = (concept) => codingCode(concept) || (concept && concept.text) || 'pending';
145
-
146
- module.exports = {
147
- storeIntervention,
148
- readInterventions,
149
- };
@@ -1,52 +0,0 @@
1
- const { Observation } = require('../resources/Observation');
2
- const { Provenance } = require('../resources/Provenance');
3
- const { RESULT_SLUG } = require('../constants/projectionSlugs');
4
- const { storeResources } = require('./fhirService');
5
-
6
- function resolveEffective(raw, now) {
7
- if (typeof raw === 'string' && raw.trim()) {
8
- const parsed = new Date(raw);
9
- if (!Number.isNaN(parsed.getTime())) return parsed.toISOString();
10
- }
11
- return now;
12
- }
13
-
14
- async function storeResultObservations({ patientId, turnId, observations, idBase, now = new Date().toISOString() }) {
15
- const pid = typeof patientId === 'string' ? patientId.trim() : '';
16
- const tid = typeof turnId === 'string' ? turnId.trim() : '';
17
- if (!pid || !tid) throw new Error('storeResultObservations requires non-empty patientId and turnId');
18
- const base = typeof idBase === 'string' && idBase.trim() ? idBase.trim() : tid;
19
-
20
- const ids = [];
21
- const resources = [];
22
- (observations || []).forEach((obs, index) => {
23
- if (!obs || typeof obs !== 'object') return;
24
- const code = (obs.code || '').trim();
25
- if (!code) return;
26
- const obsId = `rrobs_${base}_${index}`;
27
- ids.push(obsId);
28
- resources.push(Observation.fromResultObservation({
29
- patientId: pid,
30
- obsId,
31
- code,
32
- value: obs.value,
33
- unit: obs.unit,
34
- category: obs.category,
35
- effective: resolveEffective(obs.effective, now),
36
- now,
37
- slug: RESULT_SLUG,
38
- }));
39
- });
40
- if (!ids.length) return { count: 0, ids: [] };
41
-
42
- resources.push(Provenance.fromResultObservations({
43
- targetIds: ids,
44
- provenanceId: `rrprov_${base}`,
45
- activityText: 'patient-reported result recording',
46
- now,
47
- }));
48
- await storeResources(resources);
49
- return { count: ids.length, ids };
50
- }
51
-
52
- module.exports = { storeResultObservations };