@peopl-health/nexus 5.71.6 → 5.72.0-dev.12918

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.
@@ -68,16 +68,16 @@ function gradeEstimateSchema(description) {
68
68
  functional_anchor: {
69
69
  type: 'string',
70
70
  enum: FUNCTIONAL_ANCHORS,
71
- 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 — the grade then stands only on another part of its clause (regular laxatives, moderate pain, a measured value); with nothing else met, record the grade below. `not_established`: you have not asked. Grade from the whole clause and the whole picture — a non-functional part of the clause that is met, or intensity, regimen, cycle day and trajectory that make that grade the most likely one — name in reasoning what supports it, keep the grade below in possible_range, and ask the patient the one functional question that would confirm it in your reply. Missing ADL information is not evidence that function is preserved, and it never moves a grade more than one step above what the evidence strictly shows. When the clause names a specific finding — manual evacuation indicated, at rest, IV fluids, hospitalization — that grade needs that finding: intensity words alone do not reach it, so record the grade the evidence meets and keep the higher one in possible_range.',
71
+ 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.',
72
72
  },
73
73
  improvement_basis: {
74
74
  type: ['string', 'null'],
75
- description: 'REQUIRED when best_estimate is lower than the grade this case already holds. Name the finding that no longer holds, in the patient\'s own terms — what they can do again, or the symptom that stopped. A grade only comes down when something specific resolved; "mejoró un poco" is not that, and neither is the absence of new complaints. Correcting an over-call is the same: name the finding that rules the higher grade out ("cocina y trabaja como siempre").',
75
+ description: 'REQUIRED when best_estimate is lower than the grade this case already holds. Name the finding that no longer holds, in the patient\'s own terms — what they can do again, or the symptom that stopped. A grade only comes down when something specific resolved; "mejoró un poco" is not that, and neither is the absence of new complaints.',
76
76
  },
77
77
  source: {
78
78
  type: 'string',
79
79
  enum: GRADE_SOURCES,
80
- description: 'Where this grade came from. Required whenever you set best_estimate. `proctcae_composite`: you are carrying over the number the PRO-CTCAE triage already computed — say this even when you agree with it. When what the patient tells you this turn contradicts that number (they can do today what the questionnaire said they could not), what they tell you now wins: grade it yourself as `agent_adjudicated`. `agent_adjudicated`: you read the grade_scale clauses yourself against what the patient said. `carried_forward`: the case\'s previous grade, unchanged, with no new evidence this turn.',
80
+ description: 'Where this grade came from. Required whenever you set best_estimate. `proctcae_composite`: you are carrying over the number the PRO-CTCAE triage already computed — say this even when you agree with it. `agent_adjudicated`: you read the grade_scale clauses yourself against what the patient said. `carried_forward`: the case\'s previous grade, unchanged, with no new evidence this turn.',
81
81
  },
82
82
  matched_grade: {
83
83
  type: ['integer', 'null'],
@@ -19,13 +19,11 @@ const CONVERSATION_TURNS = 40;
19
19
  const TRIAGE_LIMIT = 3;
20
20
  const CALL_LOG_LIMIT = 10;
21
21
 
22
- const TECHNICAL_TOKEN = /\b(urgentIfPositive|blocksDrug|null)\b|\b[a-z]+_\w+|\\n/i;
23
-
24
22
  const trimmed = (value) => String(value ?? '').trim();
25
23
 
26
24
  function withholdReason(sections) {
27
25
  const { present } = flagsByStatus(sections);
28
- const modifiers = (Array.isArray(sections?.treatmentModifiers) ? sections.treatmentModifiers : []).map((modifier) => trimmed(modifier).replace(/_/g, ' ')).filter(Boolean);
26
+ const modifiers = (Array.isArray(sections?.treatmentModifiers) ? sections.treatmentModifiers : []).map(trimmed).filter(Boolean);
29
27
  const reasons = [];
30
28
  if (present.length) reasons.push(present.length === 1 ? 'hay una bandera roja presente' : 'hay banderas rojas presentes');
31
29
  if (modifiers.length) reasons.push(`condición que cambia el algoritmo (${modifiers.join(', ')})`);
@@ -174,20 +172,16 @@ async function generateEscalationDraft({
174
172
  return { completed: false, attempted: true, reason: `the treatment proposal broke the formulary rules: ${issues.join('; ')}` };
175
173
  }
176
174
 
177
- const draft = {
175
+ const doctorText = composeDoctorBody({
176
+ alarm: alarms[alarms.length - 1] ?? null,
178
177
  shared: parsed,
179
178
  symptoms: settled.map(({ block, proposal, withheldReason, conduct }) => ({ sections: block, proposal, withheldReason, conduct })),
180
- };
181
- const doctorText = composeDoctorBody({ ...draft, alarm: alarms[alarms.length - 1] ?? null });
179
+ });
182
180
 
183
181
  if (!doctorText || !patientMessage) {
184
182
  logger.warn('[escalationDraft] generation returned no usable text', { reviewId });
185
183
  return { completed: false, attempted: true, reason: 'the model returned no usable text' };
186
184
  }
187
- if (TECHNICAL_TOKEN.test(`${composeDoctorBody({ ...draft, alarm: null })}\n${patientMessage}`)) {
188
- logger.warn('[escalationDraft] generation leaked a technical token into the text', { reviewId });
189
- return { completed: false, attempted: true, reason: 'the model wrote a field name or technical token into the doctor or patient text' };
190
- }
191
185
 
192
186
  return {
193
187
  completed: true,
@@ -18,37 +18,21 @@ function hasCuratedLadder(ctcaeTerm) {
18
18
  }
19
19
  }
20
20
 
21
- function gradeClause(ctcaeTerm, grade) {
21
+ function anchorLeanedOnBy(ctcaeTerm, grade) {
22
+ if (!ctcaeTerm || !Number.isInteger(grade) || grade < 2) return null;
23
+ let clause = null;
22
24
  try {
23
- return getCtcaeCatalog().getGradeScale(ctcaeTerm)?.[String(grade)]?.description || null;
25
+ clause = getCtcaeCatalog().getGradeScale(ctcaeTerm)?.[String(grade)]?.description || null;
24
26
  } catch {
25
27
  return null;
26
28
  }
27
- }
28
-
29
- function anchorLeanedOnBy(ctcaeTerm, grade) {
30
- if (!ctcaeTerm || !Number.isInteger(grade) || grade < 2) return null;
31
- const clause = gradeClause(ctcaeTerm, grade);
32
29
  if (!clause) return null;
33
30
  if (SELF_CARE.test(clause)) return 'self_care';
34
31
  if (INSTRUMENTAL.test(clause)) return 'instrumental';
35
32
  return null;
36
33
  }
37
34
 
38
- // CTCAE clauses list criteria separated by ';' — 'regular use of laxatives; limiting instrumental ADL'
39
- // is met by either part. Only a clause with nothing but the ADL part is ruled out by 'not_limited'.
40
- function clauseHasNonFunctionalCriterion(ctcaeTerm, grade) {
41
- const clause = gradeClause(ctcaeTerm, grade) || '';
42
- return clause.split(';').some((part) => part.trim() && !SELF_CARE.test(part) && !INSTRUMENTAL.test(part));
43
- }
44
-
45
- // 'not_limited' rules the functional part out; so does an instrumental-only limitation for a grade
46
- // whose clause turns on self-care.
47
- function rulesOutFunctionalPart(leaned, anchor) {
48
- return anchor === 'not_limited' || (leaned === 'self_care' && anchor === 'instrumental_adl_limited');
49
- }
50
-
51
- function gradeAnchorError({ ctcaeTerm, grade, anchor }) {
35
+ function gradeAnchorError({ ctcaeTerm, grade, anchor, possibleRange, previous = null, continuesEpisode = false }) {
52
36
  const leaned = anchorLeanedOnBy(ctcaeTerm, grade);
53
37
  if (!leaned) return null;
54
38
  const named = leaned === 'self_care' ? 'self-care' : 'instrumental';
@@ -56,30 +40,24 @@ function gradeAnchorError({ ctcaeTerm, grade, anchor }) {
56
40
  return `functional_anchor is required for grade ${grade} of ${ctcaeTerm}: its rubric clause turns on ${named} ADL. `
57
41
  + `Say what you established — ${FUNCTIONAL_ANCHORS.join(', ')} — rather than restating the clause.`;
58
42
  }
59
- if (rulesOutFunctionalPart(leaned, anchor) && !clauseHasNonFunctionalCriterion(ctcaeTerm, grade)) {
60
- return `functional_anchor is '${anchor}', which rules out the ${named} ADL limitation that is the only criterion of grade ${grade} of ${ctcaeTerm}. `
61
- + 'Record the grade whose clause the evidence does meet.';
62
- }
63
- return null;
64
- }
65
-
66
- // Advice, not a verdict: the grade stands as recorded and the agent learns what would confirm it.
67
- function gradeAnchorNotice({ ctcaeTerm, grade, anchor }) {
68
- const leaned = anchorLeanedOnBy(ctcaeTerm, grade);
69
- if (!leaned || !anchor || SATISFIED_BY[leaned].has(anchor)) return null;
70
- const named = leaned === 'self_care' ? 'self-care' : 'instrumental';
71
- if (rulesOutFunctionalPart(leaned, anchor)) {
72
- return `Grade ${grade} of ${ctcaeTerm} can only stand on the non-functional part of its clause: the patient is not limited in ${named} ADL. `
73
- + '`reasoning` must name that criterion; if it is not met, record the grade below.';
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;
48
+ const below = grade - 1;
49
+ if (anchor === 'not_limited') {
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. `
51
+ + `Assign grade ${below} — the highest grade whose clause the evidence actually satisfies.`;
74
52
  }
75
- return `Grade ${grade} of ${ctcaeTerm} was recorded without the ${named} ADL limitation its clause mentions. `
76
- + 'That is fine when another part of the clause is met or the whole picture makes it the most likely grade — `reasoning` must name which. '
77
- + 'Ask the patient the one functional question that would confirm it in your reply, and keep the grade below in possible_range.';
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.`;
78
57
  }
79
58
 
80
59
  module.exports = {
81
60
  hasCuratedLadder,
82
61
  anchorLeanedOnBy,
83
62
  gradeAnchorError,
84
- gradeAnchorNotice,
85
63
  };
@@ -1,5 +1,5 @@
1
- const { CARRIED_FORWARD } = require('../helpers/gradeEstimateHelper');
2
- const { gradeAnchorError, gradeAnchorNotice, hasCuratedLadder } = require('./gradeAnchorService');
1
+ const { CARRIED_FORWARD, parseGradeEstimate } = require('../helpers/gradeEstimateHelper');
2
+ const { gradeAnchorError, hasCuratedLadder, anchorLeanedOnBy } = require('./gradeAnchorService');
3
3
  const { gradeSourceError, carriedForwardError } = require('./gradeSourceService');
4
4
 
5
5
  // A resolution names a finding. Length is not the test: 'ya come' names one and 'mejoró un poco'
@@ -14,11 +14,16 @@ const namesAResolution = (basis) => {
14
14
  };
15
15
 
16
16
  const GUARDS = [
17
- ({ grade, previous, recordsOngoingRead }) => {
17
+ ({ ctcaeTerm, grade, previous, recordsOngoingRead }) => {
18
18
  if (!recordsOngoingRead) return null;
19
19
  const now = grade.bestEstimate;
20
20
  const before = previous && previous.bestEstimate;
21
21
  if (!Number.isInteger(now) || !Number.isInteger(before) || now >= before) return null;
22
+ // Only 'not_established' is exempt. It means the question was not asked, so the drop is the
23
+ // anchor guard's own remediation rather than a claim that anything got better. 'not_limited'
24
+ // is the opposite: the question WAS asked and the limitation is gone — a finding, and the
25
+ // thing this field exists to record.
26
+ if (grade.functionalAnchor === 'not_established' && now === before - 1 && anchorLeanedOnBy(ctcaeTerm, before)) return null;
22
27
  if (namesAResolution(grade.improvementBasis)) return null;
23
28
  return `best_estimate ${now} is below the grade ${before} this case already holds, so this call records an improvement. `
24
29
  + 'Name what resolved in `improvement_basis`: the finding that no longer holds, in the patient\'s own terms — a placeholder is not one. '
@@ -45,10 +50,13 @@ const GUARDS = [
45
50
  hasLadder: hasCuratedLadder(ctcaeTerm),
46
51
  trace,
47
52
  }),
48
- ({ ctcaeTerm, grade }) => gradeAnchorError({
53
+ ({ ctcaeTerm, grade, previous, continuesEpisode }) => gradeAnchorError({
49
54
  ctcaeTerm,
50
55
  grade: grade.bestEstimate ?? null,
51
56
  anchor: grade.functionalAnchor ?? null,
57
+ possibleRange: grade.possibleRange ?? null,
58
+ previous,
59
+ continuesEpisode,
52
60
  }),
53
61
  ];
54
62
 
@@ -62,12 +70,4 @@ function gradeClaimError({ ctcaeTerm, grade, stated = null, previous = null, res
62
70
  return null;
63
71
  }
64
72
 
65
- function gradeClaimNotice({ ctcaeTerm, grade }) {
66
- return gradeAnchorNotice({
67
- ctcaeTerm,
68
- grade: grade?.bestEstimate ?? null,
69
- anchor: grade?.functionalAnchor ?? null,
70
- });
71
- }
72
-
73
- module.exports = { gradeClaimError, gradeClaimNotice };
73
+ module.exports = { gradeClaimError };
@@ -4,7 +4,7 @@ const { GRADE_CONFIDENCE_LEVELS, GRADE_MIN, GRADE_MAX, gradeEstimateSchema, pars
4
4
  const { ceilingGapError } = require('../helpers/ceilingGapHelper');
5
5
  const { mentionProvenanceError } = require('../helpers/evidenceAnchorHelper');
6
6
  const { ctcaeTermError } = require('../services/ctcaeTermService');
7
- const { gradeClaimError, gradeClaimNotice } = require('../services/gradeClaimService');
7
+ const { gradeClaimError } = require('../services/gradeClaimService');
8
8
  const { armOpenCeilingNet } = require('../services/contingencyDispatchService');
9
9
  const { readSymptomCases, storeSymptomCase } = require('../../fhir');
10
10
  const { ManagedSymptom, OPEN_STATUSES, CLOSED_STATUSES, FUNCTIONAL_ANCHORS } = require('../../shared/dtos/ManagedSymptom');
@@ -13,7 +13,7 @@ const { isArmOnOpenCeilingLive } = require('../flags/contingencyFlags');
13
13
 
14
14
  const definition = {
15
15
  name: 'openCondition',
16
- description: '**Does:** Opens a NEW agent-tracked symptom case (Condition + initial ClinicalImpression) for a symptom term with no open case — the longitudinal expediente that groups 1..N episodes of the same `ctcae_term`.\n\n**Required inputs:** `ctcae_term` (English snake_case catalog key — e.g. `fever`, `pain`, `dyspnea`; use `other` only when no catalog term fits), `episode_id` (the intake episode this case wraps), `mention_id` (the intake mention that opened the case), `verbatim_quote` (exact substring of the patient message). Optional `temporality`, `grade_estimate` (`{best_estimate, confidence, possible_range, reasoning}`).\n\n**When to call:** when the turn surfaces a symptom with no open case on this `ctcae_term`. Use `recordClinicalImpression` if a case is already open — do NOT call `openCondition` twice for the same symptom.\n\n**When NOT to call:**\n- For a new episode or subsequent assessment of an EXISTING open case — use `recordClinicalImpression`.\n- For a symptom that already has a case which was closed, resolved or deactivated — the same symptom returning is a recurrence of that case, not a new one. Reopen it with `updateConditionStatus(transition=\'reactivate\')`. This tool refuses the duplicate with `DORMANT_CASE_EXISTS` and names the case.\n\n**Returns:** `case_id`, `action_taken` (`opened`), `status`, `ctcae_term`, `episode_ids`, `current_episode_id`, `grade_estimate`, `temporality`, `trajectory`, and `grade_notice` when the grade was recorded without the ADL limitation its clause mentions — advisory: the grade was stored as given. Fails hard with `case_already_open_for_term` + `existing_case_id` when an open case already exists for the term — recover via `recordClinicalImpression`.\n\n**Side effects:** writes a FHIR `Condition` + head `ClinicalImpression` + `Provenance`.',
16
+ description: '**Does:** Opens a NEW agent-tracked symptom case (Condition + initial ClinicalImpression) for a symptom term with no open case — the longitudinal expediente that groups 1..N episodes of the same `ctcae_term`.\n\n**Required inputs:** `ctcae_term` (English snake_case catalog key — e.g. `fever`, `pain`, `dyspnea`; use `other` only when no catalog term fits), `episode_id` (the intake episode this case wraps), `mention_id` (the intake mention that opened the case), `verbatim_quote` (exact substring of the patient message). Optional `temporality`, `grade_estimate` (`{best_estimate, confidence, possible_range, reasoning}`).\n\n**When to call:** when the turn surfaces a symptom with no open case on this `ctcae_term`. Use `recordClinicalImpression` if a case is already open — do NOT call `openCondition` twice for the same symptom.\n\n**When NOT to call:**\n- For a new episode or subsequent assessment of an EXISTING open case — use `recordClinicalImpression`.\n- For a symptom that already has a case which was closed, resolved or deactivated — the same symptom returning is a recurrence of that case, not a new one. Reopen it with `updateConditionStatus(transition=\'reactivate\')`. This tool refuses the duplicate with `DORMANT_CASE_EXISTS` and names the case.\n\n**Returns:** `case_id`, `action_taken` (`opened`), `status`, `ctcae_term`, `episode_ids`, `current_episode_id`, `grade_estimate`, `temporality`, `trajectory`. Fails hard with `case_already_open_for_term` + `existing_case_id` when an open case already exists for the term — recover via `recordClinicalImpression`.\n\n**Side effects:** writes a FHIR `Condition` + head `ClinicalImpression` + `Provenance`.',
17
17
  strict: false,
18
18
  parameters: {
19
19
  type: 'object',
@@ -125,7 +125,6 @@ async function handler(args = {}, context = {}) {
125
125
  const gradesTheCase = hasGradeSignal(args?.grade_estimate);
126
126
  const claimGap = gradesTheCase ? gradeClaimError({ ctcaeTerm, grade, trace: runtime?.trace || null }) : null;
127
127
  if (claimGap) return JSON.stringify({ success: false, error: claimGap, data: {} });
128
- const gradeNotice = gradesTheCase ? gradeClaimNotice({ ctcaeTerm, grade }) : null;
129
128
  const ceilingGap = gradesTheCase ? ceilingGapError(args, 'openCondition') : null;
130
129
  if (ceilingGap) return JSON.stringify({ success: false, error: ceilingGap, data: {} });
131
130
  const managedSymptom = new ManagedSymptom({
@@ -184,7 +183,6 @@ async function handler(args = {}, context = {}) {
184
183
  grade_estimate: projectGradeEstimate(latestAssessment.grade),
185
184
  temporality: condition.temporality,
186
185
  trajectory: { direction: latestAssessment.trajectory || 'unknown' },
187
- ...(gradeNotice ? { grade_notice: gradeNotice } : {}),
188
186
  },
189
187
  });
190
188
  } catch (err) {
@@ -5,7 +5,7 @@ const { ceilingGapError } = require('../helpers/ceilingGapHelper');
5
5
  const { mentionProvenanceError } = require('../helpers/evidenceAnchorHelper');
6
6
  const { MEDICATION_NAMING_RULE } = require('../helpers/medicationNamingRule');
7
7
  const { resolveCloseReason, closePatch, runCloseCascade } = require('../services/conditionCloseService');
8
- const { gradeClaimError, gradeClaimNotice } = require('../services/gradeClaimService');
8
+ const { gradeClaimError } = require('../services/gradeClaimService');
9
9
  const { armOpenCeilingNet } = require('../services/contingencyDispatchService');
10
10
  const { inputConsistency, statedReasoning } = require('../services/impressionValidationService');
11
11
  const { isArmOnOpenCeilingLive } = require('../flags/contingencyFlags');
@@ -15,7 +15,7 @@ const { logger } = require('../../utils/logger');
15
15
 
16
16
  const definition = {
17
17
  name: 'recordClinicalImpression',
18
- description: '**Does:** Evolves an existing symptom case by `event`. `add_evidence`: new mention on the current episode (requires `characterizing` + a grade or trajectory). `new_episode`: a new episode for the same `ctcae_term` — attaches it and returns the case to `characterizing`. `resolve_episode`: the current episode resolved — case goes to `monitoring`. `close`: terminal — status `closed`, requires `close_reason` from the closed vocabulary.\n\nAn impression is a grading act plus the reasoning behind it: `grade_estimate` carries your most likely grade with its confidence and range, and `reasoning` names, in one or two sentences, what you think is going on. `reasoning` is REQUIRED on every call and is the canonical per-case rationale — it lands on the clinical record, and the routing trace references it rather than restating it. Omitting it does not fail the write, but the impression is then a grade with no clinical reasoning behind it and the envelope says so in `input_consistency`. Grade-specific justification (the matched `grade_scale` clause) belongs inside `grade_estimate.reasoning`.\n\n**Required inputs:** `case_id` (from `openCondition` or `getActiveSymptomLandscape`), `event`, `reasoning`. Per-event: `add_evidence`/`new_episode` require `mention_id`, `missing_for_higher_grade`, plus at least one of `grade_estimate` or `trajectory_direction`; `new_episode` also requires `episode_id`; `close` requires `close_reason`, and it must be one of `resolved` | `team_resolved` | `resolved_spontaneously` | `resolved_by_intervention` (the symptom ENDED — this stamps `abatement_at`) or `escalated_to_team` | `patient_transferred` | `stale_no_activity` (stop tracking WITHOUT claiming an end — `abatement_at` stays null). Never claim a resolution nobody observed; free text is rejected.\n\n**Returns:** `case_id`, `action_taken`, `status`, `ctcae_term`, `episode_ids`, `current_episode_id`, `mention_ids`, `grade_estimate`, `temporality`, `trajectory`, `decision_posture`, `close_reason`, `abatement_at`, plus `input_consistency` when the call under-specified something, and `grade_notice` when the grade was recorded without the ADL limitation its clause mentions — advisory: the grade was stored as given. Fails hard on status guards (e.g. `add_evidence` on a non-`characterizing` case), duplicate `mention_id`, or an already-attached `episode_id`.\n\n**Side effects:** appends a FHIR `ClinicalImpression` to the case chain and repoints the head; updates the `Condition` lifecycle. `close` also retires any contingency net guarding the case (returned as `auto_resolved_contingency_plans`) — a closed case never keeps writing to the patient — and prunes the case from its active clusters, dissolving one that drops below 2 members (`pruned_clusters`, `dissolved_clusters`).',
18
+ description: '**Does:** Evolves an existing symptom case by `event`. `add_evidence`: new mention on the current episode (requires `characterizing` + a grade or trajectory). `new_episode`: a new episode for the same `ctcae_term` — attaches it and returns the case to `characterizing`. `resolve_episode`: the current episode resolved — case goes to `monitoring`. `close`: terminal — status `closed`, requires `close_reason` from the closed vocabulary.\n\nAn impression is a grading act plus the reasoning behind it: `grade_estimate` carries your most likely grade with its confidence and range, and `reasoning` names, in one or two sentences, what you think is going on. `reasoning` is REQUIRED on every call and is the canonical per-case rationale — it lands on the clinical record, and the routing trace references it rather than restating it. Omitting it does not fail the write, but the impression is then a grade with no clinical reasoning behind it and the envelope says so in `input_consistency`. Grade-specific justification (the matched `grade_scale` clause) belongs inside `grade_estimate.reasoning`.\n\n**Required inputs:** `case_id` (from `openCondition` or `getActiveSymptomLandscape`), `event`, `reasoning`. Per-event: `add_evidence`/`new_episode` require `mention_id`, `missing_for_higher_grade`, plus at least one of `grade_estimate` or `trajectory_direction`; `new_episode` also requires `episode_id`; `close` requires `close_reason`, and it must be one of `resolved` | `team_resolved` | `resolved_spontaneously` | `resolved_by_intervention` (the symptom ENDED — this stamps `abatement_at`) or `escalated_to_team` | `patient_transferred` | `stale_no_activity` (stop tracking WITHOUT claiming an end — `abatement_at` stays null). Never claim a resolution nobody observed; free text is rejected.\n\n**Returns:** `case_id`, `action_taken`, `status`, `ctcae_term`, `episode_ids`, `current_episode_id`, `mention_ids`, `grade_estimate`, `temporality`, `trajectory`, `decision_posture`, `close_reason`, `abatement_at`, plus `input_consistency` when the call under-specified something. Fails hard on status guards (e.g. `add_evidence` on a non-`characterizing` case), duplicate `mention_id`, or an already-attached `episode_id`.\n\n**Side effects:** appends a FHIR `ClinicalImpression` to the case chain and repoints the head; updates the `Condition` lifecycle. `close` also retires any contingency net guarding the case (returned as `auto_resolved_contingency_plans`) — a closed case never keeps writing to the patient — and prunes the case from its active clusters, dissolving one that drops below 2 members (`pruned_clusters`, `dissolved_clusters`).',
19
19
  strict: false,
20
20
  parameters: {
21
21
  type: 'object',
@@ -351,8 +351,6 @@ async function handler(args = {}, context = {}) {
351
351
 
352
352
  const { condition, latestAssessment } = result;
353
353
  const consistency = inputConsistency(args);
354
- const freshRead = ONGOING_READ_EVENTS.includes(event) && hasGradeSignal(args?.grade_estimate) && !isCarriedForward(args.grade_estimate);
355
- const gradeNotice = freshRead ? gradeClaimNotice({ ctcaeTerm: condition.ctcaeTerm, grade: latestAssessment.grade }) : null;
356
354
  return JSON.stringify({
357
355
  success: true,
358
356
  data: {
@@ -373,7 +371,6 @@ async function handler(args = {}, context = {}) {
373
371
  close_reason: condition.closeReason,
374
372
  abatement_at: condition.abatementAt,
375
373
  ...(consistency.length ? { input_consistency: consistency } : {}),
376
- ...(gradeNotice ? { grade_notice: gradeNotice } : {}),
377
374
  },
378
375
  });
379
376
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.71.6",
3
+ "version": "5.72.0-dev.12918",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",