@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,101 +0,0 @@
1
- const { ZodError } = require('zod');
2
-
3
- const { fhirId } = require('../helpers/fhirHelper');
4
- const { identifierValue, extValues } = require('../helpers/fhirReadHelper');
5
- const { PATIENT_RISK_SLUG } = require('../constants/projectionSlugs');
6
- const { PROJECTOR_NAME } = require('../projections/riskProjection');
7
- const {
8
- PREDICTION_KIND_AE,
9
- PREDICTION_KIND_DISEASE,
10
- PREDICTION_KIND_COMORBIDITY,
11
- } = require('../resources/RiskAssessment');
12
- const { getFhirStore } = require('../stores/fhirStore');
13
- const { PatientRiskAssessment } = require('../../shared/dtos/PatientRiskAssessment');
14
- const { logger } = require('../../utils/logger');
15
- const { project, storeResources } = require('./fhirService');
16
-
17
- async function storePatientRisk({ patientId, riskAssessment }) {
18
- if (riskAssessment.patientId !== patientId) {
19
- throw new Error(`storePatientRisk patientId mismatch: expected ${patientId}`);
20
- }
21
- const bundle = project([{ name: PROJECTOR_NAME, aggregate: riskAssessment }]);
22
- const resources = bundle.entry.map((entry) => entry.resource);
23
- const { stored } = await storeResources(resources);
24
- return { count: stored.length, ids: stored };
25
- }
26
-
27
- async function readPatientRisk({ patientId }) {
28
- const store = getFhirStore();
29
- const patient = `Patient/${fhirId(patientId)}`;
30
- const resources = (await store.find({ patient, resourceType: 'RiskAssessment' }))
31
- .filter((resource) => identifierValue(resource, PATIENT_RISK_SLUG));
32
- for (const resource of resources) {
33
- try {
34
- return new PatientRiskAssessment(reconstructRisk(resource, patientId));
35
- } catch (error) {
36
- if (error instanceof ZodError) {
37
- logger.warn({ resourceId: resource.id }, 'skipping malformed patient-risk RiskAssessment');
38
- continue;
39
- }
40
- throw error;
41
- }
42
- }
43
- return null;
44
- }
45
-
46
- function reconstructRisk(resource, patientId) {
47
- const predictions = resource.prediction || [];
48
- return {
49
- schemaVersion: '1',
50
- assessmentId: identifierValue(resource, PATIENT_RISK_SLUG),
51
- patientId,
52
- status: resource.status,
53
- sourceRegimen: extValues(resource, 'assessed-source-regimen')[0] || null,
54
- sourceDiagnosis: extValues(resource, 'assessed-source-diagnosis')[0] || null,
55
- sourcePhase: extValues(resource, 'assessed-source-phase')[0] || null,
56
- predictedAes: predictions.filter((prediction) => predictionKind(prediction) === PREDICTION_KIND_AE).map(reconstructAe),
57
- diseaseRisks: predictions.filter((prediction) => predictionKind(prediction) === PREDICTION_KIND_DISEASE).map(reconstructDisease),
58
- comorbidityAmplifiers: predictions.filter((prediction) => predictionKind(prediction) === PREDICTION_KIND_COMORBIDITY).map(reconstructComorbidity),
59
- assessedAt: resource.occurrenceDateTime,
60
- assessedByModel: extValues(resource, 'assessed-by-model')[0] || null,
61
- };
62
- }
63
-
64
- function reconstructAe(prediction) {
65
- return {
66
- aeName: outcomeText(prediction),
67
- severityIfConfirmed:
68
- (prediction.qualitativeRisk
69
- && prediction.qualitativeRisk.coding
70
- && prediction.qualitativeRisk.coding[0]
71
- && prediction.qualitativeRisk.coding[0].code) || null,
72
- minimumSymptomSet: extValues(prediction, 'risk-minimum-symptom-set'),
73
- why: prediction.rationale || null,
74
- };
75
- }
76
-
77
- function reconstructDisease(prediction) {
78
- return {
79
- bodySite: outcomeText(prediction),
80
- complications: extValues(prediction, 'risk-disease-complication'),
81
- warningSymptoms: extValues(prediction, 'risk-disease-warning-symptom'),
82
- amplifierComorbidities: extValues(prediction, 'risk-disease-amplifier'),
83
- };
84
- }
85
-
86
- function reconstructComorbidity(prediction) {
87
- return {
88
- comorbidity: outcomeText(prediction),
89
- interaction: prediction.rationale,
90
- clinicalNote: extValues(prediction, 'risk-comorbidity-clinical-note')[0] || null,
91
- };
92
- }
93
-
94
- const predictionKind = (prediction) => extValues(prediction, 'risk-prediction-kind')[0] || null;
95
-
96
- const outcomeText = (prediction) => (prediction.outcome && prediction.outcome.text) || null;
97
-
98
- module.exports = {
99
- storePatientRisk,
100
- readPatientRisk,
101
- };
@@ -1,147 +0,0 @@
1
- const { ZodError } = require('zod');
2
-
3
- const { fhirId } = require('../helpers/fhirHelper');
4
- const { extMap, extValues, identifierValue, refToId } = require('../helpers/fhirReadHelper');
5
- const { ROUTING_ASSESSMENT_SLUG, ROUTING_DISPOSITION_SLUG } = require('../constants/projectionSlugs');
6
- const { PROJECTOR_NAME } = require('../projections/routingProjection');
7
- const { getFhirStore } = require('../stores/fhirStore');
8
- const { RoutingDecision } = require('../../shared/dtos/RoutingDecision');
9
- const { logger } = require('../../utils/logger');
10
- const { project, storeResources } = require('./fhirService');
11
-
12
- const DISPOSITION_KIND_BY_RESOURCE = {
13
- Communication: 'communication',
14
- CommunicationRequest: 'communication_request',
15
- Task: 'task',
16
- RequestOrchestration: 'orchestration',
17
- CarePlan: 'care_plan',
18
- };
19
-
20
- async function storeRoutingDecision({ patientId, routingDecision }) {
21
- if (routingDecision.patientId !== patientId) {
22
- throw new Error(`storeRoutingDecision patientId mismatch: expected ${patientId}`);
23
- }
24
- const bundle = project([{ name: PROJECTOR_NAME, aggregate: routingDecision }]);
25
- const resources = bundle.entry.map((entry) => entry.resource);
26
- const { stored } = await storeResources(resources);
27
- return { count: stored.length, ids: stored };
28
- }
29
-
30
- async function readRoutingByTurn({ patientId, turnId }) {
31
- const store = getFhirStore();
32
- const patient = `Patient/${fhirId(patientId)}`;
33
- const impressions = (await store.find({ patient, resourceType: 'ClinicalImpression' }))
34
- .filter((resource) => identifierValue(resource, ROUTING_ASSESSMENT_SLUG));
35
- const assessment = impressions.find((impression) => extMap(impression)['routing-turn-id'] === turnId);
36
- if (!assessment) return null;
37
- const provenance = await store.getByReference(`Provenance/${fhirId(`${assessment.id}-prov`)}`);
38
- if (!provenance) return null;
39
- const disposition = await findDisposition(store, assessment, provenance);
40
- if (!disposition) return null;
41
- try {
42
- return new RoutingDecision(reconstructRouting(assessment, disposition, provenance, patientId, turnId));
43
- } catch (error) {
44
- if (error instanceof ZodError) {
45
- logger.warn({ assessmentId: assessment.id }, 'skipping malformed routing ClinicalImpression');
46
- return null;
47
- }
48
- throw error;
49
- }
50
- }
51
-
52
- async function findDisposition(store, assessment, provenance) {
53
- const assessmentRef = `ClinicalImpression/${fhirId(assessment.id)}`;
54
- const dispositionRef = (provenance.target || []).find((target) => target.reference !== assessmentRef);
55
- if (!dispositionRef) return null;
56
- const resource = await store.getByReference(dispositionRef.reference);
57
- return resource && identifierValue(resource, ROUTING_DISPOSITION_SLUG) ? resource : null;
58
- }
59
-
60
- function reconstructRouting(assessment, disposition, provenance, patientId, turnId) {
61
- const ext = extMap(assessment);
62
- const agent = (provenance.agent || [])[0] || {};
63
- return {
64
- schemaVersion: '1',
65
- decisionId: ext['routing-decision-id'],
66
- patientId,
67
- turnId,
68
- committedAt: ext['routing-committed-at'],
69
- assessment: reconstructAssessment(assessment, patientId, turnId),
70
- disposition: reconstructDisposition(disposition, assessment, patientId),
71
- provenance: {
72
- agentDevice: refToId(agent.who && agent.who.reference),
73
- onBehalfOf: refToId(agent.onBehalfOf && agent.onBehalfOf.reference),
74
- recordedAt: provenance.recorded,
75
- },
76
- };
77
- }
78
-
79
- function reconstructAssessment(resource, patientId, turnId) {
80
- return {
81
- assessmentId: identifierValue(resource, ROUTING_ASSESSMENT_SLUG),
82
- patientId,
83
- turnId,
84
- performedAt: resource.effectiveDateTime,
85
- previousId: resource.previous ? refToId(resource.previous.reference) : null,
86
- summary: resource.summary || '',
87
- notes: (resource.note || []).map((note) => note.text),
88
- findings: (resource.finding || []).map(reconstructFinding),
89
- };
90
- }
91
-
92
- function reconstructFinding(finding) {
93
- const ext = extMap(finding);
94
- return {
95
- caseId: ext['routing-finding-case-id'] || refToId(finding.item && finding.item.reference),
96
- clinicalReasoning: ext['routing-finding-reasoning'] || '',
97
- decisionAction: ext['routing-finding-action'] || '',
98
- };
99
- }
100
-
101
- function reconstructDisposition(resource, assessment, patientId) {
102
- return {
103
- dispositionId: identifierValue(resource, ROUTING_DISPOSITION_SLUG),
104
- patientId,
105
- kind: DISPOSITION_KIND_BY_RESOURCE[resource.resourceType],
106
- primaryAction: dispositionAction(resource),
107
- primaryReason: dispositionReason(resource),
108
- primaryCaseId: extMap(assessment)['routing-primary-case-id'] || null,
109
- secondaryActions: (resource.action || []).slice(1).map(reconstructSecondary),
110
- deferredItems: reconstructDeferredItems(assessment),
111
- authoredAt: resource.sent || resource.authoredOn || resource.created,
112
- };
113
- }
114
-
115
- function reconstructDeferredItems(assessment) {
116
- const items = extValues(assessment, 'routing-deferred-item');
117
- const reasons = extValues(assessment, 'routing-deferred-reason');
118
- return items.map((item, i) => ({ item, deferReason: reasons[i] }));
119
- }
120
-
121
- function reconstructSecondary(action) {
122
- const description = action.description || '';
123
- return {
124
- action: action.title,
125
- composeAs: description.startsWith('compose_as: ') ? description.slice('compose_as: '.length) : description,
126
- };
127
- }
128
-
129
- function dispositionAction(resource) {
130
- if (resource.resourceType === 'Task') return ccText(resource.code);
131
- if (resource.resourceType === 'RequestOrchestration') return (resource.action || [])[0] && resource.action[0].title;
132
- return ccText((resource.category || [])[0]);
133
- }
134
-
135
- function dispositionReason(resource) {
136
- if (resource.resourceType === 'Task') return resource.description;
137
- if (resource.resourceType === 'CarePlan') return resource.description;
138
- if (resource.resourceType === 'RequestOrchestration') return (resource.action || [])[0] && resource.action[0].description;
139
- return ccText((resource.payload || [])[0] && resource.payload[0].contentCodeableConcept);
140
- }
141
-
142
- const ccText = (concept) => (concept && concept.text) || null;
143
-
144
- module.exports = {
145
- storeRoutingDecision,
146
- readRoutingByTurn,
147
- };
@@ -1,28 +0,0 @@
1
- const { fhirId } = require('../helpers/fhirHelper');
2
- const { getFhirStore } = require('../stores/fhirStore');
3
-
4
- async function patientSnapshot(patientId, { resourceTypes = null } = {}) {
5
- if (!String(patientId ?? '').trim()) throw new Error('patientId is required');
6
- if (resourceTypes != null) {
7
- if (!Array.isArray(resourceTypes)) throw new Error('resourceTypes must be an array');
8
- if (resourceTypes.some((t) => typeof t !== 'string' || !t.trim())) throw new Error('resourceTypes must contain only non-empty strings');
9
- }
10
- const requestedTypes = resourceTypes ? [...new Set(resourceTypes.map((resourceType) => resourceType.trim()))] : null;
11
- const store = getFhirStore();
12
- const patient = `Patient/${fhirId(patientId)}`;
13
- const linkedTypes = requestedTypes ? requestedTypes.filter((resourceType) => resourceType !== 'Patient') : null;
14
- const resources = linkedTypes
15
- ? (await Promise.all(linkedTypes.map((resourceType) => store.find({ patient, resourceType })))).flat()
16
- : await store.find({ patient });
17
- if (!requestedTypes || requestedTypes.includes('Patient')) {
18
- const patientResource = await store.getByReference(patient);
19
- if (patientResource) resources.push(patientResource);
20
- }
21
- const byType = Object.create(null);
22
- for (const resource of resources) {
23
- (byType[resource.resourceType] = byType[resource.resourceType] || []).push(resource);
24
- }
25
- return byType;
26
- }
27
-
28
- module.exports = { patientSnapshot };
@@ -1,139 +0,0 @@
1
- const { ZodError } = require('zod');
2
-
3
- const { fhirId } = require('../helpers/fhirHelper');
4
- const { extMap, extValues, identifierValue } = require('../helpers/fhirReadHelper');
5
- const { SYMPTOM_CASE_SLUG, SYMPTOM_ASSESSMENT_SLUG } = require('../constants/projectionSlugs');
6
- const { PROJECTOR_NAME } = require('../projections/symptomCaseProjection');
7
- const { getFhirStore } = require('../stores/fhirStore');
8
- const { ManagedSymptom } = require('../../shared/dtos/ManagedSymptom');
9
- const { logger } = require('../../utils/logger');
10
- const { project, storeResources } = require('./fhirService');
11
-
12
- async function storeSymptomCase({ patientId, managedSymptom }) {
13
- if (managedSymptom.condition.patientId !== patientId) {
14
- throw new Error(`storeSymptomCase patientId mismatch: expected ${patientId}`);
15
- }
16
- const bundle = project([{ name: PROJECTOR_NAME, aggregate: managedSymptom }]);
17
- const resources = bundle.entry.map((entry) => entry.resource);
18
- const { stored } = await storeResources(resources);
19
- return { count: stored.length, ids: stored };
20
- }
21
-
22
- async function readSymptomCases({ patientId, ctcaeTerm = null, status = null }) {
23
- const store = getFhirStore();
24
- const patient = `Patient/${fhirId(patientId)}`;
25
- const conditions = (await store.find({ patient, resourceType: 'Condition', symptomTerm: ctcaeTerm }))
26
- .filter((condition) => 'symptom-case-status' in extMap(condition) && identifierValue(condition, SYMPTOM_CASE_SLUG));
27
- const cases = [];
28
- for (const condition of conditions) {
29
- let managed;
30
- try {
31
- managed = new ManagedSymptom(await reconstructSymptomCase(condition, patientId, store));
32
- } catch (error) {
33
- if (error instanceof ZodError) {
34
- logger.warn({ conditionId: condition.id }, 'skipping malformed symptom-case Condition');
35
- continue;
36
- }
37
- throw error;
38
- }
39
- if (status && managed.condition.clinicalStatus !== status) continue;
40
- cases.push(managed);
41
- }
42
- return cases;
43
- }
44
-
45
- async function reconstructSymptomCase(condition, patientId, store) {
46
- const conditionId = identifierValue(condition, SYMPTOM_CASE_SLUG);
47
- const chain = await readAssessmentChain(condition, { patientId, conditionId }, store);
48
- const [latestAssessment, ...priorAssessments] = chain;
49
- return {
50
- condition: reconstructCondition(condition, patientId, conditionId),
51
- latestAssessment,
52
- priorAssessments,
53
- };
54
- }
55
-
56
- function reconstructCondition(condition, patientId, conditionId) {
57
- const ext = extMap(condition);
58
- const code = condition.code || {};
59
- const coding = (code.coding && code.coding[0]) || {};
60
- return {
61
- conditionId,
62
- patientId,
63
- ctcaeTerm: coding.code || code.text || null,
64
- clinicalStatus: ext['symptom-case-status'],
65
- onsetAt: condition.onsetDateTime,
66
- abatementAt: condition.abatementDateTime || ext['legacy-abatement'] || null,
67
- closeReason: ext['symptom-close-reason'] || null,
68
- temporality: ext['symptom-temporality'] || null,
69
- evidenceEpisodeIds: extValues(condition, 'symptom-evidence-episode-ids'),
70
- currentEpisodeId: ext['symptom-current-episode-id'] || null,
71
- evidenceMentionIds: extValues(condition, 'symptom-evidence-mention-ids'),
72
- lastTransitionReason: (condition.note && condition.note[0] && condition.note[0].text) || null,
73
- recurrenceCount: ext['symptom-recurrence-count'] ?? 0,
74
- reactivatedAt: ext['symptom-reactivated-at'] || null,
75
- };
76
- }
77
-
78
- async function readAssessmentChain(condition, ids, store) {
79
- const headRef = condition.stage && condition.stage[0] && condition.stage[0].assessment
80
- && condition.stage[0].assessment[0] && condition.stage[0].assessment[0].reference;
81
- const impressions = [];
82
- const seen = new Set();
83
- let ref = headRef;
84
- while (ref && !seen.has(ref)) {
85
- seen.add(ref);
86
- const impression = await store.getByReference(ref);
87
- if (!impression) break;
88
- impressions.push(impression);
89
- ref = impression.previous && impression.previous.reference;
90
- }
91
- const chain = [];
92
- for (let i = 0; i < impressions.length; i++) {
93
- const previousId = impressions[i + 1] ? identifierValue(impressions[i + 1], SYMPTOM_ASSESSMENT_SLUG) : null;
94
- chain.push(await reconstructAssessment(impressions[i], { ...ids, previousId }, store));
95
- }
96
- return chain;
97
- }
98
-
99
- async function reconstructAssessment(impression, { patientId, conditionId, previousId }, store) {
100
- const ext = extMap(impression);
101
- const findingRef = impression.finding && impression.finding[0] && impression.finding[0].item
102
- && impression.finding[0].item.reference && impression.finding[0].item.reference.reference;
103
- const gradeObs = findingRef ? await store.getByReference(findingRef) : null;
104
- const gradeExt = extMap(gradeObs || {});
105
- return {
106
- assessmentId: identifierValue(impression, SYMPTOM_ASSESSMENT_SLUG),
107
- conditionId,
108
- patientId,
109
- performedAt: impression.effectiveDateTime,
110
- previousId,
111
- grade: reconstructGrade(gradeObs),
112
- trajectory: ext.trajectory || null,
113
- decisionPosture: ext['decision-posture'] || null,
114
- gradeVerification: gradeExt['grade-verification'] || 'provisional',
115
- reasoning: ext['assessment-reasoning'] || null,
116
- postureReason: ext['posture-reason'] || null,
117
- problemRepresentation: impression.summary || null,
118
- gradeThresholdClause: gradeExt['grade-threshold-clause'] || null,
119
- };
120
- }
121
-
122
- function reconstructGrade(obs) {
123
- const grade = { bestEstimate: null, confidence: null, possibleRange: null, reasoning: '' };
124
- if (!obs) return grade;
125
- const ext = extMap(obs);
126
- if (typeof obs.valueInteger === 'number') grade.bestEstimate = obs.valueInteger;
127
- if (ext['grade-confidence']) grade.confidence = ext['grade-confidence'];
128
- if (ext['grade-range']) {
129
- const [lo, hi] = ext['grade-range'].split('-').map((n) => Number.parseInt(n, 10));
130
- if (Number.isInteger(lo) && Number.isInteger(hi)) grade.possibleRange = [lo, hi];
131
- }
132
- if (ext['grade-reasoning']) grade.reasoning = ext['grade-reasoning'];
133
- return grade;
134
- }
135
-
136
- module.exports = {
137
- storeSymptomCase,
138
- readSymptomCases,
139
- };
@@ -1,58 +0,0 @@
1
- const { PROCTCAE_ITEMS_TABLE, PROCTCAE_COMPOSITE_TABLE } = require('../config/fhirTablesConfig');
2
- const { buildCatalog } = require('../helpers/terminologyHelper');
3
-
4
- const { Ctcae_ID } = require('../../config/airtableConfig');
5
- const { getRecordByFilter } = require('../../services/airtableService');
6
- const { logger } = require('../../utils/logger');
7
-
8
- let catalogPromise = null;
9
- let loadedCompositeTableHash = null;
10
-
11
- async function getCatalog() {
12
- if (!catalogPromise) {
13
- catalogPromise = loadCatalog().catch((error) => {
14
- catalogPromise = null;
15
- throw error;
16
- });
17
- }
18
- return catalogPromise;
19
- }
20
-
21
- function clearCache() {
22
- catalogPromise = null;
23
- }
24
-
25
- async function loadCatalog() {
26
- const [items, composites] = await Promise.all([
27
- getRecordByFilter(Ctcae_ID, PROCTCAE_ITEMS_TABLE, 'TRUE()', null),
28
- getRecordByFilter(Ctcae_ID, PROCTCAE_COMPOSITE_TABLE, 'TRUE()', null),
29
- ]);
30
- if (!items) throw new Error('proctcae terminology tables not readable from Airtable');
31
- if (!composites) logger.warn('[terminologyService] composite table not readable; composite grading disabled');
32
-
33
- const source = `Airtable ${Ctcae_ID} (${PROCTCAE_ITEMS_TABLE})`;
34
- const catalog = buildCatalog(items, composites || [], source);
35
- if (!catalog._meta.n_symptoms) throw new Error('proctcae terminology catalog is empty');
36
-
37
- const compositeTableHash = catalog._meta.composite_table_hash ?? null;
38
- if (loadedCompositeTableHash && compositeTableHash && loadedCompositeTableHash !== compositeTableHash) {
39
- logger.warn('[terminologyService] composite grading table changed — grades derived after this differ from grades before it', {
40
- from: loadedCompositeTableHash,
41
- to: compositeTableHash,
42
- });
43
- }
44
- loadedCompositeTableHash = compositeTableHash;
45
-
46
- logger.info('[terminologyService] catalog loaded', {
47
- source,
48
- symptoms: catalog._meta.n_symptoms,
49
- items: catalog._meta.n_items,
50
- mappedItems: catalog._meta.n_mapped_items,
51
- composites: catalog._meta.n_composites,
52
- compositeTableHash,
53
- });
54
-
55
- return catalog;
56
- }
57
-
58
- module.exports = { getCatalog, clearCache };
@@ -1,57 +0,0 @@
1
- const { gradeComposites } = require('../helpers/compositeHelper');
2
- const { GradedSymptom } = require('../../shared/dtos/GradedSymptom');
3
- const { TriageResponse } = require('../../shared/dtos/TriageResponse');
4
- const {
5
- projectProctcaeCanonical,
6
- projectTriagePatient,
7
- projectProctcaeSymptomObservation,
8
- projectTriageIngestDevice,
9
- projectTriageQuestionnaireResponse,
10
- projectTriageSymptomCondition,
11
- } = require('../projections/proctcaeProjection');
12
- const { storeResources } = require('./fhirService');
13
- const { getCatalog: loadTerminology } = require('./terminologyService');
14
-
15
- const FOLLOW_UP_GRADE = 1;
16
-
17
- async function storeTriage(payload) {
18
- const input = new TriageResponse(payload);
19
- const catalog = await loadTerminology();
20
- const graded = gradeComposites(catalog, input).map((symptom) => new GradedSymptom(symptom));
21
- const resources = [
22
- projectProctcaeCanonical(catalog),
23
- projectTriagePatient(input.patientId),
24
- projectTriageIngestDevice(catalog),
25
- projectTriageQuestionnaireResponse(catalog, input),
26
- ...graded.map((sym) => projectProctcaeSymptomObservation(catalog, sym)),
27
- ];
28
- const conditions = graded
29
- .filter((sym) => sym.grade >= FOLLOW_UP_GRADE)
30
- .map((sym) => projectTriageSymptomCondition(catalog, sym));
31
- const { stored } = await storeResources(resources);
32
- const { stored: storedConditions } = await storeResources(conditions, { merge: mergeTriageSymptomCondition });
33
- return { stored: [...stored, ...storedConditions] };
34
- }
35
-
36
- function mergeTriageSymptomCondition(existing, next) {
37
- if (!existing) return next;
38
- const merged = { ...next };
39
- const onset = earlierOnset(existing.onsetDateTime, next.onsetDateTime);
40
- if (onset) merged.onsetDateTime = onset;
41
- const seen = new Set();
42
- merged.evidence = [...(existing.evidence || []), ...(next.evidence || [])].filter((entry) => {
43
- const ref = entry.reference && entry.reference.reference;
44
- if (!ref || seen.has(ref)) return false;
45
- seen.add(ref);
46
- return true;
47
- });
48
- return merged;
49
- }
50
-
51
- function earlierOnset(existing, next) {
52
- if (!existing) return next;
53
- if (!next) return existing;
54
- return Date.parse(next) < Date.parse(existing) ? next : existing;
55
- }
56
-
57
- module.exports = { storeTriage };
@@ -1,33 +0,0 @@
1
- const lookupValue = (value) => {
2
- const first = Array.isArray(value) ? value[0] : value;
3
- return first === undefined || first === '' ? null : first;
4
- };
5
-
6
- const integerId = (value) => {
7
- if (value == null || String(value).trim() === '') return null;
8
- const id = Number(value);
9
- return Number.isFinite(id) ? Math.trunc(id) : null;
10
- };
11
-
12
- const parseList = (value) =>
13
- String(value || '')
14
- .split(/[;|]/)
15
- .map((entry) => entry.trim())
16
- .filter(Boolean);
17
-
18
- function termKey(name) {
19
- return String(name || '')
20
- .trim()
21
- .toLowerCase()
22
- .replace(/&/g, 'and')
23
- .replace(/[/-]/g, ' ')
24
- .replace(/[^a-z0-9]+/g, '_')
25
- .replace(/^_+|_+$/g, '');
26
- }
27
-
28
- module.exports = {
29
- lookupValue,
30
- integerId,
31
- parseList,
32
- termKey,
33
- };
@@ -1,23 +0,0 @@
1
- const { z } = require('zod');
2
-
3
- const { BaseDto } = require('./BaseDto');
4
-
5
- const dateTime = z.iso.datetime({ offset: true });
6
-
7
- const schema = z.strictObject({
8
- schemaVersion: z.string().default('1'),
9
- symptomId: z.string(),
10
- patientId: z.string(),
11
- term: z.string(),
12
- grade: z.number().int().min(0).max(4),
13
- effective: dateTime.nullable().default(null),
14
- triageId: z.string(),
15
- dimensionAnswers: z.record(z.string(), z.string()).default({}),
16
- source: z.string().default('triage'),
17
- });
18
-
19
- class GradedSymptom extends BaseDto {}
20
-
21
- GradedSymptom.schema = schema;
22
-
23
- module.exports = { GradedSymptom };
@@ -1,35 +0,0 @@
1
- const { z } = require('zod');
2
-
3
- const { BaseDto } = require('./BaseDto');
4
-
5
- const dateTime = z.iso.datetime({ offset: true });
6
-
7
- const painSchema = z.strictObject({
8
- hasPain: z.string().nullable().default(null),
9
- frequency: z.string().nullable().default(null),
10
- intensity: z.string().nullable().default(null),
11
- interference: z.string().nullable().default(null),
12
- locations: z.array(z.string()).default([]),
13
- mostImportant: z.string().nullable().default(null),
14
- otherText: z.string().nullable().default(null),
15
- });
16
-
17
- const schema = z.strictObject({
18
- schemaVersion: z.string().default('1'),
19
- triageId: z.string(),
20
- patientId: z.string(),
21
- authored: dateTime.nullable().default(null),
22
- completed: z.boolean().default(true),
23
- answers: z.record(z.string(), z.string().nullable()).default({}),
24
- questionnaireId: z.string().nullable().default(null),
25
- cadence: z.string().nullable().default(null),
26
- satisfactionLabel: z.string().nullable().default(null),
27
- satisfactionValue: z.number().int().default(-1),
28
- pain: painSchema.default(() => painSchema.parse({})),
29
- });
30
-
31
- class TriageResponse extends BaseDto {}
32
-
33
- TriageResponse.schema = schema;
34
-
35
- module.exports = { TriageResponse };