@peopl-health/nexus 5.44.0-dev.5327 → 5.44.0-dev.5345
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.
|
@@ -67,7 +67,7 @@ function gradeEstimateSchema(description) {
|
|
|
67
67
|
functional_anchor: {
|
|
68
68
|
type: 'string',
|
|
69
69
|
enum: FUNCTIONAL_ANCHORS,
|
|
70
|
-
description: 'What you ESTABLISHED about function, not what the clause says. Required whenever the rubric clause for your grade turns on ADL — grade 2 on instrumental ADL, grade 3 on self-care. `instrumental_adl_limited`: they told you the symptom stops them cooking, working, shopping, driving. `self_care_adl_limited`: it stops them bathing, dressing, feeding themselves. `not_limited`: you asked and it does NOT limit them — then that grade is ruled out, so assign the grade below rather than widening the range. `not_established`: you have not asked — then the grade below
|
|
70
|
+
description: 'What you ESTABLISHED about function, not what the clause says. Required whenever the rubric clause for your grade turns on ADL — grade 2 on instrumental ADL, grade 3 on self-care. `instrumental_adl_limited`: they told you the symptom stops them cooking, working, shopping, driving. `self_care_adl_limited`: it stops them bathing, dressing, feeding themselves. `not_limited`: you asked and it does NOT limit them — then that grade is ruled out, so assign the grade below rather than widening the range. `not_established`: you have not asked — then that grade is not yours to assert yet: put the grade below in best_estimate and leave the higher one live in possible_range. The exception is a grade the case already holds on a limitation established earlier in this same episode: there `not_established` just means you did not re-ask, and the grade stands.',
|
|
71
71
|
},
|
|
72
72
|
source: {
|
|
73
73
|
type: 'string',
|
|
@@ -32,7 +32,7 @@ function anchorLeanedOnBy(ctcaeTerm, grade) {
|
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function gradeAnchorError({ ctcaeTerm, grade, anchor, possibleRange }) {
|
|
35
|
+
function gradeAnchorError({ ctcaeTerm, grade, anchor, possibleRange, previous = null, continuesEpisode = false }) {
|
|
36
36
|
const leaned = anchorLeanedOnBy(ctcaeTerm, grade);
|
|
37
37
|
if (!leaned) return null;
|
|
38
38
|
const named = leaned === 'self_care' ? 'self-care' : 'instrumental';
|
|
@@ -41,14 +41,19 @@ function gradeAnchorError({ ctcaeTerm, grade, anchor, possibleRange }) {
|
|
|
41
41
|
+ `Say what you established — ${FUNCTIONAL_ANCHORS.join(', ')} — rather than restating the clause.`;
|
|
42
42
|
}
|
|
43
43
|
if (SATISFIED_BY[leaned].has(anchor)) return null;
|
|
44
|
+
// The case already holds this grade on an anchor established earlier. 'not_established' here
|
|
45
|
+
// says "I did not re-ask this turn", not "nobody ever established it" — holding is honest.
|
|
46
|
+
if (anchor === 'not_established' && continuesEpisode && previous
|
|
47
|
+
&& previous.bestEstimate === grade && SATISFIED_BY[leaned].has(previous.functionalAnchor)) return null;
|
|
44
48
|
const below = grade - 1;
|
|
45
49
|
if (anchor === 'not_limited') {
|
|
46
50
|
return `functional_anchor is 'not_limited', so the ${named} ADL limitation that grade ${grade} of ${ctcaeTerm} turns on is ruled OUT, not merely unknown. `
|
|
47
51
|
+ `Assign grade ${below} — the highest grade whose clause the evidence actually satisfies.`;
|
|
48
52
|
}
|
|
49
|
-
|
|
50
|
-
return `functional_anchor is '${anchor}',
|
|
51
|
-
+ `
|
|
53
|
+
const top = Array.isArray(possibleRange) ? possibleRange[1] : grade;
|
|
54
|
+
return `functional_anchor is '${anchor}', so the ${named} ADL limitation that grade ${grade} of ${ctcaeTerm} turns on is not established. `
|
|
55
|
+
+ `best_estimate is the grade the evidence already carries, not the one it might reach: assign ${below} and keep ${grade} live as possible_range [${below}, ${top}]. `
|
|
56
|
+
+ `Establish the ${named} ADL limitation and ${grade} becomes assertable.`;
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
module.exports = {
|
|
@@ -24,17 +24,19 @@ const GUARDS = [
|
|
|
24
24
|
hasLadder: hasCuratedLadder(ctcaeTerm),
|
|
25
25
|
trace,
|
|
26
26
|
}),
|
|
27
|
-
({ ctcaeTerm, grade }) => gradeAnchorError({
|
|
27
|
+
({ ctcaeTerm, grade, previous, continuesEpisode }) => gradeAnchorError({
|
|
28
28
|
ctcaeTerm,
|
|
29
29
|
grade: grade.bestEstimate ?? null,
|
|
30
30
|
anchor: grade.functionalAnchor ?? null,
|
|
31
31
|
possibleRange: grade.possibleRange ?? null,
|
|
32
|
+
previous,
|
|
33
|
+
continuesEpisode,
|
|
32
34
|
}),
|
|
33
35
|
];
|
|
34
36
|
|
|
35
|
-
function gradeClaimError({ ctcaeTerm, grade, stated = null, previous = null, restatesPriorEvidence = true, trace = null }) {
|
|
37
|
+
function gradeClaimError({ ctcaeTerm, grade, stated = null, previous = null, restatesPriorEvidence = true, continuesEpisode = false, trace = null }) {
|
|
36
38
|
const resolved = grade || {};
|
|
37
|
-
const claim = { ctcaeTerm, grade: resolved, stated: stated || resolved, previous, restatesPriorEvidence, trace };
|
|
39
|
+
const claim = { ctcaeTerm, grade: resolved, stated: stated || resolved, previous, restatesPriorEvidence, continuesEpisode, trace };
|
|
38
40
|
for (const guard of GUARDS) {
|
|
39
41
|
const error = guard(claim);
|
|
40
42
|
if (error) return error;
|
|
@@ -78,9 +78,12 @@ const EVENTS = definition.parameters.properties.event.enum;
|
|
|
78
78
|
const TRAJECTORIES = definition.parameters.properties.trajectory_direction.enum;
|
|
79
79
|
const POSTURES = definition.parameters.properties.decision_posture.enum;
|
|
80
80
|
|
|
81
|
+
// Only new_episode starts a fresh episode; every other event acts on the one the case holds.
|
|
82
|
+
const normalisedEvent = (args) => (typeof args?.event === 'string' ? args.event : '').trim().toLowerCase();
|
|
83
|
+
|
|
81
84
|
const uid = (prefix) => `${prefix}_${crypto.randomUUID().replace(/-/g, '').slice(0, 12)}`;
|
|
82
85
|
|
|
83
|
-
function coerceGrade(raw, base) {
|
|
86
|
+
function coerceGrade(raw, base, continuesEpisode = false) {
|
|
84
87
|
if (isCarriedForward(raw)) {
|
|
85
88
|
const parsed = parseGradeEstimate(raw);
|
|
86
89
|
if (parsed?.error) return parsed;
|
|
@@ -89,7 +92,13 @@ function coerceGrade(raw, base) {
|
|
|
89
92
|
if (!hasGradeSignal(raw)) return base;
|
|
90
93
|
const parsed = parseGradeEstimate(raw);
|
|
91
94
|
if (parsed?.error) return parsed;
|
|
92
|
-
|
|
95
|
+
// The anchor records the case's functional state, not whether this turn re-asked. An unchanged
|
|
96
|
+
// grade keeps the anchor already established, so the finding survives more than one quiet turn.
|
|
97
|
+
const holdsGrade = parsed.bestEstimate === base?.bestEstimate;
|
|
98
|
+
const notReasked = parsed.functionalAnchor === null || parsed.functionalAnchor === 'not_established';
|
|
99
|
+
if (continuesEpisode && holdsGrade && notReasked && base?.functionalAnchor) {
|
|
100
|
+
parsed.functionalAnchor = base.functionalAnchor;
|
|
101
|
+
} else if (parsed.functionalAnchor === null && holdsGrade) {
|
|
93
102
|
parsed.functionalAnchor = base?.functionalAnchor ?? null;
|
|
94
103
|
}
|
|
95
104
|
return parsed;
|
|
@@ -98,7 +107,8 @@ function coerceGrade(raw, base) {
|
|
|
98
107
|
const statesTrajectory = (args) => args?.trajectory_direction !== null && args?.trajectory_direction !== undefined;
|
|
99
108
|
|
|
100
109
|
function buildAssessment(condition, previous, args, runtime, restatesPriorEvidence = false) {
|
|
101
|
-
const
|
|
110
|
+
const continuesEpisode = normalisedEvent(args) !== 'new_episode';
|
|
111
|
+
const grade = coerceGrade(args?.grade_estimate, previous.grade, continuesEpisode);
|
|
102
112
|
if (grade?.error) return grade;
|
|
103
113
|
if (hasGradeSignal(args?.grade_estimate)) {
|
|
104
114
|
const claimGap = gradeClaimError({
|
|
@@ -107,6 +117,7 @@ function buildAssessment(condition, previous, args, runtime, restatesPriorEviden
|
|
|
107
117
|
stated: parseGradeEstimate(args.grade_estimate),
|
|
108
118
|
previous: previous.grade,
|
|
109
119
|
restatesPriorEvidence,
|
|
120
|
+
continuesEpisode,
|
|
110
121
|
trace: runtime?.trace || null,
|
|
111
122
|
});
|
|
112
123
|
if (claimGap) return { error: claimGap };
|
|
@@ -280,7 +291,7 @@ async function handler(args = {}, context = {}) {
|
|
|
280
291
|
return JSON.stringify({ success: false, error: 'recordClinicalImpression requires an active turn context (turnId, patientCode).', data: {} });
|
|
281
292
|
}
|
|
282
293
|
const caseId = (typeof args?.case_id === 'string' ? args.case_id : '').trim();
|
|
283
|
-
const event = (
|
|
294
|
+
const event = normalisedEvent(args);
|
|
284
295
|
if (!caseId) return fail('case_id is required');
|
|
285
296
|
if (!EVENTS.includes(event)) {
|
|
286
297
|
return fail(`unknown event: '${event}'. Must be one of ${EVENTS.join(' | ')}.`);
|