feat(admissions): publish the latest cut-off only, holding history back
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m4s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 16s
PR Checks / Build Frontend (no push) (pull_request) Successful in 45s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Failing after 3m17s
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m4s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 16s
PR Checks / Build Frontend (no push) (pull_request) Successful in 45s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Failing after 3m17s
Earlier years are to become a paid feature, so they stop being published.
The load-bearing part is that this is a change to the API, not only to the
page. /api/schools/{urn} is public and unauthenticated: leaving
admission_distance_history in the payload while declining to render it would
have handed the whole record to anyone who opened the network tab. It is
withheld at the source, and the page follows.
Nothing changes upstream. The tap, the plausibility band and
fact_admission_distance are untouched and still load every published year, so
restoring history for entitled callers is a change to one function in
data_loader rather than a re-collection.
What the reader now gets is the latest figure on the Admissions tile, and a
Distance section that answers the question the number alone cannot: whether
their own address falls inside it. Retitled to "How far away are you?", which
is what it now does — the previous title described a record that is no longer
there.
Removed with the history: the trend chart, the year-by-year table, the
per-year verdict strip, the trend summary and the coverage note, along with
their CSS. The section goes from 743px to 417px.
One consequence worth naming. A run of years used to soften a single close
call — a home just outside one year's cut-off was usually inside another. With
one year published, the "too close to call" band is the entire safety margin
between a parent and a place they do not have, so the verdict now names its
year, and the three outcomes are tinted apart rather than distinguished by
wording alone.
The existing stylesheet test earned its keep here: the three verdict classes
were referenced before they were written, and it caught them. Unstyled, a
"beyond the cut-off" result would have been indistinguishable from an "inside"
one — the exact failure the longhand class map was written to prevent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDvkyqqHABm4bmth2kjAxE
This commit is contained in:
1 parent
50b599a09b
commit
c9a1892bfb
20 files changed
+382
-1215
No files matched your search
@@ -9,8 +9,7 @@
|
||||
|
||||
import { formatCutoffDistance, formatEntryYear } from '@/lib/utils';
|
||||
import {
|
||||
describeCutoff, cutoffYearRows, cutoffTrendSummary, cutoffCoverageNote,
|
||||
describeCutoffAbsence, compareToCutoffs, entryYearOf, CUTOFF_UNCERTAINTY_M,
|
||||
describeCutoff, describeCutoffAbsence, compareToCutoff, CUTOFF_UNCERTAINTY_M,
|
||||
} from '@/components/school/lastDistanceOffered';
|
||||
import type { SchoolAdmissionDistance } from '@/lib/types';
|
||||
|
||||
@@ -82,97 +81,42 @@ describe('describeCutoff', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Year-by-year history ───────────────────────────────────────────────
|
||||
// ── "Would we have got in?" ────────────────────────────────────────────
|
||||
|
||||
const d = (year: number, distance_m: number | null, route_count = 1): SchoolAdmissionDistance => ({
|
||||
year, distance_m, route_count, la_name: 'Camden', distance_unit_raw: 'miles',
|
||||
});
|
||||
|
||||
describe('entryYearOf', () => {
|
||||
it('reduces EES academic codes and plain entry years to one key', () => {
|
||||
// Without this the two histories never join and every year looks unpublished.
|
||||
expect(entryYearOf(202425)).toBe(2024);
|
||||
expect(entryYearOf(2024)).toBe(2024);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cutoffYearRows', () => {
|
||||
it('emits a row for every year in the span, including the empty ones', () => {
|
||||
const rows = cutoffYearRows([d(2021, 900), d(2024, 700)]);
|
||||
expect(rows.map((r) => r.year)).toEqual([2024, 2023, 2022, 2021]);
|
||||
expect(rows.map((r) => r.status)).toEqual([
|
||||
'published', 'not-published', 'not-published', 'published',
|
||||
]);
|
||||
describe('compareToCutoff', () => {
|
||||
it('calls a clearly nearer home inside, and names the year', () => {
|
||||
const r = compareToCutoff(300, 800, 2026);
|
||||
expect(r.verdict).toBe('inside');
|
||||
expect(r.headline).toContain('September 2026');
|
||||
});
|
||||
|
||||
it('separates "nothing published" from "was not oversubscribed"', () => {
|
||||
// The two look identical in the distance data and mean opposite things.
|
||||
const rows = cutoffYearRows(
|
||||
[d(2021, 900), d(2023, 700)],
|
||||
[{ year: 2022, oversubscribed: false }],
|
||||
);
|
||||
expect(rows.find((r) => r.year === 2022)!.status).toBe('not-oversubscribed');
|
||||
it('calls a clearly further home beyond', () => {
|
||||
expect(compareToCutoff(4000, 800, 2026).verdict).toBe('outside');
|
||||
});
|
||||
|
||||
it('matches EES six-digit years against plain cut-off years', () => {
|
||||
const rows = cutoffYearRows(
|
||||
[d(2023, 700)],
|
||||
[{ year: 202324, places_offered: 60, oversubscribed: true }],
|
||||
);
|
||||
expect(rows.find((r) => r.year === 2023)!.placesOffered).toBe(60);
|
||||
it('refuses to call a result inside the measurement error, either way', () => {
|
||||
// A postcode centroid covers several addresses, so a margin this fine is
|
||||
// noise. With one published year there is no other year to fall back on,
|
||||
// which makes this band the only thing standing between a parent and a
|
||||
// place they do not have.
|
||||
expect(compareToCutoff(800 - CUTOFF_UNCERTAINTY_M / 2, 800, 2026).verdict).toBe('too-close');
|
||||
expect(compareToCutoff(800 + CUTOFF_UNCERTAINTY_M / 2, 800, 2026).verdict).toBe('too-close');
|
||||
expect(compareToCutoff(800, 800, 2026).detail).toContain('measurement error');
|
||||
// The explanation is not welded to the headline, so it does not run at
|
||||
// headline weight in the result block.
|
||||
expect(compareToCutoff(800, 800, 2026).headline).not.toContain('measurement error');
|
||||
expect(compareToCutoff(300, 800, 2026).detail).toBeNull();
|
||||
});
|
||||
|
||||
it('does not stretch the span back over admissions years with no cut-offs', () => {
|
||||
// EES reaches back further than councils publish; padding the chart with a
|
||||
// decade of blanks would bury the years that carry a figure.
|
||||
const rows = cutoffYearRows(
|
||||
[d(2024, 700)],
|
||||
[{ year: 2015, oversubscribed: true }, { year: 2024, oversubscribed: true }],
|
||||
);
|
||||
expect(rows.map((r) => r.year)).toEqual([2024]);
|
||||
});
|
||||
|
||||
it('is safe when the backend does not send the field at all', () => {
|
||||
expect(cutoffYearRows(undefined as never)).toEqual([]);
|
||||
expect(cutoffYearRows([], [])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cutoffTrendSummary', () => {
|
||||
const rowsFor = (pts: [number, number][]) => cutoffYearRows(pts.map(([y, m]) => d(y, m)));
|
||||
|
||||
it('says nothing below four published points', () => {
|
||||
expect(cutoffTrendSummary(rowsFor([[2022, 900], [2023, 800], [2024, 700]]))).toBeNull();
|
||||
});
|
||||
|
||||
it('names both endpoints and their years rather than passing a verdict', () => {
|
||||
const s = cutoffTrendSummary(rowsFor([
|
||||
[2021, 1000], [2022, 900], [2023, 800], [2024, 500],
|
||||
]));
|
||||
expect(s).toContain('2021');
|
||||
expect(s).toContain('2024');
|
||||
expect(s).toContain('tightened');
|
||||
});
|
||||
|
||||
it('does not call a small wobble a direction', () => {
|
||||
const s = cutoffTrendSummary(rowsFor([
|
||||
[2021, 1000], [2022, 1010], [2023, 990], [2024, 1030],
|
||||
]));
|
||||
expect(s).toContain('stayed broadly the same');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cutoffCoverageNote', () => {
|
||||
it('warns while the record is too thin to read as a trend', () => {
|
||||
expect(cutoffCoverageNote(cutoffYearRows([d(2024, 700)]))).toContain('Only 1 year');
|
||||
expect(cutoffCoverageNote(cutoffYearRows([
|
||||
d(2021, 1000), d(2022, 900), d(2023, 800), d(2024, 700),
|
||||
]))).toBeNull();
|
||||
it('treats the band as exclusive at its edge', () => {
|
||||
// Exactly on the boundary is still too close; one metre past it is not.
|
||||
expect(compareToCutoff(800 - CUTOFF_UNCERTAINTY_M, 800, 2026).verdict).toBe('too-close');
|
||||
expect(compareToCutoff(800 - CUTOFF_UNCERTAINTY_M - 1, 800, 2026).verdict).toBe('inside');
|
||||
});
|
||||
});
|
||||
|
||||
describe('describeCutoffAbsence', () => {
|
||||
it('explains a selective school by how it admits, not by missing data', () => {
|
||||
it('explains a selective school by how it admits, not as missing data', () => {
|
||||
const s = describeCutoffAbsence({ localAuthority: 'Kent', admissionsPolicy: 'Selective' });
|
||||
expect(s).toContain('entrance test');
|
||||
expect(s).not.toContain('has not published');
|
||||
@@ -195,37 +139,3 @@ describe('describeCutoffAbsence', () => {
|
||||
.toContain('Camden has not published');
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareToCutoffs', () => {
|
||||
const rows = cutoffYearRows(
|
||||
[d(2022, 1000), d(2024, 800)],
|
||||
[{ year: 2023, oversubscribed: false }],
|
||||
);
|
||||
|
||||
it('marks a clearly nearer home as inside every comparable year', () => {
|
||||
const r = compareToCutoffs(300, rows);
|
||||
expect(r.insideCount).toBe(2);
|
||||
expect(r.comparableCount).toBe(2);
|
||||
expect(r.headline).toContain('inside the cut-off in all 2 years');
|
||||
});
|
||||
|
||||
it('marks a clearly further home as outside', () => {
|
||||
const r = compareToCutoffs(4000, rows);
|
||||
expect(r.insideCount).toBe(0);
|
||||
expect(r.headline).toContain('outside the cut-off in every year');
|
||||
});
|
||||
|
||||
it('refuses to call a result inside the measurement error', () => {
|
||||
// A postcode centroid covers several addresses; claiming a place on a 20 m
|
||||
// margin would be inventing precision the inputs do not have.
|
||||
const r = compareToCutoffs(800 - CUTOFF_UNCERTAINTY_M / 2, rows);
|
||||
expect(r.years.find((y) => y.year === 2024)!.verdict).toBe('too-close');
|
||||
expect(r.detail).toContain('too close to call');
|
||||
});
|
||||
|
||||
it('never counts an unpublished year as a pass or a fail', () => {
|
||||
const r = compareToCutoffs(300, rows);
|
||||
expect(r.years.find((y) => y.year === 2023)!.verdict).toBe('unknown');
|
||||
expect(r.detail).toContain('no published figure');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user