diff --git a/nextjs-app/__tests__/components/SpecialSchoolNote.test.tsx b/nextjs-app/__tests__/components/SpecialSchoolNote.test.tsx
new file mode 100644
index 0000000..62c7f25
--- /dev/null
+++ b/nextjs-app/__tests__/components/SpecialSchoolNote.test.tsx
@@ -0,0 +1,33 @@
+/**
+ * SpecialSchoolNote Component Tests
+ */
+
+import '@testing-library/jest-dom';
+import { render, screen } from '@testing-library/react';
+import { SpecialSchoolNote } from '@/components/SpecialSchoolNote';
+
+describe('SpecialSchoolNote', () => {
+ it('renders nothing for a mainstream school', () => {
+ const { container } = render();
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('describes a special school as teaching pupils with SEND', () => {
+ render();
+ expect(screen.getByText(/This is a special school/i)).toBeInTheDocument();
+ expect(screen.getByText(/special educational needs/i)).toBeInTheDocument();
+ });
+
+ it('does NOT call PRU pupils SEND — they are educated outside a mainstream school', () => {
+ render();
+ expect(screen.getByText(/This is a pupil referral unit/i)).toBeInTheDocument();
+ expect(screen.getByText(/educated outside a mainstream school/i)).toBeInTheDocument();
+ expect(screen.queryByText(/special educational needs/i)).not.toBeInTheDocument();
+ });
+
+ it('does NOT call alternative-provision pupils SEND', () => {
+ render();
+ expect(screen.getByText(/This is an alternative provision setting/i)).toBeInTheDocument();
+ expect(screen.queryByText(/special educational needs/i)).not.toBeInTheDocument();
+ });
+});
diff --git a/nextjs-app/components/SchoolDetailView.module.css b/nextjs-app/components/SchoolDetailView.module.css
index cfe453b..b2b419a 100644
--- a/nextjs-app/components/SchoolDetailView.module.css
+++ b/nextjs-app/components/SchoolDetailView.module.css
@@ -1558,21 +1558,3 @@
.closingStrip strong {
color: #8a6200;
}
-
-/* Special-school context note: soft, informational (teal), not a warning. It
- explains why the mainstream attainment measures and England comparison are
- dropped for special schools / PRUs / AP. */
-.specialNote {
- background: var(--bg-secondary, #f3ede4);
- border-left: 4px solid var(--accent-teal, #2d7d7d);
- border-radius: 0 6px 6px 0;
- padding: 0.7rem 1rem;
- margin: 0 0 1rem;
- font-size: 0.9rem;
- line-height: 1.5;
- color: var(--text-secondary, #5c564d);
- max-width: 72ch;
-}
-.specialNote strong {
- color: var(--text-primary, #1a1612);
-}
diff --git a/nextjs-app/components/SchoolDetailView.tsx b/nextjs-app/components/SchoolDetailView.tsx
index 99f7788..6038f4f 100644
--- a/nextjs-app/components/SchoolDetailView.tsx
+++ b/nextjs-app/components/SchoolDetailView.tsx
@@ -21,6 +21,7 @@ import {
formatPercentage, formatProgress, formatAcademicYear, isProposedToClose, ofstedLegacyAreas, isSpecialSchool,
} from '@/lib/utils';
import { DeltaChip } from './DeltaChip';
+import { SpecialSchoolNote } from './SpecialSchoolNote';
import { summariseAdmissions } from '@/lib/compareLogic';
const PerformanceChart = dynamic(
@@ -656,20 +657,10 @@ export function SchoolDetailView({
: 'End-of-primary-school tests taken by Year 6 pupils. England averages shown for comparison.'}
- {/* Special schools sit the same national assessments but teach pupils
- with SEND, so very few reach the mainstream "expected standard".
- Explain that up front and drop the England comparison below, so a
- 0% headline never reads as a failing grade against a benchmark
- that doesn't fit the school. */}
- {isSpecial && (
-
- This is a special school. Its pupils have special educational
- needs and work towards individual targets. They sit the same national tests, but
- very few reach the mainstream “expected standard” these measures report,
- so a comparison with the England average isn’t a meaningful guide to the
- school. Where available, the progress pupils make (below) is a fairer measure.
-
- )}
+ {/* Explains up front why the England comparison is dropped below, so
+ a 0% headline never reads as a failing grade against a benchmark
+ that doesn't fit. Type-aware copy (special vs PRU vs AP). */}
+
{/* ── Primary / KS2 content ── */}
{hasKS2Results && (
diff --git a/nextjs-app/components/SchoolRow.tsx b/nextjs-app/components/SchoolRow.tsx
index dbafd05..16e73ad 100644
--- a/nextjs-app/components/SchoolRow.tsx
+++ b/nextjs-app/components/SchoolRow.tsx
@@ -38,24 +38,22 @@ 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. Also guard a placeholder all-zero
- // row (every subject 0 — a special/suppressed signature), matching
- // SchoolDetailView's ks2Placeholder. A genuine 0% combined (some pupils met
- // individual subjects but not all three) is NOT all-zero, so it stays
- // comparable and shows its real figure.
+ // A placeholder all-zero row (every subject 0 — a special/suppressed
+ // signature, matching SchoolDetailView's ks2Placeholder) isn't a real score,
+ // so its figure is hidden. A genuine 0% combined (some pupils met individual
+ // subjects but not all three) is NOT all-zero and stays shown.
const rwmPlaceholder =
school.rwm_expected_pct === 0 &&
(school.reading_expected_pct ?? 0) === 0 &&
(school.writing_expected_pct ?? 0) === 0 &&
(school.maths_expected_pct ?? 0) === 0;
- const rwmComparable =
- school.rwm_expected_pct != null && !rwmPlaceholder && !isSpecialSchool(school);
-
- // vs-national delta
+ // The school's OWN figure and its year-over-year trend are same-school
+ // measures — shown whenever there's a real value, special schools included.
+ const showRwmValue = school.rwm_expected_pct != null && !rwmPlaceholder;
+ // The vs-England delta is a mainstream benchmark: additionally dropped for
+ // special schools / PRUs / AP, whose pupils aren't measured against it fairly.
const rwmDelta =
- rwmComparable && nationalAvgRwm != null
+ showRwmValue && !isSpecialSchool(school) && nationalAvgRwm != null
? Math.round((school.rwm_expected_pct as number) - nationalAvgRwm)
: null;
@@ -102,9 +100,9 @@ export function SchoolRow({
- {rwmComparable ? formatPercentage(school.rwm_expected_pct, 0) : '—'}
+ {showRwmValue ? formatPercentage(school.rwm_expected_pct, 0) : '—'}
- {rwmComparable && school.prev_rwm_expected_pct != null && (
+ {showRwmValue && school.prev_rwm_expected_pct != null && (
- {isSpecial && (
-
- This is a special school. Its pupils have special educational
- needs and work towards individual targets. They sit the same GCSEs, but their
- headline attainment is far below the mainstream average by design, so a comparison
- with the England average isn’t a meaningful guide to the school.
-
- )}
+
{p8Suspended && (
diff --git a/nextjs-app/components/SecondarySchoolRow.tsx b/nextjs-app/components/SecondarySchoolRow.tsx
index 0477d36..e10ca0a 100644
--- a/nextjs-app/components/SecondarySchoolRow.tsx
+++ b/nextjs-app/components/SecondarySchoolRow.tsx
@@ -55,15 +55,14 @@ export function SecondarySchoolRow({
const ofstedBadge = buildOfstedListBadge(school);
const phase = getPhaseStyle(school.phase);
const att8 = school.attainment_8_score;
- // Special schools / PRUs / AP: Attainment 8 vs the LA average isn't a fair
- // comparison (their pupils have SEND), so drop the delta and the number
- // rather than show them trailing "the average" by design. Attainment 8 is a
- // single 0–80 score with no subject breakdown to test for a placeholder, so
- // this keys off establishment type only — a genuine (if extreme) 0.0 at a
- // mainstream school still shows its real value.
- const att8Comparable = att8 != null && !isSpecialSchool(school);
+ // The school's own Attainment 8 is a same-school figure — shown whenever it
+ // exists (special schools included; their type tag on line 2 gives context).
+ // Only the vs-LA-average delta, a benchmark comparison, is dropped for
+ // special schools / PRUs / AP, whose pupils aren't measured against it fairly.
const laDelta =
- att8Comparable && laAvgAttainment8 != null ? (att8 as number) - laAvgAttainment8 : null;
+ att8 != null && !isSpecialSchool(school) && laAvgAttainment8 != null
+ ? att8 - laAvgAttainment8
+ : null;
const admissionsTag = detectAdmissionsTag(school);
const sixthForm = hasSixthForm(school);
@@ -113,7 +112,7 @@ export function SecondarySchoolRow({
- {att8Comparable ? (att8 as number).toFixed(1) : '—'}
+ {att8 != null ? att8.toFixed(1) : '—'}
Attainment 8
diff --git a/nextjs-app/components/SpecialSchoolNote.module.css b/nextjs-app/components/SpecialSchoolNote.module.css
new file mode 100644
index 0000000..138b39e
--- /dev/null
+++ b/nextjs-app/components/SpecialSchoolNote.module.css
@@ -0,0 +1,16 @@
+/* Special-school / PRU / AP context note: soft, informational (teal), not a
+ warning. Shared by both detail views so the styling can't drift. */
+.note {
+ background: var(--bg-secondary, #f3ede4);
+ border-left: 4px solid var(--accent-teal, #2d7d7d);
+ border-radius: 0 6px 6px 0;
+ padding: 0.7rem 1rem;
+ margin: 0 0 1rem;
+ font-size: 0.9rem;
+ line-height: 1.5;
+ color: var(--text-secondary, #5c564d);
+ max-width: 72ch;
+}
+.note strong {
+ color: var(--text-primary, #1a1612);
+}
diff --git a/nextjs-app/components/SpecialSchoolNote.tsx b/nextjs-app/components/SpecialSchoolNote.tsx
new file mode 100644
index 0000000..f737328
--- /dev/null
+++ b/nextjs-app/components/SpecialSchoolNote.tsx
@@ -0,0 +1,58 @@
+/**
+ * SpecialSchoolNote — the context note shown on special-school / PRU / AP
+ * detail pages explaining why the mainstream England-average comparison is
+ * dropped. Renders nothing for mainstream schools.
+ *
+ * The copy is type-aware: only genuine special schools have pupils with special
+ * educational needs. Pupil referral units and alternative provision teach
+ * pupils educated outside a mainstream setting (e.g. after exclusion, or for
+ * medical reasons) who are not necessarily SEND — so their note says so rather
+ * than mischaracterising them. Suppressing the England comparison is reasonable
+ * for all three.
+ */
+
+import { isSpecialSchool } from '@/lib/utils';
+import styles from './SpecialSchoolNote.module.css';
+
+type SpecialKind = 'special' | 'pru' | 'ap';
+
+function specialKind(schoolType: string | null | undefined): SpecialKind {
+ const t = (schoolType ?? '').toLowerCase();
+ if (/pupil referral/.test(t)) return 'pru';
+ if (/alternative provision/.test(t)) return 'ap';
+ return 'special';
+}
+
+export function SpecialSchoolNote({ school }: { school: { school_type?: string | null } }) {
+ if (!isSpecialSchool(school)) return null;
+ const kind = specialKind(school.school_type);
+
+ return (
+
+ {kind === 'special' && (
+ <>
+ This is a special school. Its pupils have special educational needs and
+ work towards individual targets. They sit the same national assessments, but very few
+ reach the mainstream “expected standard” these measures report — so a
+ comparison with the England average isn’t a meaningful guide to the school.
+ >
+ )}
+ {kind === 'pru' && (
+ <>
+ This is a pupil referral unit. It teaches pupils educated outside a
+ mainstream school — for example after exclusion, or for medical or behavioural reasons.
+ The mainstream “expected standard” and the England-average comparison
+ aren’t a meaningful guide to the school.
+ >
+ )}
+ {kind === 'ap' && (
+ <>
+ This is an alternative provision setting. It teaches pupils educated
+ outside a mainstream school. The mainstream “expected standard” and the
+ England-average comparison aren’t a meaningful guide to the school.
+ >
+ )}{' '}
+ Where available, the progress its pupils make is a fairer measure.
+
+ );
+}