Skip to Content

Intervention Fidelity Tracker

The fidelity tracker (WP-FID-BE) answers one question before any tier escalation: was the plan actually delivered? It computes a fidelity rate (sessions completed ÷ planned) for an active bundle assignment, maps it to a rating, cross-references the growth signal from progress monitoring, and returns a verdict plus an escalationBlocked flag. The RTI engine reads this gate inline before any Tier 2 → Tier 3 move.

The pedagogical point: a plan that did not work is different from a plan that was not done. Faithful delivery (≥80%) with weak growth MAY escalate (the plan was tried and did not work). Under-80% with weak growth is BLOCKED, because you cannot indict a plan that was never run. No session data returns cannot_evaluate_yet, never a guessed pass.

Routes

MethodPathPermission
GET/api/fidelity/{bundleAssignmentId}/checkVIEW_OWN_CLASSES (ClassTeacher) or RUN_BACKOFFICE
GET/api/fidelity/{bundleAssignmentId}VIEW_OWN_CLASSES (ClassTeacher) or RUN_BACKOFFICE
POST/api/fidelity/{bundleAssignmentId}/recomputeVIEW_OWN_CLASSES (ClassTeacher) or RUN_BACKOFFICE
GET/api/students/{id}/fidelity-trackerVIEW_OWN_CLASSES (ClassTeacher) or RUN_BACKOFFICE
GET/api/fidelity/admin/low-fidelity-cohortRUN_BACKOFFICE (admin only)

bundleAssignmentId and student id are positive integers. The teacher routes gate on the student’s class through the in-handler ClassTeacher guard; a cross-org or out-of-scope id returns 404 (existence non-disclosure). There is deliberately no student endpoint; students never see fidelity.

GET /check: the escalation gate

This is the synchronous verdict the RTI engine calls inline before an escalation. It returns the latest persisted projection; it does not recompute.

curl https://localhost:3000/api/fidelity/8124/check \ -H "Authorization: Bearer <accessToken>"
{ "bundleAssignmentId": 8124, "fidelityRate": 0.86, "fidelityRating": "high", "fidelityIssueFlag": false, "fidelityIssueCode": null, "impactOnDecision": "no_issue", "growthSignal": "low_growth", "verdict": "adjust_intervention", "escalationBlocked": false, "recommendedAction": "...", "autoDraftedParagraphAr": "...", "sessionsCompleted": 12, "sessionsPlanned": 14 }

The five-value fidelityRating is high (≥0.80) / adequate (0.60 to 0.79) / partial (0.40 to 0.59) / low (below 0.40) / unknown (no data). The four-value verdict is continue / adjust_intervention / cannot_evaluate_yet / fortuitous_complete_dose. impactOnDecision is block_escalation / reduce_confidence / no_issue. The growthSignal (good_growth / partial_growth / low_growth / insufficient_data) is read from the monitoring module.

The rating × growth matrix

The verdict and escalationBlocked come from a pure matrix of rating against growth:

fidelity \ growthgoodlowhalt (do_not_decide_yet / acute)
high (≥0.80)continue (not blocked)adjust_intervention (not blocked)cannot_evaluate_yet (blocked)
adequate (0.60–0.79)continue (not blocked)cannot_evaluate_yet (blocked)cannot_evaluate_yet (blocked)
partial (0.40–0.59)fortuitous_complete_dose (not blocked)cannot_evaluate_yet (blocked)cannot_evaluate_yet (blocked)
low (below 0.40)fortuitous_complete_dose (not blocked)cannot_evaluate_yet (blocked)cannot_evaluate_yet (blocked)
unknown (no data)cannot_evaluate_yet (blocked)cannot_evaluate_yet (blocked)cannot_evaluate_yet (blocked)

The halt sentinel always returns cannot_evaluate_yet + blocked (the Tie Rule). Escalation is permitted only at high or adequate fidelity with a growth signal that the matrix lets through.

POST /recompute: append a fresh snapshot

Recompute forces a fresh computation and appends a new immutable snapshot row. It carries no body: there is no free-text field on any fidelity path; the only issue context is the controlled fidelityIssueCode vocabulary (missed_sessions / short_duration / wrong_grouping / incomplete_material / unknown).

curl -X POST https://localhost:3000/api/fidelity/8124/recompute \ -H "Authorization: Bearer <accessToken>"

The recompute is advisory-locked per (org, assignment) and idempotent on unchanged inputs. It returns the fresh projection (the same shape as /check) with a 201.

The auto-drafted paragraph

Each verdict auto-drafts a short Arabic review-meeting paragraph (autoDraftedParagraphAr). It is rule-written, never an LLM, and is filtered through the language-safety layer for clinical, framework, and deficit language. It is a teacher/admin surface only.

Invariants

  • Fail-closed. A missing assignment is a 404; a missing growth read or no snapshot row yields unknown / escalationBlocked = true, never a fabricated adequate. Absence of fidelity data blocks escalation; it never allows it.
  • Append-only (V-6). intervention_fidelity and intervention_session_log are pure audit tables: no update, no delete. The config version is pinned on every row, so a recompute of the same inputs replays byte-for-byte.
  • No free text. No reason / note / comment body field exists on any path; issue context is the controlled fidelityIssueCode vocabulary.
  • No LLM (V-5). The verdict and the paragraph are deterministic and rule-written.
  • Tenant-isolated. organizationId scopes every read and write; cross-org returns 404.
  • Teacher-scoped (ClassTeacher) / admin only; never student-facing.