placementt-core 1.400.1016 → 1.400.1018

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { ExternalEvent, StudentActivity } from "./typeDefinitions";
1
+ import { CareersRecordEntry, ExternalEvent, StudentActivity } from "./typeDefinitions";
2
2
  /**
3
3
  * ─── Single source of truth for Gatsby Benchmark *evidence* ───────────────────
4
4
  *
@@ -20,7 +20,25 @@ export type GatsbyBenchmarkNumber = (typeof GATSBY_BENCHMARK_NUMBERS)[number];
20
20
  * Where a benchmark's evidence comes from on CareerThread. Drives how the dashboard
21
21
  * renders each card and whether it can show a coverage percentage at all.
22
22
  */
23
- export type BenchmarkEvidenceSource = "provision" | "destinations" | "events" | "eventsAndActivities" | "eventsAndPlacements" | "untracked";
23
+ export type BenchmarkEvidenceSource = "provision" | "destinations" | "events" | "eventsAndActivities" | "eventsAndPlacements" | "guidance";
24
+ /**
25
+ * ─── Time model ───────────────────────────────────────────────────────────────
26
+ * The period a benchmark's coverage is measured over. This is NOT cosmetic: the
27
+ * benchmarks genuinely differ, and measuring them all annually (as this module
28
+ * originally did) understates coverage badly.
29
+ *
30
+ * BM5 renews every year → "Every year, from the age of 11, at least one
31
+ * meaningful encounter with an employer."
32
+ * BM2 runs per Key Stage → "During each Key Stage, all pupils should access
33
+ * and use information about careers…"
34
+ * BM6/7/8 are cumulative → "By the age of 16… and a further one by 18."
35
+ *
36
+ * The cumulative case is the one that bites. Measured annually, a Year 11 who did
37
+ * their work experience in Year 10 reads as uncovered, and every pupil in Years 7-9
38
+ * sits in the denominator of a duty that does not apply to them until 16 — which is
39
+ * why `biggestGap` currently keeps nominating Year 7 as a school's worst gap on BM6.
40
+ */
41
+ export type BenchmarkTimeModel = "annual" | "keyStage" | "cumulativeByAge" | "provision";
24
42
  export type BenchmarkMeta = {
25
43
  number: GatsbyBenchmarkNumber;
26
44
  /** Short title (no "GB"/"score" wording). */
@@ -30,22 +48,61 @@ export type BenchmarkMeta = {
30
48
  source: BenchmarkEvidenceSource;
31
49
  /**
32
50
  * Whether this benchmark yields a per-student coverage number. False for BM1
33
- * (provision-level, shown as an informational card) and BM8 (untracked, shown
34
- * as an honest empty state). The dashboard must not render a coverage % for a
35
- * benchmark where this is false.
51
+ * (provision-level, shown as an informational card). The dashboard must not
52
+ * render a coverage % for a benchmark where this is false.
36
53
  */
37
54
  countsTowardCoverage: boolean;
38
55
  /** True for 5-7: the PAL Compliance flag is meaningful (mirrors COMPASS_PAL_BENCHMARKS). */
39
56
  palRelevant: boolean;
57
+ /** How coverage is periodised. Drives both the denominator and the card's label. */
58
+ timeModel: BenchmarkTimeModel;
40
59
  };
41
60
  /**
42
- * Benchmarks that produce a coverage percentage on the matrix / cards. Deliberately
43
- * BM2-8 excluding the two that can't be a per-student coverage number: BM1
44
- * (provision) and BM8 (untracked). Derived from the Compass export's valid set so
45
- * the two files can't drift.
61
+ * Benchmarks that produce a coverage percentage on the matrix / cards. BM1 is the
62
+ * only exclusion: it is whole-programme provision and can never be a per-pupil
63
+ * number. Derived from the Compass export's valid set (2-8) so the two files can't
64
+ * drift.
65
+ *
66
+ * BM8 was previously excluded here because personal guidance was untracked. It is
67
+ * now recorded on the careers record, so it earns a coverage number like the rest —
68
+ * but a cumulative, age-banded one. See BENCHMARK_AGE_CHECKPOINTS.
46
69
  */
47
70
  export declare const BENCHMARK_COVERAGE_NUMBERS: GatsbyBenchmarkNumber[];
48
71
  export declare const BENCHMARK_META: Record<GatsbyBenchmarkNumber, BenchmarkMeta>;
72
+ /**
73
+ * ─── Age checkpoints for cumulative benchmarks ────────────────────────────────
74
+ * BM6, BM7 and BM8 are all phrased as "by the age of 16… and by 18". Coverage for
75
+ * these is judged at the two points where the duty actually falls due, not spread
76
+ * across every year group: asking what proportion of Year 7 has had a personal
77
+ * guidance meeting measures nothing, because none of them are due one yet.
78
+ *
79
+ * The headline figure for each checkpoint is therefore the *due* cohort — the year
80
+ * group at which the duty has just become expected. Earlier year groups still get a
81
+ * progression figure (are we on track?), but they must not drag the headline down.
82
+ */
83
+ export type BenchmarkAgeCheckpoint = {
84
+ /** The age the benchmark names. */
85
+ age: 16 | 18;
86
+ /** The year group at which the duty has fallen due in full. */
87
+ dueYearGroup: number;
88
+ /** Year groups the duty is accumulating across, up to and including the due one. */
89
+ accumulatingYearGroups: number[];
90
+ label: string;
91
+ };
92
+ export declare const BENCHMARK_AGE_CHECKPOINTS: BenchmarkAgeCheckpoint[];
93
+ /** The checkpoint a year group is working towards, or undefined if off-scale. */
94
+ export declare function ageCheckpointForYearGroup(yearGroup: number): BenchmarkAgeCheckpoint | undefined;
95
+ /**
96
+ * The pupil's year group at some earlier date, back-derived from their current year
97
+ * group. Records written before `yearGroupAtEntry` existed (and every placement and
98
+ * event, which never carried one) have no snapshot, so cumulative coverage has to
99
+ * reconstruct it: each academic year that has elapsed since the record is one year
100
+ * group to subtract.
101
+ *
102
+ * Returns undefined when the result falls outside schooling, which is the honest
103
+ * answer for a record whose date predates the pupil joining.
104
+ */
105
+ export declare function yearGroupAtDate(currentYearGroup: number, recordDate: string | undefined, now?: Date | string): number | undefined;
49
106
  /**
50
107
  * StudentActivity.activityType → the benchmark(s) that activity is evidence for.
51
108
  * Employer-facing encounters count towards BM5; first-hand workplace experiences
@@ -80,6 +137,23 @@ export declare function eventEvidenceBenchmarks(event: Pick<ExternalEvent, "gats
80
137
  export declare function eventProvidesBenchmark(event: Pick<ExternalEvent, "gatsbyBenchmarks">, benchmark: GatsbyBenchmarkNumber): boolean;
81
138
  /** The benchmarks a logged student activity is evidence for. */
82
139
  export declare function activityEvidenceBenchmarks(activity: Pick<StudentActivity, "activityType">): GatsbyBenchmarkNumber[];
140
+ /**
141
+ * ─── Careers record entries as evidence ───────────────────────────────────────
142
+ * Every entry is BM3 evidence: the updated Benchmark 3 asks schools to record the
143
+ * individual advice given to each pupil and any decisions subsequently agreed, and
144
+ * makes no distinction between a formal guidance interview and an option-choice
145
+ * conversation for that purpose.
146
+ *
147
+ * BM8 is far narrower. It is satisfied only by a personal guidance meeting with a
148
+ * careers adviser trained to an appropriate level. A school that logs fifty tutor-time
149
+ * careers chats has kept excellent records and has NOT delivered personal guidance, so
150
+ * only `personalGuidance` entries with `adviserQualified` count here. Loosening this
151
+ * would manufacture compliance with a statutory expectation, which is the one thing
152
+ * this module exists to prevent.
153
+ */
154
+ export declare function isPersonalGuidanceEntry(entry: Pick<CareersRecordEntry, "entryType" | "adviserQualified">): boolean;
155
+ /** The benchmarks a careers record entry is evidence for. */
156
+ export declare function careersRecordEvidenceBenchmarks(entry: Pick<CareersRecordEntry, "entryType" | "adviserQualified">): GatsbyBenchmarkNumber[];
83
157
  /**
84
158
  * A PAL-compliant provider encounter: the event carries the existing PAL flag AND
85
159
  * is tagged with a PAL-relevant benchmark (5-7). Same rule the export uses when
@@ -1,9 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PAL_PHASES = exports.PAL_ENCOUNTERS_PER_PHASE = exports.ACTIVITY_TYPE_BENCHMARKS = exports.BENCHMARK_META = exports.BENCHMARK_COVERAGE_NUMBERS = exports.GATSBY_BENCHMARK_NUMBERS = void 0;
3
+ exports.PAL_PHASES = exports.PAL_ENCOUNTERS_PER_PHASE = exports.ACTIVITY_TYPE_BENCHMARKS = exports.BENCHMARK_AGE_CHECKPOINTS = exports.BENCHMARK_META = exports.BENCHMARK_COVERAGE_NUMBERS = exports.GATSBY_BENCHMARK_NUMBERS = void 0;
4
+ exports.ageCheckpointForYearGroup = ageCheckpointForYearGroup;
5
+ exports.yearGroupAtDate = yearGroupAtDate;
4
6
  exports.eventEvidenceBenchmarks = eventEvidenceBenchmarks;
5
7
  exports.eventProvidesBenchmark = eventProvidesBenchmark;
6
8
  exports.activityEvidenceBenchmarks = activityEvidenceBenchmarks;
9
+ exports.isPersonalGuidanceEntry = isPersonalGuidanceEntry;
10
+ exports.careersRecordEvidenceBenchmarks = careersRecordEvidenceBenchmarks;
7
11
  exports.isPalCompliantProviderEncounter = isPalCompliantProviderEncounter;
8
12
  exports.palPhaseForYearGroup = palPhaseForYearGroup;
9
13
  exports.academicYearForDate = academicYearForDate;
@@ -28,12 +32,16 @@ const compassExport_1 = require("./compassExport");
28
32
  */
29
33
  exports.GATSBY_BENCHMARK_NUMBERS = [1, 2, 3, 4, 5, 6, 7, 8];
30
34
  /**
31
- * Benchmarks that produce a coverage percentage on the matrix / cards. Deliberately
32
- * BM2-8 excluding the two that can't be a per-student coverage number: BM1
33
- * (provision) and BM8 (untracked). Derived from the Compass export's valid set so
34
- * the two files can't drift.
35
+ * Benchmarks that produce a coverage percentage on the matrix / cards. BM1 is the
36
+ * only exclusion: it is whole-programme provision and can never be a per-pupil
37
+ * number. Derived from the Compass export's valid set (2-8) so the two files can't
38
+ * drift.
39
+ *
40
+ * BM8 was previously excluded here because personal guidance was untracked. It is
41
+ * now recorded on the careers record, so it earns a coverage number like the rest —
42
+ * but a cumulative, age-banded one. See BENCHMARK_AGE_CHECKPOINTS.
35
43
  */
36
- exports.BENCHMARK_COVERAGE_NUMBERS = compassExport_1.COMPASS_VALID_ACTIVITY_BENCHMARKS.filter((b) => b !== 8);
44
+ exports.BENCHMARK_COVERAGE_NUMBERS = [...compassExport_1.COMPASS_VALID_ACTIVITY_BENCHMARKS];
37
45
  exports.BENCHMARK_META = {
38
46
  1: {
39
47
  number: 1,
@@ -42,6 +50,7 @@ exports.BENCHMARK_META = {
42
50
  source: "provision",
43
51
  countsTowardCoverage: false,
44
52
  palRelevant: false,
53
+ timeModel: "provision",
45
54
  },
46
55
  2: {
47
56
  number: 2,
@@ -50,6 +59,9 @@ exports.BENCHMARK_META = {
50
59
  source: "events",
51
60
  countsTowardCoverage: true,
52
61
  palRelevant: false,
62
+ // The benchmark's unit is the Key Stage, not the year. Still measured annually
63
+ // until the Key Stage denominator lands — see BENCHMARK_AGE_CHECKPOINTS.
64
+ timeModel: "annual",
53
65
  },
54
66
  3: {
55
67
  number: 3,
@@ -58,6 +70,7 @@ exports.BENCHMARK_META = {
58
70
  source: "destinations",
59
71
  countsTowardCoverage: true,
60
72
  palRelevant: false,
73
+ timeModel: "annual",
61
74
  },
62
75
  4: {
63
76
  number: 4,
@@ -66,6 +79,7 @@ exports.BENCHMARK_META = {
66
79
  source: "events",
67
80
  countsTowardCoverage: true,
68
81
  palRelevant: false,
82
+ timeModel: "annual",
69
83
  },
70
84
  5: {
71
85
  number: 5,
@@ -74,6 +88,9 @@ exports.BENCHMARK_META = {
74
88
  source: "eventsAndActivities",
75
89
  countsTowardCoverage: true,
76
90
  palRelevant: true,
91
+ // The one benchmark whose annual model matches its own wording: "every year,
92
+ // from the age of 11, at least one meaningful encounter with an employer".
93
+ timeModel: "annual",
77
94
  },
78
95
  6: {
79
96
  number: 6,
@@ -82,6 +99,7 @@ exports.BENCHMARK_META = {
82
99
  source: "eventsAndPlacements",
83
100
  countsTowardCoverage: true,
84
101
  palRelevant: true,
102
+ timeModel: "cumulativeByAge",
85
103
  },
86
104
  7: {
87
105
  number: 7,
@@ -90,16 +108,46 @@ exports.BENCHMARK_META = {
90
108
  source: "events",
91
109
  countsTowardCoverage: true,
92
110
  palRelevant: true,
111
+ timeModel: "cumulativeByAge",
93
112
  },
94
113
  8: {
95
114
  number: 8,
96
115
  name: "Personal guidance",
97
- description: "Every pupil has personal guidance interviews with a careers adviser when they need them.",
98
- source: "untracked",
99
- countsTowardCoverage: false,
116
+ description: "Every pupil has a personal guidance meeting with a careers adviser by 16, and a further one by 18.",
117
+ source: "guidance",
118
+ countsTowardCoverage: true,
100
119
  palRelevant: false,
120
+ timeModel: "cumulativeByAge",
101
121
  },
102
122
  };
123
+ exports.BENCHMARK_AGE_CHECKPOINTS = [
124
+ { age: 16, dueYearGroup: 11, accumulatingYearGroups: [7, 8, 9, 10, 11], label: "By age 16" },
125
+ { age: 18, dueYearGroup: 13, accumulatingYearGroups: [12, 13], label: "By age 18" },
126
+ ];
127
+ /** The checkpoint a year group is working towards, or undefined if off-scale. */
128
+ function ageCheckpointForYearGroup(yearGroup) {
129
+ return exports.BENCHMARK_AGE_CHECKPOINTS.find((c) => c.accumulatingYearGroups.includes(yearGroup));
130
+ }
131
+ /**
132
+ * The pupil's year group at some earlier date, back-derived from their current year
133
+ * group. Records written before `yearGroupAtEntry` existed (and every placement and
134
+ * event, which never carried one) have no snapshot, so cumulative coverage has to
135
+ * reconstruct it: each academic year that has elapsed since the record is one year
136
+ * group to subtract.
137
+ *
138
+ * Returns undefined when the result falls outside schooling, which is the honest
139
+ * answer for a record whose date predates the pupil joining.
140
+ */
141
+ function yearGroupAtDate(currentYearGroup, recordDate, now = new Date()) {
142
+ if (!recordDate)
143
+ return undefined;
144
+ const recordYear = parseInt(academicYearForDate(recordDate).split("-")[0], 10);
145
+ const currentYear = parseInt(academicYearForDate(now).split("-")[0], 10);
146
+ if (isNaN(recordYear) || isNaN(currentYear))
147
+ return undefined;
148
+ const derived = currentYearGroup - (currentYear - recordYear);
149
+ return derived >= 1 && derived <= 15 ? derived : undefined;
150
+ }
103
151
  /**
104
152
  * StudentActivity.activityType → the benchmark(s) that activity is evidence for.
105
153
  * Employer-facing encounters count towards BM5; first-hand workplace experiences
@@ -174,6 +222,27 @@ function eventProvidesBenchmark(event, benchmark) {
174
222
  function activityEvidenceBenchmarks(activity) {
175
223
  return exports.ACTIVITY_TYPE_BENCHMARKS[activity.activityType] || [];
176
224
  }
225
+ /**
226
+ * ─── Careers record entries as evidence ───────────────────────────────────────
227
+ * Every entry is BM3 evidence: the updated Benchmark 3 asks schools to record the
228
+ * individual advice given to each pupil and any decisions subsequently agreed, and
229
+ * makes no distinction between a formal guidance interview and an option-choice
230
+ * conversation for that purpose.
231
+ *
232
+ * BM8 is far narrower. It is satisfied only by a personal guidance meeting with a
233
+ * careers adviser trained to an appropriate level. A school that logs fifty tutor-time
234
+ * careers chats has kept excellent records and has NOT delivered personal guidance, so
235
+ * only `personalGuidance` entries with `adviserQualified` count here. Loosening this
236
+ * would manufacture compliance with a statutory expectation, which is the one thing
237
+ * this module exists to prevent.
238
+ */
239
+ function isPersonalGuidanceEntry(entry) {
240
+ return entry.entryType === "personalGuidance" && Boolean(entry.adviserQualified);
241
+ }
242
+ /** The benchmarks a careers record entry is evidence for. */
243
+ function careersRecordEvidenceBenchmarks(entry) {
244
+ return isPersonalGuidanceEntry(entry) ? [3, 8] : [3];
245
+ }
177
246
  /**
178
247
  * A PAL-compliant provider encounter: the event carries the existing PAL flag AND
179
248
  * is tagged with a PAL-relevant benchmark (5-7). Same rule the export uses when
@@ -1 +1 @@
1
- {"version":3,"file":"benchmarkEvidenceRules.js","sourceRoot":"","sources":["../src/benchmarkEvidenceRules.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"benchmarkEvidenceRules.js","sourceRoot":"","sources":["../src/benchmarkEvidenceRules.ts"],"names":[],"mappings":"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"}
@@ -4,16 +4,27 @@ const benchmarkEvidenceRules_1 = require("./benchmarkEvidenceRules");
4
4
  const compassExport_1 = require("./compassExport");
5
5
  describe("benchmarkEvidenceRules single-source consistency", () => {
6
6
  it("stays consistent with the Compass export's valid activity benchmarks (2-8)", () => {
7
- // Coverage numbers are the export-valid set minus BM8 (untracked on CareerThread).
8
- expect(benchmarkEvidenceRules_1.BENCHMARK_COVERAGE_NUMBERS).toEqual(compassExport_1.COMPASS_VALID_ACTIVITY_BENCHMARKS.filter((b) => b !== 8));
7
+ // Coverage numbers are exactly the export-valid set. BM8 joined it when personal
8
+ // guidance became recordable on the careers record; BM1 was never in it.
9
+ expect(benchmarkEvidenceRules_1.BENCHMARK_COVERAGE_NUMBERS).toEqual([...compassExport_1.COMPASS_VALID_ACTIVITY_BENCHMARKS]);
9
10
  expect(benchmarkEvidenceRules_1.BENCHMARK_COVERAGE_NUMBERS).not.toContain(1);
10
- expect(benchmarkEvidenceRules_1.BENCHMARK_COVERAGE_NUMBERS).not.toContain(8);
11
+ expect(benchmarkEvidenceRules_1.BENCHMARK_COVERAGE_NUMBERS).toContain(8);
11
12
  });
12
- it("marks BM1 as provision-level and BM8 as untracked — neither counts toward coverage", () => {
13
+ it("marks BM1 as the only provision-level benchmark, and BM8 as guidance-sourced", () => {
13
14
  expect(benchmarkEvidenceRules_1.BENCHMARK_META[1].countsTowardCoverage).toBe(false);
14
15
  expect(benchmarkEvidenceRules_1.BENCHMARK_META[1].source).toBe("provision");
15
- expect(benchmarkEvidenceRules_1.BENCHMARK_META[8].countsTowardCoverage).toBe(false);
16
- expect(benchmarkEvidenceRules_1.BENCHMARK_META[8].source).toBe("untracked");
16
+ expect(benchmarkEvidenceRules_1.BENCHMARK_META[8].countsTowardCoverage).toBe(true);
17
+ expect(benchmarkEvidenceRules_1.BENCHMARK_META[8].source).toBe("guidance");
18
+ });
19
+ it("periodises each benchmark the way its own wording does", () => {
20
+ // BM5 is the only benchmark that genuinely renews annually ("every year, from
21
+ // the age of 11"). BM6/7/8 are cumulative duties pinned to 16 and 18, and
22
+ // measuring them annually understates coverage — see BenchmarkTimeModel.
23
+ expect(benchmarkEvidenceRules_1.BENCHMARK_META[5].timeModel).toBe("annual");
24
+ expect(benchmarkEvidenceRules_1.BENCHMARK_META[6].timeModel).toBe("cumulativeByAge");
25
+ expect(benchmarkEvidenceRules_1.BENCHMARK_META[7].timeModel).toBe("cumulativeByAge");
26
+ expect(benchmarkEvidenceRules_1.BENCHMARK_META[8].timeModel).toBe("cumulativeByAge");
27
+ expect(benchmarkEvidenceRules_1.BENCHMARK_META[1].timeModel).toBe("provision");
17
28
  });
18
29
  it("flags PAL relevance for exactly benchmarks 5-7", () => {
19
30
  [1, 2, 3, 4, 5, 6, 7, 8].forEach((b) => {
@@ -74,4 +85,59 @@ describe("coverage bands are neutral (no pass/fail theatre)", () => {
74
85
  expect((0, benchmarkEvidenceRules_1.coverageBand)(1)).toBe("full");
75
86
  });
76
87
  });
88
+ describe("careers record entries as benchmark evidence", () => {
89
+ it("counts a qualified adviser's personal guidance towards BM8", () => {
90
+ const entry = { entryType: "personalGuidance", adviserQualified: true };
91
+ expect((0, benchmarkEvidenceRules_1.isPersonalGuidanceEntry)(entry)).toBe(true);
92
+ expect((0, benchmarkEvidenceRules_1.careersRecordEvidenceBenchmarks)(entry)).toEqual([3, 8]);
93
+ });
94
+ it("does NOT count personal guidance from an unqualified adviser towards BM8", () => {
95
+ // The benchmark is satisfied by a meeting with a careers adviser trained to an
96
+ // appropriate level. Counting an untrained member of staff would fabricate
97
+ // compliance with a statutory expectation.
98
+ const entry = { entryType: "personalGuidance", adviserQualified: false };
99
+ expect((0, benchmarkEvidenceRules_1.isPersonalGuidanceEntry)(entry)).toBe(false);
100
+ expect((0, benchmarkEvidenceRules_1.careersRecordEvidenceBenchmarks)(entry)).toEqual([3]);
101
+ });
102
+ it("does NOT let other entry types inflate BM8, however many are logged", () => {
103
+ const others = [
104
+ "careersConversation", "optionsMeeting", "applicationSupport",
105
+ "sendReview", "parentMeeting", "groupGuidance", "other",
106
+ ];
107
+ others.forEach((entryType) => {
108
+ const entry = { entryType, adviserQualified: true };
109
+ expect((0, benchmarkEvidenceRules_1.isPersonalGuidanceEntry)(entry)).toBe(false);
110
+ expect((0, benchmarkEvidenceRules_1.careersRecordEvidenceBenchmarks)(entry)).toEqual([3]);
111
+ });
112
+ });
113
+ it("still treats every entry as BM3 record-keeping evidence", () => {
114
+ expect((0, benchmarkEvidenceRules_1.careersRecordEvidenceBenchmarks)({ entryType: "careersConversation", adviserQualified: false })).toContain(3);
115
+ });
116
+ });
117
+ describe("cumulative age-banded coverage", () => {
118
+ it("maps year groups onto the checkpoint they are working towards", () => {
119
+ expect((0, benchmarkEvidenceRules_1.ageCheckpointForYearGroup)(9)?.age).toBe(16);
120
+ expect((0, benchmarkEvidenceRules_1.ageCheckpointForYearGroup)(11)?.age).toBe(16);
121
+ expect((0, benchmarkEvidenceRules_1.ageCheckpointForYearGroup)(12)?.age).toBe(18);
122
+ expect((0, benchmarkEvidenceRules_1.ageCheckpointForYearGroup)(13)?.age).toBe(18);
123
+ expect((0, benchmarkEvidenceRules_1.ageCheckpointForYearGroup)(6)).toBeUndefined();
124
+ });
125
+ it("back-derives the year group a pupil was in when an older record was made", () => {
126
+ // A Year 11 pupil "now" (2025-26) did work experience in 2024-25, so they were
127
+ // in Year 10 at the time. Without this, BM6 reads them as uncovered.
128
+ expect((0, benchmarkEvidenceRules_1.yearGroupAtDate)(11, "2025-03-01", "2026-03-01")).toBe(10);
129
+ expect((0, benchmarkEvidenceRules_1.yearGroupAtDate)(11, "2025-11-01", "2026-03-01")).toBe(11);
130
+ expect((0, benchmarkEvidenceRules_1.yearGroupAtDate)(13, "2023-10-01", "2026-03-01")).toBe(11);
131
+ });
132
+ it("rolls over at the start of the academic year, not the calendar year", () => {
133
+ // August starts the new academic year, so a record dated 2025-09-01 and one
134
+ // dated 2026-01-01 sit in the same year group.
135
+ expect((0, benchmarkEvidenceRules_1.yearGroupAtDate)(11, "2025-09-01", "2026-01-01")).toBe(11);
136
+ expect((0, benchmarkEvidenceRules_1.yearGroupAtDate)(11, "2025-07-01", "2026-01-01")).toBe(10);
137
+ });
138
+ it("returns undefined rather than guessing when a date predates schooling", () => {
139
+ expect((0, benchmarkEvidenceRules_1.yearGroupAtDate)(11, undefined)).toBeUndefined();
140
+ expect((0, benchmarkEvidenceRules_1.yearGroupAtDate)(7, "2005-01-01", "2026-03-01")).toBeUndefined();
141
+ });
142
+ });
77
143
  //# sourceMappingURL=benchmarkEvidenceRules.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"benchmarkEvidenceRules.test.js","sourceRoot":"","sources":["../src/benchmarkEvidenceRules.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"benchmarkEvidenceRules.test.js","sourceRoot":"","sources":["../src/benchmarkEvidenceRules.test.ts"],"names":[],"mappings":"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"}
@@ -16,6 +16,11 @@ import { InstituteData } from "./typeDefinitions";
16
16
  /** The product a surface can gate on. */
17
17
  export type ProductKey = "wex" | "network" | "parents" | "events" | "skills" | "activities" | "destinations" | "alumni";
18
18
  export type ResolvedProducts = Record<ProductKey, boolean> & {
19
+ /** How many parents are actually in the database, across `options.oIds`.
20
+ * `parents` is just this over zero; the number is surfaced so a caller that
21
+ * wants to show the count doesn't run the same aggregate a second time.
22
+ * Zero until the count lands, the same as the flag. */
23
+ parentCount: number;
19
24
  /** Package shape, so callers stop testing `package ===` themselves. */
20
25
  isTrust: boolean;
21
26
  isHub: boolean;
@@ -68,6 +68,7 @@ function useProductResolver(institute, options) {
68
68
  const products = { wex, network, parents, events, skills, activities, destinations, alumni };
69
69
  return {
70
70
  ...products,
71
+ parentCount,
71
72
  isTrust,
72
73
  isHub,
73
74
  isHubSchool,
@@ -1 +1 @@
1
- {"version":3,"file":"productResolver.js","sourceRoot":"","sources":["../src/productResolver.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"productResolver.js","sourceRoot":"","sources":["../src/productResolver.ts"],"names":[],"mappings":"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"}
@@ -3323,6 +3323,68 @@ export type StudentActivity = {
3323
3323
  reflectionCompletedByUid?: string;
3324
3324
  evidenceFileIds?: string[];
3325
3325
  };
3326
+ /**
3327
+ * ─── Careers record ───────────────────────────────────────────────────────────
3328
+ * A staff-authored record of a careers interaction with one pupil. Deliberately
3329
+ * modelled as the pupil's *careers record* rather than a meetings log, because
3330
+ * Gatsby Benchmark 3 asks schools to keep systematic records of "the individual
3331
+ * advice given to each pupil, and any subsequent agreed decisions" — not only of
3332
+ * formal guidance interviews.
3333
+ *
3334
+ * Only `personalGuidance` entries delivered by a qualified adviser are evidence
3335
+ * for Benchmark 8. Every other type is BM3 record-keeping and must NOT move the
3336
+ * BM8 number: a school that logs fifty option-choice chats has not delivered
3337
+ * personal guidance, and showing otherwise would fabricate compliance.
3338
+ */
3339
+ export type CareersRecordEntryType = "personalGuidance" | "careersConversation" | "optionsMeeting" | "applicationSupport" | "sendReview" | "parentMeeting" | "groupGuidance" | "other";
3340
+ /** A single agreed next step arising from a careers record entry. */
3341
+ export type CareersRecordAction = {
3342
+ label: string;
3343
+ owner?: "student" | "staff";
3344
+ dueDate?: string;
3345
+ done?: boolean;
3346
+ };
3347
+ export type CareersRecordEntry = {
3348
+ id: string;
3349
+ uid: string;
3350
+ oId: string;
3351
+ entryType: CareersRecordEntryType;
3352
+ date: string;
3353
+ /** Staff member who delivered it. Advisers are always platform users. */
3354
+ adviserUid: string;
3355
+ /**
3356
+ * Whether the adviser was, at the time of this entry, trained to a level the
3357
+ * school considers appropriate for personal guidance. Snapshotted rather than
3358
+ * read live from the staff record so that later changes to a staff member's
3359
+ * qualification cannot silently restate a previous year's BM8 coverage.
3360
+ *
3361
+ * Note the framework says "trained to an appropriate level", not Level 6 —
3362
+ * Level 6 is the CDI's professional standard and what most hubs expect, but
3363
+ * gating strictly on it would wrongly zero out schools whose adviser
3364
+ * qualified by another route. The school asserts this; we record it.
3365
+ */
3366
+ adviserQualified: boolean;
3367
+ /**
3368
+ * The pupil's year group when the interaction happened. Snapshotted because
3369
+ * BM8 is a cumulative "by age 16 / by age 18" duty and pupils roll up a year
3370
+ * group every August — without this, a Year 9 meeting becomes unattributable
3371
+ * once the pupil reaches Year 11.
3372
+ */
3373
+ yearGroupAtEntry?: number;
3374
+ /** Academic year label, e.g. "2025-26". See academicYearForDate. */
3375
+ academicYear: string;
3376
+ mode: "inPerson" | "video" | "phone";
3377
+ durationMins?: number;
3378
+ /** Shared with the pupil. BM3 expects pupils to be able to access their record. */
3379
+ summary: string;
3380
+ /** Never shown to the pupil. Kept separate so the summary can be surfaced safely. */
3381
+ staffNotes?: string;
3382
+ /** "Any subsequent agreed decisions" — named in the benchmark, so evidence in its own right. */
3383
+ actions?: CareersRecordAction[];
3384
+ createdBy: string;
3385
+ dateCreated: string;
3386
+ dateUpdated?: string;
3387
+ };
3326
3388
  export type SuggestedFixRepo = "placementt-web" | "placementt-backend" | "placementt-core";
3327
3389
  export type SuggestedFix = {
3328
3390
  isCodeChange: boolean;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "private": false,
3
3
  "name": "placementt-core",
4
4
  "author": "Placementt",
5
- "version": "1.400.1016",
5
+ "version": "1.400.1018",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
8
8
  "scripts": {
@@ -2,31 +2,46 @@ import {
2
2
  academicYearDateRange,
3
3
  academicYearForDate,
4
4
  activityEvidenceBenchmarks,
5
+ ageCheckpointForYearGroup,
5
6
  BENCHMARK_COVERAGE_NUMBERS,
6
7
  BENCHMARK_META,
8
+ careersRecordEvidenceBenchmarks,
7
9
  coverageBand,
8
10
  dateInAcademicYear,
9
11
  eventEvidenceBenchmarks,
10
12
  eventProvidesBenchmark,
11
13
  isPalCompliantProviderEncounter,
14
+ isPersonalGuidanceEntry,
12
15
  palPhaseForYearGroup,
16
+ yearGroupAtDate,
13
17
  } from "./benchmarkEvidenceRules";
14
18
  import {COMPASS_PAL_BENCHMARKS, COMPASS_VALID_ACTIVITY_BENCHMARKS} from "./compassExport";
15
19
 
16
20
  describe("benchmarkEvidenceRules single-source consistency", () => {
17
21
  it("stays consistent with the Compass export's valid activity benchmarks (2-8)", () => {
18
- // Coverage numbers are the export-valid set minus BM8 (untracked on CareerThread).
19
- expect(BENCHMARK_COVERAGE_NUMBERS).toEqual(
20
- COMPASS_VALID_ACTIVITY_BENCHMARKS.filter((b) => b !== 8));
22
+ // Coverage numbers are exactly the export-valid set. BM8 joined it when personal
23
+ // guidance became recordable on the careers record; BM1 was never in it.
24
+ expect(BENCHMARK_COVERAGE_NUMBERS).toEqual([...COMPASS_VALID_ACTIVITY_BENCHMARKS]);
21
25
  expect(BENCHMARK_COVERAGE_NUMBERS).not.toContain(1);
22
- expect(BENCHMARK_COVERAGE_NUMBERS).not.toContain(8);
26
+ expect(BENCHMARK_COVERAGE_NUMBERS).toContain(8);
23
27
  });
24
28
 
25
- it("marks BM1 as provision-level and BM8 as untracked — neither counts toward coverage", () => {
29
+ it("marks BM1 as the only provision-level benchmark, and BM8 as guidance-sourced", () => {
26
30
  expect(BENCHMARK_META[1].countsTowardCoverage).toBe(false);
27
31
  expect(BENCHMARK_META[1].source).toBe("provision");
28
- expect(BENCHMARK_META[8].countsTowardCoverage).toBe(false);
29
- expect(BENCHMARK_META[8].source).toBe("untracked");
32
+ expect(BENCHMARK_META[8].countsTowardCoverage).toBe(true);
33
+ expect(BENCHMARK_META[8].source).toBe("guidance");
34
+ });
35
+
36
+ it("periodises each benchmark the way its own wording does", () => {
37
+ // BM5 is the only benchmark that genuinely renews annually ("every year, from
38
+ // the age of 11"). BM6/7/8 are cumulative duties pinned to 16 and 18, and
39
+ // measuring them annually understates coverage — see BenchmarkTimeModel.
40
+ expect(BENCHMARK_META[5].timeModel).toBe("annual");
41
+ expect(BENCHMARK_META[6].timeModel).toBe("cumulativeByAge");
42
+ expect(BENCHMARK_META[7].timeModel).toBe("cumulativeByAge");
43
+ expect(BENCHMARK_META[8].timeModel).toBe("cumulativeByAge");
44
+ expect(BENCHMARK_META[1].timeModel).toBe("provision");
30
45
  });
31
46
 
32
47
  it("flags PAL relevance for exactly benchmarks 5-7", () => {
@@ -96,3 +111,67 @@ describe("coverage bands are neutral (no pass/fail theatre)", () => {
96
111
  expect(coverageBand(1)).toBe("full");
97
112
  });
98
113
  });
114
+
115
+ describe("careers record entries as benchmark evidence", () => {
116
+ it("counts a qualified adviser's personal guidance towards BM8", () => {
117
+ const entry = {entryType: "personalGuidance", adviserQualified: true} as const;
118
+ expect(isPersonalGuidanceEntry(entry)).toBe(true);
119
+ expect(careersRecordEvidenceBenchmarks(entry)).toEqual([3, 8]);
120
+ });
121
+
122
+ it("does NOT count personal guidance from an unqualified adviser towards BM8", () => {
123
+ // The benchmark is satisfied by a meeting with a careers adviser trained to an
124
+ // appropriate level. Counting an untrained member of staff would fabricate
125
+ // compliance with a statutory expectation.
126
+ const entry = {entryType: "personalGuidance", adviserQualified: false} as const;
127
+ expect(isPersonalGuidanceEntry(entry)).toBe(false);
128
+ expect(careersRecordEvidenceBenchmarks(entry)).toEqual([3]);
129
+ });
130
+
131
+ it("does NOT let other entry types inflate BM8, however many are logged", () => {
132
+ const others = [
133
+ "careersConversation", "optionsMeeting", "applicationSupport",
134
+ "sendReview", "parentMeeting", "groupGuidance", "other",
135
+ ] as const;
136
+ others.forEach((entryType) => {
137
+ const entry = {entryType, adviserQualified: true} as const;
138
+ expect(isPersonalGuidanceEntry(entry)).toBe(false);
139
+ expect(careersRecordEvidenceBenchmarks(entry)).toEqual([3]);
140
+ });
141
+ });
142
+
143
+ it("still treats every entry as BM3 record-keeping evidence", () => {
144
+ expect(careersRecordEvidenceBenchmarks(
145
+ {entryType: "careersConversation", adviserQualified: false})).toContain(3);
146
+ });
147
+ });
148
+
149
+ describe("cumulative age-banded coverage", () => {
150
+ it("maps year groups onto the checkpoint they are working towards", () => {
151
+ expect(ageCheckpointForYearGroup(9)?.age).toBe(16);
152
+ expect(ageCheckpointForYearGroup(11)?.age).toBe(16);
153
+ expect(ageCheckpointForYearGroup(12)?.age).toBe(18);
154
+ expect(ageCheckpointForYearGroup(13)?.age).toBe(18);
155
+ expect(ageCheckpointForYearGroup(6)).toBeUndefined();
156
+ });
157
+
158
+ it("back-derives the year group a pupil was in when an older record was made", () => {
159
+ // A Year 11 pupil "now" (2025-26) did work experience in 2024-25, so they were
160
+ // in Year 10 at the time. Without this, BM6 reads them as uncovered.
161
+ expect(yearGroupAtDate(11, "2025-03-01", "2026-03-01")).toBe(10);
162
+ expect(yearGroupAtDate(11, "2025-11-01", "2026-03-01")).toBe(11);
163
+ expect(yearGroupAtDate(13, "2023-10-01", "2026-03-01")).toBe(11);
164
+ });
165
+
166
+ it("rolls over at the start of the academic year, not the calendar year", () => {
167
+ // August starts the new academic year, so a record dated 2025-09-01 and one
168
+ // dated 2026-01-01 sit in the same year group.
169
+ expect(yearGroupAtDate(11, "2025-09-01", "2026-01-01")).toBe(11);
170
+ expect(yearGroupAtDate(11, "2025-07-01", "2026-01-01")).toBe(10);
171
+ });
172
+
173
+ it("returns undefined rather than guessing when a date predates schooling", () => {
174
+ expect(yearGroupAtDate(11, undefined)).toBeUndefined();
175
+ expect(yearGroupAtDate(7, "2005-01-01", "2026-03-01")).toBeUndefined();
176
+ });
177
+ });
@@ -1,4 +1,4 @@
1
- import {ExternalEvent, StudentActivity} from "./typeDefinitions";
1
+ import {CareersRecordEntry, ExternalEvent, StudentActivity} from "./typeDefinitions";
2
2
  import {COMPASS_PAL_BENCHMARKS, COMPASS_VALID_ACTIVITY_BENCHMARKS} from "./compassExport";
3
3
 
4
4
  /**
@@ -30,7 +30,30 @@ export type BenchmarkEvidenceSource =
30
30
  | "events" // tagged events only.
31
31
  | "eventsAndActivities" // tagged events + StudentActivity encounter records.
32
32
  | "eventsAndPlacements" // tagged events + work-experience placements.
33
- | "untracked"; // BM8 — not yet captured on CareerThread. Honest empty state, never faked.
33
+ | "guidance"; // BM8 — personal guidance entries on the pupil's careers record.
34
+
35
+ /**
36
+ * ─── Time model ───────────────────────────────────────────────────────────────
37
+ * The period a benchmark's coverage is measured over. This is NOT cosmetic: the
38
+ * benchmarks genuinely differ, and measuring them all annually (as this module
39
+ * originally did) understates coverage badly.
40
+ *
41
+ * BM5 renews every year → "Every year, from the age of 11, at least one
42
+ * meaningful encounter with an employer."
43
+ * BM2 runs per Key Stage → "During each Key Stage, all pupils should access
44
+ * and use information about careers…"
45
+ * BM6/7/8 are cumulative → "By the age of 16… and a further one by 18."
46
+ *
47
+ * The cumulative case is the one that bites. Measured annually, a Year 11 who did
48
+ * their work experience in Year 10 reads as uncovered, and every pupil in Years 7-9
49
+ * sits in the denominator of a duty that does not apply to them until 16 — which is
50
+ * why `biggestGap` currently keeps nominating Year 7 as a school's worst gap on BM6.
51
+ */
52
+ export type BenchmarkTimeModel =
53
+ | "annual" // renews each academic year (BM5)
54
+ | "keyStage" // once per Key Stage (BM2)
55
+ | "cumulativeByAge" // ever, judged at the age-16 and age-18 checkpoints (BM6, BM7, BM8)
56
+ | "provision"; // no per-pupil period (BM1, BM4)
34
57
 
35
58
  export type BenchmarkMeta = {
36
59
  number: GatsbyBenchmarkNumber,
@@ -41,23 +64,28 @@ export type BenchmarkMeta = {
41
64
  source: BenchmarkEvidenceSource,
42
65
  /**
43
66
  * Whether this benchmark yields a per-student coverage number. False for BM1
44
- * (provision-level, shown as an informational card) and BM8 (untracked, shown
45
- * as an honest empty state). The dashboard must not render a coverage % for a
46
- * benchmark where this is false.
67
+ * (provision-level, shown as an informational card). The dashboard must not
68
+ * render a coverage % for a benchmark where this is false.
47
69
  */
48
70
  countsTowardCoverage: boolean,
49
71
  /** True for 5-7: the PAL Compliance flag is meaningful (mirrors COMPASS_PAL_BENCHMARKS). */
50
72
  palRelevant: boolean,
73
+ /** How coverage is periodised. Drives both the denominator and the card's label. */
74
+ timeModel: BenchmarkTimeModel,
51
75
  };
52
76
 
53
77
  /**
54
- * Benchmarks that produce a coverage percentage on the matrix / cards. Deliberately
55
- * BM2-8 excluding the two that can't be a per-student coverage number: BM1
56
- * (provision) and BM8 (untracked). Derived from the Compass export's valid set so
57
- * the two files can't drift.
78
+ * Benchmarks that produce a coverage percentage on the matrix / cards. BM1 is the
79
+ * only exclusion: it is whole-programme provision and can never be a per-pupil
80
+ * number. Derived from the Compass export's valid set (2-8) so the two files can't
81
+ * drift.
82
+ *
83
+ * BM8 was previously excluded here because personal guidance was untracked. It is
84
+ * now recorded on the careers record, so it earns a coverage number like the rest —
85
+ * but a cumulative, age-banded one. See BENCHMARK_AGE_CHECKPOINTS.
58
86
  */
59
87
  export const BENCHMARK_COVERAGE_NUMBERS: GatsbyBenchmarkNumber[] =
60
- COMPASS_VALID_ACTIVITY_BENCHMARKS.filter((b): b is GatsbyBenchmarkNumber => b !== 8);
88
+ [...COMPASS_VALID_ACTIVITY_BENCHMARKS] as GatsbyBenchmarkNumber[];
61
89
 
62
90
  export const BENCHMARK_META: Record<GatsbyBenchmarkNumber, BenchmarkMeta> = {
63
91
  1: {
@@ -67,6 +95,7 @@ export const BENCHMARK_META: Record<GatsbyBenchmarkNumber, BenchmarkMeta> = {
67
95
  source: "provision",
68
96
  countsTowardCoverage: false,
69
97
  palRelevant: false,
98
+ timeModel: "provision",
70
99
  },
71
100
  2: {
72
101
  number: 2,
@@ -75,6 +104,9 @@ export const BENCHMARK_META: Record<GatsbyBenchmarkNumber, BenchmarkMeta> = {
75
104
  source: "events",
76
105
  countsTowardCoverage: true,
77
106
  palRelevant: false,
107
+ // The benchmark's unit is the Key Stage, not the year. Still measured annually
108
+ // until the Key Stage denominator lands — see BENCHMARK_AGE_CHECKPOINTS.
109
+ timeModel: "annual",
78
110
  },
79
111
  3: {
80
112
  number: 3,
@@ -83,6 +115,7 @@ export const BENCHMARK_META: Record<GatsbyBenchmarkNumber, BenchmarkMeta> = {
83
115
  source: "destinations",
84
116
  countsTowardCoverage: true,
85
117
  palRelevant: false,
118
+ timeModel: "annual",
86
119
  },
87
120
  4: {
88
121
  number: 4,
@@ -91,6 +124,7 @@ export const BENCHMARK_META: Record<GatsbyBenchmarkNumber, BenchmarkMeta> = {
91
124
  source: "events",
92
125
  countsTowardCoverage: true,
93
126
  palRelevant: false,
127
+ timeModel: "annual",
94
128
  },
95
129
  5: {
96
130
  number: 5,
@@ -99,6 +133,9 @@ export const BENCHMARK_META: Record<GatsbyBenchmarkNumber, BenchmarkMeta> = {
99
133
  source: "eventsAndActivities",
100
134
  countsTowardCoverage: true,
101
135
  palRelevant: true,
136
+ // The one benchmark whose annual model matches its own wording: "every year,
137
+ // from the age of 11, at least one meaningful encounter with an employer".
138
+ timeModel: "annual",
102
139
  },
103
140
  6: {
104
141
  number: 6,
@@ -107,6 +144,7 @@ export const BENCHMARK_META: Record<GatsbyBenchmarkNumber, BenchmarkMeta> = {
107
144
  source: "eventsAndPlacements",
108
145
  countsTowardCoverage: true,
109
146
  palRelevant: true,
147
+ timeModel: "cumulativeByAge",
110
148
  },
111
149
  7: {
112
150
  number: 7,
@@ -115,17 +153,73 @@ export const BENCHMARK_META: Record<GatsbyBenchmarkNumber, BenchmarkMeta> = {
115
153
  source: "events",
116
154
  countsTowardCoverage: true,
117
155
  palRelevant: true,
156
+ timeModel: "cumulativeByAge",
118
157
  },
119
158
  8: {
120
159
  number: 8,
121
160
  name: "Personal guidance",
122
- description: "Every pupil has personal guidance interviews with a careers adviser when they need them.",
123
- source: "untracked",
124
- countsTowardCoverage: false,
161
+ description: "Every pupil has a personal guidance meeting with a careers adviser by 16, and a further one by 18.",
162
+ source: "guidance",
163
+ countsTowardCoverage: true,
125
164
  palRelevant: false,
165
+ timeModel: "cumulativeByAge",
126
166
  },
127
167
  };
128
168
 
169
+ /**
170
+ * ─── Age checkpoints for cumulative benchmarks ────────────────────────────────
171
+ * BM6, BM7 and BM8 are all phrased as "by the age of 16… and by 18". Coverage for
172
+ * these is judged at the two points where the duty actually falls due, not spread
173
+ * across every year group: asking what proportion of Year 7 has had a personal
174
+ * guidance meeting measures nothing, because none of them are due one yet.
175
+ *
176
+ * The headline figure for each checkpoint is therefore the *due* cohort — the year
177
+ * group at which the duty has just become expected. Earlier year groups still get a
178
+ * progression figure (are we on track?), but they must not drag the headline down.
179
+ */
180
+ export type BenchmarkAgeCheckpoint = {
181
+ /** The age the benchmark names. */
182
+ age: 16|18,
183
+ /** The year group at which the duty has fallen due in full. */
184
+ dueYearGroup: number,
185
+ /** Year groups the duty is accumulating across, up to and including the due one. */
186
+ accumulatingYearGroups: number[],
187
+ label: string,
188
+ };
189
+
190
+ export const BENCHMARK_AGE_CHECKPOINTS: BenchmarkAgeCheckpoint[] = [
191
+ {age: 16, dueYearGroup: 11, accumulatingYearGroups: [7, 8, 9, 10, 11], label: "By age 16"},
192
+ {age: 18, dueYearGroup: 13, accumulatingYearGroups: [12, 13], label: "By age 18"},
193
+ ];
194
+
195
+ /** The checkpoint a year group is working towards, or undefined if off-scale. */
196
+ export function ageCheckpointForYearGroup(yearGroup: number): BenchmarkAgeCheckpoint|undefined {
197
+ return BENCHMARK_AGE_CHECKPOINTS.find((c) => c.accumulatingYearGroups.includes(yearGroup));
198
+ }
199
+
200
+ /**
201
+ * The pupil's year group at some earlier date, back-derived from their current year
202
+ * group. Records written before `yearGroupAtEntry` existed (and every placement and
203
+ * event, which never carried one) have no snapshot, so cumulative coverage has to
204
+ * reconstruct it: each academic year that has elapsed since the record is one year
205
+ * group to subtract.
206
+ *
207
+ * Returns undefined when the result falls outside schooling, which is the honest
208
+ * answer for a record whose date predates the pupil joining.
209
+ */
210
+ export function yearGroupAtDate(
211
+ currentYearGroup: number,
212
+ recordDate: string|undefined,
213
+ now: Date|string = new Date(),
214
+ ): number|undefined {
215
+ if (!recordDate) return undefined;
216
+ const recordYear = parseInt(academicYearForDate(recordDate).split("-")[0], 10);
217
+ const currentYear = parseInt(academicYearForDate(now).split("-")[0], 10);
218
+ if (isNaN(recordYear) || isNaN(currentYear)) return undefined;
219
+ const derived = currentYearGroup - (currentYear - recordYear);
220
+ return derived >= 1 && derived <= 15 ? derived : undefined;
221
+ }
222
+
129
223
  /**
130
224
  * StudentActivity.activityType → the benchmark(s) that activity is evidence for.
131
225
  * Employer-facing encounters count towards BM5; first-hand workplace experiences
@@ -204,6 +298,33 @@ export function activityEvidenceBenchmarks(activity: Pick<StudentActivity, "acti
204
298
  return ACTIVITY_TYPE_BENCHMARKS[activity.activityType] || [];
205
299
  }
206
300
 
301
+ /**
302
+ * ─── Careers record entries as evidence ───────────────────────────────────────
303
+ * Every entry is BM3 evidence: the updated Benchmark 3 asks schools to record the
304
+ * individual advice given to each pupil and any decisions subsequently agreed, and
305
+ * makes no distinction between a formal guidance interview and an option-choice
306
+ * conversation for that purpose.
307
+ *
308
+ * BM8 is far narrower. It is satisfied only by a personal guidance meeting with a
309
+ * careers adviser trained to an appropriate level. A school that logs fifty tutor-time
310
+ * careers chats has kept excellent records and has NOT delivered personal guidance, so
311
+ * only `personalGuidance` entries with `adviserQualified` count here. Loosening this
312
+ * would manufacture compliance with a statutory expectation, which is the one thing
313
+ * this module exists to prevent.
314
+ */
315
+ export function isPersonalGuidanceEntry(
316
+ entry: Pick<CareersRecordEntry, "entryType"|"adviserQualified">,
317
+ ): boolean {
318
+ return entry.entryType === "personalGuidance" && Boolean(entry.adviserQualified);
319
+ }
320
+
321
+ /** The benchmarks a careers record entry is evidence for. */
322
+ export function careersRecordEvidenceBenchmarks(
323
+ entry: Pick<CareersRecordEntry, "entryType"|"adviserQualified">,
324
+ ): GatsbyBenchmarkNumber[] {
325
+ return isPersonalGuidanceEntry(entry) ? [3, 8] : [3];
326
+ }
327
+
207
328
  /**
208
329
  * A PAL-compliant provider encounter: the event carries the existing PAL flag AND
209
330
  * is tagged with a PAL-relevant benchmark (5-7). Same rule the export uses when
@@ -23,6 +23,11 @@ export type ProductKey =
23
23
  "wex"|"network"|"parents"|"events"|"skills"|"activities"|"destinations"|"alumni";
24
24
 
25
25
  export type ResolvedProducts = Record<ProductKey, boolean> & {
26
+ /** How many parents are actually in the database, across `options.oIds`.
27
+ * `parents` is just this over zero; the number is surfaced so a caller that
28
+ * wants to show the count doesn't run the same aggregate a second time.
29
+ * Zero until the count lands, the same as the flag. */
30
+ parentCount: number,
26
31
  /** Package shape, so callers stop testing `package ===` themselves. */
27
32
  isTrust: boolean,
28
33
  isHub: boolean,
@@ -145,6 +150,7 @@ export function useProductResolver(
145
150
 
146
151
  return {
147
152
  ...products,
153
+ parentCount,
148
154
  isTrust,
149
155
  isHub,
150
156
  isHubSchool,
@@ -3380,6 +3380,89 @@ export type StudentActivity = {
3380
3380
  evidenceFileIds?: string[];
3381
3381
  };
3382
3382
 
3383
+ /**
3384
+ * ─── Careers record ───────────────────────────────────────────────────────────
3385
+ * A staff-authored record of a careers interaction with one pupil. Deliberately
3386
+ * modelled as the pupil's *careers record* rather than a meetings log, because
3387
+ * Gatsby Benchmark 3 asks schools to keep systematic records of "the individual
3388
+ * advice given to each pupil, and any subsequent agreed decisions" — not only of
3389
+ * formal guidance interviews.
3390
+ *
3391
+ * Only `personalGuidance` entries delivered by a qualified adviser are evidence
3392
+ * for Benchmark 8. Every other type is BM3 record-keeping and must NOT move the
3393
+ * BM8 number: a school that logs fifty option-choice chats has not delivered
3394
+ * personal guidance, and showing otherwise would fabricate compliance.
3395
+ */
3396
+ export type CareersRecordEntryType =
3397
+ | "personalGuidance" // 1:1 with a careers adviser — the only BM8-valid type
3398
+ | "careersConversation" // tutor-time or drop-in conversation
3399
+ | "optionsMeeting" // GCSE/A-level option choices
3400
+ | "applicationSupport" // UCAS, apprenticeship or college applications
3401
+ | "sendReview" // careers input to an EHCP annual review (Y9+)
3402
+ | "parentMeeting" // careers conversation with a parent or carer
3403
+ | "groupGuidance" // small-group guidance session
3404
+ | "other";
3405
+
3406
+ /** A single agreed next step arising from a careers record entry. */
3407
+ export type CareersRecordAction = {
3408
+ label: string,
3409
+ owner?: "student"|"staff",
3410
+ dueDate?: string, // ISO date
3411
+ done?: boolean,
3412
+ };
3413
+
3414
+ export type CareersRecordEntry = {
3415
+ id: string,
3416
+ uid: string, // pupil
3417
+ oId: string,
3418
+
3419
+ entryType: CareersRecordEntryType,
3420
+ date: string, // ISO date the interaction happened
3421
+
3422
+ /** Staff member who delivered it. Advisers are always platform users. */
3423
+ adviserUid: string,
3424
+
3425
+ /**
3426
+ * Whether the adviser was, at the time of this entry, trained to a level the
3427
+ * school considers appropriate for personal guidance. Snapshotted rather than
3428
+ * read live from the staff record so that later changes to a staff member's
3429
+ * qualification cannot silently restate a previous year's BM8 coverage.
3430
+ *
3431
+ * Note the framework says "trained to an appropriate level", not Level 6 —
3432
+ * Level 6 is the CDI's professional standard and what most hubs expect, but
3433
+ * gating strictly on it would wrongly zero out schools whose adviser
3434
+ * qualified by another route. The school asserts this; we record it.
3435
+ */
3436
+ adviserQualified: boolean,
3437
+
3438
+ /**
3439
+ * The pupil's year group when the interaction happened. Snapshotted because
3440
+ * BM8 is a cumulative "by age 16 / by age 18" duty and pupils roll up a year
3441
+ * group every August — without this, a Year 9 meeting becomes unattributable
3442
+ * once the pupil reaches Year 11.
3443
+ */
3444
+ yearGroupAtEntry?: number,
3445
+
3446
+ /** Academic year label, e.g. "2025-26". See academicYearForDate. */
3447
+ academicYear: string,
3448
+
3449
+ mode: "inPerson"|"video"|"phone",
3450
+ durationMins?: number,
3451
+
3452
+ /** Shared with the pupil. BM3 expects pupils to be able to access their record. */
3453
+ summary: string,
3454
+
3455
+ /** Never shown to the pupil. Kept separate so the summary can be surfaced safely. */
3456
+ staffNotes?: string,
3457
+
3458
+ /** "Any subsequent agreed decisions" — named in the benchmark, so evidence in its own right. */
3459
+ actions?: CareersRecordAction[],
3460
+
3461
+ createdBy: string, // uid of the staff member who wrote the record
3462
+ dateCreated: string, // ISO timestamp
3463
+ dateUpdated?: string,
3464
+ };
3465
+
3383
3466
  export type SuggestedFixRepo = "placementt-web"|"placementt-backend"|"placementt-core";
3384
3467
 
3385
3468
  export type SuggestedFix = {