Follows #150, which merged before these three commits landed on its branch —
so the behaviour reported from staging is currently what is in main.
Reported: a Catholic primary showed six Catholic primaries, none close enough
to be a real option, and omitted the community school down the road.
Three causes, compounding
Ranking put tier before distance. A faith match at 2.9 miles outranked a
community school at 0.3.
The ENOUGH = 3 stopping rule filled the row from the best tier before it
ever widened. That rule was added in #150 to stop a cap of six dragging in
weak distant matches — and it is what guaranteed all six cards were Catholic.
The guard against one failure mode made the opposite one certain.
A 3-mile tier-1 radius is sane for a secondary and most of a city for a
primary, whose catchments are routinely under a mile.
The premise was backwards
Distance is a constraint; intake is a preference. A parent cannot act on a
school outside their reach however well it matches, and they can notice a shared
denomination perfectly well themselves if we show it to them.
So distance now decides the order and nothing else does. Hard filters are
untouched — they were always where the defensibility lived. Similarity moved
from the ranking to the card: shared reports what a school genuinely has in
common, may be empty, and renders no chips when there is nothing to report.
Reach is capped per phase as a sanity bound rather than a target — ordering
already handles density, so it only decides what happens where an area is
sparse:
Phase
Reach
Primary, middle deemed primary, all-through
2 miles
Secondary, middle deemed secondary
6 miles
16 plus
10 miles
A primary with nothing inside two miles now renders no section, which is the
honest answer rather than a gap.
Net deletion
Gone: the tier system, the stopping rule, the tier-dependent lede, the tier
field, the tier-3 fallback chip and its style, and phase_label(). About 60
lines out. select_nearby also stops taking is_secondary — it reads the phase
from the subject's own row, so no caller can hand it one that disagrees with the
data it selects from.
Also in here
Renamed similar → nearby throughout (own commit): module, payload key,
type, components, prop, section id, nav label. The section ranks on distance and
is headed "Other schools nearby"; code calling it "similar" is the drift that
leaves a later reader trusting the name over the behaviour. The payload key
rename is safe in either deploy order — both sides treat absent and empty
identically, so a mismatched pair renders no section rather than breaking.
Spec revised (own commit), keeping the failed design and why it failed
rather than overwriting it. The mockup link is annotated as one revision behind
rather than left looking current.
Verification
backend + pipeline + CI: 253 passed
frontend: 475 passed across 55 suites, tsc --noEmit clean
a direct regression test pins the reported defect: a Catholic primary ringed
by Catholic primaries must lead with the community school at 0.3 miles
E2E journeys updated for the #nearby anchor; they only prove out on the
post-merge staging run
Follows #150, which merged before these three commits landed on its branch —
so the behaviour reported from staging is currently what is in `main`.
Reported: a Catholic primary showed six Catholic primaries, none close enough
to be a real option, and omitted the community school down the road.
## Three causes, compounding
1. **Ranking put tier before distance.** A faith match at 2.9 miles outranked a
community school at 0.3.
2. **The `ENOUGH = 3` stopping rule filled the row from the best tier before it
ever widened.** That rule was added in #150 to stop a cap of six dragging in
weak distant matches — and it is what guaranteed all six cards were Catholic.
The guard against one failure mode made the opposite one certain.
3. **A 3-mile tier-1 radius** is sane for a secondary and most of a city for a
primary, whose catchments are routinely under a mile.
## The premise was backwards
**Distance is a constraint; intake is a preference.** A parent cannot act on a
school outside their reach however well it matches, and they can notice a shared
denomination perfectly well themselves if we show it to them.
So distance now decides the order and nothing else does. Hard filters are
untouched — they were always where the defensibility lived. Similarity moved
from the ranking to the card: `shared` reports what a school genuinely has in
common, may be empty, and renders no chips when there is nothing to report.
Reach is capped per phase as a sanity bound rather than a target — ordering
already handles density, so it only decides what happens where an area is
sparse:
| Phase | Reach |
|---|---|
| Primary, middle deemed primary, all-through | 2 miles |
| Secondary, middle deemed secondary | 6 miles |
| 16 plus | 10 miles |
A primary with nothing inside two miles now renders no section, which is the
honest answer rather than a gap.
## Net deletion
Gone: the tier system, the stopping rule, the tier-dependent lede, the `tier`
field, the tier-3 fallback chip and its style, and `phase_label()`. About 60
lines out. `select_nearby` also stops taking `is_secondary` — it reads the phase
from the subject's own row, so no caller can hand it one that disagrees with the
data it selects from.
## Also in here
**Renamed `similar` → `nearby` throughout** (own commit): module, payload key,
type, components, prop, section id, nav label. The section ranks on distance and
is headed "Other schools nearby"; code calling it "similar" is the drift that
leaves a later reader trusting the name over the behaviour. The payload key
rename is safe in either deploy order — both sides treat absent and empty
identically, so a mismatched pair renders no section rather than breaking.
**Spec revised** (own commit), keeping the failed design and why it failed
rather than overwriting it. The mockup link is annotated as one revision behind
rather than left looking current.
## Verification
- backend + pipeline + CI: 253 passed
- frontend: 475 passed across 55 suites, `tsc --noEmit` clean
- a direct regression test pins the reported defect: a Catholic primary ringed
by Catholic primaries must lead with the community school at 0.3 miles
- E2E journeys updated for the `#nearby` anchor; they only prove out on the
post-merge staging run
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reported from staging: a Catholic primary showed six Catholic primaries, none
of them close enough to be a real option, and omitted the community school
down the road.
Three causes, compounding. Ranking put tier before distance, so a faith match
at 2.9 miles outranked a community school at 0.3. The ENOUGH=3 stopping rule —
added so a cap of six would not drag in weak distant matches — filled the row
from the best tier before it ever widened, which is what made every card
Catholic. And a 3-mile tier-1 radius is sane for a secondary and most of a city
for a primary, whose catchments are routinely under a mile.
The premise was backwards. For a parent, distance is a constraint and intake is
a preference; a school beyond a primary catchment is not a weaker option, it is
not an option. So distance now decides the order and nothing else does. The
hard filters are untouched — they were always where the defensibility lived.
Similarity survives as chips on the card: reported, so a reader applies their
own weighting, rather than ranked, so we apply ours for them.
Reach is capped per phase (primary 2, secondary 6, post-16 10) as a sanity
bound, not a target: ordering already handles density, so the cap only decides
what happens where an area is sparse. A primary with nothing inside two miles
now renders no section, which is the honest answer.
Deleted: the tier system, the stopping rule, the tier-dependent lede, the
`tier` field, the tier-3 fallback chip and its style. select_similar also stops
taking is_secondary — it reads the phase from the subject's own row, so no
caller can hand it one that disagrees with the data.
The heading is now "Other schools nearby". The hard filters still guarantee a
comparable set, but nothing ranks on likeness, so the heading no longer says it
does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The section ranks on distance and is headed "Other schools nearby", but every
identifier still called it "similar" — the exact drift that leaves a later
reader trusting a name over the behaviour.
Mechanical: files, the module, the payload key, the type, the components, the
prop. No behaviour change; the suites are unchanged in count and still green.
Free to do now because #150 has not merged, so the payload key rename needs no
lockstep deploy. Uses of "similar" that are ordinary English — progress
measures compared to similar pupils, and unrelated comments — are untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The spec described the tier system as the design of record. It is gone, so
the document was describing something the code deliberately does not do.
The revision note and the "why not, having built it the other way first"
passage are kept rather than overwritten. The mistake is the instructive part:
treating a preference as a constraint inverted the ranking, and the stopping
rule added to prevent weak distant matches is what guaranteed six Catholic
schools and no community school down the road. A spec that quietly presents the
second design as the plan teaches nobody why the first one failed.
The mockup link is annotated as one revision behind rather than silently left
to look current.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This PR replaces the tiered intake-similarity ranking for the school detail page's 'nearby schools' section with pure distance-first ranking (renamed similar_schools → nearby_schools throughout backend, frontend, tests, e2e, and docs), based on a documented staging defect where faith matches outranked closer schools. The refactor is thorough and internally consistent: hard filters (phase, selectivity, special provision, gender) are preserved unchanged, the new radius-by-phase cap and shared-characteristics reporting are covered by updated unit/component/e2e tests, and no stale references to the old module, type, or CSS class names remain in shipped code.
✅ No issues found.
## 🤖 AI Code Review (Claude Code)
This PR replaces the tiered intake-similarity ranking for the school detail page's 'nearby schools' section with pure distance-first ranking (renamed similar_schools → nearby_schools throughout backend, frontend, tests, e2e, and docs), based on a documented staging defect where faith matches outranked closer schools. The refactor is thorough and internally consistent: hard filters (phase, selectivity, special provision, gender) are preserved unchanged, the new radius-by-phase cap and shared-characteristics reporting are covered by updated unit/component/e2e tests, and no stale references to the old module, type, or CSS class names remain in shipped code.
✅ No issues found.
tudor
merged commit 029fe8d8a6 into main2026-09-22 13:09:38 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Follows #150, which merged before these three commits landed on its branch —
so the behaviour reported from staging is currently what is in
main.Reported: a Catholic primary showed six Catholic primaries, none close enough
to be a real option, and omitted the community school down the road.
Three causes, compounding
community school at 0.3.
ENOUGH = 3stopping rule filled the row from the best tier before itever widened. That rule was added in #150 to stop a cap of six dragging in
weak distant matches — and it is what guaranteed all six cards were Catholic.
The guard against one failure mode made the opposite one certain.
primary, whose catchments are routinely under a mile.
The premise was backwards
Distance is a constraint; intake is a preference. A parent cannot act on a
school outside their reach however well it matches, and they can notice a shared
denomination perfectly well themselves if we show it to them.
So distance now decides the order and nothing else does. Hard filters are
untouched — they were always where the defensibility lived. Similarity moved
from the ranking to the card:
sharedreports what a school genuinely has incommon, may be empty, and renders no chips when there is nothing to report.
Reach is capped per phase as a sanity bound rather than a target — ordering
already handles density, so it only decides what happens where an area is
sparse:
A primary with nothing inside two miles now renders no section, which is the
honest answer rather than a gap.
Net deletion
Gone: the tier system, the stopping rule, the tier-dependent lede, the
tierfield, the tier-3 fallback chip and its style, and
phase_label(). About 60lines out.
select_nearbyalso stops takingis_secondary— it reads the phasefrom the subject's own row, so no caller can hand it one that disagrees with the
data it selects from.
Also in here
Renamed
similar→nearbythroughout (own commit): module, payload key,type, components, prop, section id, nav label. The section ranks on distance and
is headed "Other schools nearby"; code calling it "similar" is the drift that
leaves a later reader trusting the name over the behaviour. The payload key
rename is safe in either deploy order — both sides treat absent and empty
identically, so a mismatched pair renders no section rather than breaking.
Spec revised (own commit), keeping the failed design and why it failed
rather than overwriting it. The mockup link is annotated as one revision behind
rather than left looking current.
Verification
tsc --noEmitcleanby Catholic primaries must lead with the community school at 0.3 miles
#nearbyanchor; they only prove out on thepost-merge staging run
🤖 Generated with Claude Code
🤖 AI Code Review (Claude Code)
This PR replaces the tiered intake-similarity ranking for the school detail page's 'nearby schools' section with pure distance-first ranking (renamed similar_schools → nearby_schools throughout backend, frontend, tests, e2e, and docs), based on a documented staging defect where faith matches outranked closer schools. The refactor is thorough and internally consistent: hard filters (phase, selectivity, special provision, gender) are preserved unchanged, the new radius-by-phase cap and shared-characteristics reporting are covered by updated unit/component/e2e tests, and no stale references to the old module, type, or CSS class names remain in shipped code.
✅ No issues found.