@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,42 +0,0 @@
1
- const { getExtBase } = require('../config/fhirConfig');
2
- const { identifierSystem, extensionUrl } = require('./fhirHelper');
3
-
4
- function extMap(resource) {
5
- const base = getExtBase();
6
- const out = {};
7
- for (const ext of resource.extension || []) {
8
- if (!String(ext.url || '').startsWith(base)) continue;
9
- const name = ext.url.slice(base.length + 1);
10
- out[name] = ext.valueString ?? ext.valueDateTime ?? ext.valueBoolean ?? ext.valueInteger ?? ext.valueDecimal ?? null;
11
- }
12
- return out;
13
- }
14
-
15
- function identifierValue(resource, slug) {
16
- const system = identifierSystem(slug);
17
- const match = (resource.identifier || []).find((id) => id.system === system);
18
- return (match && match.value) || null;
19
- }
20
-
21
- function refToId(reference) {
22
- if (!reference) return null;
23
- return String(reference).split('/').slice(1).join('/') || null;
24
- }
25
-
26
- function extValues(resource, name) {
27
- const url = extensionUrl(name);
28
- const values = [];
29
- for (const ext of resource.extension || []) {
30
- if (ext.url !== url) continue;
31
- const value = ext.valueString ?? ext.valueDateTime ?? ext.valueBoolean ?? ext.valueInteger ?? ext.valueDecimal ?? null;
32
- if (value !== null) values.push(value);
33
- }
34
- return values;
35
- }
36
-
37
- module.exports = {
38
- extMap,
39
- identifierValue,
40
- refToId,
41
- extValues,
42
- };
@@ -1,260 +0,0 @@
1
- const crypto = require('node:crypto');
2
-
3
- const { getSuspectedGradeDisplays, getSystems } = require('../config/fhirConfig');
4
- const { lookupValue, integerId, parseList, termKey } = require('../utils/normalizeUtils');
5
- const { codeSystemUrl } = require('./fhirHelper');
6
-
7
- const { safeParse } = require('../../utils/jsonUtils');
8
-
9
- const DIMENSION_SCALES = {
10
- frequency: ['Nunca', 'Rara vez', 'A veces', 'A menudo', 'Casi siempre'],
11
- intensity: ['Ninguna', 'Leve', 'Moderada', 'Intensa', 'Muy intensa'],
12
- interference: ['Nada', 'Un poco', 'Algo', 'Mucho', 'Muchísimo'],
13
- amount: ['Ninguna', 'Leve', 'Moderada', 'Intensa', 'Muy intensa'],
14
- presence: ['No', 'Sí'],
15
- };
16
-
17
- const DIMENSION_ORDER = ['presence', 'frequency', 'intensity', 'interference', 'amount'];
18
-
19
- const CANONICAL_SOURCE = 'original';
20
-
21
- function buildCatalog(items, composites = [], source = null) {
22
- const symptoms = {};
23
- const questionIndex = {};
24
- const answerMap = {};
25
-
26
- for (const row of items) {
27
- const questionId = integerId(row.question_id);
28
- const map = answerMapOf(row);
29
- if (questionId != null && map) answerMap[String(questionId)] = map;
30
-
31
- const en = row.symptom_en;
32
- if (!en) continue;
33
- const term = termKey(en);
34
- const dimension = row.dimension ?? null;
35
-
36
- const record = (symptoms[term] = symptoms[term] || {
37
- term,
38
- en,
39
- es: row.symptom_es ?? null,
40
- ctcae_v5: lookupValue(row.ctcae_v5),
41
- meddra: lookupValue(row.meddra_code),
42
- ae_category: lookupValue(row.AEcategory),
43
- dimensions: [],
44
- });
45
- record.dimensions.push({
46
- dimension,
47
- options: map ? [...(DIMENSION_SCALES[dimension] || [])] : parseList(row.options),
48
- question_id: questionId != null ? String(questionId) : null,
49
- source: row.source ?? null,
50
- });
51
-
52
- if (questionId != null && dimension) questionIndex[String(questionId)] = [term, dimension];
53
- }
54
-
55
- for (const record of Object.values(symptoms)) {
56
- record.dimensions = pickCanonicalDimensions(record.dimensions);
57
- }
58
- const compositeGrades = buildCompositeGrades(composites);
59
-
60
- return {
61
- _meta: {
62
- source,
63
- n_symptoms: Object.keys(symptoms).length,
64
- n_items: Object.keys(questionIndex).length,
65
- n_mapped_items: Object.keys(answerMap).length,
66
- n_composites: Object.keys(compositeGrades).length,
67
- composite_table_hash: Object.keys(compositeGrades).length ? hashCompositeGrades(compositeGrades) : null,
68
- dimension_scales: DIMENSION_SCALES,
69
- },
70
- symptoms,
71
- question_index: questionIndex,
72
- answer_map: answerMap,
73
- composite_grades: compositeGrades,
74
- };
75
- }
76
-
77
- function answerMapOf(row) {
78
- const map = safeParse(row.proctcae_map, null);
79
- return map && typeof map === 'object' && Object.keys(map).length ? map : null;
80
- }
81
-
82
- function canonicalAnswer(catalog, questionId, answer) {
83
- const map = catalog.answer_map[String(questionId).trim()];
84
- if (!map) return answer;
85
- return map[String(answer).trim()] ?? answer;
86
- }
87
-
88
- function pickCanonicalDimensions(dimensions) {
89
- const best = {};
90
- for (const d of dimensions) {
91
- if (d.dimension == null) continue;
92
- const prev = best[d.dimension];
93
- if (!prev || outranks(d, prev)) best[d.dimension] = d;
94
- }
95
- return Object.values(best).sort((a, b) => {
96
- const rank = (d) => (DIMENSION_ORDER.includes(d.dimension) ? DIMENSION_ORDER.indexOf(d.dimension) : 99);
97
- return rank(a) - rank(b);
98
- });
99
- }
100
-
101
- function outranks(candidate, current) {
102
- const canonical = (d) => d.source === CANONICAL_SOURCE;
103
- if (canonical(candidate) !== canonical(current)) return canonical(candidate);
104
- return (integerId(candidate.question_id) || 0) > (integerId(current.question_id) || 0);
105
- }
106
-
107
- function buildCompositeGrades(composites) {
108
- const compositeGrades = {};
109
- for (const row of composites) {
110
- const code = row.combination_code;
111
- const raw = String(row.composite_ctcae ?? '').trim();
112
- const grade = raw === '' ? NaN : Number(raw);
113
- if (code && Number.isInteger(grade)) compositeGrades[code] = grade;
114
- }
115
- return compositeGrades;
116
- }
117
-
118
- function hashCompositeGrades(compositeGrades) {
119
- const lines = Object.entries(compositeGrades)
120
- .sort(([a], [b]) => (a < b ? -1 : 1))
121
- .map(([code, grade]) => `${code}=${grade}`)
122
- .join('\n');
123
- return crypto.createHash('sha256').update(lines, 'utf8').digest('hex').slice(0, 12);
124
- }
125
-
126
- function allTerms(catalog) {
127
- return Object.keys(catalog.symptoms).sort();
128
- }
129
-
130
- function compositeTableHash(catalog) {
131
- return catalog._meta.composite_table_hash;
132
- }
133
-
134
- function getSymptom(catalog, term) {
135
- return catalog.symptoms[term] || null;
136
- }
137
-
138
- function itemForQuestionId(catalog, questionId) {
139
- const entry = catalog.question_index[String(questionId).trim()];
140
- return Array.isArray(entry) && entry.length >= 2 ? [entry[0], entry[1]] : null;
141
- }
142
-
143
- function standardScale(catalog, dimension) {
144
- return [...(catalog._meta.dimension_scales[dimension] || [])];
145
- }
146
-
147
- function standardOrdinal(catalog, dimension, answer) {
148
- return ordinalIn(standardScale(catalog, dimension), answer);
149
- }
150
-
151
- function compositeGrade(catalog, ordinals = {}) {
152
- const code = COMPOSITE_DIMENSIONS.map((d) => (ordinals[d] == null ? 'x' : String(ordinals[d]))).join('');
153
- const grade = catalog.composite_grades[code];
154
- return grade === undefined ? null : grade;
155
- }
156
-
157
- function codeText(catalog, term) {
158
- const rec = getSymptom(catalog, term);
159
- return rec ? rec.es || rec.en : term;
160
- }
161
-
162
- function dimensionsFor(catalog, term) {
163
- const rec = getSymptom(catalog, term);
164
- return (rec ? rec.dimensions : []).map((d) => d.dimension).filter(Boolean);
165
- }
166
-
167
- function answerOptions(catalog, term, dimension) {
168
- return optionsForRec(getSymptom(catalog, term), dimension);
169
- }
170
-
171
- function ordinalValue(catalog, term, dimension, answer) {
172
- return ordinalIn(answerOptions(catalog, term, dimension), answer);
173
- }
174
-
175
- function codingsFor(catalog, term) {
176
- const rec = getSymptom(catalog, term);
177
- const codings = [
178
- { system: codeSystemUrl('pro-ctcae'), code: term, ...(rec && rec.en ? { display: rec.en } : {}) },
179
- ];
180
- if (rec && rec.ctcae_v5) {
181
- codings.push({
182
- system: codeSystemUrl('ctcae-v5-term'),
183
- code: ctcaeSlug(rec.ctcae_v5),
184
- display: rec.ctcae_v5,
185
- });
186
- }
187
- if (rec && rec.meddra) {
188
- codings.push({
189
- system: getSystems().meddra,
190
- code: String(rec.meddra),
191
- ...(rec.ctcae_v5 ? { display: rec.ctcae_v5 } : {}),
192
- });
193
- }
194
- return codings;
195
- }
196
-
197
- function aeCategory(catalog, term) {
198
- const rec = getSymptom(catalog, term);
199
- if (!rec || !rec.ae_category) return null;
200
- return { code: ctcaeSlug(rec.ae_category), display: rec.ae_category };
201
- }
202
-
203
- function gradeConcept(grade) {
204
- const g = Number(grade);
205
- if (!Number.isInteger(g)) throw new Error(`grade is not an integer: '${grade}'`);
206
- const display = getSuspectedGradeDisplays()[g] || `Grado CTCAE ${g} sospechado`;
207
- return {
208
- coding: [
209
- {
210
- system: codeSystemUrl('suspected-ctcae-grade'),
211
- code: String(g),
212
- display,
213
- extension: [{ url: getSystems().itemWeight, valueDecimal: g }],
214
- },
215
- ],
216
- text: display,
217
- };
218
- }
219
-
220
- function ordinalIn(options, answer) {
221
- const norm = String(answer ?? '').trim().toLowerCase();
222
- const i = options.findIndex((o) => o.trim().toLowerCase() === norm);
223
- return i === -1 ? null : i;
224
- }
225
-
226
- const AFFIRMATIVE = ['sí', 'si', 'yes', 'true', '1'];
227
- function affirmative(value) {
228
- return AFFIRMATIVE.includes(String(value).trim().toLowerCase());
229
- }
230
-
231
- function optionsForRec(rec, dimension) {
232
- for (const d of rec ? rec.dimensions : []) {
233
- if (d.dimension === dimension) return [...d.options];
234
- }
235
- return [];
236
- }
237
-
238
- const ctcaeSlug = (value) =>
239
- String(value).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
240
-
241
- const COMPOSITE_DIMENSIONS = ['frequency', 'intensity', 'interference', 'amount'];
242
-
243
- module.exports = {
244
- buildCatalog,
245
- allTerms,
246
- compositeTableHash,
247
- itemForQuestionId,
248
- canonicalAnswer,
249
- affirmative,
250
- standardScale,
251
- standardOrdinal,
252
- compositeGrade,
253
- codeText,
254
- aeCategory,
255
- dimensionsFor,
256
- answerOptions,
257
- ordinalValue,
258
- codingsFor,
259
- gradeConcept,
260
- };
@@ -1,24 +0,0 @@
1
- const { ClinicalImpression } = require('../resources/ClinicalImpression');
2
- const { Provenance } = require('../resources/Provenance');
3
-
4
- const PROJECTOR_NAME = 'cluster';
5
-
6
- function projectCluster({ cluster, history = [] }) {
7
- const impression = ClinicalImpression.fromClusterImpression({ cluster });
8
- const records = history.length
9
- ? [...history].sort((a, b) => Date.parse(a.recordedAt) - Date.parse(b.recordedAt))
10
- : [{ turnId: 'opened', action: 'opened', recordedAt: cluster.openedAt, transitionReason: null }];
11
- const provenances = records.map((record) => Provenance.fromClusterImpression({
12
- clusterId: cluster.clusterId,
13
- turnId: record.turnId,
14
- action: record.action,
15
- recordedAt: record.recordedAt,
16
- transitionReason: record.transitionReason,
17
- }));
18
- return [impression, ...provenances];
19
- }
20
-
21
- module.exports = {
22
- projectCluster,
23
- PROJECTOR_NAME,
24
- };
@@ -1,36 +0,0 @@
1
- const { CarePlan } = require('../resources/CarePlan');
2
- const { CommunicationRequest } = require('../resources/CommunicationRequest');
3
- const { Provenance } = require('../resources/Provenance');
4
-
5
- const PROJECTOR_NAME = 'contingency';
6
- const ACTIVITY_TEXT = 'contingency-safety-net';
7
-
8
- function projectContingency(net) {
9
- const { carePlan, teamEscalation, patientReminder } = net;
10
- const stepArgs = {
11
- planId: carePlan.planId,
12
- patientId: carePlan.patientId,
13
- createdAt: carePlan.createdAt,
14
- parentStatus: carePlan.status,
15
- };
16
-
17
- const steps = [teamEscalation];
18
- if (patientReminder) steps.push(patientReminder);
19
-
20
- const out = [CarePlan.fromContingency({ carePlan })];
21
- for (const step of steps) {
22
- out.push(CommunicationRequest.fromContingencyStep({ step, ...stepArgs }));
23
- }
24
- out.push(Provenance.fromContingency({
25
- planId: carePlan.planId,
26
- recipients: steps.map((step) => step.recipient),
27
- recordedAt: carePlan.createdAt,
28
- activityText: ACTIVITY_TEXT,
29
- }));
30
- return out;
31
- }
32
-
33
- module.exports = {
34
- projectContingency,
35
- PROJECTOR_NAME,
36
- };
@@ -1,25 +0,0 @@
1
- const { getDeviceId } = require('../config/fhirConfig');
2
- const { CommunicationRequest } = require('../resources/CommunicationRequest');
3
- const { Task } = require('../resources/Task');
4
- const { Provenance } = require('../resources/Provenance');
5
-
6
- const PROJECTOR_NAME = 'dispatchedEscalation';
7
- const ACTIVITY_TEXT = 'dispatched-escalation';
8
- const TICKET_KIND = 'open_ticket';
9
-
10
- function projectDispatchedEscalation(escalation) {
11
- const primary = escalation.kind === TICKET_KIND
12
- ? Task.fromTicketEscalation({ ticket: escalation, deviceId: getDeviceId() })
13
- : CommunicationRequest.fromNotifyEscalation({ notify: escalation });
14
- const provenance = Provenance.fromDispatchedEscalation({
15
- target: primary,
16
- recordedAt: escalation.createdAt,
17
- activityText: ACTIVITY_TEXT,
18
- });
19
- return [primary, provenance];
20
- }
21
-
22
- module.exports = {
23
- projectDispatchedEscalation,
24
- PROJECTOR_NAME,
25
- };
@@ -1,42 +0,0 @@
1
- const { fhirId } = require('../helpers/fhirHelper');
2
- const { Procedure } = require('../resources/Procedure');
3
- const { Communication } = require('../resources/Communication');
4
- const { Task } = require('../resources/Task');
5
- const { CommunicationRequest } = require('../resources/CommunicationRequest');
6
- const { Provenance } = require('../resources/Provenance');
7
-
8
- const PROJECTOR_NAME = 'intervention';
9
- const ACTIVITY_TEXT = 'intervention-record';
10
- const PROCEDURE_KINDS = new Set(['otc', 'self_care', 'other']);
11
-
12
- function projectIntervention(intervention) {
13
- const primary = primaryResource(intervention);
14
-
15
- return [primary, Provenance.fromSymptomCase({
16
- provenanceId: `${intervention.interventionId}-prov`,
17
- targetRefs: [{ reference: `${primary.resourceType}/${fhirId(intervention.interventionId)}` }],
18
- recordedAt: intervention.deliveredAt,
19
- activityText: ACTIVITY_TEXT,
20
- })];
21
- }
22
-
23
- function primaryResource(intervention) {
24
- if (PROCEDURE_KINDS.has(intervention.kind)) {
25
- return Procedure.fromIntervention({ intervention });
26
- }
27
- if (intervention.kind === 'education') {
28
- return Communication.fromIntervention({ intervention });
29
- }
30
- if (intervention.kind === 'team_escalation') {
31
- return Task.fromIntervention({ intervention });
32
- }
33
- if (intervention.kind === 'follow_up_check') {
34
- return CommunicationRequest.fromIntervention({ intervention });
35
- }
36
- throw new Error(`unknown intervention variant: ${intervention.kind}`);
37
- }
38
-
39
- module.exports = {
40
- projectIntervention,
41
- PROJECTOR_NAME,
42
- };
@@ -1,179 +0,0 @@
1
- const { logger } = require('../../utils/logger');
2
- const { getSystems } = require('../config/fhirConfig');
3
- const { codeSystemUrl } = require('../helpers/fhirHelper');
4
- const { codeableConcept } = require('../helpers/elementHelper');
5
- const terminology = require('../helpers/terminologyHelper');
6
- const { PAIN_TERM } = require('../helpers/compositeHelper');
7
- const { Condition } = require('../resources/Condition');
8
- const { Device } = require('../resources/Device');
9
- const { Observation } = require('../resources/Observation');
10
- const { Patient } = require('../resources/Patient');
11
- const { Questionnaire } = require('../resources/Questionnaire');
12
- const { QuestionnaireResponse } = require('../resources/QuestionnaireResponse');
13
-
14
- const GRADE_METHOD = 'PRO-CTCAE composite → CTCAE (unofficial approximation)';
15
- const INGEST_DEVICE_ID = 'lookup-table-grade-estimator';
16
-
17
- const PAIN_PRESENCE_LINK = 'pain.presence';
18
- const PAIN_LOCATION_LINK = 'pain.location';
19
-
20
- function painLocationText(pain) {
21
- const locations = (pain.locations || []).filter(Boolean).map(String);
22
- return locations.length ? locations.join(', ') : String(pain.mostImportant || '').trim();
23
- }
24
-
25
- const linkId = (term, dimension) => `${term}.${dimension}`;
26
-
27
- const answerOption = (catalog, dimension, options) =>
28
- options.map((label, i) => {
29
- const weight = terminology.standardOrdinal(catalog, dimension, label);
30
- return {
31
- valueCoding: {
32
- system: codeSystemUrl('pro-ctcae-answer'),
33
- code: String(i),
34
- display: label,
35
- ...(weight === null
36
- ? {}
37
- : { extension: [{ url: getSystems().itemWeight, valueDecimal: weight }] }),
38
- },
39
- };
40
- });
41
-
42
- function painCanonicalItems(catalog) {
43
- return [
44
- {
45
- linkId: PAIN_PRESENCE_LINK,
46
- text: 'Dolor — presencia',
47
- type: 'coding',
48
- answerOption: answerOption(catalog, 'presence', terminology.standardScale(catalog, 'presence')),
49
- },
50
- { linkId: PAIN_LOCATION_LINK, text: 'Dolor más importante — ubicación', type: 'string' },
51
- ];
52
- }
53
-
54
- function projectProctcaeCanonical(catalog) {
55
- const items = painCanonicalItems(catalog);
56
- for (const term of terminology.allTerms(catalog)) {
57
- const textEs = terminology.codeText(catalog, term);
58
- for (const dim of terminology.dimensionsFor(catalog, term)) {
59
- if (!terminology.standardScale(catalog, dim).length) {
60
- logger.warn('[projectProctcaeCanonical] unknown dimension, item skipped', { term, dimension: dim });
61
- continue;
62
- }
63
- items.push({
64
- linkId: linkId(term, dim),
65
- text: `${textEs} — ${dim}`,
66
- type: 'coding',
67
- code: terminology.codingsFor(catalog, term),
68
- answerOption: answerOption(catalog, dim, terminology.answerOptions(catalog, term, dim)),
69
- });
70
- }
71
- }
72
- return Questionnaire.fromProctcaeCatalog({ items });
73
- }
74
-
75
- function projectTriagePatient(patientId) {
76
- return Patient.fromTriageProjection({ patientId });
77
- }
78
-
79
- function projectTriageIngestDevice(catalog) {
80
- return Device.fromTriageProjection({
81
- deviceId: INGEST_DEVICE_ID,
82
- compositeTableHash: terminology.compositeTableHash(catalog),
83
- });
84
- }
85
-
86
- function projectProctcaeSymptomObservation(catalog, sym) {
87
- const term = sym.term;
88
- const components = [];
89
- for (const dim of terminology.dimensionsFor(catalog, term)) {
90
- const answer = (sym.dimensionAnswers || {})[dim];
91
- if (answer == null) continue;
92
- const code = codeableConcept(dim, { code: dim, system: codeSystemUrl('pro-ctcae-dimension') });
93
- if (dim === 'presence') {
94
- components.push({ code, valueBoolean: terminology.affirmative(answer) });
95
- } else {
96
- const ordinal = terminology.standardOrdinal(catalog, dim, answer);
97
- components.push({
98
- code,
99
- ...(ordinal !== null ? { valueInteger: ordinal } : { valueString: String(answer) }),
100
- });
101
- }
102
- }
103
- const aeCategory = terminology.aeCategory(catalog, term);
104
- return Observation.fromTriageProjection({
105
- sym,
106
- code: { coding: terminology.codingsFor(catalog, term), text: terminology.codeText(catalog, term) },
107
- aeCategoryConcept: aeCategory
108
- ? codeableConcept(aeCategory.display, { code: aeCategory.code, system: codeSystemUrl('pro-ctcae-ae-category') })
109
- : null,
110
- components,
111
- gradeConcept: terminology.gradeConcept(sym.grade),
112
- method: codeableConcept(GRADE_METHOD),
113
- deviceId: INGEST_DEVICE_ID,
114
- });
115
- }
116
-
117
- function projectTriageQuestionnaireResponse(catalog, qr) {
118
- const byLink = new Map();
119
- const bestQid = new Map();
120
- for (const [rawQid, label] of Object.entries(qr.answers || {})) {
121
- if (label == null || String(label).trim() === '') continue;
122
- const mapped = terminology.itemForQuestionId(catalog, rawQid);
123
- if (!mapped) continue;
124
- const [term, dim] = mapped;
125
- const link = linkId(term, dim);
126
- const qidInt = /^-?\d+$/.test(String(rawQid).trim()) ? Number(rawQid) : -1;
127
- if (byLink.has(link) && qidInt <= (bestQid.get(link) ?? -1)) continue;
128
- const canonical = terminology.canonicalAnswer(catalog, rawQid, label);
129
- const ordinal = terminology.ordinalValue(catalog, term, dim, String(canonical));
130
- const answer =
131
- ordinal !== null
132
- ? { valueCoding: { system: codeSystemUrl('pro-ctcae-answer'), code: String(ordinal), display: String(label) } }
133
- : { valueString: String(label) };
134
- byLink.set(link, { linkId: link, answer: [answer] });
135
- bestQid.set(link, qidInt);
136
- }
137
- const pain = qr.pain || {};
138
- if (pain.hasPain != null && pain.hasPain !== '') {
139
- const has = terminology.affirmative(pain.hasPain);
140
- byLink.set(PAIN_PRESENCE_LINK, {
141
- linkId: PAIN_PRESENCE_LINK,
142
- answer: [{ valueCoding: { system: codeSystemUrl('pro-ctcae-answer'), code: String(has ? 1 : 0), display: String(pain.hasPain) } }],
143
- });
144
- if (has) {
145
- for (const dim of terminology.dimensionsFor(catalog, PAIN_TERM)) {
146
- const label = pain[dim];
147
- if (!label) continue;
148
- const link = linkId(PAIN_TERM, dim);
149
- const ordinal = terminology.ordinalValue(catalog, PAIN_TERM, dim, String(label));
150
- const answer =
151
- ordinal !== null
152
- ? { valueCoding: { system: codeSystemUrl('pro-ctcae-answer'), code: String(ordinal), display: String(label) } }
153
- : { valueString: String(label) };
154
- byLink.set(link, { linkId: link, answer: [answer] });
155
- }
156
- const location = painLocationText(pain);
157
- if (location) {
158
- byLink.set(PAIN_LOCATION_LINK, { linkId: PAIN_LOCATION_LINK, answer: [{ valueString: location }] });
159
- }
160
- }
161
- }
162
- return QuestionnaireResponse.fromTriageProjection({ qr, items: [...byLink.values()] });
163
- }
164
-
165
- function projectTriageSymptomCondition(catalog, sym) {
166
- return Condition.fromTriageProjection({
167
- sym,
168
- code: { coding: terminology.codingsFor(catalog, sym.term), text: terminology.codeText(catalog, sym.term) },
169
- });
170
- }
171
-
172
- module.exports = {
173
- projectProctcaeCanonical,
174
- projectTriagePatient,
175
- projectProctcaeSymptomObservation,
176
- projectTriageIngestDevice,
177
- projectTriageQuestionnaireResponse,
178
- projectTriageSymptomCondition,
179
- };
@@ -1,20 +0,0 @@
1
- const { CommunicationRequest } = require('../resources/CommunicationRequest');
2
- const { Provenance } = require('../resources/Provenance');
3
-
4
- const PROJECTOR_NAME = 'reminder';
5
- const ACTIVITY_TEXT = 'reminder-scheduled';
6
-
7
- function projectReminder(reminder) {
8
- const communicationRequest = CommunicationRequest.fromReminder({ reminder });
9
- const provenance = Provenance.fromDispatchedEscalation({
10
- target: communicationRequest,
11
- recordedAt: reminder.authoredAt,
12
- activityText: ACTIVITY_TEXT,
13
- });
14
- return [communicationRequest, provenance];
15
- }
16
-
17
- module.exports = {
18
- projectReminder,
19
- PROJECTOR_NAME,
20
- };
@@ -1,16 +0,0 @@
1
- const { RiskAssessment } = require('../resources/RiskAssessment');
2
- const { Provenance } = require('../resources/Provenance');
3
-
4
- const PROJECTOR_NAME = 'risk';
5
-
6
- function projectPatientRisk(assessment) {
7
- return [
8
- RiskAssessment.fromPatientRisk({ assessment }),
9
- Provenance.fromPatientRisk({ assessment }),
10
- ];
11
- }
12
-
13
- module.exports = {
14
- projectPatientRisk,
15
- PROJECTOR_NAME,
16
- };