Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
260 lines
8.3 KiB
TypeScript
260 lines
8.3 KiB
TypeScript
/**
|
||
* compareLogic encodes the expert-reviewed comprehension rules for the
|
||
* compare screen: report-card summarisation (safeguarding never counted),
|
||
* three-regime Ofsted display, one consistent admissions chip metric,
|
||
* CI-based progress banding, verdict chips and dot-strip geometry.
|
||
*/
|
||
|
||
import {
|
||
OFSTED_LEGACY_GRADES,
|
||
ofstedDisplay,
|
||
progressBand,
|
||
rcAreaLabel,
|
||
stripPositions,
|
||
summariseAdmissions,
|
||
summariseReportCard,
|
||
verdict,
|
||
} from '@/lib/compareLogic';
|
||
import type { OfstedInspection, SchoolAdmissions } from '@/lib/types';
|
||
|
||
function ofsted(partial: Partial<OfstedInspection>): OfstedInspection {
|
||
return {
|
||
framework: null,
|
||
inspection_date: null,
|
||
inspection_type: null,
|
||
overall_effectiveness: null,
|
||
quality_of_education: null,
|
||
behaviour_attitudes: null,
|
||
personal_development: null,
|
||
leadership_management: null,
|
||
early_years_provision: null,
|
||
previous_overall: null,
|
||
rc_safeguarding_met: null,
|
||
rc_inclusion: null,
|
||
rc_curriculum_teaching: null,
|
||
rc_achievement: null,
|
||
rc_attendance_behaviour: null,
|
||
rc_personal_development: null,
|
||
rc_leadership_governance: null,
|
||
rc_early_years: null,
|
||
rc_sixth_form: null,
|
||
...partial,
|
||
};
|
||
}
|
||
|
||
const REPORT_CARD = {
|
||
rc_achievement: { code: 2, label: 'Strong standard' },
|
||
rc_curriculum_teaching: { code: 2, label: 'Strong standard' },
|
||
rc_personal_development: { code: 2, label: 'Strong standard' },
|
||
rc_leadership_governance: { code: 2, label: 'Strong standard' },
|
||
rc_inclusion: { code: 3, label: 'Expected standard' },
|
||
rc_early_years: { code: 3, label: 'Expected standard' },
|
||
rc_attendance_behaviour: { code: 4, label: 'Needs attention' },
|
||
};
|
||
|
||
describe('summariseReportCard', () => {
|
||
it('counts graded areas best-first and NAMES problem areas', () => {
|
||
const s = summariseReportCard(
|
||
ofsted({ report_card: REPORT_CARD, rc_safeguarding_met: true }),
|
||
);
|
||
expect(s.counts).toEqual([
|
||
{ label: 'Strong standard', count: 4 },
|
||
{ label: 'Expected standard', count: 2 },
|
||
]);
|
||
expect(s.problems).toEqual([
|
||
{ areaLabel: 'Attendance & behaviour', label: 'Needs attention' },
|
||
]);
|
||
expect(s.safeguarding).toBe('met');
|
||
expect(s.allClear).toBe(false);
|
||
});
|
||
|
||
it('never counts safeguarding as a graded area', () => {
|
||
const s = summariseReportCard(
|
||
ofsted({
|
||
report_card: { rc_achievement: { code: 3, label: 'Expected standard' } },
|
||
rc_safeguarding_met: true,
|
||
}),
|
||
);
|
||
const total = s.counts.reduce((n, c) => n + c.count, 0);
|
||
expect(total).toBe(1);
|
||
});
|
||
|
||
it('is allClear when everything is Expected standard or better and safeguarding met', () => {
|
||
const s = summariseReportCard(
|
||
ofsted({
|
||
report_card: {
|
||
rc_achievement: { code: 3, label: 'Expected standard' },
|
||
rc_inclusion: { code: 1, label: 'Exceptional' },
|
||
},
|
||
rc_safeguarding_met: true,
|
||
}),
|
||
);
|
||
expect(s.allClear).toBe(true);
|
||
expect(s.counts[0]).toEqual({ label: 'Exceptional', count: 1 });
|
||
});
|
||
|
||
it('passes labels through from the API — never invents wording', () => {
|
||
const s = summariseReportCard(
|
||
ofsted({ report_card: { rc_inclusion: { code: 4, label: 'Needs attention' } } }),
|
||
);
|
||
expect(JSON.stringify(s)).not.toContain('Attention needed');
|
||
});
|
||
});
|
||
|
||
describe('ofstedDisplay', () => {
|
||
it('prefers the report card over any legacy grade', () => {
|
||
const d = ofstedDisplay(
|
||
ofsted({ overall_effectiveness: 2, report_card: REPORT_CARD }),
|
||
);
|
||
expect(d.kind).toBe('report_card');
|
||
});
|
||
|
||
it('distinguishes graded from carried-forward grades', () => {
|
||
const graded = ofstedDisplay(
|
||
ofsted({ overall_effectiveness: 1, grade_source: 'graded' }),
|
||
);
|
||
expect(graded).toMatchObject({ kind: 'graded', gradeLabel: 'Outstanding', carriedForward: false });
|
||
|
||
const carried = ofstedDisplay(
|
||
ofsted({ overall_effectiveness: 2, grade_source: 'ungraded_carried_forward' }),
|
||
);
|
||
expect(carried).toMatchObject({ kind: 'carried_forward', gradeLabel: 'Good', carriedForward: true });
|
||
});
|
||
|
||
it('handles missing data', () => {
|
||
expect(ofstedDisplay(null).kind).toBe('none');
|
||
expect(ofstedDisplay(ofsted({})).kind).toBe('none');
|
||
});
|
||
|
||
it('uses the four legacy grade words', () => {
|
||
expect(OFSTED_LEGACY_GRADES).toEqual({
|
||
1: 'Outstanding',
|
||
2: 'Good',
|
||
3: 'Requires improvement',
|
||
4: 'Inadequate',
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('rcAreaLabel', () => {
|
||
it('maps rc keys to the mockups’ area labels', () => {
|
||
expect(rcAreaLabel('rc_attendance_behaviour')).toBe('Attendance & behaviour');
|
||
expect(rcAreaLabel('rc_curriculum_teaching')).toBe('Curriculum & teaching');
|
||
expect(rcAreaLabel('rc_leadership_governance')).toBe('Leadership & governance');
|
||
});
|
||
});
|
||
|
||
describe('summariseAdmissions', () => {
|
||
function admissions(partial: Partial<SchoolAdmissions>): SchoolAdmissions {
|
||
return {
|
||
year: 202627,
|
||
places_offered: null,
|
||
total_applications: null,
|
||
first_preference_offer_pct: null,
|
||
oversubscribed: null,
|
||
...partial,
|
||
};
|
||
}
|
||
|
||
it('97% → good chip with the mockup wording', () => {
|
||
const s = summariseAdmissions(
|
||
admissions({ first_preference_offer_pct: 96.98, total_applications: 457, places_offered: 180 }),
|
||
);
|
||
expect(s.chip).toEqual({ tone: 'good', text: '97% of first choices offered' });
|
||
expect(s.interest).toBe('Named on 457 forms · 180 places');
|
||
});
|
||
|
||
it('73% → warn chip "Over 1 in 4 first choices missed out"', () => {
|
||
const s = summariseAdmissions(admissions({ first_preference_offer_pct: 73.4 }));
|
||
expect(s.chip).toEqual({ tone: 'warn', text: 'Over 1 in 4 first choices missed out' });
|
||
});
|
||
|
||
it('100% → "All first choices offered"', () => {
|
||
const s = summariseAdmissions(admissions({ first_preference_offer_pct: 100 }));
|
||
expect(s.chip).toEqual({ tone: 'good', text: 'All first choices offered' });
|
||
});
|
||
|
||
it('no data → null chip and interest', () => {
|
||
const s = summariseAdmissions(null);
|
||
expect(s.chip).toBeNull();
|
||
expect(s.interest).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('progressBand', () => {
|
||
it('CI entirely above zero → above', () => {
|
||
expect(progressBand(1.2, 0.4, 2.0)).toBe('above');
|
||
});
|
||
it('CI entirely below zero → below', () => {
|
||
expect(progressBand(-1.2, -2.0, -0.4)).toBe('below');
|
||
});
|
||
it('CI straddling zero → average', () => {
|
||
expect(progressBand(0.3, -0.5, 1.1)).toBe('average');
|
||
});
|
||
it('missing CI → null (no naive thresholding)', () => {
|
||
expect(progressBand(1.2, null, null)).toBeNull();
|
||
expect(progressBand(null, null, null)).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('verdict', () => {
|
||
it('above / close / below with a 2pp tolerance', () => {
|
||
expect(verdict(87, 62)).toBe('above');
|
||
expect(verdict(61, 62)).toBe('close');
|
||
expect(verdict(40, 62)).toBe('below');
|
||
});
|
||
});
|
||
|
||
describe('stripPositions', () => {
|
||
it('maps a custom domain', () => {
|
||
const pts = stripPositions([106], 100, 120);
|
||
expect(pts[0].pos).toBe(30);
|
||
});
|
||
|
||
it('flips a colliding label above', () => {
|
||
const pts = stripPositions([91, 92], 0, 100);
|
||
const sorted = [...pts].sort((a, b) => a.value - b.value);
|
||
expect(sorted[0].labelAbove).toBe(false);
|
||
expect(sorted[1].labelAbove).toBe(true);
|
||
});
|
||
|
||
it('skips nulls and keeps school indices', () => {
|
||
const pts = stripPositions([50, null, 70], 0, 100);
|
||
expect(pts).toHaveLength(2);
|
||
expect(pts.map((p) => p.schoolIndex)).toEqual([0, 2]);
|
||
});
|
||
|
||
it('clamps out-of-domain values', () => {
|
||
const pts = stripPositions([95], 100, 120);
|
||
expect(pts[0].pos).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('latestValues', () => {
|
||
const data = {
|
||
'1': {
|
||
yearly_data: [
|
||
{ year: 202324, rwm_expected_pct: 75 },
|
||
{ year: 202425, rwm_expected_pct: 87 },
|
||
],
|
||
},
|
||
'2': {
|
||
yearly_data: [
|
||
{ year: 202324, rwm_expected_pct: 82 },
|
||
{ year: 202425, rwm_expected_pct: null },
|
||
],
|
||
},
|
||
};
|
||
|
||
it('takes the latest non-null value per school in urn order', async () => {
|
||
const { latestValues } = await import('@/lib/compareLogic');
|
||
expect(latestValues(data, [1, 2], 'rwm_expected_pct')).toEqual([87, 82]);
|
||
});
|
||
|
||
it('returns null for unknown schools and metrics', async () => {
|
||
const { latestValues } = await import('@/lib/compareLogic');
|
||
expect(latestValues(data, [3], 'rwm_expected_pct')).toEqual([null]);
|
||
expect(latestValues(data, [1], 'nope')).toEqual([null]);
|
||
});
|
||
});
|