@peopl-health/nexus 5.45.0-dev.7142 → 5.45.0-dev.7159
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.
|
@@ -2,6 +2,8 @@ const { Config_ID } = require('../../config/airtableConfig');
|
|
|
2
2
|
|
|
3
3
|
const { getRecordByFilter } = require('../../services/airtableService');
|
|
4
4
|
|
|
5
|
+
const { ESCALATION_ROUTES } = require('../../shared/dtos/RecommendationSafetyGate');
|
|
6
|
+
|
|
5
7
|
const { MapCache } = require('../../utils/MapCache');
|
|
6
8
|
const { safeParse, safeParseArray, isPlainObject } = require('../../utils/jsonUtils');
|
|
7
9
|
|
|
@@ -14,6 +16,70 @@ const STOCHASTIC_DEFAULTS = { enabled: false, presetId: '' };
|
|
|
14
16
|
|
|
15
17
|
const cache = new MapCache({ maxSize: 1, ttl: CACHE_TTL });
|
|
16
18
|
|
|
19
|
+
function parseSafetyFlagPatterns(value) {
|
|
20
|
+
if (!value) return [];
|
|
21
|
+
const parsedPatterns = safeParseArray(value);
|
|
22
|
+
if (!parsedPatterns) throw new Error('divergence config SAFETY_FLAG_PATTERNS is not a JSON array');
|
|
23
|
+
return parsedPatterns.map((row) => {
|
|
24
|
+
if (!isPlainObject(row)) throw new Error('divergence config SAFETY_FLAG_PATTERNS row is not an object');
|
|
25
|
+
const { flag, pattern } = row;
|
|
26
|
+
if (!flag || !pattern) throw new Error('divergence config SAFETY_FLAG_PATTERNS row missing flag or pattern');
|
|
27
|
+
try {
|
|
28
|
+
return { flag, re: new RegExp(pattern, 'i') };
|
|
29
|
+
} catch (err) {
|
|
30
|
+
throw new Error(`divergence config SAFETY_FLAG_PATTERNS pattern for ${flag} is not a valid regex: ${err.message}`);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseRedFlagMinRoute(row) {
|
|
36
|
+
if (!isPlainObject(row)) throw new Error('divergence config RED_FLAG_MIN_ROUTES row is not an object');
|
|
37
|
+
const {
|
|
38
|
+
flag,
|
|
39
|
+
ctcae_terms: ctcaeTerms,
|
|
40
|
+
min_route: minRoute,
|
|
41
|
+
max_action_time_hours: maxActionTimeHours,
|
|
42
|
+
requires_risk: requiresRisk = true,
|
|
43
|
+
} = row;
|
|
44
|
+
if (typeof flag !== 'string' || !flag.trim()) throw new Error('divergence config RED_FLAG_MIN_ROUTES row missing flag');
|
|
45
|
+
if (!Array.isArray(ctcaeTerms) || !ctcaeTerms.length || !ctcaeTerms.every((term) => typeof term === 'string' && term.trim())) {
|
|
46
|
+
throw new Error(`divergence config RED_FLAG_MIN_ROUTES ctcae_terms for ${flag} is not a non-empty list of terms`);
|
|
47
|
+
}
|
|
48
|
+
if (!ESCALATION_ROUTES.includes(minRoute)) {
|
|
49
|
+
throw new Error(`divergence config RED_FLAG_MIN_ROUTES min_route for ${flag} is not one of ${ESCALATION_ROUTES.join(', ')}`);
|
|
50
|
+
}
|
|
51
|
+
if (!Number.isInteger(maxActionTimeHours) || maxActionTimeHours < 1 || maxActionTimeHours > 24) {
|
|
52
|
+
throw new Error(`divergence config RED_FLAG_MIN_ROUTES max_action_time_hours for ${flag} is not an integer from 1 to 24`);
|
|
53
|
+
}
|
|
54
|
+
if (typeof requiresRisk !== 'boolean') throw new Error(`divergence config RED_FLAG_MIN_ROUTES requires_risk for ${flag} is not a boolean`);
|
|
55
|
+
return {
|
|
56
|
+
flag, ctcaeTerms, minRoute, maxActionTimeHours, requiresRisk,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseRedFlagMinRoutes(value) {
|
|
61
|
+
const routes = { rows: [], invalidRows: [] };
|
|
62
|
+
if (!value) return routes;
|
|
63
|
+
const parsedRoutes = safeParseArray(value);
|
|
64
|
+
if (!parsedRoutes) throw new Error('divergence config RED_FLAG_MIN_ROUTES is not a JSON array');
|
|
65
|
+
for (const row of parsedRoutes) {
|
|
66
|
+
try {
|
|
67
|
+
routes.rows.push(parseRedFlagMinRoute(row));
|
|
68
|
+
} catch (err) {
|
|
69
|
+
routes.invalidRows.push(err.message);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return routes;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseIsolated(parse) {
|
|
76
|
+
try {
|
|
77
|
+
return { value: parse(), error: null };
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return { value: null, error };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
17
83
|
async function load() {
|
|
18
84
|
const cached = cache.get(CACHE_KEY);
|
|
19
85
|
if (cached) return cached;
|
|
@@ -46,23 +112,13 @@ async function load() {
|
|
|
46
112
|
const stochastic = { ...STOCHASTIC_DEFAULTS, ...(parsedStochastic || {}) };
|
|
47
113
|
if (stochastic.enabled && !stochastic.presetId) throw new Error('divergence config STOCHASTIC.presetId is required when enabled');
|
|
48
114
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (!flag || !pattern) throw new Error('divergence config SAFETY_FLAG_PATTERNS row missing flag or pattern');
|
|
57
|
-
try {
|
|
58
|
-
return { flag, re: new RegExp(pattern, 'i') };
|
|
59
|
-
} catch (err) {
|
|
60
|
-
throw new Error(`divergence config SAFETY_FLAG_PATTERNS pattern for ${flag} is not a valid regex: ${err.message}`);
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const config = { FIELDS: fields, ORACLE_CONNECTION: oracleConnection, STOCHASTIC: stochastic, SAFETY_FLAG_PATTERNS: safetyFlagPatterns };
|
|
115
|
+
const config = {
|
|
116
|
+
FIELDS: fields,
|
|
117
|
+
ORACLE_CONNECTION: oracleConnection,
|
|
118
|
+
STOCHASTIC: stochastic,
|
|
119
|
+
SAFETY_FLAG_PATTERNS: parseIsolated(() => parseSafetyFlagPatterns(byKey.SAFETY_FLAG_PATTERNS)),
|
|
120
|
+
RED_FLAG_MIN_ROUTES: parseIsolated(() => parseRedFlagMinRoutes(byKey.RED_FLAG_MIN_ROUTES)),
|
|
121
|
+
};
|
|
66
122
|
cache.set(CACHE_KEY, config);
|
|
67
123
|
return config;
|
|
68
124
|
}
|
|
@@ -72,9 +128,16 @@ async function get(key) {
|
|
|
72
128
|
return config[key];
|
|
73
129
|
}
|
|
74
130
|
|
|
131
|
+
async function getIsolated(key) {
|
|
132
|
+
const { value, error } = await get(key);
|
|
133
|
+
if (error) throw error;
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
|
|
75
137
|
module.exports = {
|
|
76
138
|
getFields: () => get('FIELDS'),
|
|
77
139
|
getOracleConnection: () => get('ORACLE_CONNECTION'),
|
|
78
140
|
getStochastic: () => get('STOCHASTIC'),
|
|
79
|
-
getSafetyFlagPatterns: () =>
|
|
141
|
+
getSafetyFlagPatterns: () => getIsolated('SAFETY_FLAG_PATTERNS'),
|
|
142
|
+
getRedFlagMinRoutes: () => getIsolated('RED_FLAG_MIN_ROUTES'),
|
|
80
143
|
};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
const { ESCALATION_ROUTES, RED_FLAG_STATUSES } = require('../../shared/dtos/RecommendationSafetyGate');
|
|
2
|
+
const { aliasClosure } = require('../../fhir/helpers/symptomAliasHelper');
|
|
2
3
|
const { str, arr, strList } = require('../../utils/coercionUtils');
|
|
3
4
|
const { foldAccents } = require('../../utils/formatUtils');
|
|
4
5
|
|
|
5
6
|
const ECHO_SHARE = 0.5;
|
|
7
|
+
const ROUTE_FLOOR_UNVERIFIABLE_CODE = 'route_floor_unverifiable';
|
|
6
8
|
|
|
7
9
|
const tokensOf = (text) => foldAccents(str(text)).toLowerCase().match(/[a-z0-9]{4,}/g) || [];
|
|
8
10
|
|
|
@@ -107,6 +109,93 @@ function normalizeGateEscalation(raw, repairs) {
|
|
|
107
109
|
return { urgent, maxActionTimeHours, escalationRoute, reasonForEscalation };
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
const routeUrgency = (route) => ESCALATION_ROUTES.indexOf(route);
|
|
113
|
+
|
|
114
|
+
const aliasKeySet = (catalog, terms) => new Set(terms.flatMap((term) => aliasClosure(catalog, term)));
|
|
115
|
+
|
|
116
|
+
function riskSymptomKeys(catalog, riskAssessment) {
|
|
117
|
+
return aliasKeySet(catalog, [
|
|
118
|
+
...arr(riskAssessment?.predictedAes)
|
|
119
|
+
.filter((ae) => ae?.severityIfConfirmed === 'emergency')
|
|
120
|
+
.flatMap((ae) => arr(ae.relatedSymptomTerms)),
|
|
121
|
+
...arr(riskAssessment?.diseaseRisks).flatMap((risk) => arr(risk?.relatedSymptomTerms)),
|
|
122
|
+
]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const unverifiableFloor = (subject, reason) => ({
|
|
126
|
+
path: 'escalation_route',
|
|
127
|
+
code: ROUTE_FLOOR_UNVERIFIABLE_CODE,
|
|
128
|
+
warning: true,
|
|
129
|
+
problem: `${subject}: ${reason}; el gate no se elevó`,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
function applyRouteFloor(escalation, {
|
|
133
|
+
catalog,
|
|
134
|
+
rows = [],
|
|
135
|
+
invalidRows = [],
|
|
136
|
+
affirmedKeys = new Set(),
|
|
137
|
+
riskAssessment = null,
|
|
138
|
+
riskReadFailed = false,
|
|
139
|
+
freeTextPending = false,
|
|
140
|
+
configError = null,
|
|
141
|
+
} = {}, repairs = [], warnings = []) {
|
|
142
|
+
if (configError) {
|
|
143
|
+
warnings.push(unverifiableFloor('rutas mínimas no verificables', `no se pudo leer RED_FLAG_MIN_ROUTES (${configError})`));
|
|
144
|
+
return escalation;
|
|
145
|
+
}
|
|
146
|
+
if (invalidRows.length) {
|
|
147
|
+
warnings.push({
|
|
148
|
+
path: 'escalation_route',
|
|
149
|
+
code: ROUTE_FLOOR_UNVERIFIABLE_CODE,
|
|
150
|
+
warning: true,
|
|
151
|
+
problem: `rutas mínimas incompletas: se ignoraron filas inválidas de RED_FLAG_MIN_ROUTES (${invalidRows.join('; ')}); esas filas no pueden elevar el gate`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const riskKeys = riskSymptomKeys(catalog, riskAssessment);
|
|
155
|
+
const fired = [];
|
|
156
|
+
for (const row of rows) {
|
|
157
|
+
const termKeys = [...aliasKeySet(catalog, row.ctcaeTerms)];
|
|
158
|
+
const eligibleKeys = termKeys.filter((key) => !row.requiresRisk || riskKeys.has(key));
|
|
159
|
+
if (eligibleKeys.some((key) => affirmedKeys.has(key))) {
|
|
160
|
+
fired.push(row);
|
|
161
|
+
} else if (row.requiresRisk && !riskAssessment && termKeys.some((key) => affirmedKeys.has(key))) {
|
|
162
|
+
const reason = riskReadFailed ? 'no se pudo leer el perfil de riesgo' : 'no hay perfil de riesgo';
|
|
163
|
+
warnings.push(unverifiableFloor(`ruta mínima ${row.minRoute} de ${row.flag} no verificable`, reason));
|
|
164
|
+
} else if (freeTextPending && eligibleKeys.length) {
|
|
165
|
+
warnings.push(unverifiableFloor(`ruta mínima ${row.minRoute} de ${row.flag} no verificable`, 'el clasificador del texto libre no ha terminado'));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!fired.length) return escalation;
|
|
169
|
+
const currentRoute = escalation.urgent ? escalation.escalationRoute : null;
|
|
170
|
+
const currentHours = escalation.urgent ? escalation.maxActionTimeHours : null;
|
|
171
|
+
const floorRoute = fired.map((row) => row.minRoute).sort((left, right) => routeUrgency(left) - routeUrgency(right))[0];
|
|
172
|
+
const floorHours = Math.min(...fired.map((row) => row.maxActionTimeHours));
|
|
173
|
+
const route = currentRoute && routeUrgency(currentRoute) <= routeUrgency(floorRoute) ? currentRoute : floorRoute;
|
|
174
|
+
const hours = Number.isInteger(currentHours) ? Math.min(currentHours, floorHours) : floorHours;
|
|
175
|
+
const routeRaised = route !== currentRoute;
|
|
176
|
+
const windowTightened = hours !== currentHours;
|
|
177
|
+
if (!routeRaised && !windowTightened) return escalation;
|
|
178
|
+
const flags = [...new Set(fired
|
|
179
|
+
.filter((row) => (routeRaised && row.minRoute === route) || (windowTightened && row.maxActionTimeHours === hours))
|
|
180
|
+
.map((row) => row.flag))].join(', ');
|
|
181
|
+
const changes = [
|
|
182
|
+
routeRaised ? `escalation_route ${currentRoute ? `'${currentRoute}'` : 'null'} → '${route}'` : null,
|
|
183
|
+
windowTightened ? `max_action_time_hours ${currentHours ?? 'null'} → ${hours}` : null,
|
|
184
|
+
escalation.urgent ? null : 'urgent_under_24h false → true',
|
|
185
|
+
].filter(Boolean);
|
|
186
|
+
repairs.push(`ruta mínima por término reportado (${flags}): ${changes.join(', ')}`);
|
|
187
|
+
return {
|
|
188
|
+
...escalation,
|
|
189
|
+
urgent: true,
|
|
190
|
+
maxActionTimeHours: hours,
|
|
191
|
+
escalationRoute: route,
|
|
192
|
+
reasonForEscalation: escalation.reasonForEscalation || `Término reportado en este triaje (${flags}): ruta mínima ${route}, máximo ${hours} h.`,
|
|
193
|
+
summary: [str(escalation.summary).trim(), `Ruta mínima aplicada por un término reportado en este triaje (${flags}): ${route}, máximo ${hours} h.`]
|
|
194
|
+
.filter(Boolean)
|
|
195
|
+
.join(' '),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
110
199
|
module.exports = {
|
|
111
200
|
str,
|
|
112
201
|
arr,
|
|
@@ -115,4 +204,5 @@ module.exports = {
|
|
|
115
204
|
normalizeStatus,
|
|
116
205
|
buildRedFlag,
|
|
117
206
|
normalizeGateEscalation,
|
|
207
|
+
applyRouteFloor,
|
|
118
208
|
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
const { getTriageGate, getTriagePlan } = require('../config/subAgentsConfig');
|
|
2
|
-
const { getSafetyFlagPatterns } = require('../config/divergenceConfig');
|
|
2
|
+
const { getSafetyFlagPatterns, getRedFlagMinRoutes } = require('../config/divergenceConfig');
|
|
3
3
|
const { requireGatewayProvider } = require('../config/llmConfig');
|
|
4
4
|
const { getTriageRecommendationEventModel } = require('../models/triageRecommendationEventModel');
|
|
5
5
|
const { isShadowRun } = require('../helpers/shadowHelper');
|
|
6
6
|
const { readOutputText } = require('../helpers/clinicalMentionHelper');
|
|
7
|
-
const {
|
|
7
|
+
const {
|
|
8
|
+
str, arr, buildRedFlag, normalizeGateEscalation, applyRouteFloor,
|
|
9
|
+
} = require('../helpers/recommendationGateHelper');
|
|
8
10
|
const { buildElement, nullableStr } = require('../helpers/recommendationPlanHelper');
|
|
9
11
|
const { readClinicalProfile } = require('../helpers/clinicalProfileHelper');
|
|
10
12
|
const {
|
|
@@ -265,7 +267,13 @@ async function generateTriageRecommendations({ payload, provider, runId = null }
|
|
|
265
267
|
if (profile === undefined) return { ...settle, status: 'failed', stage: 'assemble', reason: 'profile_read_failed' };
|
|
266
268
|
if (!profile) return { ...settle, status: 'failed', stage: 'assemble', reason: 'no_clinical_profile' };
|
|
267
269
|
|
|
268
|
-
const
|
|
270
|
+
const riskRead = await readPatientRiskAssessments({ patientId: code, limit: 1 })
|
|
271
|
+
.then((assessments) => ({ assessments, failed: false }))
|
|
272
|
+
.catch((err) => {
|
|
273
|
+
logger.warn('[triageRecommendation] risk assessment unavailable; section degraded', { code, error: err?.message });
|
|
274
|
+
return { assessments: [], failed: true };
|
|
275
|
+
});
|
|
276
|
+
const [riskAssessment] = riskRead.assessments;
|
|
269
277
|
// A failed read degrades the section; it must never fail the run.
|
|
270
278
|
const [openCases, escalations] = await Promise.all([
|
|
271
279
|
readSymptomCases({ patientId: code })
|
|
@@ -315,16 +323,40 @@ async function generateTriageRecommendations({ payload, provider, runId = null }
|
|
|
315
323
|
...freeTextDerivedSymptoms,
|
|
316
324
|
];
|
|
317
325
|
const reportedKeys = symptomKeySet(catalog, reportedTerms);
|
|
326
|
+
const floorConfig = await getRedFlagMinRoutes()
|
|
327
|
+
.then(({ rows, invalidRows }) => ({ rows, invalidRows, configError: null }))
|
|
328
|
+
.catch((err) => {
|
|
329
|
+
logger.warn('[triageRecommendation] red-flag minimum routes unavailable; no route floor', { code, error: err?.message });
|
|
330
|
+
return { rows: [], invalidRows: [], configError: err?.message || 'desconocido' };
|
|
331
|
+
});
|
|
332
|
+
const routeFloor = {
|
|
333
|
+
catalog,
|
|
334
|
+
...floorConfig,
|
|
335
|
+
affirmedKeys: reportedKeys,
|
|
336
|
+
riskAssessment,
|
|
337
|
+
riskReadFailed: riskRead.failed,
|
|
338
|
+
freeTextPending: settle.classifierPending === true && freeTextBits.length > 0,
|
|
339
|
+
};
|
|
318
340
|
|
|
319
341
|
const gateStart = Date.now();
|
|
320
|
-
const gateOutcome = await runGate({
|
|
342
|
+
const gateOutcome = await runGate({
|
|
343
|
+
provider, presetId: gatePresetId, casoBase: casoBaseFor('gate'), routeFloor, rid, code, payload,
|
|
344
|
+
});
|
|
321
345
|
settle.gateLatencyMs = Date.now() - gateStart;
|
|
322
346
|
settle.gateAttempts = gateOutcome.attempts;
|
|
323
|
-
if (gateOutcome.failure)
|
|
324
|
-
|
|
347
|
+
if (gateOutcome.failure) {
|
|
348
|
+
return {
|
|
349
|
+
...settle,
|
|
350
|
+
...gateOutcome.failure,
|
|
351
|
+
autoRepairs: gateOutcome.gateRepairs || [],
|
|
352
|
+
qualityWarnings: gateOutcome.gateWarnings || settle.qualityWarnings,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
const { gate, gateRepairs, gateWarnings } = gateOutcome;
|
|
325
356
|
settle.gateVerdict = gate.urgentUnder24h ? 'urgent' : 'clear';
|
|
326
357
|
settle.urgentUnder24h = gate.urgentUnder24h;
|
|
327
358
|
settle.escalationRoute = gate.escalationRoute;
|
|
359
|
+
settle.qualityWarnings = gateWarnings;
|
|
328
360
|
|
|
329
361
|
const planStart = Date.now();
|
|
330
362
|
const boundary = {
|
|
@@ -344,7 +376,7 @@ async function generateTriageRecommendations({ payload, provider, runId = null }
|
|
|
344
376
|
const { plan, planRepairs, qualityWarnings } = planOutcome;
|
|
345
377
|
settle.elementCount = plan.elements.length;
|
|
346
378
|
settle.autoRepairs = [...gateRepairs, ...planRepairs];
|
|
347
|
-
settle.qualityWarnings = qualityWarnings;
|
|
379
|
+
settle.qualityWarnings = [...gateWarnings, ...qualityWarnings];
|
|
348
380
|
settle.rankReordered = planOutcome.rankReordered;
|
|
349
381
|
settle.deniedCardsDropped = planOutcome.deniedCardsDropped;
|
|
350
382
|
settle.deniedTargetsStripped = planOutcome.deniedTargetsStripped;
|
|
@@ -404,7 +436,9 @@ function rejectedGateShape(rawGate, redFlags) {
|
|
|
404
436
|
};
|
|
405
437
|
}
|
|
406
438
|
|
|
407
|
-
async function runGate({
|
|
439
|
+
async function runGate({
|
|
440
|
+
provider, presetId, casoBase, routeFloor, rid, code, payload,
|
|
441
|
+
}) {
|
|
408
442
|
let attempts = 0;
|
|
409
443
|
let gateRepairs = [];
|
|
410
444
|
let accepted = null;
|
|
@@ -468,7 +502,10 @@ async function runGate({ provider, presetId, casoBase, rid, code, payload }) {
|
|
|
468
502
|
break;
|
|
469
503
|
}
|
|
470
504
|
if (!accepted) return { attempts, gateRepairs, failure: { status: 'failed', stage: 'safety_gate', reason: 'gate_malformed' } };
|
|
471
|
-
const
|
|
505
|
+
const gateWarnings = [];
|
|
506
|
+
const {
|
|
507
|
+
summary, redFlags, urgent, maxActionTimeHours, escalationRoute, reasonForEscalation,
|
|
508
|
+
} = applyRouteFloor(accepted, routeFloor, gateRepairs, gateWarnings);
|
|
472
509
|
|
|
473
510
|
let gate;
|
|
474
511
|
try {
|
|
@@ -487,15 +524,21 @@ async function runGate({ provider, presetId, casoBase, rid, code, payload }) {
|
|
|
487
524
|
});
|
|
488
525
|
} catch (err) {
|
|
489
526
|
logger.warn('[triageRecommendation] gate failed DTO validation', { code, runId: rid, error: err?.message });
|
|
490
|
-
return {
|
|
527
|
+
return {
|
|
528
|
+
attempts, gateRepairs, gateWarnings, failure: { status: 'failed', stage: 'safety_gate', reason: 'gate_malformed' },
|
|
529
|
+
};
|
|
491
530
|
}
|
|
492
531
|
try {
|
|
493
532
|
await storeSafetyGate({ patientId: code, safetyGate: gate });
|
|
494
533
|
} catch (err) {
|
|
495
534
|
logger.error('[triageRecommendation] gate store failed', { code, runId: rid, error: err?.message });
|
|
496
|
-
return {
|
|
535
|
+
return {
|
|
536
|
+
attempts, gateRepairs, gateWarnings, failure: { status: 'failed', stage: 'fhir', reason: 'gate_store_failed' },
|
|
537
|
+
};
|
|
497
538
|
}
|
|
498
|
-
return {
|
|
539
|
+
return {
|
|
540
|
+
attempts, gate, gateRepairs, gateWarnings,
|
|
541
|
+
};
|
|
499
542
|
}
|
|
500
543
|
|
|
501
544
|
async function runPlan({ provider, presetId, casoBase, gate, boundary = null, rid, code, payload }) {
|