/** * Comprehension rules for the compare screen, kept pure and unit-tested. * * These encode the expert-review requirements (spec §8 of the compare * redesign): report-card summaries count graded areas only (safeguarding is * a separate binary judgement), problem areas are always NAMED rather than * folded into counts, grade labels pass through from the API (live-sampled * Ofsted vocabulary — never invented here), admissions chips use one * consistent metric, and progress bands follow DfE's confidence-interval * methodology instead of thresholding point estimates. */ import type { OfstedInspection, SchoolAdmissions } from './types'; // --------------------------------------------------------------------------- // Verdicts against an anchor (England average or state-school benchmark) // --------------------------------------------------------------------------- export type Verdict = 'above' | 'close' | 'below'; export function verdict(value: number, anchor: number, tolerance = 2): Verdict { if (value >= anchor + tolerance) return 'above'; if (value <= anchor - tolerance) return 'below'; return 'close'; } // --------------------------------------------------------------------------- // Ofsted — three regimes, one display model // --------------------------------------------------------------------------- export const OFSTED_LEGACY_GRADES: Record = { 1: 'Outstanding', 2: 'Good', 3: 'Requires improvement', 4: 'Inadequate', }; /** rc_ key → the area label used across the reviewed mockups. */ const RC_AREA_LABELS: Record = { rc_inclusion: 'Inclusion', rc_curriculum_teaching: 'Curriculum & teaching', rc_achievement: 'Achievement', rc_attendance_behaviour: 'Attendance & behaviour', rc_personal_development: 'Personal development', rc_leadership_governance: 'Leadership & governance', rc_early_years: 'Early years', rc_sixth_form: 'Sixth form', }; export function rcAreaLabel(key: string): string { return RC_AREA_LABELS[key] ?? key; } export interface ReportCardSummary { /** Graded areas only, grouped by label, best grade first. */ counts: Array<{ label: string; count: number }>; /** Areas rated Needs attention / Urgent improvement — always named. */ problems: Array<{ areaLabel: string; label: string }>; safeguarding: 'met' | 'not_met' | null; /** True when every graded area is Expected standard or better and * safeguarding is not "not met". */ allClear: boolean; } const PROBLEM_CODES = new Set([4, 5]); export function summariseReportCard(ofsted: OfstedInspection): ReportCardSummary { const entries = Object.entries(ofsted.report_card ?? {}); const byCode = new Map(); const problems: ReportCardSummary['problems'] = []; for (const [key, entry] of entries) { if (PROBLEM_CODES.has(entry.code)) { problems.push({ areaLabel: rcAreaLabel(key), label: entry.label }); } else { const existing = byCode.get(entry.code); if (existing) existing.count += 1; else byCode.set(entry.code, { label: entry.label, count: 1 }); } } const counts = [...byCode.entries()] .sort(([a], [b]) => a - b) .map(([, v]) => v); const safeguarding = ofsted.rc_safeguarding_met === true ? 'met' : ofsted.rc_safeguarding_met === false ? 'not_met' : null; return { counts, problems, safeguarding, allClear: entries.length > 0 && problems.length === 0 && safeguarding !== 'not_met', }; } export type OfstedDisplay = | { kind: 'none' } | { kind: 'graded'; grade: number; gradeLabel: string; carriedForward: false } | { kind: 'carried_forward'; grade: number; gradeLabel: string; carriedForward: true } | { kind: 'report_card'; summary: ReportCardSummary }; export function ofstedDisplay( ofsted: OfstedInspection | null | undefined, ): OfstedDisplay { if (!ofsted) return { kind: 'none' }; // A report card is the newest inspection format; when present it wins — // never derive or prefer an overall grade alongside it. if (ofsted.report_card && Object.keys(ofsted.report_card).length > 0) { return { kind: 'report_card', summary: summariseReportCard(ofsted) }; } const grade = ofsted.overall_effectiveness; const gradeLabel = grade != null ? OFSTED_LEGACY_GRADES[grade] : undefined; if (grade == null || gradeLabel === undefined) return { kind: 'none' }; if (ofsted.grade_source === 'ungraded_carried_forward') { return { kind: 'carried_forward', grade, gradeLabel, carriedForward: true }; } return { kind: 'graded', grade, gradeLabel, carriedForward: false }; } // --------------------------------------------------------------------------- // Admissions — one consistent chip metric (first-preference success) // --------------------------------------------------------------------------- export interface AdmissionsSummary { firstPrefPct: number | null; chip: { tone: 'good' | 'warn' | 'neutral'; text: string } | null; /** e.g. "Named on 457 forms · 180 places" — total preferences at any rank, * deliberately not phrased as head-to-head applications. */ interest: string | null; } export function summariseAdmissions( a: SchoolAdmissions | null | undefined, ): AdmissionsSummary { if (!a) return { firstPrefPct: null, chip: null, interest: null }; const pct = a.first_preference_offer_pct != null ? Math.round(a.first_preference_offer_pct) : null; let chip: AdmissionsSummary['chip'] = null; if (pct != null) { if (pct >= 100) { chip = { tone: 'good', text: 'All first choices offered' }; } else if (pct < 75) { chip = { tone: 'warn', text: 'Over 1 in 4 first choices missed out' }; } else { chip = { tone: pct >= 90 ? 'good' : 'neutral', text: `${pct}% of first choices offered` }; } } const interest = a.total_applications != null && a.places_offered != null ? `Named on ${a.total_applications.toLocaleString('en-GB')} forms · ${a.places_offered.toLocaleString('en-GB')} places` : null; return { firstPrefPct: pct, chip, interest }; } // --------------------------------------------------------------------------- // Progress bands — DfE confidence-interval methodology // --------------------------------------------------------------------------- export function progressBand( score: number | null, lower: number | null, upper: number | null, ): 'above' | 'average' | 'below' | null { if (score == null || lower == null || upper == null) return null; if (lower > 0) return 'above'; if (upper < 0) return 'below'; return 'average'; } // --------------------------------------------------------------------------- // Dot-strip geometry // --------------------------------------------------------------------------- export interface StripPoint { /** 0–100 percentage position along the track. */ pos: number; labelAbove: boolean; value: number; schoolIndex: number; } /** Labels within 4% of the domain of a lower neighbour flip above the strip * (the reviewed mockups' collision nudge). */ export function stripPositions( values: Array, min = 0, max = 100, ): StripPoint[] { const span = max - min; const points = values .map((value, schoolIndex) => ({ value, schoolIndex })) .filter((p): p is { value: number; schoolIndex: number } => p.value != null) .map((p) => ({ value: p.value, schoolIndex: p.schoolIndex, pos: Math.min(100, Math.max(0, ((p.value - min) / span) * 100)), labelAbove: false, })); const nudge = span * 0.04; let lastBelow = -Infinity; for (const p of [...points].sort((a, b) => a.value - b.value)) { if (p.value - lastBelow < nudge) { p.labelAbove = true; } else { lastBelow = p.value; } } return points; } // --------------------------------------------------------------------------- // Metric extraction // --------------------------------------------------------------------------- /** Latest non-null yearly value of `metricKey` per school, in `urns` order. */ export function latestValues( data: Record & { year: number }> }>, urns: number[], metricKey: string, ): Array { return urns.map((urn) => { const rows = data[String(urn)]?.yearly_data ?? []; for (let i = rows.length - 1; i >= 0; i--) { const v = rows[i][metricKey]; if (typeof v === 'number' && !Number.isNaN(v)) return v; } return null; }); }