Files
school_compare/docs/superpowers/specs/2026-10-05-ofsted-current-status-design.md
T
TudorandClaude Opus 5.5 12c52244ee docs(spec): one current Ofsted status per school
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>
2026-10-05 16:00:35 +01:00

13 KiB
Raw Blame History

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_date
  • latest_visit_kind: report_card, graded or ungraded
  • latest_visit_outcome: the ungraded outcome text when the kind is ungraded, 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:

  1. Report card: "Report Card · RC year" (unchanged)
  2. Current grade: "Grade · year of current_grade_date"
  3. Latest visit: "Inspected · year of latest_visit_date"
  4. "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: keep graded_inspection_date, ungraded_inspection_date and rc_inspection_date as separate typed columns, with the report-card date's existing guard. Keep inspection_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 by latest_visit_date descending, then rc_inspection_date, ungraded_inspection_date and graded_inspection_date descending (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 from int_ofsted_latest with every column the pages need (status, area grades, report-card grades, ungraded outcome, report URL). It does not join dim_school, so only the monthly Ofsted DAG builds it.
  • dim_school is not changed in PR 1: the daily DAG does not rebuild int_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_inspection now 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 read marts.fact_ofsted_latest instead of picking the latest row of fact_ofsted_inspection. _ofsted_block reads the status columns and loses its fallback to ungraded_grade.
  • List rows: ofsted_grade becomes current_grade; ofsted_date becomes latest_visit_date; new ofsted_grade_date. ofsted_rc_date stays.
  • ofsted block: overall_effectiveness and inspection_date are the graded inspection's own result and date (they label the area grid); new current_grade {grade, date, basis} (or null) and latest_visit {date, kind, outcome}; grade_source is removed. Report-card fields are unchanged.
  • dim_school.ofsted_grade becomes current_grade (Typesense's rating follows at the next sync); ofsted_date becomes latest_visit_date.
  • Sitemap: lastmod from latest_visit_date; _PUBLISHABLE_FIELDS also 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 buildOfstedHeroChip and buildSchoolSummary in 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-null urn; accepted values for latest_visit_kind and current_grade_basis; current_grade null 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's dbt 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 ofsted fields (style of test_school_page_flag_fields.py); _ofsted_block from a fact_ofsted_latest row; sitemap publishability.
  • Jest: a badge case per table row; OfstedSection for graded, confirmed, no grade, the latest-visit line and a sixth-form area; ofstedDisplay kinds. Rewrite tests that assert carried_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

  1. 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.

  2. Promote PR 1 to production; run the Ofsted DAG there; repeat the checks.

  3. Merge PR 2. Let the daily DAG run (or trigger it) so dim_school and Typesense pick up the change; run the E2E journeys; re-run the audit's C1, M1 and M2 checks against staging: expect 0.

  4. 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
    1. If PR 2 is promoted first, the backend reads a missing table: promote in order.