Compare commits

..
Author SHA1 Message Date
TudorandClaude Fable 5 e36125b24a fix(detail): label official DfE figures as 'England average' (provenance)
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m0s
PR Checks / Backend Smoke (pull_request) Successful in 6s
PR Checks / Build Backend (no push) (pull_request) Successful in 11s
PR Checks / Build Frontend (no push) (pull_request) Successful in 43s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 13s
Batch B (points 5-6): the detail page called every national figure a
'National avg'. All of them come from the official-DfE national-averages
marts (KS2/KS4 headlines), so they are England averages — relabel to
match the compare screen's provenance convention ('England average' for
official figures; the detail page has no computed benchmarks, so no
'state-school average (computed)' label is needed). Point 6 (anchoring
every number) is already satisfied on the detail page via DeltaChip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
2026-07-20 07:19:22 +01:00
4 changed files with 59 additions and 103 deletions
-30
View File
@@ -9,7 +9,6 @@ import {
isValidPostcode,
debounce,
buildOfstedListBadge,
ofstedLegacyAreas,
metricKind,
shortName,
computeYBounds,
@@ -112,35 +111,6 @@ describe('debounce', () => {
});
});
describe('ofstedLegacyAreas', () => {
it('returns only real grades (1-4) and drops sentinel/null values', () => {
const areas = ofstedLegacyAreas({
quality_of_education: 1,
behaviour_attitudes: 2,
personal_development: null,
leadership_management: 9, // sentinel "not applicable" — must be dropped
early_years_provision: 9, // secondary school — not applicable
sixth_form_provision: 2,
});
expect(areas.map((a) => a.label)).toEqual([
'Quality of Teaching',
'Behaviour in School',
'Sixth Form',
]);
expect(areas.every((a) => a.value >= 1 && a.value <= 4)).toBe(true);
});
it('includes the sixth-form judgement where present (was previously dropped)', () => {
const areas = ofstedLegacyAreas({ sixth_form_provision: 1 });
expect(areas).toEqual([{ label: 'Sixth Form', value: 1 }]);
});
it('includes Early Years for a primary school', () => {
const areas = ofstedLegacyAreas({ quality_of_education: 2, early_years_provision: 1 });
expect(areas.map((a) => a.label)).toContain('Early Years (Reception)');
});
});
describe('buildOfstedListBadge', () => {
it('returns grade word + year for OEIF Outstanding', () => {
const badge = buildOfstedListBadge({ ofsted_grade: 1, ofsted_date: '2023-11-15', ofsted_framework: 'OEIF' });
+33 -22
View File
@@ -18,7 +18,7 @@ import type {
SchoolDeprivation, SchoolFinance, NationalAverages,
} from '@/lib/types';
import {
formatPercentage, formatProgress, formatAcademicYear, isProposedToClose, ofstedLegacyAreas,
formatPercentage, formatProgress, formatAcademicYear, isProposedToClose,
} from '@/lib/utils';
import { DeltaChip } from './DeltaChip';
@@ -285,12 +285,17 @@ export function SchoolDetailView({
: ofsted?.inspection_date ?? null;
// ── Ofsted: detect if all OEIF sub-grades match the overall ───────────
const oeifAreas = ofsted ? ofstedLegacyAreas(ofsted) : [];
const oeifAllSameGrade =
!!ofsted &&
!isReportCard &&
oeifAreas.length >= 3 &&
oeifAreas.every((a) => a.value === ofsted.overall_effectiveness);
const oeifAllSameGrade = (() => {
if (!ofsted || isReportCard) return false;
const subs = [
ofsted.quality_of_education,
ofsted.behaviour_attitudes,
ofsted.personal_development,
ofsted.leadership_management,
...(ofsted.early_years_provision != null ? [ofsted.early_years_provision] : []),
].filter((v): v is number => v != null);
return subs.length >= 3 && subs.every(v => v === ofsted.overall_effectiveness);
})();
// Label shown in the mobile "section" menu button — the section in view.
const activeNavLabel = (navItems.find((n) => n.id === activeSection) ?? navItems[0])?.label ?? '';
@@ -578,9 +583,7 @@ export function SchoolDetailView({
)}
</div>
<p className={styles.ofstedDisclaimer}>
{ofsted.grade_source === 'ungraded_carried_forward'
? 'This overall grade is carried forward from an earlier inspection — Ofsted has since visited without issuing a new overall grade. From September 2024, Ofsted no longer makes an overall effectiveness judgement.'
: 'From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections of state-funded schools.'}
From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections of state-funded schools.
</p>
{oeifAllSameGrade ? (
<p className={styles.ofstedAllSame}>
@@ -588,7 +591,15 @@ export function SchoolDetailView({
</p>
) : (
<div className={styles.metricsGrid}>
{oeifAreas.map(({ label, value }) => (
{[
{ label: 'Quality of Teaching', value: ofsted.quality_of_education },
{ label: 'Behaviour in School', value: ofsted.behaviour_attitudes },
{ label: 'Pupils\' Wider Development', value: ofsted.personal_development },
{ label: 'School Leadership', value: ofsted.leadership_management },
...(ofsted.early_years_provision != null
? [{ label: 'Early Years (Reception)', value: ofsted.early_years_provision }]
: []),
].map(({ label, value }) => value != null && (
<div key={label} className={styles.metricCard}>
<div className={styles.metricLabel}>{label}</div>
<div className={`${styles.metricValue} ${styles[`ofstedGrade${value}`]}`}>
@@ -611,8 +622,8 @@ export function SchoolDetailView({
</h2>
<p className={styles.sectionSubtitle}>
{isSecondary
? 'GCSE results for Year 11 pupils. National averages shown for comparison.'
: 'End-of-primary-school tests taken by Year 6 pupils. National averages shown for comparison.'}
? 'GCSE results for Year 11 pupils. England averages shown for comparison.'
: 'End-of-primary-school tests taken by Year 6 pupils. England averages shown for comparison.'}
</p>
{/* ── Primary / KS2 content ── */}
@@ -637,7 +648,7 @@ export function SchoolDetailView({
)}
</div>
{primaryAvg.rwm_expected_pct != null && (
<div className={styles.heroStatHint}>National avg: {primaryAvg.rwm_expected_pct.toFixed(0)}%</div>
<div className={styles.heroStatHint}>England avg: {primaryAvg.rwm_expected_pct.toFixed(0)}%</div>
)}
</div>
)}
@@ -659,7 +670,7 @@ export function SchoolDetailView({
)}
</div>
{primaryAvg.rwm_high_pct != null && (
<div className={styles.heroStatHint}>National avg: {primaryAvg.rwm_high_pct.toFixed(0)}%</div>
<div className={styles.heroStatHint}>England avg: {primaryAvg.rwm_high_pct.toFixed(0)}%</div>
)}
</div>
)}
@@ -765,7 +776,7 @@ export function SchoolDetailView({
</div>
<div className={styles.metricValue}>{latestResults.attainment_8_score.toFixed(1)}</div>
{secondaryAvg.attainment_8_score != null && (
<div className={styles.metricHint}>National avg: {secondaryAvg.attainment_8_score.toFixed(1)}</div>
<div className={styles.metricHint}>England avg: {secondaryAvg.attainment_8_score.toFixed(1)}</div>
)}
</div>
)}
@@ -789,7 +800,7 @@ export function SchoolDetailView({
</div>
<div className={styles.metricValue}>{formatPercentage(latestResults.english_maths_standard_pass_pct)}</div>
{secondaryAvg.english_maths_standard_pass_pct != null && (
<div className={styles.metricHint}>National avg: {secondaryAvg.english_maths_standard_pass_pct.toFixed(0)}%</div>
<div className={styles.metricHint}>England avg: {secondaryAvg.english_maths_standard_pass_pct.toFixed(0)}%</div>
)}
</div>
)}
@@ -801,7 +812,7 @@ export function SchoolDetailView({
</div>
<div className={styles.metricValue}>{formatPercentage(latestResults.english_maths_strong_pass_pct)}</div>
{secondaryAvg.english_maths_strong_pass_pct != null && (
<div className={styles.metricHint}>National avg: {secondaryAvg.english_maths_strong_pass_pct.toFixed(0)}%</div>
<div className={styles.metricHint}>England avg: {secondaryAvg.english_maths_strong_pass_pct.toFixed(0)}%</div>
)}
</div>
)}
@@ -957,7 +968,7 @@ export function SchoolDetailView({
)}
</div>
{primaryAvg.eal_pct != null && (
<div className={styles.heroStatHint}>National avg: {primaryAvg.eal_pct.toFixed(0)}%</div>
<div className={styles.heroStatHint}>England avg: {primaryAvg.eal_pct.toFixed(0)}%</div>
)}
</div>
)}
@@ -974,7 +985,7 @@ export function SchoolDetailView({
)}
</div>
{primaryAvg.sen_support_pct != null && (
<div className={styles.heroStatHint}>National avg: {primaryAvg.sen_support_pct.toFixed(0)}%</div>
<div className={styles.heroStatHint}>England avg: {primaryAvg.sen_support_pct.toFixed(0)}%</div>
)}
</div>
)}
@@ -1146,7 +1157,7 @@ export function SchoolDetailView({
</div>
<div className={styles.metricValue}>{formatPercentage(absenceData.overall_absence_rate)}</div>
{primaryAvg.overall_absence_pct != null && (
<div className={styles.metricHint}>National avg: ~{primaryAvg.overall_absence_pct.toFixed(1)}%</div>
<div className={styles.metricHint}>England avg: ~{primaryAvg.overall_absence_pct.toFixed(1)}%</div>
)}
</div>
)}
@@ -1158,7 +1169,7 @@ export function SchoolDetailView({
</div>
<div className={styles.metricValue}>{formatPercentage(absenceData.persistent_absence_rate)}</div>
{primaryAvg.persistent_absence_pct != null && (
<div className={styles.metricHint}>National avg: ~{primaryAvg.persistent_absence_pct.toFixed(0)}%</div>
<div className={styles.metricHint}>England avg: ~{primaryAvg.persistent_absence_pct.toFixed(0)}%</div>
)}
</div>
)}
@@ -23,7 +23,7 @@ import type {
SchoolAdmissions, SenDetail, Phonics,
SchoolDeprivation, SchoolFinance, NationalAverages,
} from '@/lib/types';
import { formatPercentage, formatProgress, formatAcademicYear, formatAgeRange, isProposedToClose, ofstedLegacyAreas } from '@/lib/utils';
import { formatPercentage, formatProgress, formatAcademicYear, formatAgeRange, isProposedToClose } from '@/lib/utils';
import { DeltaChip } from './DeltaChip';
import { track, getNavigationSource } from '@/lib/analytics';
import styles from './SecondarySchoolDetailView.module.css';
@@ -201,12 +201,17 @@ export function SecondarySchoolDetailView({
: ofsted?.inspection_date ?? null;
// ── Ofsted: detect if all OEIF sub-grades match the overall ───────────
const oeifAreas = ofsted ? ofstedLegacyAreas(ofsted) : [];
const oeifAllSameGrade =
!!ofsted &&
!isReportCard &&
oeifAreas.length >= 3 &&
oeifAreas.every((a) => a.value === ofsted.overall_effectiveness);
const oeifAllSameGrade = (() => {
if (!ofsted || isReportCard) return false;
const subs = [
ofsted.quality_of_education,
ofsted.behaviour_attitudes,
ofsted.personal_development,
ofsted.leadership_management,
...(ofsted.early_years_provision != null ? [ofsted.early_years_provision] : []),
].filter((v): v is number => v != null);
return subs.length >= 3 && subs.every(v => v === ofsted.overall_effectiveness);
})();
// National Attainment 8 baseline for the "Results Over Time" chart.
const heroAtt8Nat = secondaryAvg.attainment_8_score ?? null;
@@ -399,9 +404,7 @@ export function SecondarySchoolDetailView({
)}
</div>
<p className={styles.ofstedDisclaimer}>
{ofsted.grade_source === 'ungraded_carried_forward'
? 'This overall grade is carried forward from an earlier inspection — Ofsted has since visited without issuing a new overall grade. From September 2024, Ofsted no longer makes an overall effectiveness judgement.'
: 'From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections.'}
From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections.
</p>
{oeifAllSameGrade ? (
<p className={styles.ofstedAllSame}>
@@ -409,7 +412,15 @@ export function SecondarySchoolDetailView({
</p>
) : (
<div className={styles.metricsGrid}>
{oeifAreas.map(({ label, value }) => (
{[
{ label: 'Quality of Teaching', value: ofsted.quality_of_education },
{ label: 'Behaviour in School', value: ofsted.behaviour_attitudes },
{ label: 'Pupils\' Wider Development', value: ofsted.personal_development },
{ label: 'School Leadership', value: ofsted.leadership_management },
...(ofsted.early_years_provision != null
? [{ label: 'Early Years (Reception)', value: ofsted.early_years_provision }]
: []),
].map(({ label, value }) => value != null && (
<div key={label} className={styles.metricCard}>
<div className={styles.metricLabel}>{label}</div>
<div className={`${styles.metricValue} ${styles[`ofstedGrade${value}`]}`}>
@@ -452,7 +463,7 @@ export function SecondarySchoolDetailView({
GCSE Results ({formatAcademicYear(latestResults.year)})
</h2>
<p className={styles.sectionSubtitle}>
GCSE results for Year 11 pupils. National averages shown for comparison.
GCSE results for Year 11 pupils. England averages shown for comparison.
</p>
{p8Suspended && (
@@ -481,7 +492,7 @@ export function SecondarySchoolDetailView({
)}
</div>
{secondaryAvg.attainment_8_score != null && (
<div className={styles.heroStatHint}>National avg: {secondaryAvg.attainment_8_score.toFixed(1)}</div>
<div className={styles.heroStatHint}>England avg: {secondaryAvg.attainment_8_score.toFixed(1)}</div>
)}
</div>
)}
@@ -521,7 +532,7 @@ export function SecondarySchoolDetailView({
)}
</div>
{secondaryAvg.english_maths_strong_pass_pct != null && (
<div className={styles.heroStatHint}>National avg: {secondaryAvg.english_maths_strong_pass_pct.toFixed(0)}%</div>
<div className={styles.heroStatHint}>England avg: {secondaryAvg.english_maths_strong_pass_pct.toFixed(0)}%</div>
)}
</div>
)}
@@ -543,7 +554,7 @@ export function SecondarySchoolDetailView({
)}
</div>
{secondaryAvg.english_maths_standard_pass_pct != null && (
<div className={styles.heroStatHint}>National avg: {secondaryAvg.english_maths_standard_pass_pct.toFixed(0)}%</div>
<div className={styles.heroStatHint}>England avg: {secondaryAvg.english_maths_standard_pass_pct.toFixed(0)}%</div>
)}
</div>
)}
-36
View File
@@ -680,42 +680,6 @@ export function buildSchoolSummary(
return parts.join(', ') + '.';
}
// ─── Legacy (OEIF) sub-judgement areas ────────────────────────────────────────
export interface OfstedLegacyArea {
label: string;
value: number;
}
/**
* The published OEIF sub-judgement areas for the legacy Ofsted layout, in
* display order. Only real grades (14) are returned: Ofsted's sentinel
* codes for "not applicable / no judgement" (9, and any 0/8 variants) and
* nulls are filtered out, so a cryptic "9" never renders as a rating.
* Sixth Form provision is included where a school has one — it was
* previously dropped from the detail grid entirely.
*/
export function ofstedLegacyAreas(ofsted: {
quality_of_education?: number | null;
behaviour_attitudes?: number | null;
personal_development?: number | null;
leadership_management?: number | null;
early_years_provision?: number | null;
sixth_form_provision?: number | null;
}): OfstedLegacyArea[] {
const candidates: Array<[string, number | null | undefined]> = [
['Quality of Teaching', ofsted.quality_of_education],
['Behaviour in School', ofsted.behaviour_attitudes],
["Pupils' Wider Development", ofsted.personal_development],
['School Leadership', ofsted.leadership_management],
['Early Years (Reception)', ofsted.early_years_provision],
['Sixth Form', ofsted.sixth_form_provision],
];
return candidates
.filter((c): c is [string, number] => c[1] != null && c[1] >= 1 && c[1] <= 4)
.map(([label, value]) => ({ label, value }));
}
// ─── List-level Ofsted badge ──────────────────────────────────────────────────
export interface OfstedListBadge {