@peopl-health/nexus 5.11.0-dev.1120 → 5.11.0-dev.1122

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 (30) hide show
  1. package/lib/clinical/tools/analyzeSymptomPatternsTool.js +3 -6
  2. package/lib/clinical/tools/getActiveSymptomLandscapeTool.js +2 -2
  3. package/lib/clinical/tools/getPatientHistoryTool.js +1 -1
  4. package/lib/clinical/tools/openClusterHypothesisTool.js +3 -7
  5. package/lib/clinical/tools/openConditionTool.js +2 -2
  6. package/lib/clinical/tools/recordClinicalImpressionTool.js +1 -2
  7. package/lib/clinical/tools/recordInterventionTool.js +3 -5
  8. package/lib/clinical/tools/setContingencyPlanTool.js +3 -7
  9. package/lib/clinical/tools/submitSafetyGateTool.js +1 -5
  10. package/lib/clinical/tools/updateClusterHypothesisTool.js +4 -7
  11. package/lib/clinical/tools/updateConditionStatusTool.js +1 -2
  12. package/lib/fhir/constants/extensionSlugs.js +111 -0
  13. package/lib/fhir/resources/CarePlan.js +8 -7
  14. package/lib/fhir/resources/ClinicalImpression.js +39 -38
  15. package/lib/fhir/resources/CommunicationRequest.js +5 -4
  16. package/lib/fhir/resources/Condition.js +10 -9
  17. package/lib/fhir/resources/Observation.js +7 -6
  18. package/lib/fhir/resources/RiskAssessment.js +13 -12
  19. package/lib/fhir/resources/Task.js +4 -3
  20. package/lib/fhir/services/clusterService.js +9 -8
  21. package/lib/fhir/services/contingencyService.js +14 -11
  22. package/lib/fhir/services/palliativeService.js +23 -22
  23. package/lib/fhir/services/riskService.js +11 -10
  24. package/lib/fhir/services/routingService.js +10 -9
  25. package/lib/fhir/services/symptomCaseService.js +21 -20
  26. package/lib/shared/dtos/ClusterImpression.js +16 -5
  27. package/lib/shared/dtos/ContingencySafetyNet.js +3 -1
  28. package/lib/shared/dtos/Intervention.js +10 -3
  29. package/lib/shared/dtos/ManagedSymptom.js +15 -5
  30. package/package.json +1 -1
@@ -2,6 +2,7 @@ const { getSystems } = require('../config/fhirConfig');
2
2
  const { fhirId } = require('../helpers/fhirHelper');
3
3
  const { identifier, reference, patientReference, codeableConcept, extension, toIso } = require('../helpers/elementHelper');
4
4
  const { SYMPTOM_GRADE_SLUG } = require('../constants/projectionSlugs');
5
+ const { GRADE_EXT } = require('../constants/extensionSlugs');
5
6
 
6
7
  const OBSERVED_FIELD_SYSTEM_KEY = 'observedField';
7
8
 
@@ -61,7 +62,7 @@ class Observation {
61
62
  valueCodeableConcept: gradeConcept,
62
63
  method,
63
64
  extension: [
64
- extension('grade-verification', { valueCode: 'provisional' }),
65
+ extension(GRADE_EXT.VERIFICATION, { valueCode: 'provisional' }),
65
66
  extension('grade-derivation', { valueCode: 'pro-ctcae-composite' }),
66
67
  extension('symptom-source', { valueString: String(sym.source || 'triage') }),
67
68
  ],
@@ -77,11 +78,11 @@ class Observation {
77
78
  const grade = assessment.grade;
78
79
  const gradeId = `${assessment.assessmentId}-grade`;
79
80
  const extensions = [];
80
- if (grade.confidence !== null) extensions.push(extension('grade-confidence', { valueString: grade.confidence }));
81
- if (grade.possibleRange !== null) extensions.push(extension('grade-range', { valueString: `${grade.possibleRange[0]}-${grade.possibleRange[1]}` }));
82
- if (grade.reasoning) extensions.push(extension('grade-reasoning', { valueString: grade.reasoning }));
83
- if (assessment.gradeVerification) extensions.push(extension('grade-verification', { valueString: assessment.gradeVerification }));
84
- if (assessment.gradeThresholdClause) extensions.push(extension('grade-threshold-clause', { valueString: assessment.gradeThresholdClause }));
81
+ if (grade.confidence !== null) extensions.push(extension(GRADE_EXT.CONFIDENCE, { valueString: grade.confidence }));
82
+ if (grade.possibleRange !== null) extensions.push(extension(GRADE_EXT.RANGE, { valueString: `${grade.possibleRange[0]}-${grade.possibleRange[1]}` }));
83
+ if (grade.reasoning) extensions.push(extension(GRADE_EXT.REASONING, { valueString: grade.reasoning }));
84
+ if (assessment.gradeVerification) extensions.push(extension(GRADE_EXT.VERIFICATION, { valueString: assessment.gradeVerification }));
85
+ if (assessment.gradeThresholdClause) extensions.push(extension(GRADE_EXT.THRESHOLD_CLAUSE, { valueString: assessment.gradeThresholdClause }));
85
86
  const payload = {
86
87
  resourceType: 'Observation',
87
88
  id: fhirId(gradeId),
@@ -9,6 +9,7 @@ const {
9
9
  toIso,
10
10
  } = require('../helpers/elementHelper');
11
11
  const { PATIENT_RISK_SLUG, SAFETY_GATE_SLUG } = require('../constants/projectionSlugs');
12
+ const { RISK_EXT } = require('../constants/extensionSlugs');
12
13
 
13
14
  const RISK_SEVERITY_CODESYSTEM = 'risk-severity';
14
15
  const PREDICTION_KIND_AE = 'ae';
@@ -39,11 +40,11 @@ function aePrediction(ae) {
39
40
  code: ae.severityIfConfirmed,
40
41
  system: codeSystemUrl(RISK_SEVERITY_CODESYSTEM),
41
42
  }),
42
- extension: [extension('risk-prediction-kind', { valueString: PREDICTION_KIND_AE })],
43
+ extension: [extension(RISK_EXT.PREDICTION_KIND, { valueString: PREDICTION_KIND_AE })],
43
44
  };
44
45
  if (ae.why) prediction.rationale = ae.why;
45
46
  for (const symptom of ae.minimumSymptomSet) {
46
- prediction.extension.push(extension('risk-minimum-symptom-set', { valueString: symptom }));
47
+ prediction.extension.push(extension(RISK_EXT.MINIMUM_SYMPTOM_SET, { valueString: symptom }));
47
48
  }
48
49
  return prediction;
49
50
  }
@@ -51,16 +52,16 @@ function aePrediction(ae) {
51
52
  function diseasePrediction(risk) {
52
53
  const prediction = {
53
54
  outcome: codeableConcept(risk.bodySite),
54
- extension: [extension('risk-prediction-kind', { valueString: PREDICTION_KIND_DISEASE })],
55
+ extension: [extension(RISK_EXT.PREDICTION_KIND, { valueString: PREDICTION_KIND_DISEASE })],
55
56
  };
56
57
  for (const complication of risk.complications) {
57
- prediction.extension.push(extension('risk-disease-complication', { valueString: complication }));
58
+ prediction.extension.push(extension(RISK_EXT.DISEASE_COMPLICATION, { valueString: complication }));
58
59
  }
59
60
  for (const symptom of risk.warningSymptoms) {
60
- prediction.extension.push(extension('risk-disease-warning-symptom', { valueString: symptom }));
61
+ prediction.extension.push(extension(RISK_EXT.DISEASE_WARNING_SYMPTOM, { valueString: symptom }));
61
62
  }
62
63
  for (const comorbidity of risk.amplifierComorbidities) {
63
- prediction.extension.push(extension('risk-disease-amplifier', { valueString: comorbidity }));
64
+ prediction.extension.push(extension(RISK_EXT.DISEASE_AMPLIFIER, { valueString: comorbidity }));
64
65
  }
65
66
  return prediction;
66
67
  }
@@ -69,10 +70,10 @@ function comorbidityPrediction(amplifier) {
69
70
  const prediction = {
70
71
  outcome: codeableConcept(amplifier.comorbidity),
71
72
  rationale: amplifier.interaction,
72
- extension: [extension('risk-prediction-kind', { valueString: PREDICTION_KIND_COMORBIDITY })],
73
+ extension: [extension(RISK_EXT.PREDICTION_KIND, { valueString: PREDICTION_KIND_COMORBIDITY })],
73
74
  };
74
75
  if (amplifier.clinicalNote) {
75
- prediction.extension.push(extension('risk-comorbidity-clinical-note', { valueString: amplifier.clinicalNote }));
76
+ prediction.extension.push(extension(RISK_EXT.COMORBIDITY_CLINICAL_NOTE, { valueString: amplifier.clinicalNote }));
76
77
  }
77
78
  return prediction;
78
79
  }
@@ -99,10 +100,10 @@ class RiskAssessment {
99
100
  ];
100
101
  if (prediction.length) payload.prediction = prediction;
101
102
  const extensions = [];
102
- if (assessment.sourceRegimen) extensions.push(extension('assessed-source-regimen', { valueString: assessment.sourceRegimen }));
103
- if (assessment.sourceDiagnosis) extensions.push(extension('assessed-source-diagnosis', { valueString: assessment.sourceDiagnosis }));
104
- if (assessment.sourcePhase) extensions.push(extension('assessed-source-phase', { valueString: assessment.sourcePhase }));
105
- if (assessment.assessedByModel) extensions.push(extension('assessed-by-model', { valueString: assessment.assessedByModel }));
103
+ if (assessment.sourceRegimen) extensions.push(extension(RISK_EXT.SOURCE_REGIMEN, { valueString: assessment.sourceRegimen }));
104
+ if (assessment.sourceDiagnosis) extensions.push(extension(RISK_EXT.SOURCE_DIAGNOSIS, { valueString: assessment.sourceDiagnosis }));
105
+ if (assessment.sourcePhase) extensions.push(extension(RISK_EXT.SOURCE_PHASE, { valueString: assessment.sourcePhase }));
106
+ if (assessment.assessedByModel) extensions.push(extension(RISK_EXT.ASSESSED_BY_MODEL, { valueString: assessment.assessedByModel }));
106
107
  if (extensions.length) payload.extension = extensions;
107
108
  return new RiskAssessment(payload);
108
109
  }
@@ -20,6 +20,7 @@ const {
20
20
  PALLIATIVE_ESCALATION_SLUG,
21
21
  PALLIATIVE_CAREPLAN_REC_SLUG,
22
22
  } = require('../constants/projectionSlugs');
23
+ const { PALLIATIVE_EXT } = require('../constants/extensionSlugs');
23
24
  const { SEVERITY_TO_PRIORITY } = require('../constants/severityPriority');
24
25
  const { interventionExtensions } = require('./interventionExtensions');
25
26
 
@@ -133,7 +134,7 @@ class Task {
133
134
  authoredOn: toIso(committedAt),
134
135
  requester: reference('Device', deviceId),
135
136
  restriction: { period: { end: toIso(end) } },
136
- extension: [extension('palliative-escalation-rank', { valueInteger: finding.rank })],
137
+ extension: [extension(PALLIATIVE_EXT.ESCALATION_RANK, { valueInteger: finding.rank })],
137
138
  };
138
139
  if (escalation.escalationRoutes.length) {
139
140
  payload.requestedPerformer = escalation.escalationRoutes.map((route) => ({ concept: codeableConcept(route) }));
@@ -158,10 +159,10 @@ class Task {
158
159
  for: patientReference(patientId),
159
160
  authoredOn: toIso(committedAt),
160
161
  requester: reference('Device', deviceId),
161
- extension: [extension('palliative-careplan-rec-rank', { valueInteger: recommendation.rank })],
162
+ extension: [extension(PALLIATIVE_EXT.CAREPLAN_REC_RANK, { valueInteger: recommendation.rank })],
162
163
  };
163
164
  if (recommendation.otcOffer && recommendation.otcOffer.label) {
164
- payload.extension.push(extension('palliative-careplan-otc-offer', { valueString: JSON.stringify(recommendation.otcOffer) }));
165
+ payload.extension.push(extension(PALLIATIVE_EXT.CAREPLAN_OTC_OFFER, { valueString: JSON.stringify(recommendation.otcOffer) }));
165
166
  }
166
167
  if (recommendation.mainRecommendation) payload.description = recommendation.mainRecommendation;
167
168
  const noteTexts = [];
@@ -4,6 +4,7 @@ const { fhirId, identifierSystem } = require('../helpers/fhirHelper');
4
4
  const { extMap, identifierValue, refToId } = require('../helpers/fhirReadHelper');
5
5
  const { CLUSTER_IMPRESSION_SLUG, CLUSTER_LABEL_SLUG } = require('../constants/projectionSlugs');
6
6
  const { CLUSTER_LIST_SEPARATOR } = require('../resources/ClinicalImpression');
7
+ const { CLUSTER_EXT } = require('../constants/extensionSlugs');
7
8
  const { PROJECTOR_NAME } = require('../projections/clusterProjection');
8
9
  const { getFhirStore } = require('../stores/fhirStore');
9
10
  const { ClusterImpression } = require('../../shared/dtos/ClusterImpression');
@@ -54,19 +55,19 @@ function reconstructCluster(resource, patientId) {
54
55
  clusterId: identifierValue(resource, CLUSTER_IMPRESSION_SLUG),
55
56
  patientId,
56
57
  label: identifierValue(resource, CLUSTER_LABEL_SLUG) || '',
57
- hypothesisType: ext.clusterHypothesisType,
58
- status: ext['cluster-status'],
59
- confidence: ext.clusterHypothesisConfidence,
60
- clinicalSignificance: ext.clusterClinicalSignificance,
58
+ hypothesisType: ext[CLUSTER_EXT.HYPOTHESIS_TYPE],
59
+ status: ext[CLUSTER_EXT.STATUS],
60
+ confidence: ext[CLUSTER_EXT.CONFIDENCE],
61
+ clinicalSignificance: ext[CLUSTER_EXT.CLINICAL_SIGNIFICANCE],
61
62
  memberCaseIds: (resource.problem || []).map((problemRef) => refToId(problemRef.reference)).filter(Boolean),
62
- consultationId: ext['cluster-consultation-id'] || null,
63
+ consultationId: ext[CLUSTER_EXT.CONSULTATION_ID] || null,
63
64
  relationshipModel: resource.summary || '',
64
- strengthensIf: ext.clusterStrengthensIf ? ext.clusterStrengthensIf.split(CLUSTER_LIST_SEPARATOR) : [],
65
- weakensIf: ext.clusterWeakensIf ? ext.clusterWeakensIf.split(CLUSTER_LIST_SEPARATOR) : [],
65
+ strengthensIf: ext[CLUSTER_EXT.STRENGTHENS_IF] ? ext[CLUSTER_EXT.STRENGTHENS_IF].split(CLUSTER_LIST_SEPARATOR) : [],
66
+ weakensIf: ext[CLUSTER_EXT.WEAKENS_IF] ? ext[CLUSTER_EXT.WEAKENS_IF].split(CLUSTER_LIST_SEPARATOR) : [],
66
67
  openingReasoning: resource.description || '',
67
68
  openedAt: resource.effectiveDateTime || period.start,
68
69
  dissolvedAt: period.end || null,
69
- dissolveReason: ext['cluster-dissolve-reason'] || null,
70
+ dissolveReason: ext[CLUSTER_EXT.DISSOLVE_REASON] || null,
70
71
  dissolveCategory: (resource.statusReason && resource.statusReason.text) || null,
71
72
  };
72
73
  }
@@ -6,10 +6,11 @@ const {
6
6
  CONTINGENCY_PLAN_SLUG,
7
7
  CONTINGENCY_STEP_SLUG,
8
8
  } = require('../constants/projectionSlugs');
9
+ const { CONTINGENCY_EXT, CONTINGENCY_CATEGORY_PREFIX } = require('../constants/extensionSlugs');
9
10
  const { CONTINGENCY_STATUS_TO_FHIR } = require('../resources/CarePlan');
10
11
  const { PROJECTOR_NAME } = require('../projections/contingencyProjection');
11
12
  const { getFhirStore } = require('../stores/fhirStore');
12
- const { ContingencySafetyNet } = require('../../shared/dtos/ContingencySafetyNet');
13
+ const { ContingencySafetyNet, RECIPIENTS } = require('../../shared/dtos/ContingencySafetyNet');
13
14
  const { logger } = require('../../utils/logger');
14
15
  const { project, storeResources } = require('./fhirService');
15
16
 
@@ -69,13 +70,13 @@ function reconstructContingency(plan, requests, patientId) {
69
70
  relatedCaseId: refToId(address && address.reference),
70
71
  relatedClusterId: refToId(plan.supportingInfo && plan.supportingInfo[0] && plan.supportingInfo[0].reference),
71
72
  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',
73
+ awaitingCharacterization: ext[CONTINGENCY_EXT.AWAITING_CHARACTERIZATION] || '',
74
+ whyThisMatters: ext[CONTINGENCY_EXT.WHY_THIS_MATTERS] || '',
75
+ creationReasoning: ext[CONTINGENCY_EXT.CREATION_REASONING] || '',
76
+ status: ext[CONTINGENCY_EXT.STATUS] || FHIR_TO_CONTINGENCY_STATUS[plan.status] || 'armed',
76
77
  createdAt: plan.created,
77
78
  resolvedAt: (plan.period && plan.period.end) || null,
78
- resolvedReason: ext['contingency-resolved-reason'] || null,
79
+ resolvedReason: ext[CONTINGENCY_EXT.RESOLVED_REASON] || null,
79
80
  },
80
81
  teamEscalation,
81
82
  patientReminder,
@@ -84,21 +85,23 @@ function reconstructContingency(plan, requests, patientId) {
84
85
 
85
86
  function reconstructStep(resource) {
86
87
  const ext = extMap(resource);
87
- const silence = ext['fires-after-silence-minutes'];
88
+ const silence = ext[CONTINGENCY_EXT.FIRES_AFTER_SILENCE_MINUTES];
88
89
  const firstPayload = resource.payload && resource.payload[0];
89
90
  return {
90
91
  recipient: recipientOf(resource),
91
92
  message: (firstPayload && firstPayload.contentCodeableConcept && firstPayload.contentCodeableConcept.text) || null,
92
- priority: ext['contingency-step-priority'] || resource.priority || null,
93
- actionRequested: ext['contingency-action-requested'] || null,
93
+ priority: ext[CONTINGENCY_EXT.STEP_PRIORITY] || resource.priority || null,
94
+ actionRequested: ext[CONTINGENCY_EXT.ACTION_REQUESTED] || null,
94
95
  firesAfterSilenceMinutes: typeof silence === 'number' ? silence : null,
95
96
  };
96
97
  }
97
98
 
98
99
  function recipientOf(resource) {
99
100
  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';
101
+ if (category && category.startsWith(CONTINGENCY_CATEGORY_PREFIX)) {
102
+ const recipient = category.slice(CONTINGENCY_CATEGORY_PREFIX.length);
103
+ if (RECIPIENTS.includes(recipient)) return recipient;
104
+ }
102
105
  const ref = resource.recipient && resource.recipient[0] && resource.recipient[0].reference;
103
106
  return ref && ref.startsWith('Organization/') ? 'care_team' : 'patient';
104
107
  }
@@ -3,6 +3,7 @@ const { ZodError } = require('zod');
3
3
  const { fhirId } = require('../helpers/fhirHelper');
4
4
  const { extMap, extValues, identifierValue } = require('../helpers/fhirReadHelper');
5
5
  const { PALLIATIVE_ASSESSMENT_SLUG, PALLIATIVE_ESCALATION_SLUG, PALLIATIVE_CAREPLAN_SLUG } = require('../constants/projectionSlugs');
6
+ const { PALLIATIVE_EXT } = require('../constants/extensionSlugs');
6
7
  const { PROJECTOR_NAME } = require('../projections/palliativeProjection');
7
8
  const { PROJECTOR_NAME: CAREPLAN_PROJECTOR_NAME } = require('../projections/palliativeCarePlanProjection');
8
9
  const { getFhirStore } = require('../stores/fhirStore');
@@ -46,7 +47,7 @@ async function readPalliativeCarePlanByTurn({ patientId, turnId }) {
46
47
  const patient = `Patient/${fhirId(patientId)}`;
47
48
  const plans = (await store.find({ patient, resourceType: 'CarePlan' }))
48
49
  .filter((resource) => identifierValue(resource, PALLIATIVE_CAREPLAN_SLUG));
49
- const plan = plans.find((resource) => extMap(resource)['palliative-turn-id'] === turnId) || null;
50
+ const plan = plans.find((resource) => extMap(resource)[PALLIATIVE_EXT.TURN_ID] === turnId) || null;
50
51
  if (!plan) return null;
51
52
  const careplanId = identifierValue(plan, PALLIATIVE_CAREPLAN_SLUG);
52
53
  const provenance = await store.getByReference(`Provenance/${fhirId(`${careplanId}-prov`)}`);
@@ -56,7 +57,7 @@ async function readPalliativeCarePlanByTurn({ patientId, turnId }) {
56
57
 
57
58
  async function reconstructCarePlan(store, plan, patientId, turnId) {
58
59
  const careplanId = identifierValue(plan, PALLIATIVE_CAREPLAN_SLUG);
59
- const assessmentId = extMap(plan)['palliative-assessment-id'];
60
+ const assessmentId = extMap(plan)[PALLIATIVE_EXT.ASSESSMENT_ID];
60
61
  const taskRefs = (plan.activity || [])
61
62
  .map((activity) => activity.plannedActivityReference && activity.plannedActivityReference.reference)
62
63
  .filter(Boolean);
@@ -98,12 +99,12 @@ function reconstructRecommendation(task) {
98
99
  const ext = extMap(task);
99
100
  const ctcaeTerms = ((task.code && task.code.coding) || []).map((coding) => coding.code).filter(Boolean);
100
101
  return {
101
- rank: ext['palliative-careplan-rec-rank'],
102
+ rank: ext[PALLIATIVE_EXT.CAREPLAN_REC_RANK],
102
103
  elementNameShort: (task.code && task.code.text) || 'recommendation',
103
104
  isPharmacologicalMedNeeded: medsText === undefined ? null : medsText === 'true',
104
105
  ctcaeTerms,
105
106
  mainRecommendation: task.description || null,
106
- otcOffer: parseOtcOffer(ext['palliative-careplan-otc-offer']),
107
+ otcOffer: parseOtcOffer(ext[PALLIATIVE_EXT.CAREPLAN_OTC_OFFER]),
107
108
  whenToEscalate: noteMap.when_to_escalate || null,
108
109
  expectedEffectiveness: noteMap.expected_effectiveness || null,
109
110
  followup,
@@ -124,7 +125,7 @@ async function findImpression(store, patientId, turnId) {
124
125
  const patient = `Patient/${fhirId(patientId)}`;
125
126
  const impressions = (await store.find({ patient, resourceType: 'ClinicalImpression' }))
126
127
  .filter((resource) => identifierValue(resource, PALLIATIVE_ASSESSMENT_SLUG));
127
- return impressions.find((impression) => extMap(impression)['palliative-turn-id'] === turnId) || null;
128
+ return impressions.find((impression) => extMap(impression)[PALLIATIVE_EXT.TURN_ID] === turnId) || null;
128
129
  }
129
130
 
130
131
  async function reconstruct(store, impression, provenance, patientId, turnId) {
@@ -158,7 +159,7 @@ async function readEscalations(store, provenance, patientId) {
158
159
  for (const ref of taskRefs) {
159
160
  const task = await store.getByReference(ref);
160
161
  if (!task) continue;
161
- const rank = extMap(task)['palliative-escalation-rank'];
162
+ const rank = extMap(task)[PALLIATIVE_EXT.ESCALATION_RANK];
162
163
  if (rank === undefined || rank === null) continue;
163
164
  byRank[rank] = reconstructEscalation(task, patientId);
164
165
  }
@@ -168,25 +169,25 @@ async function readEscalations(store, provenance, patientId) {
168
169
  function reconstructFinding(finding, escalations) {
169
170
  const ext = extMap(finding);
170
171
  const nameShort = ccText(finding.item && finding.item.concept);
171
- const rank = ext['palliative-finding-rank'];
172
+ const rank = ext[PALLIATIVE_EXT.FINDING_RANK];
172
173
  return {
173
174
  rank,
174
- elementType: ext['palliative-finding-element-type'],
175
+ elementType: ext[PALLIATIVE_EXT.FINDING_ELEMENT_TYPE],
175
176
  nameShort,
176
- timeWindow: ext['palliative-finding-time-window'] || '',
177
- symptomTerms: extValues(finding, 'palliative-finding-symptom-term'),
178
- clinicalDescription: ext['palliative-finding-clinical-description'] || '',
179
- whyRelevant: ext['palliative-finding-why-relevant'] || '',
180
- qolImpact: ext['palliative-finding-qol-impact'] || null,
181
- qolCriteria: ext['palliative-finding-qol-criteria'] || '',
182
- improvementCapacity: ext['palliative-finding-improvement-capacity'] || null,
183
- improvementCriteria: ext['palliative-finding-improvement-criteria'] || '',
184
- areMedsNeeded: ext['palliative-finding-are-meds-needed'] ?? null,
185
- needsCharacterization: ext['palliative-finding-needs-characterization'] ?? null,
186
- characterizationCriteria: ext['palliative-finding-characterization-criteria'] || '',
187
- probeQuestions: extValues(finding, 'palliative-finding-probe-question'),
188
- auditNotes: ext['palliative-finding-audit-notes'] || '',
189
- clusterNote: ext['palliative-finding-cluster-note'] ?? null,
177
+ timeWindow: ext[PALLIATIVE_EXT.FINDING_TIME_WINDOW] || '',
178
+ symptomTerms: extValues(finding, PALLIATIVE_EXT.FINDING_SYMPTOM_TERM),
179
+ clinicalDescription: ext[PALLIATIVE_EXT.FINDING_CLINICAL_DESCRIPTION] || '',
180
+ whyRelevant: ext[PALLIATIVE_EXT.FINDING_WHY_RELEVANT] || '',
181
+ qolImpact: ext[PALLIATIVE_EXT.FINDING_QOL_IMPACT] || null,
182
+ qolCriteria: ext[PALLIATIVE_EXT.FINDING_QOL_CRITERIA] || '',
183
+ improvementCapacity: ext[PALLIATIVE_EXT.FINDING_IMPROVEMENT_CAPACITY] || null,
184
+ improvementCriteria: ext[PALLIATIVE_EXT.FINDING_IMPROVEMENT_CRITERIA] || '',
185
+ areMedsNeeded: ext[PALLIATIVE_EXT.FINDING_ARE_MEDS_NEEDED] ?? null,
186
+ needsCharacterization: ext[PALLIATIVE_EXT.FINDING_NEEDS_CHARACTERIZATION] ?? null,
187
+ characterizationCriteria: ext[PALLIATIVE_EXT.FINDING_CHARACTERIZATION_CRITERIA] || '',
188
+ probeQuestions: extValues(finding, PALLIATIVE_EXT.FINDING_PROBE_QUESTION),
189
+ auditNotes: ext[PALLIATIVE_EXT.FINDING_AUDIT_NOTES] || '',
190
+ clusterNote: ext[PALLIATIVE_EXT.FINDING_CLUSTER_NOTE] ?? null,
190
191
  escalation: escalations[rank] || null,
191
192
  };
192
193
  }
@@ -3,6 +3,7 @@ const { ZodError } = require('zod');
3
3
  const { fhirId } = require('../helpers/fhirHelper');
4
4
  const { identifierValue, extValues } = require('../helpers/fhirReadHelper');
5
5
  const { PATIENT_RISK_SLUG } = require('../constants/projectionSlugs');
6
+ const { RISK_EXT } = require('../constants/extensionSlugs');
6
7
  const { PROJECTOR_NAME } = require('../projections/riskProjection');
7
8
  const { PROJECTOR_NAME: SAFETY_GATE_PROJECTOR_NAME } = require('../projections/safetyGateProjection');
8
9
  const {
@@ -67,14 +68,14 @@ function reconstructRisk(resource, patientId) {
67
68
  assessmentId: identifierValue(resource, PATIENT_RISK_SLUG),
68
69
  patientId,
69
70
  status: resource.status,
70
- sourceRegimen: extValues(resource, 'assessed-source-regimen')[0] || null,
71
- sourceDiagnosis: extValues(resource, 'assessed-source-diagnosis')[0] || null,
72
- sourcePhase: extValues(resource, 'assessed-source-phase')[0] || null,
71
+ sourceRegimen: extValues(resource, RISK_EXT.SOURCE_REGIMEN)[0] || null,
72
+ sourceDiagnosis: extValues(resource, RISK_EXT.SOURCE_DIAGNOSIS)[0] || null,
73
+ sourcePhase: extValues(resource, RISK_EXT.SOURCE_PHASE)[0] || null,
73
74
  predictedAes: predictions.filter((prediction) => predictionKind(prediction) === PREDICTION_KIND_AE).map(reconstructAe),
74
75
  diseaseRisks: predictions.filter((prediction) => predictionKind(prediction) === PREDICTION_KIND_DISEASE).map(reconstructDisease),
75
76
  comorbidityAmplifiers: predictions.filter((prediction) => predictionKind(prediction) === PREDICTION_KIND_COMORBIDITY).map(reconstructComorbidity),
76
77
  assessedAt: resource.occurrenceDateTime,
77
- assessedByModel: extValues(resource, 'assessed-by-model')[0] || null,
78
+ assessedByModel: extValues(resource, RISK_EXT.ASSESSED_BY_MODEL)[0] || null,
78
79
  };
79
80
  }
80
81
 
@@ -86,7 +87,7 @@ function reconstructAe(prediction) {
86
87
  && prediction.qualitativeRisk.coding
87
88
  && prediction.qualitativeRisk.coding[0]
88
89
  && prediction.qualitativeRisk.coding[0].code) || null,
89
- minimumSymptomSet: extValues(prediction, 'risk-minimum-symptom-set'),
90
+ minimumSymptomSet: extValues(prediction, RISK_EXT.MINIMUM_SYMPTOM_SET),
90
91
  why: prediction.rationale || null,
91
92
  };
92
93
  }
@@ -94,9 +95,9 @@ function reconstructAe(prediction) {
94
95
  function reconstructDisease(prediction) {
95
96
  return {
96
97
  bodySite: outcomeText(prediction),
97
- complications: extValues(prediction, 'risk-disease-complication'),
98
- warningSymptoms: extValues(prediction, 'risk-disease-warning-symptom'),
99
- amplifierComorbidities: extValues(prediction, 'risk-disease-amplifier'),
98
+ complications: extValues(prediction, RISK_EXT.DISEASE_COMPLICATION),
99
+ warningSymptoms: extValues(prediction, RISK_EXT.DISEASE_WARNING_SYMPTOM),
100
+ amplifierComorbidities: extValues(prediction, RISK_EXT.DISEASE_AMPLIFIER),
100
101
  };
101
102
  }
102
103
 
@@ -104,11 +105,11 @@ function reconstructComorbidity(prediction) {
104
105
  return {
105
106
  comorbidity: outcomeText(prediction),
106
107
  interaction: prediction.rationale,
107
- clinicalNote: extValues(prediction, 'risk-comorbidity-clinical-note')[0] || null,
108
+ clinicalNote: extValues(prediction, RISK_EXT.COMORBIDITY_CLINICAL_NOTE)[0] || null,
108
109
  };
109
110
  }
110
111
 
111
- const predictionKind = (prediction) => extValues(prediction, 'risk-prediction-kind')[0] || null;
112
+ const predictionKind = (prediction) => extValues(prediction, RISK_EXT.PREDICTION_KIND)[0] || null;
112
113
 
113
114
  const outcomeText = (prediction) => (prediction.outcome && prediction.outcome.text) || null;
114
115
 
@@ -3,6 +3,7 @@ const { ZodError } = require('zod');
3
3
  const { fhirId } = require('../helpers/fhirHelper');
4
4
  const { extMap, extValues, identifierValue, refToId } = require('../helpers/fhirReadHelper');
5
5
  const { ROUTING_ASSESSMENT_SLUG, ROUTING_DISPOSITION_SLUG } = require('../constants/projectionSlugs');
6
+ const { ROUTING_EXT } = require('../constants/extensionSlugs');
6
7
  const { PROJECTOR_NAME } = require('../projections/routingProjection');
7
8
  const { getFhirStore } = require('../stores/fhirStore');
8
9
  const { RoutingDecision } = require('../../shared/dtos/RoutingDecision');
@@ -32,7 +33,7 @@ async function readRoutingByTurn({ patientId, turnId }) {
32
33
  const patient = `Patient/${fhirId(patientId)}`;
33
34
  const impressions = (await store.find({ patient, resourceType: 'ClinicalImpression' }))
34
35
  .filter((resource) => identifierValue(resource, ROUTING_ASSESSMENT_SLUG));
35
- const assessment = impressions.find((impression) => extMap(impression)['routing-turn-id'] === turnId);
36
+ const assessment = impressions.find((impression) => extMap(impression)[ROUTING_EXT.TURN_ID] === turnId);
36
37
  if (!assessment) return null;
37
38
  const provenance = await store.getByReference(`Provenance/${fhirId(`${assessment.id}-prov`)}`);
38
39
  if (!provenance) return null;
@@ -62,10 +63,10 @@ function reconstructRouting(assessment, disposition, provenance, patientId, turn
62
63
  const agent = (provenance.agent || [])[0] || {};
63
64
  return {
64
65
  schemaVersion: '1',
65
- decisionId: ext['routing-decision-id'],
66
+ decisionId: ext[ROUTING_EXT.DECISION_ID],
66
67
  patientId,
67
68
  turnId,
68
- committedAt: ext['routing-committed-at'],
69
+ committedAt: ext[ROUTING_EXT.COMMITTED_AT],
69
70
  assessment: reconstructAssessment(assessment, patientId, turnId),
70
71
  disposition: reconstructDisposition(disposition, assessment, patientId),
71
72
  provenance: {
@@ -92,9 +93,9 @@ function reconstructAssessment(resource, patientId, turnId) {
92
93
  function reconstructFinding(finding) {
93
94
  const ext = extMap(finding);
94
95
  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'] || '',
96
+ caseId: ext[ROUTING_EXT.FINDING_CASE_ID] || refToId(finding.item && finding.item.reference),
97
+ clinicalReasoning: ext[ROUTING_EXT.FINDING_REASONING] || '',
98
+ decisionAction: ext[ROUTING_EXT.FINDING_ACTION] || '',
98
99
  };
99
100
  }
100
101
 
@@ -105,7 +106,7 @@ function reconstructDisposition(resource, assessment, patientId) {
105
106
  kind: DISPOSITION_KIND_BY_RESOURCE[resource.resourceType],
106
107
  primaryAction: dispositionAction(resource),
107
108
  primaryReason: dispositionReason(resource),
108
- primaryCaseId: extMap(assessment)['routing-primary-case-id'] || null,
109
+ primaryCaseId: extMap(assessment)[ROUTING_EXT.PRIMARY_CASE_ID] || null,
109
110
  secondaryActions: (resource.action || []).slice(1).map(reconstructSecondary),
110
111
  deferredItems: reconstructDeferredItems(assessment),
111
112
  authoredAt: resource.sent || resource.authoredOn || resource.created,
@@ -113,8 +114,8 @@ function reconstructDisposition(resource, assessment, patientId) {
113
114
  }
114
115
 
115
116
  function reconstructDeferredItems(assessment) {
116
- const items = extValues(assessment, 'routing-deferred-item');
117
- const reasons = extValues(assessment, 'routing-deferred-reason');
117
+ const items = extValues(assessment, ROUTING_EXT.DEFERRED_ITEM);
118
+ const reasons = extValues(assessment, ROUTING_EXT.DEFERRED_REASON);
118
119
  return items.map((item, i) => ({ item, deferReason: reasons[i] }));
119
120
  }
120
121
 
@@ -3,6 +3,7 @@ const { ZodError } = require('zod');
3
3
  const { fhirId } = require('../helpers/fhirHelper');
4
4
  const { extMap, extValues, identifierValue } = require('../helpers/fhirReadHelper');
5
5
  const { SYMPTOM_CASE_SLUG, SYMPTOM_ASSESSMENT_SLUG } = require('../constants/projectionSlugs');
6
+ const { SYMPTOM_CASE_EXT, ASSESSMENT_EXT, GRADE_EXT } = require('../constants/extensionSlugs');
6
7
  const { PROJECTOR_NAME } = require('../projections/symptomCaseProjection');
7
8
  const { getFhirStore } = require('../stores/fhirStore');
8
9
  const { ManagedSymptom } = require('../../shared/dtos/ManagedSymptom');
@@ -23,7 +24,7 @@ async function readSymptomCases({ patientId, ctcaeTerm = null, status = null })
23
24
  const store = getFhirStore();
24
25
  const patient = `Patient/${fhirId(patientId)}`;
25
26
  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
+ .filter((condition) => SYMPTOM_CASE_EXT.STATUS in extMap(condition) && identifierValue(condition, SYMPTOM_CASE_SLUG));
27
28
  const cases = [];
28
29
  for (const condition of conditions) {
29
30
  let managed;
@@ -61,17 +62,17 @@ function reconstructCondition(condition, patientId, conditionId) {
61
62
  conditionId,
62
63
  patientId,
63
64
  ctcaeTerm: coding.code || code.text || null,
64
- clinicalStatus: ext['symptom-case-status'],
65
+ clinicalStatus: ext[SYMPTOM_CASE_EXT.STATUS],
65
66
  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'),
67
+ abatementAt: condition.abatementDateTime || ext[SYMPTOM_CASE_EXT.LEGACY_ABATEMENT] || null,
68
+ closeReason: ext[SYMPTOM_CASE_EXT.CLOSE_REASON] || null,
69
+ temporality: ext[SYMPTOM_CASE_EXT.TEMPORALITY] || null,
70
+ evidenceEpisodeIds: extValues(condition, SYMPTOM_CASE_EXT.EVIDENCE_EPISODE_IDS),
71
+ currentEpisodeId: ext[SYMPTOM_CASE_EXT.CURRENT_EPISODE_ID] || null,
72
+ evidenceMentionIds: extValues(condition, SYMPTOM_CASE_EXT.EVIDENCE_MENTION_IDS),
72
73
  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,
74
+ recurrenceCount: ext[SYMPTOM_CASE_EXT.RECURRENCE_COUNT] ?? 0,
75
+ reactivatedAt: ext[SYMPTOM_CASE_EXT.REACTIVATED_AT] || null,
75
76
  };
76
77
  }
77
78
 
@@ -109,13 +110,13 @@ async function reconstructAssessment(impression, { patientId, conditionId, previ
109
110
  performedAt: impression.effectiveDateTime,
110
111
  previousId,
111
112
  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,
113
+ trajectory: ext[ASSESSMENT_EXT.TRAJECTORY] || null,
114
+ decisionPosture: ext[ASSESSMENT_EXT.DECISION_POSTURE] || null,
115
+ gradeVerification: gradeExt[GRADE_EXT.VERIFICATION] || 'provisional',
116
+ reasoning: ext[ASSESSMENT_EXT.REASONING] || null,
117
+ postureReason: ext[ASSESSMENT_EXT.POSTURE_REASON] || null,
117
118
  problemRepresentation: impression.summary || null,
118
- gradeThresholdClause: gradeExt['grade-threshold-clause'] || null,
119
+ gradeThresholdClause: gradeExt[GRADE_EXT.THRESHOLD_CLAUSE] || null,
119
120
  };
120
121
  }
121
122
 
@@ -124,12 +125,12 @@ function reconstructGrade(obs) {
124
125
  if (!obs) return grade;
125
126
  const ext = extMap(obs);
126
127
  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));
128
+ if (ext[GRADE_EXT.CONFIDENCE]) grade.confidence = ext[GRADE_EXT.CONFIDENCE];
129
+ if (ext[GRADE_EXT.RANGE]) {
130
+ const [lo, hi] = ext[GRADE_EXT.RANGE].split('-').map((n) => Number.parseInt(n, 10));
130
131
  if (Number.isInteger(lo) && Number.isInteger(hi)) grade.possibleRange = [lo, hi];
131
132
  }
132
- if (ext['grade-reasoning']) grade.reasoning = ext['grade-reasoning'];
133
+ if (ext[GRADE_EXT.REASONING]) grade.reasoning = ext[GRADE_EXT.REASONING];
133
134
  return grade;
134
135
  }
135
136
 
@@ -4,15 +4,20 @@ const { BaseDto } = require('./BaseDto');
4
4
 
5
5
  const dateTime = z.iso.datetime({ offset: true });
6
6
 
7
+ const HYPOTHESIS_TYPES = ['ae_profile', 'symptom_syndrome', 'disease_complication', 'relational_causal'];
8
+ const CLUSTER_STATUSES = ['active', 'dissolved'];
9
+ const CONFIDENCE_LEVELS = ['speculative', 'moderate', 'strong'];
10
+ const CLINICAL_SIGNIFICANCE = ['benign', 'moderate', 'safety_relevant', 'emergency'];
11
+
7
12
  const schema = z.strictObject({
8
13
  schemaVersion: z.string().default('1'),
9
14
  clusterId: z.string(),
10
15
  patientId: z.string(),
11
16
  label: z.string(),
12
- hypothesisType: z.enum(['ae_profile', 'symptom_syndrome', 'disease_complication', 'relational_causal']),
13
- status: z.enum(['active', 'dissolved']).default('active'),
14
- confidence: z.enum(['speculative', 'moderate', 'strong']),
15
- clinicalSignificance: z.enum(['benign', 'moderate', 'safety_relevant', 'emergency']),
17
+ hypothesisType: z.enum(HYPOTHESIS_TYPES),
18
+ status: z.enum(CLUSTER_STATUSES).default('active'),
19
+ confidence: z.enum(CONFIDENCE_LEVELS),
20
+ clinicalSignificance: z.enum(CLINICAL_SIGNIFICANCE),
16
21
  memberCaseIds: z.array(z.string()).min(1),
17
22
  consultationId: z.string().nullable().default(null),
18
23
  relationshipModel: z.string(),
@@ -37,4 +42,10 @@ class ClusterImpression extends BaseDto {}
37
42
 
38
43
  ClusterImpression.schema = schema;
39
44
 
40
- module.exports = { ClusterImpression };
45
+ module.exports = {
46
+ ClusterImpression,
47
+ HYPOTHESIS_TYPES,
48
+ CLUSTER_STATUSES,
49
+ CONFIDENCE_LEVELS,
50
+ CLINICAL_SIGNIFICANCE,
51
+ };
@@ -6,9 +6,10 @@ const dateTime = z.iso.datetime({ offset: true });
6
6
 
7
7
  const ESCALATION_PRIORITIES = ['critical', 'high', 'medium', 'low', 'routine', 'urgent', 'asap', 'stat'];
8
8
  const ACTIVE_CARE_PLAN_STATUSES = ['armed', 'fired_reminder', 'fired_team'];
9
+ const RECIPIENTS = ['patient', 'care_team'];
9
10
 
10
11
  const escalationStepSchema = z.strictObject({
11
- recipient: z.enum(['patient', 'care_team']),
12
+ recipient: z.enum(RECIPIENTS),
12
13
  message: z.string(),
13
14
  priority: z.enum(ESCALATION_PRIORITIES).nullable().default(null),
14
15
  actionRequested: z.string().nullable().default(null),
@@ -51,4 +52,5 @@ module.exports = {
51
52
  ContingencySafetyNet,
52
53
  ESCALATION_PRIORITIES,
53
54
  ACTIVE_CARE_PLAN_STATUSES,
55
+ RECIPIENTS,
54
56
  };
@@ -4,16 +4,19 @@ const { BaseDto } = require('./BaseDto');
4
4
 
5
5
  const dateTime = z.iso.datetime({ offset: true });
6
6
 
7
+ const KINDS = ['otc', 'self_care', 'other', 'education', 'team_escalation', 'follow_up_check'];
8
+ const OUTCOMES = ['pending', 'improved', 'no_relief', 'side_effects', 'worsened', 'unknown'];
9
+
7
10
  const schema = z.strictObject({
8
11
  schemaVersion: z.string().default('1'),
9
- kind: z.enum(['otc', 'self_care', 'other', 'education', 'team_escalation', 'follow_up_check']),
12
+ kind: z.enum(KINDS),
10
13
  interventionId: z.string(),
11
14
  patientId: z.string(),
12
15
  caseId: z.string(),
13
16
  interventionLabel: z.string(),
14
17
  rationale: z.string().nullable().default(null),
15
18
  deliveredAt: dateTime,
16
- outcome: z.enum(['pending', 'improved', 'no_relief', 'side_effects', 'worsened', 'unknown']).default('pending'),
19
+ outcome: z.enum(OUTCOMES).default('pending'),
17
20
  outcomeNotes: z.string().nullable().default(null),
18
21
  recipientKind: z.enum(['patient', 'related_person']).default('patient'),
19
22
  recipientRefId: z.string().nullable().default(null),
@@ -26,4 +29,8 @@ class Intervention extends BaseDto {}
26
29
 
27
30
  Intervention.schema = schema;
28
31
 
29
- module.exports = { Intervention };
32
+ module.exports = {
33
+ Intervention,
34
+ KINDS,
35
+ OUTCOMES,
36
+ };