---
title: "Student Data Room"
url: https://docs.growamal.com/en/guides/data-room/
language: en
---

# Student Data Room

The Data Room (WP-DATA-ROOM-BE) is the per-student **decision-documentation layer**: it assembles a
child's decision-relevant evidence into a reviewable, audit-anchored dossier, and provides the
decision-packet shell the tier-movement workflow builds on. It never decides; the rules in the
skill-status, monitoring, RTI, and profile engines still decide. The Data Room makes every sensitive
decision show its work.

> **Note:** The Data Room never writes a tier. It reads and composes the `tier_decision_record`; the only tier
> write path is the RTI engine. Evidence is indexed, never copied: every index row points back to its
> real source record.

## Routes

| Method | Path | Permission |
|---|---|---|
| `GET` | `/api/students/{id}/data-room` | `VIEW_OWN_CLASSES` (ClassTeacher) or `RUN_BACKOFFICE` |
| `GET` | `/api/students/{id}/data-room/evidence` | `VIEW_OWN_CLASSES` (ClassTeacher) or `RUN_BACKOFFICE` |
| `POST` | `/api/students/{id}/decision-packets` | `VIEW_OWN_CLASSES` (ClassTeacher) or `RUN_BACKOFFICE` |
| `GET` | `/api/decision-packets/{id}` | `VIEW_OWN_CLASSES` (ClassTeacher) or `RUN_BACKOFFICE` |
| `GET` | `/api/students/{id}/decision-history` | `VIEW_OWN_CLASSES` (ClassTeacher) or `RUN_BACKOFFICE` |

Student `id` and packet `id` are positive integers. All routes gate on the student's class through
the in-handler ClassTeacher guard; a cross-org or out-of-scope id returns `404`. There is
deliberately **no student endpoint**; students never see the Data Room.

## `GET /data-room`: the dossier

The dossier returns the eight summary rollups and a data-sufficiency status. `?refresh=true` forces
a fresh refresh before reading.

```bash
curl https://localhost:3000/api/students/42/data-room \
  -H "Authorization: Bearer <accessToken>"
```

```json
{
  "studentId": 42,
  "dataSufficiencyStatus": "insufficient",
  "nextRecommendedAction": "...",
  "summaryBlocks": {
    "benchmark": { },
    "cbmTrend": { },
    "practice": { },
    "paperChecks": { },
    "maintenance": { },
    "errorPatterns": { },
    "fidelity": { },
    "behaviorFlags": { }
  },
  "updatedAt": "2026-06-20T10:00:00.000Z"
}
```

`dataSufficiencyStatus` is `sufficient` / `insufficient` / `contextual` (or null when no data room
exists yet). The summary blocks are system-rendered rollups of referenced evidence, never free
text, never an input to a rule. When a contributing skill is `do_not_decide_yet`, or the evidence is
below the comparable floor, the status reports `insufficient` and `nextRecommendedAction` carries the
next data action, never a movement.

## `GET /data-room/evidence`: the evidence index

The evidence index pages the referenced evidence; every row points back to its source record. It
filters on `evidenceType`, `validityStatus` (`valid` / `use_with_caution` / `invalid`), and
`includedInDecisionPacket`, with `limit` (1 to 200, default 50) and `offset`.

```bash
curl "https://localhost:3000/api/students/42/data-room/evidence?validityStatus=valid&limit=20" \
  -H "Authorization: Bearer <accessToken>"
```

Invalid-administration evidence stays indexed (nothing is deleted) but is excluded from the
sufficiency count.

## `POST /decision-packets`: open a packet shell

Opening a packet snapshots the evidence by reference and pins the rule versions it read, so the
packet replays byte-for-byte years later. The body carries only the `reviewType`, the trigger
metadata, and optional controlled context-flag ids. There is **no free-text field**.

```bash
curl -X POST https://localhost:3000/api/students/42/decision-packets \
  -H "Authorization: Bearer <accessToken>" \
  -H "Content-Type: application/json" \
  -d '{
    "reviewType": "tier_review",
    "triggerSource": "system",
    "triggerRuleId": "DATA_ROOM_OPEN_PACKET",
    "contextFlagIds": ["CTX_RECENT_ABSENCE"]
  }'
```

`reviewType` is `profile_bundle` / `tier_review` / `plan_adjustment` / `maintenance_review`.
`triggerSource` is `system` / `v3` / `add`. A second open of the same `reviewType` returns `409`; a
blocked or unknown context flag returns `422`. The response carries the `packetId` and the
`snapshottedEvidenceCount`.

`GET /api/decision-packets/{id}` returns the full packet including the pinned rule versions and the
evidence snapshot (references). A closed packet is immutable forever.

## `GET /decision-history`

The decision history is a computed chronology: a newest-first merge of `tier_decision_record`,
`decision_packet`, and `teacher_action_log`. It is read-only; the Data Room composes the timeline but
writes none of the underlying records.

## Invariants

- **The Data Room never writes a tier.** It reads and composes the `tier_decision_record`; the only
  tier write path is the RTI engine (proven by `lint:data-room-single-write-path`).
- **Evidence is indexed, never copied.** Invalid rows stay indexed (nothing is deleted) but are
  excluded from the sufficiency count.
- **Weak data blocks a strong decision.** A `do_not_decide_yet` skill or sub-floor evidence yields
  `insufficient` and the next data action, never a movement.
- **Immutable packet (V-6).** Opening a packet pins every version it read; a closed packet replays
  byte-for-byte.
- **Append-only** (only `summaryBlocks` / `includedInDecisionPacket` / `status` are mutable),
  **no free text** (controlled context flags only), **no LLM (V-5)**, **tenant-isolated**
  (`organizationId`; cross-org `404`), and **never student-facing**.

## Related guides

- [RTI Decisions & Tiers](https://docs.growamal.com/en/guides/rti-decisions/): the engine that owns the only tier write path
- [Decision Packets](https://docs.growamal.com/en/guides/decision-packets/): the workflow that assembles a rich packet on top of this shell
- [Intervention Fidelity Tracker](https://docs.growamal.com/en/guides/fidelity-tracker/): the fidelity rollup read into the dossier
- [Progress Monitoring](https://docs.growamal.com/en/guides/progress-monitoring/): a source of the evidence the dossier indexes
