Skip to Content

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 typeWhat it does
clinical_termReplaces or blocks clinical/diagnostic vocabulary
international_framework_labelStrips 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_phrasingSoftens categorical deficit phrasing to growth framing
cbm_alert_scriptApplies CBM-specific growth-language scripts
parent_stricter_guardrailGL-001..GL-005 guardrails (parent tier, see below)
profile_do_not_sayPer-profile banned phrases loaded at render time
numeric_score_blockStrips raw numeric scores from parent-tier copy
internal_label_blockStrips 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).

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:

TierWhoAdditional restrictions
ui / reports / teacher_note / adminTeacher, adminFull clinical-term and framework-label filter; categorical softening
parentParentAll of the above plus parent-specific growth scripts
parent_stricterParent (reports, PDF)All of the above, PLUS: no numeric scores, no internal profile/bundle codes, no internal labels, plus the 5 GL guardrails
studentStudentSeparate 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

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 IDWhen to use
CTX_SESSION_INTERRUPTEDThe session was interrupted before natural completion
CTX_TECHNICAL_ISSUEA device or connectivity problem affected the session
CTX_AUDIO_PROBLEMAudio playback was impaired
CTX_GROUP_NOT_COMPLETEDA group activity could not be finished
CTX_STUDENT_ABSENTStudent was absent during a scheduled probe
CTX_NEEDED_REPETITIONStudent needed repeated instruction beyond normal
CTX_PAPER_CHECK_ADMINA 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

curl -X POST https://localhost:3000/api/students/{studentId}/context-flag \ -H "Authorization: Bearer <accessToken>" \ -H "Content-Type: application/json" \ -d '{ "flagId": "CTX_SESSION_INTERRUPTED", "relatedEvidenceId": 42 }'

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

Reading context flags

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)

EndpointPermissionWhat it does
GET /api/language-rulesVIEW_OWN_CLASSES or RUN_BACKOFFICEList all rules (active and superseded)
POST /api/language-rulesRUN_BACKOFFICEPublish a new rule version
PATCH /api/language-rules/{id}/deactivateRUN_BACKOFFICEDeactivate an old rule version
POST /api/language-rules/validateVIEW_OWN_CLASSES or RUN_BACKOFFICEDry-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.

  • System 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: the broader set of cross-cutting safety rules
  • Student Profiles: SPOT narrative templates and the per-profile do_not_say mechanism
Last updated on