fix: don't portray special schools as failing the mainstream benchmark
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m8s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 10s
PR Checks / Build Frontend (no push) (pull_request) Successful in 46s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 2m1s

Special schools, PRUs and alternative provision teach pupils with SEND who
sit the same KS2/KS4 assessments but very few reach the mainstream "expected
standard". Their headline attainment is therefore ~0% (or a very low
Attainment 8), and the site was comparing that to the England average and
painting it red — e.g. Greenmead School (a community special school) rendered
as "0.0% — −62 pts below England average" with three 0% red SATs bars. That
portrays a special school as catastrophically failing against a benchmark
that doesn't fit it.

Add a shared `isSpecialSchool()` helper (detects every DfE special-school
establishment type — all contain "special" — plus PRUs / alternative
provision) and drop the mainstream England comparison + "below" framing for
these schools across every surface:

- Detail (primary + secondary): a plain-English context note explaining the
  school is special and why the comparison isn't shown; England-average delta
  chips, "England avg" hints, the SATs national markers, the Attainment-8
  "vs national" bar and the trend chart's England overlay are all suppressed.
  An all-zero placeholder SATs row hides the (empty) subject bar chart and the
  "why is combined lower" bridge.
- Rankings / search rows (primary + secondary): the mainstream RWM / Attainment
  8 stat shows "—" with no "vs national" delta, instead of "0% · −62 vs
  national".
- Compare: special schools' attainment values are dropped (no misleading dot
  at 0% / no "Below England average" chip); progress banding, which IS a fair
  measure for special schools, is kept.

Belt-and-braces zero-guard: a whole-row zero attainment (special or a
suppressed cohort) is also treated as not-comparable, while a legitimate
single 0 (e.g. 0% exceeding at a mainstream school) stays comparable.

Tests: new isSpecialSchool unit tests (every DfE special type matched, no
mainstream false positives); an e2e journey asserts Greenmead shows the
special-school note and no England-average comparison. tsc clean; 108/108 unit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
This commit is contained in:
Tudor
2026-07-20 20:39:16 +01:00
co-authored by Claude Opus 4.8
parent 5e370d09f1
commit ae6ef6860b
10 changed files with 276 additions and 71 deletions
+14 -5
View File
@@ -9,7 +9,7 @@
*/
import type { School } from '@/lib/types';
import { formatPercentage, calculateTrend, getPhaseStyle, schoolUrl, buildOfstedListBadge, formatAgeRange, isProposedToClose } from '@/lib/utils';
import { formatPercentage, calculateTrend, getPhaseStyle, schoolUrl, buildOfstedListBadge, formatAgeRange, isProposedToClose, isSpecialSchool } from '@/lib/utils';
import styles from './SchoolRow.module.css';
interface SchoolRowProps {
@@ -38,10 +38,19 @@ export function SchoolRow({
school.religious_denomination &&
school.religious_denomination !== 'Does not apply';
// Special schools / PRUs / AP: the mainstream RWM measure and its England
// comparison aren't a fair judgement (their pupils have SEND), so a "0% ·
// 62 vs national" row misrepresents them. Treat the mainstream stat as not
// comparable — same for a placeholder all-zero value.
const rwmComparable =
school.rwm_expected_pct != null &&
school.rwm_expected_pct !== 0 &&
!isSpecialSchool(school);
// vs-national delta
const rwmDelta =
school.rwm_expected_pct != null && nationalAvgRwm != null
? Math.round(school.rwm_expected_pct - nationalAvgRwm)
rwmComparable && nationalAvgRwm != null
? Math.round((school.rwm_expected_pct as number) - nationalAvgRwm)
: null;
const handleCompareClick = () => {
@@ -87,9 +96,9 @@ export function SchoolRow({
<div className={styles.line3}>
<span className={styles.stat}>
<strong className={styles.statValue}>
{school.rwm_expected_pct != null ? formatPercentage(school.rwm_expected_pct, 0) : '—'}
{rwmComparable ? formatPercentage(school.rwm_expected_pct, 0) : '—'}
</strong>
{school.prev_rwm_expected_pct != null && (
{rwmComparable && school.prev_rwm_expected_pct != null && (
<span
className={`${styles.trend} ${styles[`trend${trend.charAt(0).toUpperCase() + trend.slice(1)}`]}`}
title={`Previous year: ${formatPercentage(school.prev_rwm_expected_pct)}`}