' link by parsing its date,
never matches[0].
"""
resp = requests.get(GOV_UK_PAGE, timeout=30)
resp.raise_for_status()
csv_links = re.findall(
r'href="(https://assets\.publishing\.service\.gov\.uk/[^"]+\.csv)"',
resp.text,
)
months = {
'january': 1, 'february': 2, 'march': 3, 'april': 4, 'may': 5, 'june': 6,
'july': 7, 'august': 8, 'september': 9, 'october': 10, 'november': 11, 'december': 12,
'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'jun': 6,
'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12,
}
parsed_links = []
for link in csv_links:
normalized = link.lower().replace('-', '_')
if 'latest_inspections_as_at' not in normalized:
continue
match = re.search(r'as_at_(\d{1,2})_([a-z]+)_(\d{4})', normalized)
if match:
day, month_str, year = match.groups()
month = months.get(month_str)
if month:
try:
parsed_links.append((datetime(int(year), month, int(day)), link))
except ValueError:
continue
parsed_links.sort(reverse=True)
if parsed_links:
return parsed_links[0][1]
if csv_links:
return csv_links[-1]
matches = re.findall(
r'href="(https://assets\.publishing\.service\.gov\.uk/[^"]+\.ods)"',
resp.text,
)
return matches[0] if matches else None
```
(`mi_url` config still wins when set — `self.config.get("mi_url") or discover_csv_url()` is unchanged.)
- [ ] **Step 3: Parse and guard the date in staging**
In `stg_ofsted_inspections.sql`, inside the `renamed` CTE after the `rc_sixth_form` line, add:
```sql
-- Start date of the latest FULL inspection (the report-card
-- inspection in the renewed framework). Guarded below: only kept
-- when the row actually carries report-card grades, because in
-- legacy-format files this column is the legacy inspection date.
to_date(nullif(trim(rc_inspection_date), 'NULL'), 'DD/MM/YYYY') as rc_inspection_date_raw,
```
and replace the final select:
```sql
select
*,
case
when rc_safeguarding_met is not null
or rc_inclusion is not null
or rc_curriculum_teaching is not null
or rc_achievement is not null
or rc_attendance_behaviour is not null
or rc_personal_development is not null
or rc_leadership_governance is not null
then rc_inspection_date_raw
end as rc_inspection_date
from renamed
where inspection_date is not null
```
- [ ] **Step 4: Propagate through int + mart**
Add `rc_inspection_date,` to the explicit column lists of `int_ofsted_latest.sql` and `fact_ofsted_inspection.sql` (after `rc_sixth_form`). Do NOT propagate `rc_inspection_date_raw`.
- [ ] **Step 5: Parse-check dbt**
Run: `cd pipeline/transform && uv run --with dbt-postgres python -m dbt.cli.main parse --profiles-dir .`
Expected: parse OK, no compilation errors.
- [ ] **Step 6: Commit**
```bash
git add pipeline/plugins/extractors/tap-uk-ofsted pipeline/transform/models
git commit -m "feat(pipeline): carry the report-card inspection's own date; pick newest MI file in discovery"
```
---
### Task 3: Report-card date in the API and UI
**Files:**
- Modify: `backend/models.py` (FactOfstedInspection)
- Modify: `backend/data_loader.py` (`_ofsted_block`)
- Test: `backend/tests/test_supplementary_enrichment.py` (extend the existing `_ofsted_block` tests)
- Modify: `nextjs-app/lib/types.ts` (OfstedInspection)
- Modify: `nextjs-app/components/compare/CompareOfsted.tsx` ("Inspected" measure)
- Test: `nextjs-app/__tests__/components/CompareOfsted.test.tsx`
**Interfaces:**
- Consumes: `marts.fact_ofsted_inspection.rc_inspection_date` (Task 2).
- Produces: API `ofsted.rc_inspection_date: string | null` (ISO date). UI rule: report-card displays are dated with `rc_inspection_date` only; when null, show "—" (never the legacy date).
- [ ] **Step 1: Failing backend test**
In `backend/tests/test_supplementary_enrichment.py`, alongside the existing `_ofsted_block` tests, add (reuse the file's existing fake-row helper/style):
```python
def test_ofsted_block_carries_rc_inspection_date():
o = _fake_ofsted_row( # use this file's existing fake/stub construction
overall_effectiveness=None,
ungraded_grade=2,
rc_achievement=1,
rc_inspection_date=date(2026, 2, 3),
inspection_date=date(2021, 10, 7),
)
block = _ofsted_block(o, 138690)
assert block["rc_inspection_date"] == "2026-02-03"
# The legacy inspection date is still present, unchanged.
assert block["inspection_date"] == "2021-10-07"
def test_ofsted_block_rc_inspection_date_none_when_absent():
o = _fake_ofsted_row(overall_effectiveness=1, inspection_date=date(2021, 10, 13))
block = _ofsted_block(o, 136276)
assert block["rc_inspection_date"] is None
```
Run: `uv run --with-requirements requirements.txt --with pytest --with "httpx==0.27.0" python -m pytest backend/tests/test_supplementary_enrichment.py -q`
Expected: FAIL (KeyError / AttributeError on `rc_inspection_date`).
- [ ] **Step 2: Backend implementation**
`backend/models.py`, in `FactOfstedInspection` after `rc_sixth_form`:
```python
# Start date of the report-card inspection itself (renewed framework,
# Nov 2025+). Null for rows without report-card grades.
rc_inspection_date = Column(Date)
```
`backend/data_loader.py` `_ofsted_block`, after the `"inspection_date"` entry:
```python
"rc_inspection_date": (
o.rc_inspection_date.isoformat()
if getattr(o, "rc_inspection_date", None)
else None
),
```
(`getattr` default keeps old test stubs working.) Run the backend suite; expected: PASS.
- [ ] **Step 3: Failing frontend test**
`nextjs-app/lib/types.ts`, in `OfstedInspection`, after `inspection_date`:
```ts
/** Start date of the report-card inspection itself (Nov 2025+); null otherwise. */
rc_inspection_date?: string | null;
```
In `nextjs-app/__tests__/components/CompareOfsted.test.tsx`, add to the existing suite (reusing its fixture style):
```tsx
it('dates a report card with the report-card inspection date, never the legacy date', () => {
const ofsted = reportCardOfsted({
inspection_date: '2021-10-07',
rc_inspection_date: '2026-02-03',
});
render();
expect(screen.getByText(/3 Feb 2026/)).toBeInTheDocument();
expect(screen.queryByText(/7 Oct 2021/)).toBeNull();
expect(screen.queryByText('4+ years ago')).toBeNull();
});
it('shows an em dash when a report card has no rc_inspection_date yet', () => {
const ofsted = reportCardOfsted({ inspection_date: '2021-10-07', rc_inspection_date: null });
render();
expect(screen.getByText('—')).toBeInTheDocument();
expect(screen.queryByText(/7 Oct 2021/)).toBeNull();
});
```
(`reportCardOfsted` = the file's existing report-card fixture builder, or build inline matching its other tests.) Run just this file; expected: FAIL.
- [ ] **Step 4: Frontend implementation**
In `CompareOfsted.tsx`, replace the body of the "Inspected" measure's map:
```tsx
{schools.map((school, i) => {
const ofsted = data[String(school.urn)]?.ofsted;
// A report card is dated by its OWN inspection date. The legacy
// inspection_date belongs to an older inspection and must never
// be shown against a report card (report cards exist only from
// Nov 2025).
const dateIso =
displays[i].kind === 'report_card'
? ofsted?.rc_inspection_date ?? null
: ofsted?.inspection_date ?? null;
const age = yearsSince(dateIso);
return (
|
{formatInspectionDate(dateIso)}{' '}
{age != null && age > 4 && 4+ years ago}
|
);
})}
```
- [ ] **Step 5: Run frontend checks**
Run: `cd nextjs-app && npx tsc --noEmit && npm test`
Expected: PASS.
- [ ] **Step 6: Commit**
```bash
git add backend/models.py backend/data_loader.py backend/tests nextjs-app/lib/types.ts nextjs-app/components/compare/CompareOfsted.tsx nextjs-app/__tests__/components/CompareOfsted.test.tsx
git commit -m "fix(compare): date report cards with their own inspection date, never the legacy one"
```
---
### Task 4: Census-based context benchmarks; kill the FSM fallback
**Files:**
- Create: `pipeline/transform/models/marts/fact_census_benchmarks.sql`
- Modify: `pipeline/transform/models/marts/_marts_schema.yml`
- Modify: `backend/models.py` (new `CensusBenchmark` model)
- Modify: `backend/data_loader.py` (`compute_benchmarks`)
- Modify: `backend/app.py` (compare endpoint call site, only if the signature change requires it)
- Test: `backend/tests/test_benchmarks.py`
- Modify: `nextjs-app/components/compare/CompareCommunity.tsx:36`
- Test: `nextjs-app/__tests__/lib/compareLogic.test.ts` or the community section's existing test home (add a fallback-removal test where the FSM chip logic is tested today)
**Interfaces:**
- Consumes: `marts.fact_pupil_characteristics` (urn, year, phase_type_grouping, total_pupils, fsm_pct, eal_pct).
- Produces: `marts.fact_census_benchmarks` — one row per phase (`'primary'`/`'secondary'`), columns `phase, year, fsm_pct, eal_pct, median_pupils`. `fsm_pct`/`eal_pct` are **pupil-weighted means** (so they approximate the national pupil-level rate, answering the expert's objection to school-median anchors). API `benchmarks.{primary,secondary}` keeps its existing keys; `fsm_pct`/`eal_pct`/`median_pupils` now come from this mart; `disadvantaged_pct` becomes primary-only (the KS2-column median was junk for secondary).
- [ ] **Step 1: dbt mart**
Create `pipeline/transform/models/marts/fact_census_benchmarks.sql`:
```sql
{{ config(materialized='table') }}
-- Mart: state-school context benchmarks from the pupil census — one row per
-- phase, latest census year. Computed at import time (never per request).
-- fsm_pct / eal_pct are pupil-weighted means, i.e. "what % of pupils", not
-- "the median school" — this matches how DfE quotes national FSM/EAL rates.
-- Consumers must label these "state-school average (computed from our
-- dataset)" (spec §8.6), never "England average".
with latest as (
select max(year) as year from {{ ref('fact_pupil_characteristics') }}
),
classified as (
select
case
when p.phase_type_grouping ilike '%primary%' then 'primary'
when p.phase_type_grouping ilike '%secondary%' then 'secondary'
end as phase,
p.total_pupils,
p.fsm_pct,
p.eal_pct,
l.year
from {{ ref('fact_pupil_characteristics') }} p
join latest l on p.year = l.year
where p.total_pupils is not null and p.total_pupils > 0
)
select
phase,
max(year) as year,
round((sum(fsm_pct * total_pupils) filter (where fsm_pct is not null)
/ nullif(sum(total_pupils) filter (where fsm_pct is not null), 0))::numeric, 1) as fsm_pct,
round((sum(eal_pct * total_pupils) filter (where eal_pct is not null)
/ nullif(sum(total_pupils) filter (where eal_pct is not null), 0))::numeric, 1) as eal_pct,
round(percentile_cont(0.5) within group (order by total_pupils))::integer as median_pupils
from classified
where phase is not null
group by phase
```
Add a `fact_census_benchmarks` entry to `_marts_schema.yml` in the file's existing style (name + description; column tests only if sibling marts have them).
Run: `cd pipeline/transform && uv run --with dbt-postgres python -m dbt.cli.main parse --profiles-dir .` — expected PASS.
- [ ] **Step 2: Failing backend test**
In `backend/tests/test_benchmarks.py` add:
```python
def test_benchmarks_use_census_mart_for_context(monkeypatch):
census = {
"primary": {"year": 202425, "fsm_pct": 25.3, "eal_pct": 21.8, "median_pupils": 240},
"secondary": {"year": 202425, "fsm_pct": 24.1, "eal_pct": 18.9, "median_pupils": 980},
}
result = compute_benchmarks(_sample_df(), census_benchmarks=census)
assert result["primary"]["fsm_pct"] == 25.3
assert result["secondary"]["eal_pct"] == 18.9
assert result["secondary"]["median_pupils"] == 980
# KS2-only columns must not produce a fake secondary disadvantaged anchor.
assert result["secondary"]["disadvantaged_pct"] is None
def test_benchmarks_context_none_when_mart_missing():
result = compute_benchmarks(_sample_df(), census_benchmarks=None)
assert result["primary"]["fsm_pct"] is None # never silently fall back
```
(`_sample_df()` = this file's existing dataframe fixture.) Run the file; expected: FAIL (unexpected keyword `census_benchmarks`).
- [ ] **Step 3: Backend implementation**
`backend/models.py` (next to the national-average models):
```python
class CensusBenchmark(Base):
"""State-school context benchmarks from the pupil census — one row per phase."""
__tablename__ = "fact_census_benchmarks"
__table_args__ = MARTS
phase = Column(String(20), primary_key=True)
year = Column(Integer)
fsm_pct = Column(Float) # pupil-weighted mean
eal_pct = Column(Float) # pupil-weighted mean
median_pupils = Column(Integer)
```
`backend/data_loader.py` — change the signature and `_block`:
```python
def compute_benchmarks(df: pd.DataFrame, census_benchmarks: dict | None = None) -> dict:
```
Inside, keep `_median` and `_weighted_disadvantaged` as-is, and replace `_block` with:
```python
def _block(sub, phase, with_disadvantaged):
census = (census_benchmarks or {}).get(phase) or {}
block = {
# Context measures come from the census mart (pupil-weighted):
# the performance df has no fsm_pct, and its eal/disadvantaged
# columns are KS2-only — medianing them for "secondary" produced
# junk anchors from the handful of all-through schools.
"eal_pct": census.get("eal_pct"),
"sen_support_pct": _median(sub, "sen_support_pct"),
"disadvantaged_pct": _median(sub, "disadvantaged_pct") if with_disadvantaged else None,
"fsm_pct": census.get("fsm_pct"),
"median_pupils": census.get("median_pupils"),
}
if with_disadvantaged:
block["disadvantaged_rwm_expected_pct"] = _weighted_disadvantaged(sub)
return block
```
and the return:
```python
return {
"source": "state-school average (computed from our dataset)",
"year": int(latest_year),
"primary": _block(prim, "primary", with_disadvantaged=True),
"secondary": _block(sec, "secondary", with_disadvantaged=False),
}
```
In `backend/app.py`'s compare endpoint, load the mart and pass it (same defensive style as the national-averages queries):
```python
census_benchmarks = None
try:
rows = db.query(CensusBenchmark).all()
if rows:
census_benchmarks = {
r.phase: {
"year": r.year,
"fsm_pct": r.fsm_pct,
"eal_pct": r.eal_pct,
"median_pupils": r.median_pupils,
}
for r in rows
}
except Exception:
db.rollback()
...
"benchmarks": compute_benchmarks(df, census_benchmarks=census_benchmarks),
```
(Import `CensusBenchmark`; use the endpoint's existing db session pattern.) Run the backend suite; expected: PASS (update any existing benchmark tests that asserted the old median-sourced fsm/eal values).
- [ ] **Step 4: Frontend — remove the cross-definition fallback**
`nextjs-app/components/compare/CompareCommunity.tsx:36`:
```tsx
const anchor = bench?.fsm_pct ?? null;
```
If the FSM chip has unit coverage, update/add the case: `anchor` null ⇒ no verdict chip rendered (bare value only). Run `cd nextjs-app && npx tsc --noEmit && npm test` — expected PASS.
- [ ] **Step 5: Commit**
```bash
git add pipeline/transform/models/marts backend/models.py backend/data_loader.py backend/app.py backend/tests/test_benchmarks.py nextjs-app/components/compare/CompareCommunity.tsx nextjs-app/__tests__
git commit -m "fix(compare): census-sourced FSM/EAL benchmarks; never fall back across measure definitions"
```
---
### Task 5: Official KS4 national averages
**Files:**
- Modify: `pipeline/plugins/extractors/tap-uk-ees/tap_uk_ees/tap.py` (new stream, registered in `discover_streams`)
- Create: `pipeline/transform/models/staging/stg_ees_ks4_national.sql`
- Modify: `pipeline/transform/models/staging/_stg_sources.yml` (add raw table `ees_ks4_national`)
- Modify: `pipeline/transform/models/marts/fact_ks4_national_averages.sql` (rewrite)
- Modify: `backend/models.py` (Ks4NationalAverage docstring), `backend/app.py` (`_national_averages_payload` — remove the computed fallback)
- Test: `backend/tests/test_national_averages_marts.py`
**Interfaces:**
- Consumes: EES data-catalogue CSV `https://explore-education-statistics.service.gov.uk/data-catalogue/data-set/1b649e16-01e8-435b-a814-56be2faf9054/csv` (columns verified: `time_period, geographic_level, establishment_type_group, breakdown_topic, breakdown, attainment8_average, progress8_average, engmath_95_percent, engmath_94_percent, ebacc_entering_percent, ebacc_95_percent, ebacc_94_percent, ebacc_aps_average, …`).
- Produces: `marts.fact_ks4_national_averages` with the SAME columns as today (so `Ks4NationalAverage` needs no schema change), now holding official DfE figures; `gcse_grade_91_pct` is NULL (not in the official series — the England anchor for that measure disappears, which is correct: it was noise).
- [ ] **Step 1: Tap stream**
In `tap.py`, after the KS2 national stream, add:
```python
# ── KS4 National Headlines (national level only — one row per year) ──────────
# Dataset: "National characteristics summary data" (Key stage 4 performance).
# Official England state-funded headline measures, 2018/19 → latest.
# Suppressed values ('z', 'x') → NULL downstream. Progress 8 is legitimately
# absent in years with no KS2 baseline (e.g. 2024/25) — that is DfE policy,
# not missing data.
_KS4_NATIONAL_CSV_URL = (
"https://explore-education-statistics.service.gov.uk/data-catalogue/"
"data-set/1b649e16-01e8-435b-a814-56be2faf9054/csv"
)
_KS4_NATIONAL_COL_MAP = {
"attainment8_average": "attainment_8_score",
"progress8_average": "progress_8_score",
"engmath_94_percent": "english_maths_standard_pass_pct",
"engmath_95_percent": "english_maths_strong_pass_pct",
"ebacc_entering_percent": "ebacc_entry_pct",
"ebacc_94_percent": "ebacc_standard_pass_pct",
"ebacc_95_percent": "ebacc_strong_pass_pct",
"ebacc_aps_average": "ebacc_avg_score",
}
class EESKs4NationalStream(Stream):
"""National KS4 headline averages — one row per academic year.
Filters to geographic_level == 'National', establishment_type_group ==
'All state-funded', breakdown_topic == 'Total', breakdown == 'Total'
so only the England-wide all-pupils row per year is emitted.
"""
name = "ees_ks4_national"
primary_keys = ["time_period"]
replication_key = None
schema = th.PropertiesList(
th.Property("time_period", th.StringType, required=True),
*[th.Property(out, th.StringType) for out in _KS4_NATIONAL_COL_MAP.values()],
).to_dict()
def get_records(self, context):
import pandas as pd
self.logger.info("Downloading KS4 national headlines: %s", _KS4_NATIONAL_CSV_URL)
resp = requests.get(_KS4_NATIONAL_CSV_URL, timeout=60)
resp.raise_for_status()
df = pd.read_csv(io.BytesIO(resp.content), dtype=str, keep_default_na=False)
df.columns = [c.strip().lower() for c in df.columns]
for col, want in [
("geographic_level", "national"),
("establishment_type_group", "all state-funded"),
("breakdown_topic", "total"),
("breakdown", "total"),
]:
if col in df.columns:
df = df[df[col].str.strip().str.lower() == want]
self.logger.info("Emitting %d national KS4 rows", len(df))
for _, row in df.iterrows():
record = {"time_period": row.get("time_period", "").strip()}
for src, out in _KS4_NATIONAL_COL_MAP.items():
record[out] = row.get(src, "")
yield record
```
Register `EESKs4NationalStream(self)` in `discover_streams` next to the KS2 national stream.
- [ ] **Step 2: Raw source + staging model**
Add to `_stg_sources.yml` under the raw source, matching the `ees_ks2_national` entry's style:
```yaml
- name: ees_ks4_national
description: Official DfE KS4 national headline averages (EES data catalogue)
```
Create `pipeline/transform/models/staging/stg_ees_ks4_national.sql`:
```sql
{{ config(materialized='table') }}
-- Staging model: official DfE KS4 national headline averages — one row per
-- academic year (England, all state-funded, all pupils). Source: EES data
-- catalogue "National characteristics summary data". Suppressed values
-- ('z', 'x') are coerced to NULL by safe_numeric — Progress 8 is 'z' in
-- years with no KS2 baseline (e.g. 2024/25): legitimately unpublished.
select
cast(trim(time_period) as integer) as year,
{{ safe_numeric('attainment_8_score') }} as attainment_8_score,
{{ safe_numeric('progress_8_score') }} as progress_8_score,
{{ safe_numeric('english_maths_standard_pass_pct') }} as english_maths_standard_pass_pct,
{{ safe_numeric('english_maths_strong_pass_pct') }} as english_maths_strong_pass_pct,
{{ safe_numeric('ebacc_entry_pct') }} as ebacc_entry_pct,
{{ safe_numeric('ebacc_standard_pass_pct') }} as ebacc_standard_pass_pct,
{{ safe_numeric('ebacc_strong_pass_pct') }} as ebacc_strong_pass_pct,
{{ safe_numeric('ebacc_avg_score') }} as ebacc_avg_score
from {{ source('raw', 'ees_ks4_national') }}
where time_period ~ '^[0-9]+$'
```
- [ ] **Step 3: Rewrite the mart**
Replace the entire body of `fact_ks4_national_averages.sql`:
```sql
{{ config(materialized='table') }}
-- Mart: OFFICIAL DfE KS4 national headline averages — one row per academic
-- year (England, state-funded, all pupils), from the EES national dataset.
-- Replaces the previous unweighted school-level means, which were 7–15
-- points off every headline measure and produced an arithmetically
-- impossible national Progress 8. gcse_grade_91_pct has no official
-- national series and is NULL (schema kept for the API model).
select
year,
attainment_8_score,
progress_8_score,
english_maths_standard_pass_pct,
english_maths_strong_pass_pct,
ebacc_entry_pct,
ebacc_standard_pass_pct,
ebacc_strong_pass_pct,
ebacc_avg_score,
cast(null as double precision) as gcse_grade_91_pct
from {{ ref('stg_ees_ks4_national') }}
order by year
```
Run: `cd pipeline/transform && uv run --with dbt-postgres python -m dbt.cli.main parse --profiles-dir .` — expected PASS.
- [ ] **Step 4: Backend — official provenance, no computed fallback**
`backend/models.py`: change the `Ks4NationalAverage` docstring to `"""Official DfE KS4 national headline averages — one row per academic year."""`.
`backend/app.py` `_national_averages_payload`: delete the entire `if not any(secondary_by_year.values()):` fallback block (it computes dataset means that the UI footnote then labels official). Update the function docstring's KS4 sentence to: `official DfE KS4 figures (fact_ks4_national_averages). If the KS4 mart hasn't been built yet, the secondary series is empty — never a computed stand-in, because the UI labels these figures as official.`
Update `backend/tests/test_national_averages_marts.py`: the test that exercised the fallback now asserts the opposite —
```python
def test_ks4_secondary_empty_when_mart_missing(...):
# No computed stand-in: the UI labels national figures as official DfE
# data, so an empty mart must yield an empty secondary series.
payload = _national_averages_payload(df)
assert all(not e["secondary"] for e in payload["by_year"])
```
(adapt to the file's existing fixtures/monkeypatching). Run the backend suite — expected PASS.
- [ ] **Step 5: Commit**
```bash
git add pipeline/plugins/extractors/tap-uk-ees pipeline/transform backend
git commit -m "fix(data): official DfE KS4 national headline averages; drop mislabelled computed means"
```
---
### Task 6: Honest 2021/22 footnote
**Files:**
- Modify: `nextjs-app/components/ComparisonChart.tsx:243-247`
- Modify: `nextjs-app/lib/compareChartData.ts` (comment lines 7, 53–55 — comments only, no logic)
- Modify: `nextjs-app/__tests__/lib/compareChartData.test.ts` (test name/comment wording only)
- Modify: `docs/superpowers/specs/2026-07-11-compare-screen-redesign-design.md` §8.1
**Interfaces:** none — copy and docs only. This is the one place the plan changes reviewed copy, because the reviewed copy is factually wrong (Global Constraints exception).
- [ ] **Step 1: Fix the user-facing copy**
In `ComparisonChart.tsx` replace the note:
```tsx
{built.showUnpublished202122Note && (
No national tests were held in 2019/20 and 2020/21 (COVID), and our dataset doesn't
yet include school-level figures for 2021/22 — the England average is shown for that
year.
)}
```
- [ ] **Step 2: Fix the lying comments**
In `compareChartData.ts`, update the header comment (line 7) and the `showUnpublished202122Note` doc comment (lines 53–55) to say the 2021/22 school-level figures are *absent from our dataset* (DfE published them in Dec 2022; ingesting them is a backlog pipeline task), not "unpublished". Rename nothing (the flag name stays — pure rename churn). In `compareChartData.test.ts`, adjust the test description/comment wording the same way.
- [ ] **Step 3: Correct spec §8.1**
In the spec's §8.1, replace any wording that calls 2021/22 school-level KS2 a "permanent DfE gap" with: DfE published school-level KS2 results for 2021/22 in December 2022 (with comparability caveats); they are not yet ingested — loading them remains an open pipeline task, and the chart footnote says "our dataset doesn't yet include" accordingly.
- [ ] **Step 4: Verify + commit**
Run: `cd nextjs-app && npx tsc --noEmit && npm test` — expected PASS.
```bash
git add nextjs-app docs/superpowers/specs/2026-07-11-compare-screen-redesign-design.md
git commit -m "fix(compare): stop attributing the missing 2021/22 school-level year to DfE"
```
---
### Task 7: Full verification, PR, and post-deploy checklist
**Files:** none new (verification + PR).
- [ ] **Step 1: Run everything**
```bash
uv run --with-requirements requirements.txt --with pytest --with "httpx==0.27.0" python -m pytest backend/tests -q
cd nextjs-app && npx tsc --noEmit && npm test && cd ..
cd pipeline/transform && uv run --with dbt-postgres python -m dbt.cli.main parse --profiles-dir . && cd ../..
```
Expected: all PASS.
- [ ] **Step 2: Open the PR**
Push `fix/compare-final-review-mustfix`; open a PR via the Gitea API using `git credential fill` basic auth (token-header auth 401s). PR body: summarize the five findings and fixes, link the expert review, end with the standard Claude Code attribution + session URL. Note in the body that findings 2 and 4 also need a **DAG run after the staging deploy** before the UI shows corrected data.
- [ ] **Step 3: Post-merge staging verification (after the user merges and the daily DAG runs — record results, do not promote)**
```bash
# Report card dated by its own inspection (Barclay): expect 2026-02-03
curl -sk "https://stx.schoolcompare.co.uk/api/compare?urns=138690" | python3 -c "import json,sys; o=json.load(sys.stdin)['comparison']['138690']['ofsted']; print(o['rc_inspection_date'], o['inspection_date'])"
# Stale Watford rc grades cleared by the fresh extract: expect report_card == {}
curl -sk "https://stx.schoolcompare.co.uk/api/compare?urns=136276" | python3 -c "import json,sys; print(json.load(sys.stdin)['comparison']['136276']['ofsted']['report_card'])"
# Official KS4 nationals: expect A8 46.0 for 202425, progress_8_score absent
curl -sk "https://stx.schoolcompare.co.uk/api/national-averages" | python3 -c "import json,sys; print(json.load(sys.stdin)['secondary'])"
# FSM benchmark real (~24-26), secondary disadvantaged_pct gone
curl -sk "https://stx.schoolcompare.co.uk/api/compare?urns=138690,136276" | python3 -c "import json,sys; print(json.load(sys.stdin)['benchmarks'])"
```
Then re-screenshot both phase views (desktop + mobile, "More measures" expanded, Watford Grammar in the secondary set) and hand them to the Ofsted expert agent for the sign-off pass it said it expects. Production promotion remains the human's manual call.
---
## Out of Scope (expert should-fix/minor — separate follow-ups)
- 137086-style interim state (subgrades without an overall from an RI reinspection) rendering treatment (finding 6).
- Disadvantaged cohort sizes on the attainment row (finding 7, spec §8.5).
- SEN/EAL "typical school" labelling and secondary SEN benchmark (finding 8) — note Task 4 already upgrades EAL to a pupil-weighted census figure.
- Selective-school admissions copy variant (finding 9).
- Removing/relabelling `gcse_grade_91_pct` as a compare measure (finding 10) — Task 5 already removes its false England anchor.
- Palette deviation (11), trends picker label (12), "More measures" expanded-state verification (13).
- Actually ingesting the 2021/22 school-level KS2 release (the copy in Task 6 says "doesn't *yet* include").