---
title: "RTI Decisions & Tiers"
url: https://docs.growamal.com/en/guides/rti-decisions/
language: en
---

# RTI Decisions & Tiers

The RTI engine is the central decision layer. The routes are published under the `rti` tag in the
[API Reference](https://docs.growamal.com/en/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:

| `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 |

> **Note:** 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:

| Rule | Gate | Blocks what |
|---|---|---|
| `TM_BLOCK_01` | Fidelity gate: ≥80% plan delivery required | Escalation AND de-escalation when fidelity is low/unknown or the plan is off-track (no delivery for several consecutive instructional weeks). Only maintenance and insufficient-data reviews may proceed, and those still route to a Decision Packet. |
| `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:

1. Inserts an append-only `tier_decision_record`
2. Updates `student_tier_status.currentRtiTier` (only when the decision moves the tier)
3. Closes the linked `rti_alert` (only on a tier move)
4. Transitions the `tier_review_request` status to `closed`

A tier change without a corresponding `tier_decision_record` is structurally impossible.

Alert acknowledge and dismiss actions each write a `teacher_action_log` row through their own
handlers, but the tier-decision path (`applyDecision`) does not.

## 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](https://docs.growamal.com/en/guides/cbm-orf-fluency/): CBM emits the raw trend signal the RTI engine consumes
- [Progress Monitoring](https://docs.growamal.com/en/guides/progress-monitoring/): monitoring verdicts feed Layer B
- [Teacher Activation Workflow](https://docs.growamal.com/en/guides/teacher-activation/): the teacher-side surface for nominating tier reviews
- [Student Profiles](https://docs.growamal.com/en/guides/student-profiles/): the profile that feeds the RTI sentinel check
- [Safety Rules](https://docs.growamal.com/en/how-it-works/safety/): the fidelity gate and GSR rules in full context
