@peopl-health/nexus 5.9.2 → 5.10.0-dev.1021

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 (32) hide show
  1. package/lib/clinical/config/subAgentsConfig.js +1 -7
  2. package/lib/clinical/memory/DefaultMemoryManager.js +1 -1
  3. package/lib/clinical/providers/AnthropicProvider.js +0 -10
  4. package/lib/clinical/tools/{getPatientRiskAssessmentTool.js → getPatientRiskProfileTool.js} +4 -4
  5. package/lib/clinical/tools/getRouterContextBundleTool.js +1 -1
  6. package/lib/clinical/tools/registerClinicalTools.js +1 -1
  7. package/lib/fhir/constants/projectionSlugs.js +0 -12
  8. package/lib/fhir/index.js +1 -9
  9. package/lib/fhir/projections/registerProjectors.js +0 -6
  10. package/lib/fhir/resources/CarePlan.js +1 -23
  11. package/lib/fhir/resources/ClinicalImpression.js +1 -64
  12. package/lib/fhir/resources/Provenance.js +0 -20
  13. package/lib/fhir/resources/RiskAssessment.js +1 -40
  14. package/lib/fhir/resources/Task.js +0 -57
  15. package/lib/fhir/services/riskService.js +0 -19
  16. package/package.json +1 -1
  17. package/lib/clinical/helpers/palliativeAssessmentHelper.js +0 -62
  18. package/lib/clinical/models/patternConsultModel.js +0 -37
  19. package/lib/clinical/services/patternConsultService.js +0 -115
  20. package/lib/clinical/tools/requestPatternConsultTool.js +0 -70
  21. package/lib/clinical/tools/submitPalliativeAssessmentTool.js +0 -75
  22. package/lib/clinical/tools/submitRecommendationPlanTool.js +0 -242
  23. package/lib/clinical/tools/submitSafetyGateTool.js +0 -207
  24. package/lib/clinical/tools/updatePalliativeAssessmentTool.js +0 -74
  25. package/lib/fhir/projections/palliativeProjection.js +0 -37
  26. package/lib/fhir/projections/recommendationPlanProjection.js +0 -25
  27. package/lib/fhir/projections/safetyGateProjection.js +0 -16
  28. package/lib/fhir/services/palliativeService.js +0 -121
  29. package/lib/fhir/services/recommendationService.js +0 -27
  30. package/lib/shared/dtos/PalliativeAssessment.js +0 -62
  31. package/lib/shared/dtos/RecommendationPlan.js +0 -109
  32. package/lib/shared/dtos/RecommendationSafetyGate.js +0 -64
@@ -1,207 +0,0 @@
1
- const { ZodError } = require('zod');
2
-
3
- const { storeSafetyGate, safetyGateExists } = require('../../fhir');
4
- const {
5
- RecommendationSafetyGate,
6
- ESCALATION_ROUTES,
7
- RED_FLAG_STATUSES,
8
- } = require('../../shared/dtos/RecommendationSafetyGate');
9
- const { logger } = require('../../utils/logger');
10
-
11
- const definition = {
12
- name: 'submitSafetyGate',
13
- description: '**Does:** Commits the safety-gate evaluation (red flags mapped to high-risk adverse events, urgency, escalation route) for THIS turn\'s recommendation run. Call ONCE when generating a plan. Verification arithmetic and escalation-field consistency are repaired server-side (disclosed in `auto_repairs`); only a contradictory URGENT gate is rejected — an urgent gate demands `max_action_time_hours` (1-24), an `escalation_route`, and ≥1 confirmed/partially_confirmed red flag. An urgent gate does not notify anyone by itself: escalate via reportMedicalEscalation.\n\n**Required inputs:** `urgent_under_24h`, `summary`. Per red flag in `red_flags`: `name` + `verification_status`. `max_action_time_hours` / `escalation_route` / `reason_for_escalation` apply only when urgent.\n\n**Returns:** `accepted`, `run_id`, `urgent_under_24h`, `escalation_route`, `red_flags_count`, `must_escalate_now`, `auto_repairs`.\n\n**Side effects:** persists the gate as a run-scoped FHIR RiskAssessment.',
14
- strict: false,
15
- parameters: {
16
- type: 'object',
17
- properties: {
18
- urgent_under_24h: {
19
- type: 'boolean',
20
- description: 'True when the situation demands action within 24h. Requires max_action_time_hours + escalation_route + a confirmed/partially_confirmed red flag.',
21
- },
22
- max_action_time_hours: {
23
- type: 'integer',
24
- description: '1-24 when urgent_under_24h=true; null otherwise. Nulled server-side on a non-urgent gate.',
25
- },
26
- escalation_route: {
27
- type: 'string',
28
- enum: ESCALATION_ROUTES,
29
- description: 'Global escalation route: emergencia | urgencias | equipo_tratante_hoy. Required when urgent; nulled otherwise.',
30
- },
31
- reason_for_escalation: {
32
- type: 'string',
33
- description: 'Why this gate escalates. Nulled server-side on a non-urgent gate.',
34
- },
35
- summary: {
36
- type: 'string',
37
- description: 'Plain-language gate summary.',
38
- },
39
- red_flags: {
40
- type: 'array',
41
- description: 'Red flags evaluated this turn. Each: `{name, verification_status, escalation_route, events_confirmed[], events_denied[], related_aes[], definition[{clinical_event, ctcae_grades_reference[]}], grounded_in[]}`. verification_status is repaired from the events (denied ⇒ denied; no confirmed events ⇒ inconclusive; confirmed but a definition clinical_event is unconfirmed ⇒ partially_confirmed).',
42
- items: { type: 'object' },
43
- },
44
- },
45
- required: ['urgent_under_24h', 'summary'],
46
- },
47
- };
48
-
49
- const str = (value) => (typeof value === 'string' ? value : '');
50
- const arr = (value) => (Array.isArray(value) ? value : []);
51
- const strList = (value) => arr(value).map(str).filter(Boolean);
52
-
53
- function derivedStatus(redFlag) {
54
- if (redFlag.eventsDenied.length) return 'denied';
55
- if (!redFlag.eventsConfirmed.length) return 'inconclusive';
56
- if (!['confirmed', 'partially_confirmed'].includes(redFlag.verificationStatus)) return 'partially_confirmed';
57
- if (redFlag.verificationStatus === 'confirmed' && redFlag.definition.length) {
58
- const confirmed = new Set(redFlag.eventsConfirmed.map((event) => event.trim()));
59
- if (redFlag.definition.some((event) => !confirmed.has(event.clinicalEvent.trim()))) return 'partially_confirmed';
60
- }
61
- return redFlag.verificationStatus;
62
- }
63
-
64
- function normalizeStatus(raw) {
65
- const status = str(raw).trim();
66
- if (status === 'suspected') return null;
67
- return RED_FLAG_STATUSES.includes(status) ? status : null;
68
- }
69
-
70
- function normalizeRelatedAe(raw) {
71
- const probability = str(raw?.probability_given_patient).trim();
72
- return {
73
- ctcaeTerm: str(raw?.ctcae_term).trim(),
74
- probabilityGivenPatient: ['high', 'medium', 'low'].includes(probability) ? probability : 'low',
75
- whyIsRelevant: str(raw?.why_is_relevant).trim(),
76
- groundedIn: strList(raw?.grounded_in),
77
- };
78
- }
79
-
80
- function normalizeRedFlagEvent(raw) {
81
- return {
82
- clinicalEvent: str(raw?.clinical_event).trim(),
83
- ctcaeGradesReference: arr(raw?.ctcae_grades_reference)
84
- .map((grade) => Number(grade))
85
- .filter(Number.isInteger),
86
- };
87
- }
88
-
89
- function buildRedFlag(raw, repairs, index) {
90
- const requested = str(raw?.verification_status).trim();
91
- const base = {
92
- name: str(raw?.name).trim(),
93
- verificationStatus: normalizeStatus(raw?.verification_status) || 'inconclusive',
94
- escalationRoute: ESCALATION_ROUTES.includes(str(raw?.escalation_route).trim()) ? str(raw.escalation_route).trim() : null,
95
- eventsConfirmed: strList(raw?.events_confirmed),
96
- eventsDenied: strList(raw?.events_denied),
97
- relatedAes: arr(raw?.related_aes).map(normalizeRelatedAe),
98
- definition: arr(raw?.definition).map(normalizeRedFlagEvent).filter((event) => event.clinicalEvent),
99
- groundedIn: strList(raw?.grounded_in),
100
- };
101
- const want = derivedStatus(base);
102
- if (want !== base.verificationStatus) {
103
- repairs.push(`red_flags[${index}].verification_status: '${requested || base.verificationStatus}' → '${want}' (aritmética de eventos)`);
104
- base.verificationStatus = want;
105
- }
106
- return base;
107
- }
108
-
109
- async function handler(args = {}, context = {}) {
110
- try {
111
- const runtime = context?.toolRuntimeContext || null;
112
- if (!runtime?.turnId || !runtime?.patientCode) {
113
- return JSON.stringify({ success: false, error: 'submitSafetyGate requires an active turn context (turnId, patientCode).', data: {} });
114
- }
115
- if (typeof args?.urgent_under_24h !== 'boolean') {
116
- return JSON.stringify({ success: false, error: 'urgent_under_24h is required and must be a boolean.', data: {} });
117
- }
118
- const summary = str(args?.summary).trim();
119
- if (!summary) {
120
- return JSON.stringify({ success: false, error: 'summary is required.', data: {} });
121
- }
122
-
123
- const repairs = [];
124
- const runId = `recrun-chat-${runtime.turnId}`;
125
- const redFlags = arr(args?.red_flags).map((raw, index) => buildRedFlag(raw, repairs, index));
126
-
127
- const urgent = args.urgent_under_24h;
128
- let maxActionTimeHours = Number.isInteger(args?.max_action_time_hours) ? args.max_action_time_hours : null;
129
- let escalationRoute = ESCALATION_ROUTES.includes(str(args?.escalation_route).trim()) ? str(args.escalation_route).trim() : null;
130
- let reasonForEscalation = str(args?.reason_for_escalation).trim() || null;
131
- if (!urgent) {
132
- const stray = [];
133
- if (maxActionTimeHours !== null) { stray.push('max_action_time_hours'); maxActionTimeHours = null; }
134
- if (escalationRoute !== null) { stray.push('escalation_route'); escalationRoute = null; }
135
- if (reasonForEscalation !== null) { stray.push('reason_for_escalation'); reasonForEscalation = null; }
136
- if (stray.length) repairs.push(`no urgente ⇒ anulados campos de escalamiento: ${stray.join(', ')}`);
137
- }
138
-
139
- if (urgent) {
140
- const basis = redFlags.some((redFlag) => ['confirmed', 'partially_confirmed'].includes(redFlag.verificationStatus));
141
- const issues = [];
142
- if (maxActionTimeHours === null) issues.push('max_action_time_hours (1-24)');
143
- if (!escalationRoute) issues.push(`escalation_route (${ESCALATION_ROUTES.join(' | ')})`);
144
- if (!basis) issues.push('≥1 red flag confirmed/partially_confirmed');
145
- if (issues.length) {
146
- return JSON.stringify({ success: false, error: 'gate_inconsistent', data: { urgent_gate_requires: issues } });
147
- }
148
- }
149
-
150
- if (await safetyGateExists({ runId })) {
151
- return JSON.stringify({ success: false, error: 'gate_already_committed_for_turn', data: { run_id: runId } });
152
- }
153
-
154
- const safetyGate = new RecommendationSafetyGate({
155
- runId,
156
- patientId: runtime.patientCode,
157
- origin: 'chat',
158
- turnId: runtime.turnId,
159
- urgentUnder24h: urgent,
160
- maxActionTimeHours,
161
- escalationRoute,
162
- reasonForEscalation,
163
- summary,
164
- redFlags,
165
- createdAt: new Date().toISOString(),
166
- });
167
-
168
- await storeSafetyGate({ patientId: runtime.patientCode, safetyGate });
169
-
170
- const trace = runtime.trace || null;
171
- if (trace) {
172
- trace.setSignals({
173
- safetyGate: {
174
- runId,
175
- urgentUnder24h: urgent,
176
- escalationRoute,
177
- redFlagsCount: redFlags.length,
178
- },
179
- });
180
- } else {
181
- logger.warn('[submitSafetyGate] no trace on runtime context; safety-gate signal not recorded', { turnId: runtime.turnId });
182
- }
183
-
184
- return JSON.stringify({
185
- success: true,
186
- data: {
187
- accepted: true,
188
- run_id: runId,
189
- urgent_under_24h: urgent,
190
- escalation_route: escalationRoute,
191
- red_flags_count: redFlags.length,
192
- must_escalate_now: urgent,
193
- auto_repairs: repairs,
194
- },
195
- });
196
- } catch (err) {
197
- if (err instanceof ZodError) {
198
- return JSON.stringify({ success: false, error: 'gate_schema_invalid', data: { issues: err.issues.map((issue) => ({ path: issue.path.join('.'), problem: issue.message })) } });
199
- }
200
- return JSON.stringify({ success: false, error: err?.message || 'submitSafetyGate failed', data: {} });
201
- }
202
- }
203
-
204
- module.exports = {
205
- definition,
206
- handler,
207
- };
@@ -1,74 +0,0 @@
1
- const { assessmentUid, buildAssessment } = require('../helpers/palliativeAssessmentHelper');
2
- const { storePalliativeAssessment, readPalliativeByTurn } = require('../../fhir');
3
- const { fhirId } = require('../../fhir/helpers/fhirHelper');
4
-
5
- const definition = {
6
- name: 'updatePalliativeAssessment',
7
- description: '**Does:** Full-replacement correction path for the Phase-1 palliative assessment — issues a NEW assessment (new `assessment_id`); it is NOT a field-level patch. The prior assessment_id is returned as `replaced_prior_assessment_id` for audit lineage.\n\n**Required inputs:** `assessment` (one object, identical shape to `submitPalliativeAssessment`).\n\n**When NOT to call:**\n- Before a Phase-1 assessment exists for the turn — returns `no_assessment_to_update`; call `submitPalliativeAssessment` first.\n\n**Returns:** `accepted`, `assessment_id` (new), `replaced_prior_assessment_id`, `turn_id`.\n\n**Side effects:** persists a replacement FHIR `ClinicalImpression` (+ a `Task` per urgent finding) + `Provenance`.',
8
- strict: false,
9
- parameters: {
10
- type: 'object',
11
- properties: {
12
- assessment: {
13
- type: 'object',
14
- description: 'The corrected palliative assessment — full replacement, same shape as submitPalliativeAssessment.',
15
- },
16
- },
17
- required: ['assessment'],
18
- },
19
- };
20
-
21
- function fail(error, data = {}) {
22
- return JSON.stringify({ success: false, error, data });
23
- }
24
-
25
- async function handler(args = {}, context = {}) {
26
- try {
27
- const runtime = context?.toolRuntimeContext || null;
28
- if (!runtime?.turnId || !runtime?.patientCode) {
29
- return fail('updatePalliativeAssessment requires an active turn context (turnId, patientCode).');
30
- }
31
- const raw = args?.assessment;
32
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
33
- return fail('assessment is required (non-empty object)');
34
- }
35
-
36
- const patientId = runtime.patientCode;
37
- const turnId = runtime.turnId;
38
-
39
- const prior = await readPalliativeByTurn({ patientId, turnId });
40
- if (!prior) {
41
- return fail('no_assessment_to_update — call submitPalliativeAssessment first', { turn_id: turnId });
42
- }
43
-
44
- const assessment = buildAssessment({
45
- raw,
46
- assessmentId: assessmentUid(),
47
- patientId,
48
- turnId,
49
- committedAt: new Date().toISOString(),
50
- });
51
- await storePalliativeAssessment({ patientId, assessment });
52
-
53
- const assessmentId = fhirId(assessment.assessmentId);
54
- runtime.setPalliativeAssessment({ assessmentId, turnId });
55
- runtime.trace.setSignals({ palliativeAssessmentId: assessmentId });
56
-
57
- return JSON.stringify({
58
- success: true,
59
- data: {
60
- accepted: true,
61
- assessment_id: assessmentId,
62
- replaced_prior_assessment_id: fhirId(prior.assessmentId),
63
- turn_id: turnId,
64
- },
65
- });
66
- } catch (err) {
67
- return JSON.stringify({ success: false, error: err?.message || 'updatePalliativeAssessment failed', data: {} });
68
- }
69
- }
70
-
71
- module.exports = {
72
- definition,
73
- handler,
74
- };
@@ -1,37 +0,0 @@
1
- const { getDeviceId } = require('../config/fhirConfig');
2
- const { fhirId } = require('../helpers/fhirHelper');
3
- const { ClinicalImpression } = require('../resources/ClinicalImpression');
4
- const { Task } = require('../resources/Task');
5
- const { Provenance } = require('../resources/Provenance');
6
-
7
- const PROJECTOR_NAME = 'palliativeAssessment';
8
- const ACTIVITY_TEXT = 'palliative-assessment';
9
-
10
- function projectPalliativeAssessment(assessment) {
11
- const deviceId = getDeviceId();
12
- const impression = ClinicalImpression.fromPalliativeAssessment({ assessment });
13
- const tasks = assessment.findings
14
- .filter((finding) => finding.escalation !== null)
15
- .map((finding) => Task.fromPalliativeEscalation({
16
- assessmentId: assessment.assessmentId,
17
- committedAt: assessment.committedAt,
18
- finding,
19
- deviceId,
20
- }));
21
- const targetRefs = [
22
- { reference: `ClinicalImpression/${fhirId(assessment.assessmentId)}` },
23
- ...tasks.map((task) => ({ reference: `Task/${task.id}` })),
24
- ];
25
- const provenance = Provenance.fromSymptomCase({
26
- provenanceId: `${assessment.assessmentId}-prov`,
27
- targetRefs,
28
- recordedAt: assessment.committedAt,
29
- activityText: ACTIVITY_TEXT,
30
- });
31
- return [impression, ...tasks, provenance];
32
- }
33
-
34
- module.exports = {
35
- projectPalliativeAssessment,
36
- PROJECTOR_NAME,
37
- };
@@ -1,25 +0,0 @@
1
- const { ClinicalImpression } = require('../resources/ClinicalImpression');
2
- const { CarePlan } = require('../resources/CarePlan');
3
- const { Task } = require('../resources/Task');
4
- const { Provenance } = require('../resources/Provenance');
5
- const { recommendationId } = require('../../shared/dtos/RecommendationPlan');
6
-
7
- const PROJECTOR_NAME = 'recommendationPlan';
8
-
9
- function projectRecommendationPlan({ plan, hasSafetyGate }) {
10
- const ranked = [...plan.elements].sort((a, b) => a.rank - b.rank);
11
- const taskIds = ranked.map((element) => recommendationId(plan.runId, element.rank));
12
- const impression = ClinicalImpression.fromRecommendationPlan({ plan });
13
- const carePlan = CarePlan.fromRecommendationPlan({ plan, taskIds, hasSafetyGate });
14
- const tasks = ranked.map((element, index) => Task.fromRecommendationPlan({ plan, element, taskId: taskIds[index] }));
15
- const targetRefs = [impression, carePlan, ...tasks].map(
16
- (resource) => ({ reference: `${resource.resourceType}/${resource.id}` }),
17
- );
18
- const provenance = Provenance.fromRecommendationPlan({ plan, targetRefs });
19
- return [impression, carePlan, ...tasks, provenance];
20
- }
21
-
22
- module.exports = {
23
- projectRecommendationPlan,
24
- PROJECTOR_NAME,
25
- };
@@ -1,16 +0,0 @@
1
- const { RiskAssessment } = require('../resources/RiskAssessment');
2
- const { Provenance } = require('../resources/Provenance');
3
-
4
- const PROJECTOR_NAME = 'safetyGate';
5
-
6
- function projectSafetyGate(gate) {
7
- return [
8
- RiskAssessment.fromSafetyGate({ gate }),
9
- Provenance.fromSafetyGate({ gate }),
10
- ];
11
- }
12
-
13
- module.exports = {
14
- projectSafetyGate,
15
- PROJECTOR_NAME,
16
- };
@@ -1,121 +0,0 @@
1
- const { ZodError } = require('zod');
2
-
3
- const { fhirId } = require('../helpers/fhirHelper');
4
- const { extMap, extValues, identifierValue } = require('../helpers/fhirReadHelper');
5
- const { PALLIATIVE_ASSESSMENT_SLUG } = require('../constants/projectionSlugs');
6
- const { PROJECTOR_NAME } = require('../projections/palliativeProjection');
7
- const { getFhirStore } = require('../stores/fhirStore');
8
- const { PalliativeAssessment } = require('../../shared/dtos/PalliativeAssessment');
9
- const { logger } = require('../../utils/logger');
10
- const { project, storeResources } = require('./fhirService');
11
-
12
- async function storePalliativeAssessment({ patientId, assessment }) {
13
- if (assessment.patientId !== patientId) {
14
- throw new Error(`storePalliativeAssessment patientId mismatch: expected ${patientId}`);
15
- }
16
- const bundle = project([{ name: PROJECTOR_NAME, aggregate: assessment }]);
17
- const resources = bundle.entry.map((entry) => entry.resource);
18
- const { stored } = await storeResources(resources);
19
- return { count: stored.length, ids: stored };
20
- }
21
-
22
- async function readPalliativeByTurn({ patientId, turnId }) {
23
- const store = getFhirStore();
24
- const impression = await findImpression(store, patientId, turnId);
25
- if (!impression) return null;
26
- const assessmentId = identifierValue(impression, PALLIATIVE_ASSESSMENT_SLUG);
27
- const provenance = await store.getByReference(`Provenance/${fhirId(`${assessmentId}-prov`)}`);
28
- if (!provenance) return null;
29
- return reconstruct(store, impression, provenance, patientId, turnId);
30
- }
31
-
32
- async function findImpression(store, patientId, turnId) {
33
- const patient = `Patient/${fhirId(patientId)}`;
34
- const impressions = (await store.find({ patient, resourceType: 'ClinicalImpression' }))
35
- .filter((resource) => identifierValue(resource, PALLIATIVE_ASSESSMENT_SLUG));
36
- return impressions.find((impression) => extMap(impression)['palliative-turn-id'] === turnId) || null;
37
- }
38
-
39
- async function reconstruct(store, impression, provenance, patientId, turnId) {
40
- const assessmentId = identifierValue(impression, PALLIATIVE_ASSESSMENT_SLUG);
41
- const escalations = await readEscalations(store, provenance, patientId);
42
- try {
43
- return new PalliativeAssessment({
44
- assessmentId,
45
- patientId,
46
- turnId,
47
- committedAt: impression.effectiveDateTime,
48
- evaluationSummary: impression.summary || '',
49
- rankingRationale: impression.description || '',
50
- escalationReason: (impression.note && impression.note[0] && impression.note[0].text) || null,
51
- findings: (impression.finding || []).map((finding) => reconstructFinding(finding, escalations)),
52
- });
53
- } catch (error) {
54
- if (error instanceof ZodError) {
55
- logger.warn({ assessmentId: impression.id }, 'skipping malformed palliative ClinicalImpression');
56
- return null;
57
- }
58
- throw error;
59
- }
60
- }
61
-
62
- async function readEscalations(store, provenance, patientId) {
63
- const taskRefs = ((provenance && provenance.target) || [])
64
- .map((target) => target.reference)
65
- .filter((ref) => ref && ref.startsWith('Task/'));
66
- const byRank = {};
67
- for (const ref of taskRefs) {
68
- const task = await store.getByReference(ref);
69
- if (!task) continue;
70
- const rank = extMap(task)['palliative-escalation-rank'];
71
- if (rank === undefined || rank === null) continue;
72
- byRank[rank] = reconstructEscalation(task, patientId);
73
- }
74
- return byRank;
75
- }
76
-
77
- function reconstructFinding(finding, escalations) {
78
- const ext = extMap(finding);
79
- const nameShort = ccText(finding.item && finding.item.concept);
80
- const rank = ext['palliative-finding-rank'];
81
- return {
82
- rank,
83
- elementType: ext['palliative-finding-element-type'],
84
- nameShort,
85
- timeWindow: ext['palliative-finding-time-window'] || '',
86
- symptomTerms: extValues(finding, 'palliative-finding-symptom-term'),
87
- clinicalDescription: ext['palliative-finding-clinical-description'] || '',
88
- whyRelevant: ext['palliative-finding-why-relevant'] || '',
89
- qolImpact: ext['palliative-finding-qol-impact'] || null,
90
- qolCriteria: ext['palliative-finding-qol-criteria'] || '',
91
- improvementCapacity: ext['palliative-finding-improvement-capacity'] || null,
92
- improvementCriteria: ext['palliative-finding-improvement-criteria'] || '',
93
- areMedsNeeded: ext['palliative-finding-are-meds-needed'] ?? null,
94
- needsCharacterization: ext['palliative-finding-needs-characterization'] ?? null,
95
- characterizationCriteria: ext['palliative-finding-characterization-criteria'] || '',
96
- probeQuestions: extValues(finding, 'palliative-finding-probe-question'),
97
- auditNotes: ext['palliative-finding-audit-notes'] || '',
98
- clusterNote: ext['palliative-finding-cluster-note'] ?? null,
99
- escalation: escalations[rank] || null,
100
- };
101
- }
102
-
103
- function reconstructEscalation(task, patientId) {
104
- const end = task.restriction && task.restriction.period && task.restriction.period.end;
105
- const hours = end
106
- ? Math.round((new Date(end).getTime() - new Date(task.authoredOn).getTime()) / 3600000)
107
- : 24;
108
- return {
109
- patientId,
110
- maxActionTimeHours: hours,
111
- escalationRoutes: (task.requestedPerformer || []).map((performer) => ccText(performer.concept)),
112
- escalationReason: task.description || '',
113
- };
114
- }
115
-
116
- const ccText = (concept) => (concept && concept.text) || null;
117
-
118
- module.exports = {
119
- storePalliativeAssessment,
120
- readPalliativeByTurn,
121
- };
@@ -1,27 +0,0 @@
1
- const { fhirId } = require('../helpers/fhirHelper');
2
- const { PROJECTOR_NAME } = require('../projections/recommendationPlanProjection');
3
- const { getFhirStore } = require('../stores/fhirStore');
4
- const { project, storeResources } = require('./fhirService');
5
-
6
- async function storeRecommendationPlan({ patientId, plan }) {
7
- if (plan.patientId !== patientId) {
8
- throw new Error(`storeRecommendationPlan patientId mismatch: expected ${patientId}`);
9
- }
10
- const store = getFhirStore();
11
- const gate = await store.getByReference(`RiskAssessment/${fhirId(`${plan.runId}-safety`)}`);
12
- const bundle = project([{ name: PROJECTOR_NAME, aggregate: { plan, hasSafetyGate: Boolean(gate) } }]);
13
- const resources = bundle.entry.map((entry) => entry.resource);
14
- const { stored } = await storeResources(resources);
15
- return { count: stored.length, ids: stored };
16
- }
17
-
18
- async function recommendationPlanExists({ runId }) {
19
- const store = getFhirStore();
20
- const resource = await store.getByReference(`Provenance/${fhirId(`${runId}-careplan-prov`)}`);
21
- return Boolean(resource);
22
- }
23
-
24
- module.exports = {
25
- storeRecommendationPlan,
26
- recommendationPlanExists,
27
- };
@@ -1,62 +0,0 @@
1
- const { z } = require('zod');
2
-
3
- const { BaseDto } = require('./BaseDto');
4
-
5
- const dateTime = z.iso.datetime({ offset: true });
6
-
7
- const escalationSchema = z.strictObject({
8
- patientId: z.string(),
9
- maxActionTimeHours: z.number().int().min(1).max(24),
10
- escalationRoutes: z.array(z.string()).default([]),
11
- escalationReason: z.string(),
12
- });
13
-
14
- const findingSchema = z.strictObject({
15
- rank: z.number().int().min(1),
16
- elementType: z.enum(['cluster', 'individual']),
17
- nameShort: z.string().min(1),
18
- timeWindow: z.string().default(''),
19
- symptomTerms: z.array(z.string()).default([]),
20
- clinicalDescription: z.string().default(''),
21
- whyRelevant: z.string().default(''),
22
- qolImpact: z.enum(['Low', 'Medium', 'High']).nullable().default(null),
23
- qolCriteria: z.string().default(''),
24
- improvementCapacity: z.enum(['Low', 'Medium', 'High']).nullable().default(null),
25
- improvementCriteria: z.string().default(''),
26
- areMedsNeeded: z.boolean().nullable().default(null),
27
- needsCharacterization: z.boolean().nullable().default(null),
28
- characterizationCriteria: z.string().default(''),
29
- probeQuestions: z.array(z.string()).default([]),
30
- auditNotes: z.string().default(''),
31
- clusterNote: z.string().nullable().default(null),
32
- escalation: escalationSchema.nullable().default(null),
33
- });
34
-
35
- const schema = z.strictObject({
36
- schemaVersion: z.string().default('1'),
37
- assessmentId: z.string(),
38
- patientId: z.string(),
39
- turnId: z.string(),
40
- committedAt: dateTime,
41
- evaluationSummary: z.string().default(''),
42
- rankingRationale: z.string().default(''),
43
- escalationReason: z.string().nullable().default(null),
44
- findings: z.array(findingSchema).default([]).refine(hasContiguousRanks, {
45
- message: 'findings ranks must be unique and contiguous 1..N',
46
- }),
47
- });
48
-
49
- function hasContiguousRanks(findings) {
50
- const ranks = new Set(findings.map((finding) => finding.rank));
51
- if (ranks.size !== findings.length) return false;
52
- for (let expected = 1; expected <= findings.length; expected += 1) {
53
- if (!ranks.has(expected)) return false;
54
- }
55
- return true;
56
- }
57
-
58
- class PalliativeAssessment extends BaseDto {}
59
-
60
- PalliativeAssessment.schema = schema;
61
-
62
- module.exports = { PalliativeAssessment };
@@ -1,109 +0,0 @@
1
- const { z } = require('zod');
2
-
3
- const { BaseDto } = require('./BaseDto');
4
-
5
- const dateTime = z.iso.datetime({ offset: true });
6
-
7
- const ELEMENT_TYPES = ['cluster', 'individual'];
8
- const SYMPTOM_STATUSES = ['confirmed', 'suspected', 'denied'];
9
- const PALLIATIVE_ROUTES = ['equipo_medico_tratante', 'abordaje_onco_nutricion', 'abordaje_psico_onco'];
10
- const QOL_CATEGORIES = ['low', 'moderate', 'high'];
11
- const EFFECTIVENESS = ['low', 'medium', 'high'];
12
- const FOLLOWUP_TYPES = ['checkin_48h', 'team_referral', 'none'];
13
-
14
- const symptomSchema = z.strictObject({
15
- ctcaeTerm: z.string(),
16
- grade: z.number().int().min(1).max(5).nullable().default(null),
17
- verificationStatus: z.enum(SYMPTOM_STATUSES),
18
- });
19
-
20
- const escalationSchema = z.strictObject({
21
- urgentUnder24h: z.boolean().default(false),
22
- maxActionTimeHours: z.number().int().min(1).max(24).nullable().default(null),
23
- routes: z.array(z.enum(PALLIATIVE_ROUTES)).default([]),
24
- clinicalCriteria: z.string().nullable().default(null),
25
- });
26
-
27
- const otcOfferSchema = z.strictObject({
28
- label: z.string(),
29
- dose: z.string().nullable().default(null),
30
- frequency: z.string().nullable().default(null),
31
- duration: z.string().nullable().default(null),
32
- warnings: z.string().nullable().default(null),
33
- });
34
-
35
- const patientSchema = z.strictObject({
36
- mainRecommendation: z.string(),
37
- whenToEscalate: z.string().nullable().default(null),
38
- contraindications: z.string().nullable().default(null),
39
- expectedEffectiveness: z.enum(EFFECTIVENESS),
40
- });
41
-
42
- const recommendationSchema = z.strictObject({
43
- symptomsPatientFacing: z.array(z.string()).min(1).max(6),
44
- isPharmacologicalMedNeeded: z.boolean(),
45
- patient: patientSchema,
46
- otcOffer: otcOfferSchema.nullable().default(null),
47
- followup: z.enum(FOLLOWUP_TYPES).nullable().default(null),
48
- followupUrgency: z.string().nullable().default(null),
49
- });
50
-
51
- const elementSchema = z.strictObject({
52
- rank: z.number().int().min(1),
53
- type: z.enum(ELEMENT_TYPES),
54
- nameShort: z.string(),
55
- timeWindow: z.string().nullable().default(null),
56
- symptoms: z.array(symptomSchema).default([]),
57
- targetedCtcaeTerms: z.array(z.string()).min(1),
58
- whyRelevant: z.string(),
59
- qolImpact: z.enum(QOL_CATEGORIES).nullable().default(null),
60
- improvementCapacity: z.enum(QOL_CATEGORIES).nullable().default(null),
61
- escalation: escalationSchema,
62
- recommendation: recommendationSchema,
63
- groundedIn: z.array(z.string()).default([]),
64
- });
65
-
66
- const takeawaysSchema = z.strictObject({
67
- safetyConfirmationNeeded: z.boolean().default(false),
68
- safetyConfirmationQuestion: z.string().nullable().default(null),
69
- vitalRiskPossibleNow: z.boolean().default(false),
70
- summaryPatientFacing: z.string(),
71
- summaryInternal: z.string(),
72
- })
73
- .refine(
74
- (t) => !t.safetyConfirmationNeeded || Boolean(t.safetyConfirmationQuestion && t.safetyConfirmationQuestion.trim()),
75
- { path: ['safetyConfirmationQuestion'], message: 'safetyConfirmationNeeded=true ⇒ exactamente 1 pregunta cerrada' },
76
- )
77
- .refine(
78
- (t) => t.safetyConfirmationNeeded || !t.safetyConfirmationQuestion,
79
- { path: ['safetyConfirmationQuestion'], message: 'safetyConfirmationNeeded=false ⇒ null' },
80
- );
81
-
82
- const schema = z.strictObject({
83
- schemaVersion: z.string().default('1'),
84
- runId: z.string(),
85
- patientId: z.string(),
86
- origin: z.enum(['chat', 'triage']).default('chat'),
87
- turnId: z.string(),
88
- takeaways: takeawaysSchema,
89
- elements: z.array(elementSchema).min(1),
90
- createdAt: dateTime,
91
- });
92
-
93
- class RecommendationPlan extends BaseDto {}
94
-
95
- RecommendationPlan.schema = schema;
96
-
97
- const FHIR_ID_MAX = 64;
98
-
99
- const fhirSafe = (value) => String(value).replace(/[^A-Za-z0-9.-]/g, '-');
100
-
101
- const recommendationId = (runId, rank) => {
102
- const suffix = `-rec${fhirSafe(rank)}`;
103
- return `${fhirSafe(runId).slice(0, FHIR_ID_MAX - suffix.length)}${suffix}`;
104
- };
105
-
106
- module.exports = {
107
- RecommendationPlan,
108
- recommendationId,
109
- };