Design for audit findings C1 and M1. A grade carried forward from an older
ungraded visit is shown under a newer inspection's date ("Good · 2025" for
Rabbsfarm, whose 2025 inspection gave no grade), and "Inspected" dates show the
last graded inspection rather than the latest visit. The rule moves into
int_ofsted_latest and a new fact_ofsted_latest mart, shipped as a pipeline PR
and then a backend/UI PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
13 KiB
Ofsted Current Status — Design
Date: 2026-10-05
Status: approved design, not yet implemented
Scope: pipeline/transform Ofsted models, backend/data_loader.py, list and
detail API Ofsted fields, search and map badges, school-page Ofsted section,
compare Ofsted rows, Typesense rating, sitemap
Fixes: audit findings C1, M1 and (as a side effect) M2 and part of H3,
from the 3 Oct 2026 accuracy audit
Goal
Never show an Ofsted grade under a date it was not awarded or confirmed on, and always date "Inspected" by the school's latest visit.
The problem
Ofsted's management information gives each school at most three inspections: the latest graded inspection (date G, an overall grade or "Not judged", area grades), the latest ungraded inspection (date U, an outcome sentence) and the latest report card (date RC).
The site derives a grade and a date from these with two independent rules:
- grade = the graded inspection's overall grade, or else the grade parsed from the ungraded outcome ("School remains Good" → 2);
- date = G, or else U.
The two rules can pick different inspections. Every inspection from September 2024 to November 2025 was graded with "Not judged" overall, so the grade falls back to an older ungraded visit while the date stays the new one.
- C1. Rabbsfarm Primary School (102408) shows "Good · 2025". Ofsted's 17 June 2025 inspection gave no overall grade and rated quality of education, behaviour and leadership Requires Improvement. The "Good" comes from an ungraded visit on 6 February 2020. Site-wide, 932 badges pair a carried-forward grade with a newer inspection's year, and 147 of them say Good or Outstanding while that inspection rated an area Requires Improvement or Inadequate. Acre Wood Academy (151783) reads "Good · 2024" though the October 2024 inspection rated all four areas Inadequate.
- M1. When a newer ungraded visit exists, the page shows the older graded date. Washwood Heath Academy (139888) reads "Inspected 3 Mar 2020 · 4+ years ago"; Ofsted visited on 21 May 2025. 667 schools.
Ofsted's own provider page for Rabbsfarm leads with the 2025 area judgements and "From September 2024, Ofsted no longer makes an overall effectiveness judgement". It shows no overall grade.
The rule is also implemented four times: dim_school.ofsted_grade (feeds
Typesense), the list SQL in data_loader.py, _ofsted_block, and two separate
"latest row" picks over marts.fact_ofsted_inspection by inspection_date,
which tie arbitrarily on duplicate monthly rows.
Non-goals
- Predecessor inspections (audit M10): a grade Ofsted attributes to a previous URN stays unlabelled.
- Post-16 and ISI-inspected schools (H4) and the "Not yet inspected" label.
- The compare page's broken Ofsted link (M4).
- Report-card display, which is unchanged.
The rule
Computed once per URN in int_ofsted_latest.
Latest visit: the newest of RC, G and U.
latest_visit_datelatest_visit_kind:report_card,gradedorungradedlatest_visit_outcome: the ungraded outcome text when the kind isungraded, otherwise null
Current grade: the overall grade still in force, if any.
| Situation | current_grade |
current_grade_date |
current_grade_basis |
|---|---|---|---|
| A report card exists | null | null | null |
| Latest is graded, overall 1–4 | that grade | G | graded |
| Latest is graded, "Not judged" | null | null | null |
| Latest is ungraded, outcome "School remains X…" (any qualifier) | X | U | confirmed |
| Latest is ungraded, any other outcome ("Standards maintained", "Improved significantly", "Some aspects not as strong") | the graded inspection's overall grade if it is 1–4, else null | G when a grade is kept | graded when a grade is kept |
| No inspection | null | null | null |
Report cards began in November 2025, after the last legacy graded and ungraded inspections, so when a school has a report card it is always the latest visit, and no legacy grade stays in force beside it. Ties between G, U and RC on the same date resolve in the order report card, graded, ungraded.
Invariants: current_grade is null or 1–4; current_grade_date <= latest_visit_date; current_grade_basis is null exactly when current_grade
is null.
Expected results (Ofsted MI as at 31 Aug 2026)
| URN | School | Ofsted data | Latest visit | Current grade |
|---|---|---|---|---|
| 102408 | Rabbsfarm Primary School | G 17 Jun 2025 Not judged; U 6 Feb 2020 remains Good | graded, 17 Jun 2025 | none |
| 151783 | Acre Wood Academy | G 1 Oct 2024 Not judged; U 14 Mar 2023 remains Good (Concerns) | graded, 1 Oct 2024 | none |
| 139888 | Washwood Heath Academy | G 3 Mar 2020 Good; U 21 May 2025 Standards maintained | ungraded, 21 May 2025, "Standards maintained" | Good, 3 Mar 2020, graded |
| 104762 | Robins Lane Community Primary | G 7 Jan 2020 Good; U 18 Jul 2024 School remains Good | ungraded, 18 Jul 2024 | Good, 18 Jul 2024, confirmed |
| 100094 | Royal Free Hospital Children's School | G 9 Oct 2019 Outstanding; U 5 Feb 2025 Some aspects not as strong | ungraded, 5 Feb 2025 | Outstanding, 9 Oct 2019, graded |
| 136454 | Oakgrove School | U 13 Nov 2024 Standards maintained only | ungraded, 13 Nov 2024 | none |
| 137086 | Bishop Stopford School | G 1 Apr 2025 Not judged | graded, 1 Apr 2025 | none |
| 110048 | The Willink School | U 5 Oct 2023 remains Good; RC 6 May 2026 | report card, 6 May 2026 | none (report card shown) |
| 149612 | St Michael's Catholic School | RC 10 Feb 2026 only | report card, 10 Feb 2026 | none (report card shown) |
What each page shows
Search and map badge (buildOfstedListBadge), first match wins:
- Report card: "Report Card · RC year" (unchanged)
- Current grade: "Grade · year of
current_grade_date" - Latest visit: "Inspected · year of
latest_visit_date" - "Not yet inspected" (unchanged)
School page (OfstedSection, both phases):
- Title date: "Inspected latest visit date".
- Headline: the report card; or the current grade with a source line ("Graded inspection, 6 July 2016" or "Confirmed at an ungraded inspection, 14 March 2023"); or "No overall grade" with "Ofsted stopped giving overall grades in September 2024".
- "Latest visit" line when the latest visit is not the grade's source, e.g. "Ungraded inspection, 13 Nov 2024: Standards maintained".
- The area grid shows the graded inspection's judgements through
ofstedLegacyAreas(), dated by that inspection when it is not the latest visit. The primary and secondary no-grade branches merge into one; the secondary branch's four hard-coded areas (audit M2) go with it.
Compare: ofstedDisplay returns report_card, graded, confirmed,
no_overall_grade or none. The "Latest Ofsted inspection", "Result" and
"Inspected" rows use the same fields as the school page.
Delivery
Two pull requests. The mart columns exist before anything reads them, so neither needs compatibility code.
PR 1: pipeline (additive)
stg_ofsted_inspections: keepgraded_inspection_date,ungraded_inspection_dateandrc_inspection_dateas separate typed columns, with the report-card date's existing guard. Keepinspection_date(graded, else ungraded) for the current backend. Keep a row when any of the three dates is present, so report-card-only schools are no longer dropped (part of H3: 123 schools).int_ofsted_latest: pick one row per URN bylatest_visit_datedescending, thenrc_inspection_date,ungraded_inspection_dateandgraded_inspection_datedescending (nulls last). A duplicate monthly row that carries a newer report card therefore always wins. Add the five status columns.- New mart
marts.fact_ofsted_latest: one row per URN fromint_ofsted_latestwith every column the pages need (status, area grades, report-card grades, ungraded outcome, report URL). It does not joindim_school, so only the monthly Ofsted DAG builds it. dim_schoolis not changed in PR 1: the daily DAG does not rebuildint_ofsted_latest, and reading a column that the monthly DAG has not yet built would fail the daily run.- Visible effect: report-card-only schools gain their report card, because the
backend's existing reads of
fact_ofsted_inspectionnow see their rows. Nothing else changes.
PR 2: backend and UI (after the Ofsted DAG has run on PR 1)
data_loader.py: the list query and the batch query readmarts.fact_ofsted_latestinstead of picking the latest row offact_ofsted_inspection._ofsted_blockreads the status columns and loses its fallback toungraded_grade.- List rows:
ofsted_gradebecomescurrent_grade;ofsted_datebecomeslatest_visit_date; newofsted_grade_date.ofsted_rc_datestays. ofstedblock:overall_effectivenessandinspection_dateare the graded inspection's own result and date (they label the area grid); newcurrent_grade{grade, date, basis}(or null) andlatest_visit{date, kind, outcome};grade_sourceis removed. Report-card fields are unchanged.dim_school.ofsted_gradebecomescurrent_grade(Typesense's rating follows at the next sync);ofsted_datebecomeslatest_visit_date.- Sitemap:
lastmodfromlatest_visit_date;_PUBLISHABLE_FIELDSalso counts a latest visit, so schools that lose a carried grade keep their sitemap entry. - Front end:
lib/types.ts,buildOfstedListBadge,OfstedSection,PrimarySchoolSections,SecondarySchoolSections,compareLogic.ofstedDisplay,CompareAtAGlance,CompareOfsted. Place-page counts need no change. - Delete
buildOfstedHeroChipandbuildSchoolSummaryin their own commit: nothing renders them and they encode the old rule.
Testing
PR 1
- dbt unit tests on
int_ofsted_latest, one per table row above plus "report-card only" and "duplicate rows, newer report card wins". - Schema tests on
fact_ofsted_latest: unique, not-nullurn; accepted values forlatest_visit_kindandcurrent_grade_basis;current_gradenull or 1–4;current_grade_date <= latest_visit_date. - Run locally against a throwaway Postgres from
pgserver(no Docker here). If that fails, they still run in the Ofsted DAG'sdbt build, which fails on any broken case. pipeline/tests/test_dag_selectors.py(PR #181) keeps passing.
PR 2
- pytest: contract test for the list and
ofstedfields (style oftest_school_page_flag_fields.py);_ofsted_blockfrom afact_ofsted_latestrow; sitemap publishability. - Jest: a badge case per table row;
OfstedSectionfor graded, confirmed, no grade, the latest-visit line and a sixth-form area;ofstedDisplaykinds. Rewrite tests that assertcarried_forward. - E2E (same PR): Rabbsfarm's search row says "Inspected · 2025" and its page says "No overall grade" with quality of education Requires Improvement; a confirmed school says "Confirmed at an ungraded inspection". The existing report-card journey stays.
Rollout and verification
-
Merge PR 1. On staging, run
school_data_monthly_ofsted, then:-- one row per school select count(*) = count(distinct urn) from marts.fact_ofsted_latest; -- the examples above select urn, latest_visit_date, latest_visit_kind, latest_visit_outcome, current_grade, current_grade_date, current_grade_basis from marts.fact_ofsted_latest where urn in (102408, 151783, 139888, 104762, 100094, 136454, 137086, 110048, 149612); -- C1: a grade in force although the latest inspection gave none (expect 0) select count(*) from marts.fact_ofsted_latest where current_grade is not null and latest_visit_kind = 'graded' and overall_effectiveness is null; -- invariant (expect 0) select count(*) from marts.fact_ofsted_latest where current_grade_date > latest_visit_date;Check through the API that St Michael's Catholic School (149612) shows its report card.
-
Promote PR 1 to production; run the Ofsted DAG there; repeat the checks.
-
Merge PR 2. Let the daily DAG run (or trigger it) so
dim_schooland Typesense pick up the change; run the E2E journeys; re-run the audit's C1, M1 and M2 checks against staging: expect 0. -
Promote PR 2; repeat the audit checks on production.
Expected visible change
About 932 schools change from a grade badge dated by a no-grade inspection ("Good · 2025") to "Inspected · 2025". Counts of Good and Outstanding schools on place pages fall by the same schools, and Typesense's rating changes for them. Dates beside a grade can move earlier (to the inspection that awarded it); "Inspected" dates move later (to the latest visit).
Risks
- Ofsted changes its MI columns most months. Unknown grade text parses to null
(
safe_numeric), which degrades to "Inspected · year", never to a wrong grade. - PR 2 depends on the Ofsted DAG having run on the target environment after PR
- If PR 2 is promoted first, the backend reads a missing table: promote in order.