RTI Decisions & Tiers
The RTI engine is the central decision layer. The routes are published under the rti tag in the
API Reference, and the fidelity tracker and data room it depends on are now
shipped and wired in. It has two pipelines that run independently:
- Pipeline A (alerts) evaluates the full evidence picture per student and raises a teacher-visible signal when something needs attention.
- Pipeline B (tier movement) manages the formal process of changing a student’s support level through a review-recommendation-decision sequence.
The teacher and, for high-intensity support, a specialist gate every consequential change. The engine proposes; humans decide.
The sentinel: do_not_decide_yet
Before either pipeline runs any alert or tier logic, the engine checks for the sentinel condition. If any of these are true, the engine opens no alert and blocks any tier movement:
- The skill-status engine returned
do_not_decide_yetfor the student’s evidence - A domain shows
partial_domainstatus - The student’s profile is
PR2-00 DATA_INCOMPLETE - The monitoring engine has a halt verdict
- CBM has
insufficient_points
The sentinel cannot be bypassed by any role or override code (403 if attempted).
The CBM fluency handoff
The CBM module emits a raw CbmTrendSignal; the RTI input reader reads the latest one per student
(prismaCbmTrendReader, org-scoped) and maps it into the alert pipeline. This is the live wiring
between fluency monitoring and the RTI decision layer:
CbmTrendSignal | RTI effect |
|---|---|
trend_below_goal | Maps to flatComparablePmPoints, the “not progressing against the aim line” signal. It feeds Layer B’s Targeted-Support gateway (meetsStudent), so a stalling fluency trend can raise a Student_Alert |
insufficient_points | A sentinel contributor (cbm_insufficient_points): a halt guard, not an alert. Too few comparable probes to decide, so the engine holds rather than over-triggering |
trend_above_goal | Neither; the student is progressing |
trend_mixed | Neither. trend_mixed is the deliberately conservative call: mixed is ambiguous, not below-goal, so treating it as flat would over-alert. Only the unambiguous trend_below_goal maps to the flat signal |
This is the only point where a fluency trend influences a tier decision, and it influences it through an alert, never directly: the trend feeds an alert, the teacher acts on the alert, and the tier-review flow and the fidelity gate still stand between any signal and a tier move.
Pipeline A: Layer A and Layer B alerts
Layer A evaluates per-skill status against 3 states and produces an intermediate signal.
Layer B aggregates across skills and emits one of three alert kinds:
| Alert kind | Description |
|---|---|
Skill_Alert | One or more skills show a pattern that warrants teacher attention, but does not yet meet the threshold for a student-level concern |
Student_Alert | The pattern is broad enough to concern the student’s overall trajectory; a tier-review nomination may be appropriate |
Intensive_Review | The pattern is severe enough (or includes an acute regression) to require immediate review; a formal tier-review request is expected |
Only one rti_alert envelope can be open per student at a time. A higher-severity alert
automatically closes any lower-severity open alert. All alert transitions are append-only; the
history is never deleted.
RTI tier
Every student has a student_tier_status carrying currentRtiTier. The three values are
internal and are never shown to students or parents:
| Tier | Internal label |
|---|---|
| TIER_1_CORE | Core classroom instruction |
| TIER_2_TARGETED | Targeted small-group support |
| TIER_3_INTENSIVE | Intensive individualized support |
Tier labels are strictly internal. They must never appear in any student-facing or parent-facing string, notification, or report.
Pipeline B: the tier movement flow
Tier changes follow a three-step protocol:
- Open
POST /api/tier-review-requests: opens a formalTierReviewRequestand links adecision_packet(the evidence bundle used to support the case). - Recommend
POST /api/tier-review-requests/:id/recommendation: the engine computes the deterministic TM_* verdict (read-only, no write). This is the engine’s proposed answer. - Decide
POST /api/tier-review-requests/:id/decision: the teacher (or specialist for TIER_3) approves or rejects. Approval triggersapplyDecision().
The four TM_ movement rules
Every proposed movement is tested against all four rules:
| Rule | Gate | Blocks what |
|---|---|---|
TM_BLOCK_01 | Fidelity gate: ≥80% plan delivery required | Escalation when the current plan was not actually delivered |
TM_ESC_01 | Escalation criteria | Movement upward (higher support) |
TM_MAINT_01 | Maintenance criteria | Staying at current tier when evidence supports movement |
TM_DEC_01 | De-escalation criteria | Movement downward (less intensive support) |
TM_BLOCK_01 (fidelity) runs before TM_ESC_01. A plan not working is different from a plan
not done; the fidelity gate ensures escalation is only triggered when the plan was genuinely
delivered.
The fidelity gate
The fidelity tracker (WP-FID-BE, 35-fidelity-tracker) is shipped. RTI reads its verdict through
prismaFidelityReader, which delegates to the tracker’s readLatestProjection: the latest
intervention_fidelity snapshot yields a computed verdict (high / adequate / partial / low
/ unknown) plus the escalationBlocked boolean the tracker persists. TM_BLOCK_01 reads that
escalationBlocked value rather than re-deriving it from the raw rate.
The fail-closed default still applies, but only to genuinely missing data. A missing fidelity row,
a cross-org row, or a null bundleAssignmentId (no active plan) returns fidelityRating = 'unknown',
escalationBlocked = true. That is the B.21 guardrail: absence of fidelity data must block
escalation, never allow it. It is no longer a blanket block standing in for an unbuilt module; the
gate now reflects the real delivery verdict for any student who has one.
TIER_3 approval gate
TIER_3 escalation requires the REQUEST_TIER_REVIEW permission (held by teachers) on the route,
and the in-handler APP_HIGH_INTENSITY specialist gate (APPROVE_TIER_3_ACTIVATION). That
permission is keyed on AdminProfile.specialistRole. The permitted Tier-3 approver in Wave 1 is
therefore a teacher who is admin-seated as the specialist (B.20), not a plain admin. A plain
admin without specialistRole is rejected (403).
applyDecision(): the single write path
All tier mutations go through apply-decision.ts. It is the only place
student_tier_status.currentRtiTier is updated (enforced by lint:tier-status-single-write-path).
The write is atomic: in a single transaction it:
- Inserts an append-only
tier_decision_record - Updates
student_tier_status.currentRtiTier - Closes the linked
rti_alert - Transitions the
tier_review_requeststatus - Appends a
teacher_action_logentry
A tier change without a corresponding tier_decision_record is structurally impossible.
Determinism (V-6)
The four pure engine files (layer-a.ts, layer-b.ts, rti-status.ts, tm-rules.ts) take only
stored inputs and the pinned tier_movement_rule version. Given the same inputs and the same rule
version, they always produce the same alert kind and movement verdict, guaranteed by the design and
verified by the test suite.
The upstream seams (now bound)
Both producers RTI reads from are shipped, so the readers bind to real verdicts. The injectable interfaces remain so the gate stays unit-testable with a mock, and each one stays fail-closed on genuinely missing data:
| Seam | Bound to | Fail-closed on |
|---|---|---|
fidelity-reader.ts | WP-FID-BE’s readLatestProjection (the computed verdict + persisted escalationBlocked) | A missing / cross-org intervention_fidelity row, or no active plan ⇒ unknown / escalationBlocked = true |
data-room-reader.ts | WP-DATA-ROOM-BE: reads student_data_room.dataSufficiencyStatus (sufficient / insufficient / contextual); opens packets through the data room’s single writer | A missing / unreadable / cross-org packet ⇒ insufficient |
One residual is genuinely deferred: documentedException (the APP_HIGH_INTENSITY documented-exception
branch of TM_ESC_01’s adequate_or_documented clause) is not yet a field on intervention_fidelity;
it stays false, and the adequate / high verdicts satisfy the clause in the interim. It lands with
the decision-packet work.
Related guides
- CBM & ORF Fluency: CBM emits the raw trend signal the RTI engine consumes
- Progress Monitoring: monitoring verdicts feed Layer B
- Teacher Activation Workflow: the teacher-side surface for nominating tier reviews
- Student Profiles: the profile that feeds the RTI sentinel check
- Safety Rules: the fidelity gate and GSR rules in full context