fix: show the Ofsted grade still in force and the latest visit (C1/M1/M2, part 2 of 2) #184

Merged
tudor merged 9 commits from fix/ofsted-current-status-site into main 2026-10-05 21:47:35 +00:00
Owner

Do not merge until school_data_monthly_ofsted has run on staging with #183, and do not promote before it has run on production. The backend now reads marts.fact_ofsted_latest and has no fallback (agreed in design). Without the table, the school list fails to load. dim_school and its new unit tests also need int_ofsted_latest's new columns.

Why

Audit findings C1, M1 and M2 (3 Oct 2026 accuracy audit). This is part 2 of 2. #183 computes one current Ofsted status per school in dbt; this PR makes every page read it.

  • C1: Rabbsfarm Primary (102408) shows "Good · 2025". Its 2025 inspection gave no overall grade and rated three areas Requires Improvement. Site-wide, 932 badges pair an old grade with a newer inspection's year, and 147 are contradicted by that inspection.
  • M1: "Inspected" shows the last graded inspection, not the latest visit. Washwood Heath (139888) reads "3 Mar 2020 · 4+ years ago"; Ofsted visited on 21 May 2025. 667 schools.
  • M2: secondary pages drop sixth-form and early-years judgements when there's no overall grade.

Spec: docs/superpowers/specs/2026-10-05-ofsted-current-status-design.md. Plan: docs/superpowers/plans/2026-10-05-ofsted-current-status.md.

This branch contains #183 (merged in so it builds and tests). Merge main back in once #183 lands; the diff then shrinks to this PR's own commits.

Change

  • dim_school: ofsted_grade is now current_grade (Typesense's rating follows) and ofsted_date is the latest visit. New dbt unit tests in dim_school.yml failed RED on the old coalesce.
  • Backend:
    • The list query and the batch Ofsted fetch read marts.fact_ofsted_latest instead of picking "the latest row" themselves.
    • List rows: ofsted_grade (in force), ofsted_grade_date (new), ofsted_date (latest visit).
    • The ofsted block gains current_grade {grade, date, basis} and latest_visit {date, kind, outcome}, and loses grade_source. overall_effectiveness is the graded inspection's own result.
    • A school with only an inspection stays publishable in the sitemap.
  • Search and map badge: shows the grade with the year it was awarded or confirmed, else "Inspected · latest visit year".
  • School page:
    • Dated by the latest visit.
    • Headline: the grade with its source ("Graded inspection, 3 March 2020" / "Confirmed at an ungraded inspection, 18 July 2024"), or "No overall grade".
    • A "Latest visit: Ungraded inspection, 13 November 2024: Standards maintained" line when the latest visit differs from the grade's source.
    • The primary and secondary branches are merged (fixes M2), and "Not rated" is gone.
  • Compare: the "Inspected" row is the latest visit; the "Result" row names the grade's source or says "No overall grade".
  • Removed: buildOfstedHeroChip and buildSchoolSummary. They were unused and encoded the old rule.
  • E2E: Rabbsfarm reads "Inspected · 2025" with "No overall grade"; Robins Lane (104762) says its Good was confirmed on 18 July 2024. Both fail against staging today and should pass once this and #183's pipeline run are deployed.

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

Checks

  • npx tsc --noEmit && npm test: 638 passed.
  • python -m pytest backend/tests pipeline/tests scripts/ci/tests -q: 333 passed.
  • Local dbt against a throwaway Postgres, with the real 31 Aug MI rows of the spec's nine schools: 25/25, plus dim_school unit tests 2/2.
  • The new E2E journeys run against staging fail as expected for now (current_grade undefined; no confirmation line).

After merge (staging)

  1. Let the daily DAG run, or trigger it, so dim_school and Typesense pick up the change.
  2. Run the E2E journeys.
  3. Re-run the audit's C1/M1/M2 checks against staging; expect 0.

Known limitation

dim_school.yml's unit tests run in the daily build and need int_ofsted_latest with the new columns. On a brand-new database the daily build fails until the monthly Ofsted DAG has run once. Before this change, dim_school tolerated a missing int_ofsted_latest.

🤖 Generated with Claude Code

> **Do not merge until `school_data_monthly_ofsted` has run on staging with #183**, and do not promote before it has run on production. The backend now reads `marts.fact_ofsted_latest` and has no fallback (agreed in design). Without the table, the school list fails to load. `dim_school` and its new unit tests also need `int_ofsted_latest`'s new columns. ## Why Audit findings C1, M1 and M2 (3 Oct 2026 accuracy audit). This is **part 2 of 2**. #183 computes one current Ofsted status per school in dbt; this PR makes every page read it. - **C1:** Rabbsfarm Primary (102408) shows "Good · 2025". Its 2025 inspection gave no overall grade and rated three areas Requires Improvement. Site-wide, 932 badges pair an old grade with a newer inspection's year, and 147 are contradicted by that inspection. - **M1:** "Inspected" shows the last graded inspection, not the latest visit. Washwood Heath (139888) reads "3 Mar 2020 · 4+ years ago"; Ofsted visited on 21 May 2025. 667 schools. - **M2:** secondary pages drop sixth-form and early-years judgements when there's no overall grade. Spec: `docs/superpowers/specs/2026-10-05-ofsted-current-status-design.md`. Plan: `docs/superpowers/plans/2026-10-05-ofsted-current-status.md`. This branch contains #183 (merged in so it builds and tests). Merge `main` back in once #183 lands; the diff then shrinks to this PR's own commits. ## Change - **`dim_school`**: `ofsted_grade` is now `current_grade` (Typesense's rating follows) and `ofsted_date` is the latest visit. New dbt unit tests in `dim_school.yml` failed RED on the old `coalesce`. - **Backend**: - The list query and the batch Ofsted fetch read `marts.fact_ofsted_latest` instead of picking "the latest row" themselves. - List rows: `ofsted_grade` (in force), `ofsted_grade_date` (new), `ofsted_date` (latest visit). - The `ofsted` block gains `current_grade {grade, date, basis}` and `latest_visit {date, kind, outcome}`, and loses `grade_source`. `overall_effectiveness` is the graded inspection's own result. - A school with only an inspection stays publishable in the sitemap. - **Search and map badge**: shows the grade with the year it was awarded or confirmed, else "Inspected · *latest visit year*". - **School page**: - Dated by the latest visit. - Headline: the grade with its source ("Graded inspection, 3 March 2020" / "Confirmed at an ungraded inspection, 18 July 2024"), or "No overall grade". - A "Latest visit: Ungraded inspection, 13 November 2024: Standards maintained" line when the latest visit differs from the grade's source. - The primary and secondary branches are merged (fixes M2), and "Not rated" is gone. - **Compare**: the "Inspected" row is the latest visit; the "Result" row names the grade's source or says "No overall grade". - **Removed**: `buildOfstedHeroChip` and `buildSchoolSummary`. They were unused and encoded the old rule. - **E2E**: Rabbsfarm reads "Inspected · 2025" with "No overall grade"; Robins Lane (104762) says its Good was confirmed on 18 July 2024. Both fail against staging today and should pass once this and #183's pipeline run are deployed. ## 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 those 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. ## Checks - `npx tsc --noEmit && npm test`: 638 passed. - `python -m pytest backend/tests pipeline/tests scripts/ci/tests -q`: 333 passed. - Local dbt against a throwaway Postgres, with the real 31 Aug MI rows of the spec's nine schools: 25/25, plus `dim_school` unit tests 2/2. - The new E2E journeys run against staging fail as expected for now (`current_grade` undefined; no confirmation line). ## After merge (staging) 1. Let the daily DAG run, or trigger it, so `dim_school` and Typesense pick up the change. 2. Run the E2E journeys. 3. Re-run the audit's C1/M1/M2 checks against staging; expect 0. ## Known limitation `dim_school.yml`'s unit tests run in the daily build and need `int_ofsted_latest` with the new columns. On a brand-new database the daily build fails until the monthly Ofsted DAG has run once. Before this change, `dim_school` tolerated a missing `int_ofsted_latest`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
tudor added 13 commits 2026-10-05 15:35:52 +00:00
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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
int_ofsted_latest needs to know which inspection came last. Report-card-only schools are no longer dropped (123 schools, audit H3); inspection_date stays for current readers and now falls back to the report-card date, so fact_ofsted_inspection's not_null test still holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
int_ofsted_latest now picks each school's latest visit (report card, graded or ungraded inspection) and the overall grade still in force, dated by the inspection that awarded or confirmed it. A 'School remains Good' from an older ungraded visit is no longer carried past a newer inspection that gave no grade (audit C1). Duplicate monthly rows resolve to the newest visit, so a newer report card always wins. dim_school's columns are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(pipeline): add fact_ofsted_latest, one current Ofsted status per school
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m18s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 20s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m31s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 50s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 32s
dfa4928641
The backend will read this instead of picking the latest row of fact_ofsted_inspection itself (twice, with arbitrary ties). Schema tests and assert_ofsted_current_grade_consistent pin its invariants. Built by the monthly Ofsted DAG (int_ofsted_latest+), not the daily one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR 2 reads the columns PR 1 adds. Merge main back in once #183 lands.
dim_school.ofsted_grade (Typesense's rating) coalesced the graded grade with an older ungraded visit's, so Rabbsfarm's 2020 'remains Good' survived its 2025 no-grade inspection. It now reads int_ofsted_latest's current_grade, and ofsted_date is the latest visit. Unit tests pin both (audit C1, M1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The list query and the batch Ofsted fetch each picked 'the latest row' of fact_ofsted_inspection themselves, and _ofsted_block carried an older ungraded grade forward when the latest graded inspection gave none. All three now read marts.fact_ofsted_latest. List rows: ofsted_grade is the grade still in force, ofsted_grade_date when it was awarded or confirmed, ofsted_date the latest visit. The ofsted block gains current_grade and latest_visit and loses grade_source; overall_effectiveness is the graded inspection's own result. A school with only an inspection stays publishable in the sitemap.

Requires fact_ofsted_latest (PR #183's pipeline run) on the database.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Search badges date a grade by the inspection that awarded or confirmed it (ofsted_grade_date) and say 'Inspected · year' when the latest inspection gave no grade. ofstedDisplay's kinds become graded / confirmed / no_overall_grade, read from current_grade rather than a carried-forward overall_effectiveness. lib/ofstedStatus.ts holds the two sentences the school and compare pages share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The section is dated by the latest visit and headlines the grade still in force with the inspection that awarded or confirmed it, or 'No overall grade'. A later visit that isn't the grade's source gets its own line, with an ungraded outcome such as 'Standards maintained'. The primary and secondary no-grade branches merge, so the secondary page no longer drops the sixth-form and early-years judgements (audit M2), and 'Not rated' goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
'Inspected' is the school's latest visit, so Washwood Heath reads 21 May 2025 rather than '3 Mar 2020 · 4+ years ago' (audit M1). The Result row names the inspection a grade came from, or says 'No overall grade'; 'Grade carried forward' and 'transitional framework' go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
buildOfstedHeroChip and buildSchoolSummary were rendered nowhere and encoded the old carried-forward rule (and a framework value, 'ReportCard', the API never sends).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test(e2e): no grade is carried past a no-grade inspection
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m14s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m25s
PR Checks / Build Pipeline (no push) (pull_request) Canceled after 45s
PR Checks / AI Code Review (Claude) (pull_request) Canceled after 0s
f86d6f45d1
Rabbsfarm (102408) must read 'Inspected · 2025' with no overall grade, and Robins Lane (104762) must say its Good was confirmed at an ungraded inspection on 18 July 2024. Both fail against staging today (no current_grade field, no confirmation line) and pass once PR #183's pipeline run and this branch are deployed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tudor added 3 commits 2026-10-05 15:39:45 +00:00
int_ofsted_latest treated any report card as the latest visit, relying on report cards postdating legacy inspections. That holds today (0 of 2,451 report-card schools in the 31 Aug 2026 MI) but is now not assumed: the latest visit is the newest of the three dates. A report card still leaves no legacy grade in force, which is the spec's rule rather than an inference. New unit test newer_legacy_visit_after_a_report_card failed RED on the old logic. Review on #183.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: plan names the session's trailer; spec reads the latest visit from dates
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m14s
PR Checks / Backend Smoke (pull_request) Successful in 12s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m27s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 53s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 27s
480ac4b9a9
The plan hard-coded one model's Co-Authored-By trailer; an executor should use the one its own session specifies. The spec's report-card rule now matches int_ofsted_latest. Review on #183.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge #183's review fixes into the site branch
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m14s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m26s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 52s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 26s
90b7d09086

🤖 AI Code Review (Claude Code)

The PR replaces the 'latest inspection row per school' Ofsted logic with the one-row-per-school fact_ofsted_latest mart. A grade is now no longer carried forward past a newer inspection that gave none. The backend, models, list badge, school page, compare page, dim_school and tests are all updated to match. The pipeline models it depends on (int_ofsted_latest, fact_ofsted_latest) already exist in the repo, and I found no correctness, security or data-loss problems.

🟡 Minor

  • nextjs-app/components/school/OfstedSection.tsx: The unit test spreads an unknown variant prop into OfstedSection, which no longer accepts it. The 'secondary' test no longer exercises a different code path. The test is misleading and could fail type-checking.
  • backend/data_loader.py: _MAIN_QUERY and get_supplementary_data_batch now require marts.fact_ofsted_latest to exist. The backend deploy therefore has to run after a dbt build that creates the table. If staging or production has not run that build, the main school query will fail. Make sure the pipeline runs first, or add a fallback.
## 🤖 AI Code Review (Claude Code) The PR replaces the 'latest inspection row per school' Ofsted logic with the one-row-per-school `fact_ofsted_latest` mart. A grade is now no longer carried forward past a newer inspection that gave none. The backend, models, list badge, school page, compare page, dim_school and tests are all updated to match. The pipeline models it depends on (`int_ofsted_latest`, `fact_ofsted_latest`) already exist in the repo, and I found no correctness, security or data-loss problems. ### 🟡 Minor - **nextjs-app/components/school/OfstedSection.tsx**: The unit test spreads an unknown `variant` prop into `OfstedSection`, which no longer accepts it. The 'secondary' test no longer exercises a different code path. The test is misleading and could fail type-checking. - **backend/data_loader.py**: `_MAIN_QUERY` and `get_supplementary_data_batch` now require `marts.fact_ofsted_latest` to exist. The backend deploy therefore has to run after a dbt build that creates the table. If staging or production has not run that build, the main school query will fail. Make sure the pipeline runs first, or add a fallback.
tudor merged commit c26b65246f into main 2026-10-05 21:47:35 +00:00
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: tudor/school_compare#184