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
| Method | Path | Permission |
|---|---|---|
GET | /api/fidelity/{bundleAssignmentId}/check | VIEW_OWN_CLASSES (ClassTeacher) or RUN_BACKOFFICE |
GET | /api/fidelity/{bundleAssignmentId} | VIEW_OWN_CLASSES (ClassTeacher) or RUN_BACKOFFICE |
POST | /api/fidelity/{bundleAssignmentId}/recompute | VIEW_OWN_CLASSES (ClassTeacher) or RUN_BACKOFFICE |
GET | /api/students/{id}/fidelity-tracker | VIEW_OWN_CLASSES (ClassTeacher) or RUN_BACKOFFICE |
GET | /api/fidelity/admin/low-fidelity-cohort | RUN_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 \ growth | good | low | halt (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 yieldsunknown/escalationBlocked = true, never a fabricatedadequate. Absence of fidelity data blocks escalation; it never allows it. - Append-only (V-6).
intervention_fidelityandintervention_session_logare 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/commentbody field exists on any path; issue context is the controlledfidelityIssueCodevocabulary. - No LLM (V-5). The verdict and the paragraph are deterministic and rule-written.
- Tenant-isolated.
organizationIdscopes every read and write; cross-org returns404. - Teacher-scoped (ClassTeacher) / admin only; never student-facing.
Related guides
- RTI Decisions & Tiers: the engine that reads this gate before a Tier 2 → Tier 3 move
- Progress Monitoring: the source of the growth signal this module cross-references
- Decision Packets: the workflow that wraps a recommendation in an audited approval
- Intervention Bundles: the bundle assignment whose delivery fidelity is tracked