Skip to Content

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_yet for the student’s evidence
  • A domain shows partial_domain status
  • 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:

CbmTrendSignalRTI effect
trend_below_goalMaps 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_pointsA 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_goalNeither; the student is progressing
trend_mixedNeither. 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 kindDescription
Skill_AlertOne or more skills show a pattern that warrants teacher attention, but does not yet meet the threshold for a student-level concern
Student_AlertThe pattern is broad enough to concern the student’s overall trajectory; a tier-review nomination may be appropriate
Intensive_ReviewThe 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:

TierInternal label
TIER_1_CORECore classroom instruction
TIER_2_TARGETEDTargeted small-group support
TIER_3_INTENSIVEIntensive 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:

  1. Open POST /api/tier-review-requests: opens a formal TierReviewRequest and links a decision_packet (the evidence bundle used to support the case).
  2. 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.
  3. Decide POST /api/tier-review-requests/:id/decision: the teacher (or specialist for TIER_3) approves or rejects. Approval triggers applyDecision().

The four TM_ movement rules

Every proposed movement is tested against all four rules:

RuleGateBlocks what
TM_BLOCK_01Fidelity gate: ≥80% plan delivery requiredEscalation when the current plan was not actually delivered
TM_ESC_01Escalation criteriaMovement upward (higher support)
TM_MAINT_01Maintenance criteriaStaying at current tier when evidence supports movement
TM_DEC_01De-escalation criteriaMovement 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:

  1. Inserts an append-only tier_decision_record
  2. Updates student_tier_status.currentRtiTier
  3. Closes the linked rti_alert
  4. Transitions the tier_review_request status
  5. Appends a teacher_action_log entry

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:

SeamBound toFail-closed on
fidelity-reader.tsWP-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.tsWP-DATA-ROOM-BE: reads student_data_room.dataSufficiencyStatus (sufficient / insufficient / contextual); opens packets through the data room’s single writerA 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.

Last updated on