---
title: "System QA Checks"
url: https://docs.growamal.com/en/guides/qa-checks/
language: en
---

# System QA Checks

The System QA Checks module is the cross-cutting backstop of Amal's safety architecture. It maintains a versioned registry of 38 named checks, routes runtime failures deterministically, and exposes three read endpoints for monitoring, without duplicating a single line of enforcement logic from the engine modules that already own it.

## What this module does and does not do

> **Note:** **This module registers and routes. It does not re-implement.** Every check's enforcement lives in the module that owns the rule (the skill-status engine, the cluster module, the bundle catalog, the language-safety layer, etc.). The QA module provides a single place to discover what checks exist, what their severity is, and when they fired at runtime.

The three responsibilities:

1. **Register**: a versioned, publish-then-freeze config table (`qa_check_definition`, 38 rows, global with no tenant scoping) describes every check: its ID (`QA_001..QA_038`), the CI lint command that enforces it, a human-readable failure message in Arabic, and its severity.
2. **Route**: when an engine module detects a violation, it calls `routeQaFailure({ qaCheckId })`. The router looks up the check's severity and applies the deterministic routing matrix.
3. **Read**: three admin/eng-only HTTP endpoints expose the registry and the runtime alert log.

## The registry (38 checks)

The 38 checks (31 v5 partner checks + QA_032 over-assessment guard + QA_033..QA_038 R12 Tier-D
bundle-restructure guardrails, provenance `new_in_wp_qac_be`) cover the major cross-cutting categories:

| Category | Examples |
|---|---|
| Data sufficiency | QA_001 Tie Rule / `do_not_decide_yet` guard |
| Acute regression | QA_002 ≥20% drop triggers review, never auto-fail |
| Bulk activation safety | QA_003 `DATA_INCOMPLETE` exclusion, QA_004 acute-regression exclusion |
| Cluster consistency | QA_005 5-key cluster identity |
| Scaffold-tier integrity | QA_006 per-(student × skill × bundle × task) uniqueness |
| Teacher confirmation | QA_007 explicit confirmation required for bulk-activate |
| Sensitive language | QA_022 language filter applied, QA_023 parent-report safety |
| Arabic agreement | QA_008 grammar check at import |
| Comparable data | QA_009 only comparable probes compared |
| Anchor preservation | QA_020 anchor weight must remain the largest share |
| Overload prevention | QA_021 max 2 supporting threads |
| Content validity | QA_010 inference items need an `evidence_sentence` |
| Arabic feature scope | QA_011 no Arabic-feature sub-flag alone may trigger escalation |
| Runtime audit | QA_012 rule-version pinning on all decisions |
| Parent-report safety | QA_023 no internal labels in parent PDF |
| Over-assessment guard | QA_032 alert when probes exceed threshold in a window (new) |
| Bundle-restructure guardrails (R12 Tier-D) | QA_033 one primary bundle per cycle, QA_034 active-phase routing only (no `FUTURE_INACTIVE`), QA_035 no pseudo-bundle routing (reject a non-bundle in `recommended_bundle_id`), QA_036 no ORF error-type inference, QA_037 no inactive-feature routing, QA_038 content suitability / no probe-as-instruction |

> **Warning:** QA_027 (RTI fidelity gate) and QA_030 (fidelity-tracker cross-check) are registered and routing-ready but are not yet wired to their consumer WPs (WP-RTI-BE / fidelity-tracker). They will fire when those WPs bind `routeQaFailure`.

## Severity routing matrix

The routing is **deterministic by the registry `severity` field**; no call-site can override it:

| Severity | What happens |
|---|---|
| `critical` | `writeQaAlert` (alert logged, `surfacedToTeacher=true`) + `notifier.notifyCritical` |
| `high` | `writeQaAlert` + `notifier.notifyEngineering` (not teacher-surfaced) |
| `medium` | `writeQaAlert` only |
| `low` | Log to the row's `runtimeLogTarget` only; no alert written, no notifier |

In Wave 1, the notifier is a **log-only seam** (`logOnlyQaNotifier`). No network call is made. The Slack `#amal-alerts` integration is queued for post-launch (Q-QAC-1).

Runtime alerts land in the existing `platform_alert_log` table (no new table). QA-routed rows set `alertKind = "qa_NNN"` (lowercase) and a `severity` column, making filtered reads a covered index scan.

## The over-assessment guard (QA_032)

QA_032 is a new check added by this work package (not from the partner v5 spec). It fires when a student accumulates more than `over_testing_threshold` progress-monitoring probes within `over_testing_window` days.

Key design points:
- **Never a block.** The probe is always recorded. The guard fires *after* writing, as a soft advisory.
- Working defaults: threshold = 8 probes, window = 14 days (to be confirmed with partner June-25).
- The guard is injected into `monitoring/repository/evidence-writer.ts` at startup. There is no import cycle between `modules/qa` and `modules/monitoring`.

## Registry versioning

Admins publish a new check-set with `POST /api/admin/qa/checks-set`. The endpoint enforces:
- Partner approval must be supplied (`partnerApprovedBy` + `partnerApprovedAt`).
- The diff must be explicitly acknowledged (`acknowledgeDiff: true`).
- All 38 checks must be present in the payload.
- Every row's `ciLintCommand` must be a runnable workspace command.
- Every row's `failureMessageAr` must pass the language-safety filter (no banned substring).

A `422` is returned on any gap. The seed (`v1`) is the path used at initial deploy; this endpoint governs subsequent publishes.

## API endpoints

All three read routes and the version-publish route require elevated permissions; they are not accessible by teacher or student tokens (returns `403`):

| Endpoint | Permission | What it returns |
|---|---|---|
| `GET /api/qa/checks` | `VIEW_QA_REGISTRY` or `RUN_BACKOFFICE` or `SUPER_ADMIN` | The full registry (global, no tenant filter, but requires `requireTenantContext` to derive permissions) |
| `GET /api/qa/runtime-alerts` | same | Org-scoped alerts where `alertKind LIKE 'qa_%'`; supports `?severity=`, `?from=`, `?to=`, `?qaCheckId=` |
| `GET /api/admin/qa/audit-summary` | `RUN_BACKOFFICE` or `SUPER_ADMIN` | By-check and by-severity counts for a date range (the partner pre-launch walkthrough feed) |
| `POST /api/admin/qa/checks-set` | `RUN_BACKOFFICE` or `SUPER_ADMIN` | Publish a new registry version |

`VIEW_QA_REGISTRY` is the 25th permission code in the closed 25-value enum.

## `test:qa:NNN` convention

Each registry row's `ciLintCommand` is the real workspace command that enforces it. `package.json` adds a thin `test:qa:NNN` alias per row delegating to that native command (e.g. `test:qa:003 → test:cluster:exclusions`), plus the deduplicated aggregate `pnpm test:qa`. The CI `qa-registry` job runs `pnpm test:qa` and `pnpm lint:qa` and blocks the PR on failure.

## Related pages

- [Safety Rules](https://docs.growamal.com/en/how-it-works/safety/): the cross-cutting safety architecture this module backstops
- [Language Safety](https://docs.growamal.com/en/guides/language-safety/): QA_022 and QA_023 reference enforcement here
- [Smart Clustering](https://docs.growamal.com/en/guides/smart-clustering/): QA_003 and QA_004 are enforced in the cluster module
- [Progress Monitoring](https://docs.growamal.com/en/guides/progress-monitoring/): QA_032 is fired from the evidence writer
