---
title: "Reporting & Parent PDF"
url: https://docs.growamal.com/en/guides/reporting-and-parent-pdf/
language: en
---

# Reporting & Parent PDF

Two modules compose the read-side reporting surface: `modules/reporting` (WP-06-BE) produces teacher- and admin-facing sub-skill reports, and `modules/parent-pdf` (WP-09-BE) produces a growth-language parent PDF. Both are read-only: they compose already-persisted engine outputs and write nothing.

## Who sees what

| Audience | Surface | What is shown | What is never shown |
|---|---|---|---|
| **Teacher** | Sub-skill report + class summary | Profile narrative, macro-domain statuses, per-skill status sorted by need for support, active bundle plan, open alerts, class-level status counts | θ score, profile codes (e.g. `PR2-04`), benchmark numbers, internal IDs |
| **Manager / Principal** | Class summary (aggregate-only), school overview, heatmap | Status counts per skill and macro domain across classes/school | Per-student detail, θ score, any single overall percentage |
| **Parent** | Growth PDF (teacher- or parent-triggered, downloaded, shared off-platform) | Child's growth described in Arabic growth language, macro-domain status words (categorical only), teacher name, school name | All numbers and percentages, θ score, profile codes, internal IDs, benchmark comparisons |
| **Student** | Never | | Everything. Students never receive a report surface. |

> **Warning:** θ scores and internal profile codes (such as `PR2-04`) are developer-facing context. They must never appear in any teacher, parent, or student-facing surface.

## Sub-skill report (WP-06-BE)

`GET /api/reports/students/{id}/sub-skill-report` composes five sections for a teacher:

1. **Profile section**: the student's primary profile narrative (pre-rendered Arabic text from the SPOT template library), plus a `dataIncomplete: true` flag when no assignment has been made yet.
2. **Macro tiles**: the student's status on each of the 6 macro domains plus the GLOBAL meta. Statuses are categorical (`Meets`, `Approaching`, `Below`, `Severe`, `Partial`, `Contextual`, `Not_Assessed`), never a percentage, never an average. `Partial` and `Contextual` are pass-through statuses that appear when the domain-rollup rule detects incomplete evidence or a contextual pattern; both are excluded from worst-case-wins rollup alongside `Not_Assessed`.
3. **Per-skill rows**: one row per sub-skill, sorted with "needs support" skills first (the Arabic tier ordering `يحتاج تدخّل` is applied by the reporting engine's sort layer).
4. **Active plan**: the student's current bundle assignment and its status.
5. **Open alerts**: active acute-regression alerts and monitoring flags.

No measurement logic runs here. All statuses come from already-persisted snapshots produced by the
skill-status engine and profile resolution modules. Those snapshots are populated by the
[Measurement Bridge](https://docs.growamal.com/en/guides/measurement-bridge/) at diagnostic finish; before a diagnostic
completes, the report returns `dataIncomplete: true` (sub-skill report) or `Not_Assessed`
(macro-domain tiles) in an HTTP 200 response.

> **Note:** If the student has no assignment yet, `profile.dataIncomplete` is `true` and the response is still HTTP 200 (fail-closed, never a 404 for missing data).

## Class summary (WP-06-BE)

`GET /api/reports/classes/{id}/class-summary` returns:

- **Per-skill distributions**: counts of students at each status for every sub-skill, plus the Arabic action-tier distribution.
- **Class macro**: worst-case-wins status rollup across the class for each macro domain. This is a status-level operation: it returns the most concerning status observed, not an average.

The `audience` query parameter controls the response shape:

| `audience` value | Per-student rows | Use case |
|---|---|---|
| `teacher` | Included (per-student macro summary) | Teacher's own class dashboard |
| `manager` | `null` (aggregate only) | District manager or non-class-owner view |

A teacher can only call this for classes they are assigned to (`ClassTeacher` junction). A manager or admin always receives the aggregate-only shape.

## Band descriptions (WP-06-BE)

`GET /api/reports/band-descriptions` returns the 5-band Mizan reference catalog. Each band has Arabic and English labels plus an Arabic description. This is reference data for the teacher UI; it explains what each status band means in growth language, without numbers or comparisons.

These labels are working defaults pending partner sign-off; the `partnerOwed` flag on each row signals this.

## Parent PDF (WP-09-BE)

`POST /api/parent/pdf/{childId}` is teacher- or parent-triggered. A teacher holding `VIEW_OWN_CLASSES` can generate the PDF for any student in their class. A parent holding `VIEW_PARENT_REPORTS` can also generate the PDF for their own linked child directly from the parent growth page. The endpoint enforces own-child scope via `canReachStudent` (`allowParent: true`), so a parent cannot request another child's PDF. It returns a binary PDF stream (`application/pdf`), A4 format, Arabic-RTL layout with embedded Noto Naskh Arabic font. The PDF is generated deterministically: byte-identical content across runs for identical inputs (V-6).

### Parent-stricter language filter

Every string in the parent PDF passes two layers of language safety:

1. **`applyParentStricterFilter`**: the canonical parent-stricter belt from `modules/language-safety` (FR-LANG-6). It blocks internal profile codes, numeric scores, benchmark comparisons, and clinical/framework labels.
2. **`ParentSummaryDTO` field strip**: typed discipline that removes internal fields before the PDF layout engine ever sees them. θ, profile codes, and internal IDs are structurally absent, not just filtered at the string level.

### What the parent PDF contains

- Child's first name and school name.
- Macro-domain status words in Arabic, categorical descriptions only (e.g. "يحتاج دعمًا", "needs support"), never a number.
- `Not_Assessed` is rendered as "نحتاج بيانات إضافية" (we need more data); it is never hidden and never treated as a low result.
- Growth-language narrative drawn from the SPOT profile template, with sensitive-language guardrails applied.

### Halt page

If no resolved parent narrative exists for the student (for example, the student's profile is `DATA_INCOMPLETE`), the PDF renders a safe GL-005 halt page. It never fabricates a profile or falls back to a generic statement. No active language-safety rule set causes the endpoint to return 503 rather than ship unfiltered text.

## School overview and board PDF (WP-10-BE)

Two aggregate-only endpoints are gated by the `SCHOOL_READ` permission set (`VIEW_SCHOOL` for Principal and Manager, `VIEW_ORG` for Admin, `RUN_BACKOFFICE` for super-admin). Neither a Teacher, a Parent, nor a Student holds any of these permissions.

### School overview

`GET /api/schools/{schoolId}/overview` returns a `SchoolOverviewDTO` with per-class macro-domain status counts and a school-level distribution. The response contains class names and counts only. No student names, student IDs, or per-student rows appear at any level.

### Board PDF

`GET /api/schools/{schoolId}/board-pdf` returns a one-page A4 Arabic-RTL PDF (`application/pdf`) suitable for a board or leadership audience. It shows the same aggregate counts as the school overview plus 6 macro-domain tiles. The PDF is capped at 500 KB. If PDF generation fails or exceeds the size limit the endpoint returns `503` (fail-closed).

For both endpoints, in-handler `canReachSchool` enforces the per-actor school boundary: a Principal sees their own school only, a Manager sees their scoped schools only, and a cross-org school request returns `404` (existence non-disclosure, FR-TEN-9).

## No single overall score

Neither the sub-skill report, the class summary, nor the parent PDF contains a single number or percentage representing overall Arabic literacy. Every status is per-measure or per-macro-domain. This is a hard platform rule (V-3) enforced in both the reporting engine and the parent PDF composition layer.

## Re-diagnostic comparison

`GET /api/reports/students/:id/re-diagnostic-comparison/:sessionId` returns a per-sub-skill
comparison between the specified session and the preceding completed session for the same student
and cycle.

Each row in the response carries:

| Field | Description |
|---|---|
| `priorStatus` | `SkillStatus` from the session before the re-diagnostic (or `null` if none) |
| `currentStatus` | `SkillStatus` from the specified session |
| `priorActionTier` | Arabic action-tier label for the prior session (e.g. `يحتاج تدخّل`); `null` when no prior status |
| `currentActionTier` | Arabic action-tier label for the current session; `null` when `doNotDecideYet` |
| `statusTransition` | String in the form `"<prior> → <current>"` (Unicode right arrow U+2192). The prior and current values are Arabic action-tier labels (such as `يحتاج تدخّل` or `مناسب`), not raw `SkillStatus` enum tokens. Use `priorStatus` / `currentStatus` for enum-level processing. |
| `growth` | One of `improved`, `declined`, `same`, or `indeterminate` |

Growth direction is derived from action-tier movement, not raw θ. No raw θ or standard error is
included in the response. A teacher can only request this for students in their own classes
(`ClassTeacher` junction); a cross-org request returns `404`.

If no preceding completed session exists for the student and cycle, the endpoint returns `404`
with `reason: no_prior_session_for_comparison`.

## Related

- [Standards & Benchmarks](https://docs.growamal.com/en/guides/standards-and-benchmarks/): how raw scores become `MeasureStatus` verdicts
- [Skill-Status Engine](https://docs.growamal.com/en/guides/skill-status-engine/): how skill-level and domain-level statuses are derived
- [Student Profiles](https://docs.growamal.com/en/guides/student-profiles/): how the primary profile and SPOT narrative are resolved
- [Language Safety](https://docs.growamal.com/en/guides/language-safety/): parent-stricter filter and growth-language rules
- [Managers & Principals](https://docs.growamal.com/en/guides/managers-and-principals/): school-level reporting for principals
