@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.
- package/lib/clinical/AssistantProcessor.js +5 -34
- package/lib/clinical/config/divergenceConfig.js +1 -18
- package/lib/clinical/config/llmConfig.js +1 -9
- package/lib/clinical/context/ToolRuntimeContext.js +2 -1
- package/lib/clinical/helpers/divergenceHelper.js +0 -12
- package/lib/clinical/index.js +0 -2
- package/lib/clinical/memory/DefaultMemoryManager.js +9 -19
- package/lib/clinical/models/turnTraceModel.js +5 -18
- package/lib/clinical/providers/AnthropicProvider.js +29 -291
- package/lib/clinical/providers/BaseLLMProvider.js +24 -113
- package/lib/clinical/providers/GatewayProvider.js +3 -3
- package/lib/clinical/providers/OpenAIResponsesProvider.js +4 -46
- package/lib/clinical/services/divergenceService.js +5 -22
- package/lib/clinical/services/promptComposerService.js +15 -46
- package/lib/clinical/services/shadowMessageService.js +12 -28
- package/lib/clinical/services/shadowService.js +3 -12
- package/lib/clinical/stores/CtcaeCatalog.js +0 -20
- package/lib/config/airtableConfig.js +0 -3
- package/lib/core/BatchingManager.js +3 -10
- package/lib/core/NexusMessaging.js +13 -31
- package/lib/eval/EvalProvider.js +2 -3
- package/lib/fhir/config/fhirConfig.js +3 -34
- package/lib/fhir/constants/projectionSlugs.js +1 -41
- package/lib/fhir/helpers/elementHelper.js +0 -21
- package/lib/fhir/helpers/fhirHelper.js +2 -5
- package/lib/fhir/index.js +0 -24
- package/lib/fhir/models/envelopeModel.js +0 -5
- package/lib/fhir/projections/registerProjectors.js +0 -16
- package/lib/fhir/resources/Condition.js +2 -107
- package/lib/fhir/resources/Observation.js +1 -73
- package/lib/fhir/resources/Procedure.js +2 -34
- package/lib/fhir/resources/Provenance.js +2 -93
- package/lib/fhir/services/fhirService.js +2 -2
- package/lib/helpers/twilioHelper.js +1 -9
- package/lib/index.d.ts +0 -16
- package/lib/index.js +0 -9
- package/lib/models/messageModel.js +0 -6
- package/lib/routes/index.js +1 -21
- package/lib/services/airtableService.js +1 -2
- package/lib/shared/dtos/ContingencySafetyNet.js +3 -10
- package/lib/shared/dtos/DispatchedEscalation.js +8 -3
- package/lib/shared/dtos/Reminder.js +2 -2
- package/lib/storage/MongoStorage.js +0 -2
- package/package.json +1 -1
- package/lib/clinical/config/cohortConfig.js +0 -68
- package/lib/clinical/config/composerConfig.js +0 -42
- package/lib/clinical/config/subAgentsConfig.js +0 -44
- package/lib/clinical/flags/bridgeConfig.js +0 -14
- package/lib/clinical/flags/composerFlags.js +0 -14
- package/lib/clinical/helpers/clinicalFactsHelper.js +0 -108
- package/lib/clinical/helpers/clinicalMentionHelper.js +0 -68
- package/lib/clinical/helpers/cohortHelper.js +0 -22
- package/lib/clinical/helpers/gradeEstimateHelper.js +0 -29
- package/lib/clinical/helpers/resultsHelper.js +0 -34
- package/lib/clinical/helpers/safetyFlagsHelper.js +0 -10
- package/lib/clinical/services/bridgeService.js +0 -55
- package/lib/clinical/services/clinicalAirtableService.js +0 -162
- package/lib/clinical/services/clinicalExtractionService.js +0 -112
- package/lib/clinical/services/composerService.js +0 -59
- package/lib/clinical/services/dispatcherService.js +0 -137
- package/lib/clinical/services/reminderDispatchService.js +0 -36
- package/lib/clinical/services/skillService.js +0 -44
- package/lib/clinical/tools/analyzeSymptomPatternsTool.js +0 -279
- package/lib/clinical/tools/deliverPatientMessageTool.js +0 -202
- package/lib/clinical/tools/extractClinicalInfoTool.js +0 -167
- package/lib/clinical/tools/getActiveSymptomLandscapeTool.js +0 -141
- package/lib/clinical/tools/getPatientHistoryTool.js +0 -235
- package/lib/clinical/tools/getPatientRiskProfileTool.js +0 -71
- package/lib/clinical/tools/getRouterContextBundleTool.js +0 -93
- package/lib/clinical/tools/lookupCtcaeEntryTool.js +0 -35
- package/lib/clinical/tools/openClusterHypothesisTool.js +0 -160
- package/lib/clinical/tools/openConditionTool.js +0 -136
- package/lib/clinical/tools/recordClinicalImpressionTool.js +0 -251
- package/lib/clinical/tools/recordInterventionTool.js +0 -140
- package/lib/clinical/tools/registerClinicalTools.js +0 -39
- package/lib/clinical/tools/reportCrisisTool.js +0 -211
- package/lib/clinical/tools/reportMedicalEscalationTool.js +0 -187
- package/lib/clinical/tools/reportResultsReceivedTool.js +0 -153
- package/lib/clinical/tools/reportUnresolvedRequestv2Tool.js +0 -177
- package/lib/clinical/tools/schedulePatientReminderTool.js +0 -149
- package/lib/clinical/tools/setContingencyPlanTool.js +0 -176
- package/lib/clinical/tools/skillTool.js +0 -67
- package/lib/clinical/tools/submitRoutingDecisionTool.js +0 -291
- package/lib/clinical/tools/updateClusterHypothesisTool.js +0 -159
- package/lib/controllers/cohortController.js +0 -16
- package/lib/controllers/traceController.js +0 -26
- package/lib/core/workflowRunner.js +0 -35
- package/lib/fhir/config/fhirTablesConfig.js +0 -9
- package/lib/fhir/constants/severityPriority.js +0 -8
- package/lib/fhir/helpers/compositeHelper.js +0 -88
- package/lib/fhir/helpers/fhirReadHelper.js +0 -42
- package/lib/fhir/helpers/terminologyHelper.js +0 -260
- package/lib/fhir/projections/clusterProjection.js +0 -24
- package/lib/fhir/projections/contingencyProjection.js +0 -36
- package/lib/fhir/projections/dispatchedEscalationProjection.js +0 -25
- package/lib/fhir/projections/interventionProjection.js +0 -42
- package/lib/fhir/projections/proctcaeProjection.js +0 -179
- package/lib/fhir/projections/reminderProjection.js +0 -20
- package/lib/fhir/projections/riskProjection.js +0 -16
- package/lib/fhir/projections/routingProjection.js +0 -48
- package/lib/fhir/projections/symptomCaseProjection.js +0 -65
- package/lib/fhir/resources/CarePlan.js +0 -93
- package/lib/fhir/resources/ClinicalImpression.js +0 -118
- package/lib/fhir/resources/Communication.js +0 -65
- package/lib/fhir/resources/CommunicationRequest.js +0 -182
- package/lib/fhir/resources/Device.js +0 -24
- package/lib/fhir/resources/Patient.js +0 -18
- package/lib/fhir/resources/Questionnaire.js +0 -23
- package/lib/fhir/resources/QuestionnaireResponse.js +0 -29
- package/lib/fhir/resources/RequestOrchestration.js +0 -46
- package/lib/fhir/resources/RiskAssessment.js +0 -99
- package/lib/fhir/resources/Task.js +0 -106
- package/lib/fhir/resources/interventionExtensions.js +0 -28
- package/lib/fhir/services/clusterService.js +0 -77
- package/lib/fhir/services/contingencyService.js +0 -117
- package/lib/fhir/services/interventionService.js +0 -149
- package/lib/fhir/services/resultObservationService.js +0 -52
- package/lib/fhir/services/riskService.js +0 -101
- package/lib/fhir/services/routingService.js +0 -147
- package/lib/fhir/services/snapshotService.js +0 -28
- package/lib/fhir/services/symptomCaseService.js +0 -139
- package/lib/fhir/services/terminologyService.js +0 -58
- package/lib/fhir/services/triageService.js +0 -57
- package/lib/fhir/utils/normalizeUtils.js +0 -33
- package/lib/shared/dtos/GradedSymptom.js +0 -23
- 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
|
-
};
|