---
title: "Language Safety"
url: https://docs.growamal.com/en/guides/language-safety/
language: en
---

# Language Safety

Every string Amal renders, saves, or prints passes through the language-safety layer. This module enforces two absolute commitments: growth language in all user-facing copy, and no free text about students ever stored on the server.

## Why this exists

Platform architecture V-12 states: "Teacher decides; system proposes; growth language only." Two concrete problems drive the design:

1. **Deficit/clinical language harms children.** Words that label or stigmatize undermine self-concept. Every rendered string must describe growth and next steps, never a deficit.
2. **International framework labels are for internal calibration, not user-facing copy.** Terms like PIRLS, EGRA, GPF, and Lexile (and their Arabic equivalents) appear in the research literature Amal's partner draws from, but they must never appear in copy a teacher, parent, or student sees. The Q15 hard rule bans them from all user-facing surfaces.

The safety layer also owns the **only legal path for a teacher to attach context to a student session**: a controlled closed-dictionary context flag, never free text.

## The rule pipeline

When the server needs to render a string for a given audience, it calls `sanitize(text, activeRules, { appliesTo })`. Rules are loaded from the database and applied in a stable order:

| Rule type | What it does |
|---|---|
| `clinical_term` | Replaces or blocks clinical/diagnostic vocabulary |
| `international_framework_label` | Strips labels such as PIRLS, EGRA, GPF, Lexile (and Arabic equivalents) from user-facing copy; these terms are blocked here because they are the blocked list; see [Callout below] |
| `categorical_phrasing` | Softens categorical deficit phrasing to growth framing |
| `cbm_alert_script` | Applies CBM-specific growth-language scripts |
| `parent_stricter_guardrail` | GL-001..GL-005 guardrails (parent tier, see below) |
| `profile_do_not_say` | Per-profile banned phrases loaded at render time |
| `numeric_score_block` | Strips raw numeric scores from parent-tier copy |
| `internal_label_block` | Strips internal profile/bundle codes from parent-tier copy |

Each rule has a `severity` (`block` / `soften` / `strip`) and an `appliesTo` audience set (`ui` / `reports` / `parent` / `parent_stricter` / `teacher_note` / `student` / `admin`).

> **Warning:** **Why the framework labels (PIRLS, EGRA, GPF, Lexile…) are mentioned here:** this page is describing what the filter BLOCKS. Naming them is correct here; it is the only place they belong. On any teacher/parent/student-facing surface they must never appear.

## Audience tiers

The filter has four audience tiers that stack on each other:

| Tier | Who | Additional restrictions |
|---|---|---|
| `ui` / `reports` / `teacher_note` / `admin` | Teacher, admin | Full clinical-term and framework-label filter; categorical softening |
| `parent` | Parent | All of the above plus parent-specific growth scripts |
| `parent_stricter` | Parent (reports, PDF) | All of the above, PLUS: no numeric scores, no internal profile/bundle codes, no internal labels, plus the 5 GL guardrails |
| `student` | Student | Separate filter: no peer comparisons, no labels, supportive-only feedback |

### The 5 GL guardrails (GL-001..GL-005)

These are specific Arabic deficit phrases identified by the partner that are categorically banned from parent and student copy. Each is a substring ban with a required replacement or omission. The rules are in the `LanguageRule` table with `ruleType=parent_stricter_guardrail`.

### Per-profile `do_not_say`

Each primary profile has a `do_not_say` list in the Profile Grade Application (PGA) matrix. At render time, the language-safety engine loads the active set for the student's profile and adds those phrases as an overlay filter. This ensures the SPOT narrative templates never accidentally use a phrase that contradicts the profile's growth framing.

## Append-only versioning

Rules are never edited in place. To change a rule:

1. POST a new rule (a new row is inserted with a new `version`).
2. PATCH the old rule to `deactivate` it (`PATCH /api/language-rules/{id}/deactivate`).

The old version's row remains permanently for audit. Active rules are loaded fail-closed: if no active rule set exists when the engine tries to render, it throws rather than emit unfiltered text (V-6 + V-3 scientific honesty).

## Context flags: the only way to attach context to a student

> **Warning:** **There is no free-text field on any student record.** V-12 and hard rule B.24 prohibit it. The only way a teacher can attach session or instructional context to a student is by selecting a flag from the closed `ContextFlagDictionary`.

The 7 allowed flag IDs:

| Flag ID | When to use |
|---|---|
| `CTX_SESSION_INTERRUPTED` | The session was interrupted before natural completion |
| `CTX_TECHNICAL_ISSUE` | A device or connectivity problem affected the session |
| `CTX_AUDIO_PROBLEM` | Audio playback was impaired |
| `CTX_GROUP_NOT_COMPLETED` | A group activity could not be finished |
| `CTX_STUDENT_ABSENT` | Student was absent during a scheduled probe |
| `CTX_NEEDED_REPETITION` | Student needed repeated instruction beyond normal |
| `CTX_PAPER_CHECK_ADMIN` | A paper-based check was administered (alias `CTX_PAPER_CHECK_ADMINISTERED`) |

Three additional flag IDs are explicitly **blocked** (`CTX_FREE_TEXT_NOTE`, `CTX_MEDICAL_NOTE`, `CTX_PSYCHOLOGICAL_NOTE`). Attempting to submit one of these returns `422 context_flag_blocked`; the system writes a **length-only** row to `security_audit_log` (the flag value itself is never stored).

### Writing a context flag

```bash
curl -X POST https://localhost:3000/api/students/{studentId}/context-flag \
  -H "Authorization: Bearer <accessToken>" \
  -H "Content-Type: application/json" \
  -d '{
    "flag_id": "CTX_SESSION_INTERRUPTED",
    "evidence_ref": "optional-string-reference",
    "session_ref": "optional-string-reference"
  }'
```

> **Warning:** The request body is validated with a **strict schema** (`flag_id`, `evidence_ref`, `session_ref`). All three fields use snake_case. Any unknown key in the body (including camelCase variants such as `flagId` or `relatedEvidenceId`) returns `422`. `evidence_ref` and `session_ref` are optional strings.

Requires `SELECT_CONTEXT_FLAG`. Returns `{ studentId, contextFlagLogId, flagId }`.

### Reading context flags

```bash
curl https://localhost:3000/api/students/{studentId}/context-flags \
  -H "Authorization: Bearer <accessToken>"
```

Requires `VIEW_OWN_CLASSES` or `RUN_BACKOFFICE`.

## Teacher notes are print-only

When a teacher wants to annotate a student record informally, the platform supports **print-only teacher notes**. These notes exist only in the browser and on paper: they are never sent to the server, never stored, and never appear in any API response. This is a V-12 privacy commitment, not a technical limitation to be worked around.

## Language-rule management (admin/backoffice)

| Endpoint | Permission | What it does |
|---|---|---|
| `GET /api/language-rules` | `VIEW_OWN_CLASSES` or `RUN_BACKOFFICE` | List all active rules (inactive/superseded rows are excluded; full history is in the database for audit only) |
| `POST /api/language-rules` | `RUN_BACKOFFICE` | Publish a new rule version |
| `PATCH /api/language-rules/{id}/deactivate` | `RUN_BACKOFFICE` | Deactivate an old rule version |
| `POST /api/language-rules/validate` | `VIEW_OWN_CLASSES` or `RUN_BACKOFFICE` | Dry-run a string against active rules without persisting |

## In-process use

Consumers that need the filter at write time (the teacher-AI chokepoint, alert text generation) import `sanitize` and `loadActiveRules` directly from the `language-safety` module. No HTTP hop is needed.

## Related pages

- [System QA Checks](https://docs.growamal.com/en/guides/qa-checks/): QA_022 (sensitive-language check) and QA_023 (parent-report safety check) are entries in the registry that reference the enforcement here
- [Safety Rules](https://docs.growamal.com/en/how-it-works/safety/): the broader set of cross-cutting safety rules
- [Student Profiles](https://docs.growamal.com/en/guides/student-profiles/): SPOT narrative templates and the per-profile `do_not_say` mechanism
