placementt-core 1.400.1073 → 1.400.1074
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.
- package/lib/benchmarkAggregate.d.ts +249 -0
- package/lib/benchmarkAggregate.js +440 -0
- package/lib/benchmarkAggregate.js.map +1 -0
- package/lib/benchmarkAggregate.test.d.ts +1 -0
- package/lib/benchmarkAggregate.test.js +191 -0
- package/lib/benchmarkAggregate.test.js.map +1 -0
- package/lib/benchmarkEvidenceRules.test.js +25 -9
- package/lib/benchmarkEvidenceRules.test.js.map +1 -1
- package/lib/callables/benchmarks.d.ts +31 -0
- package/lib/callables/benchmarks.js +33 -0
- package/lib/callables/benchmarks.js.map +1 -0
- package/lib/callables/registry.d.ts +28 -0
- package/lib/callables/registry.js +3 -1
- package/lib/callables/registry.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/benchmarkAggregate.test.ts +217 -0
- package/src/benchmarkAggregate.ts +667 -0
- package/src/benchmarkEvidenceRules.test.ts +28 -10
- package/src/callables/benchmarks.ts +31 -0
- package/src/callables/registry.ts +3 -1
- package/src/index.ts +1 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { CoverageBand, GatsbyBenchmarkNumber } from "./benchmarkEvidenceRules";
|
|
2
|
+
import { CareersRecordEntry, ExternalEvent, ExternalEventStudent, StudentActivity, StudentPlacementData, UserData } from "./typeDefinitions";
|
|
3
|
+
import { SurveyCycle } from "./surveys/types";
|
|
4
|
+
/** The evidence sources, counted. Used for the scheduled half of a benchmark's stat. */
|
|
5
|
+
export type EvidenceCounts = {
|
|
6
|
+
events: number;
|
|
7
|
+
attendances: number;
|
|
8
|
+
activities: number;
|
|
9
|
+
placements: number;
|
|
10
|
+
guidanceEntries: number;
|
|
11
|
+
};
|
|
12
|
+
/** Per-student, per-benchmark evidence rolled up for the overview + matrix. */
|
|
13
|
+
export type BenchmarkStat = {
|
|
14
|
+
benchmark: GatsbyBenchmarkNumber;
|
|
15
|
+
/**
|
|
16
|
+
* Tagged events providing evidence, which have already taken place. Counted over
|
|
17
|
+
* this academic year for annual benchmarks and over all time for cumulative ones
|
|
18
|
+
* (BM6/7/8), matching whichever window that benchmark's coverage is measured on.
|
|
19
|
+
* Same rule for the counts below.
|
|
20
|
+
*/
|
|
21
|
+
events: number;
|
|
22
|
+
/** Student attendances across those events (joined/confirmed event-student records). */
|
|
23
|
+
attendances: number;
|
|
24
|
+
/** Logged student activities classified to this benchmark. */
|
|
25
|
+
activities: number;
|
|
26
|
+
/** Work-experience placements (BM6 only; 0 otherwise). */
|
|
27
|
+
placements: number;
|
|
28
|
+
/** Careers record entries evidencing this benchmark (BM3 and BM8; 0 otherwise). */
|
|
29
|
+
guidanceEntries: number;
|
|
30
|
+
/**
|
|
31
|
+
* The same evidence, for items dated after today: an event already in the calendar
|
|
32
|
+
* with pupils invited or signed up, a placement arranged for next term, a guidance
|
|
33
|
+
* meeting already booked. Planned delivery, not evidence.
|
|
34
|
+
*/
|
|
35
|
+
scheduled: EvidenceCounts;
|
|
36
|
+
/** Distinct on-roll students with ≥1 qualifying evidence item that has happened. */
|
|
37
|
+
coveredStudents: number;
|
|
38
|
+
/**
|
|
39
|
+
* Distinct on-roll students who either hold evidence already or are booked onto
|
|
40
|
+
* something that would give them some — what the school reaches if every planned
|
|
41
|
+
* activity goes ahead.
|
|
42
|
+
*/
|
|
43
|
+
projectedCoveredStudents: number;
|
|
44
|
+
/** On-roll student roster size (denominator). */
|
|
45
|
+
rosterTotal: number;
|
|
46
|
+
/** coveredStudents / rosterTotal, 0-1. */
|
|
47
|
+
coverage: number;
|
|
48
|
+
/** projectedCoveredStudents / rosterTotal, 0-1. Always ≥ coverage. */
|
|
49
|
+
projectedCoverage: number;
|
|
50
|
+
band: CoverageBand;
|
|
51
|
+
projectedBand: CoverageBand;
|
|
52
|
+
/** Year group with the lowest delivered coverage (the biggest gap), if any roster exists. */
|
|
53
|
+
biggestGap?: {
|
|
54
|
+
yearGroup: number;
|
|
55
|
+
missingFraction: number;
|
|
56
|
+
missingCount: number;
|
|
57
|
+
scheduledCount: number;
|
|
58
|
+
};
|
|
59
|
+
/** Coverage per year group: yearGroup → {covered, projected, total}. */
|
|
60
|
+
byYearGroup: {
|
|
61
|
+
[yearGroup: number]: {
|
|
62
|
+
covered: number;
|
|
63
|
+
projected: number;
|
|
64
|
+
total: number;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
/** Contributing events, so the drill-down can link back to each one. */
|
|
68
|
+
eventRefs: {
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
startDate?: string;
|
|
72
|
+
scheduled: boolean;
|
|
73
|
+
}[];
|
|
74
|
+
/**
|
|
75
|
+
* Coverage at each period this benchmark is judged over — the age checkpoints for
|
|
76
|
+
* BM7/8 ("by 16… and a further one by 18"), the Key Stages for BM2/6 (Compass asks
|
|
77
|
+
* each Key Stage as its own question). Absent for the annual benchmarks, which have
|
|
78
|
+
* no periodised denominator.
|
|
79
|
+
*
|
|
80
|
+
* `dueCoverage` is the headline: the proportion of the year group at which the duty
|
|
81
|
+
* has actually fallen due (Y9, Y11, Y13) holding evidence THAT PERIOD accepts — i.e.
|
|
82
|
+
* evidence gained while the pupil was in one of its `accumulatingYearGroups`. The
|
|
83
|
+
* age-18 figure therefore only counts post-16 experiences, because the benchmark asks
|
|
84
|
+
* for a further one after 16; and the Key Stage 4 figure counts nothing from Key
|
|
85
|
+
* Stage 3, because CEC asks about the two separately.
|
|
86
|
+
*
|
|
87
|
+
* Spreading this across all year groups instead — which is what measuring these
|
|
88
|
+
* annually does — asks what proportion of Year 7 has had a guidance meeting, and
|
|
89
|
+
* that measures nothing.
|
|
90
|
+
*/
|
|
91
|
+
byPeriod?: {
|
|
92
|
+
id: string;
|
|
93
|
+
label: string;
|
|
94
|
+
dueYearGroup: number;
|
|
95
|
+
dueCovered: number;
|
|
96
|
+
dueTotal: number;
|
|
97
|
+
dueCoverage: number;
|
|
98
|
+
/** The same figure once everything already booked for this period is delivered. */
|
|
99
|
+
projectedDueCovered: number;
|
|
100
|
+
projectedDueCoverage: number;
|
|
101
|
+
band: CoverageBand;
|
|
102
|
+
projectedBand: CoverageBand;
|
|
103
|
+
/**
|
|
104
|
+
* The pupils in the due year group with no evidence this period accepts —
|
|
105
|
+
* a coverage percentage turned back into a list of names.
|
|
106
|
+
*
|
|
107
|
+
* This is what a careers leader actually needs from the number: "84% of Year
|
|
108
|
+
* 11 have had a personal guidance meeting" is a figure to report, and "here
|
|
109
|
+
* are the 29 who have not" is the thing they can act on. It is the inversion
|
|
110
|
+
* of the covered set against the roster, which is why it costs nothing.
|
|
111
|
+
*
|
|
112
|
+
* `scheduled` separates the two very different names on that list: the pupil
|
|
113
|
+
* already booked onto next term's guidance week, and the pupil nobody has
|
|
114
|
+
* planned anything for. Only the second needs a decision.
|
|
115
|
+
*/
|
|
116
|
+
missing: {
|
|
117
|
+
uid: string;
|
|
118
|
+
name: string;
|
|
119
|
+
scheduled: boolean;
|
|
120
|
+
}[];
|
|
121
|
+
}[];
|
|
122
|
+
};
|
|
123
|
+
export type PalPhaseStat = {
|
|
124
|
+
phase: 1 | 2 | 3;
|
|
125
|
+
label: string;
|
|
126
|
+
/** Distinct PAL-compliant provider encounters (events) touching this phase. */
|
|
127
|
+
compliantEncounters: number;
|
|
128
|
+
/** Encounters in the calendar for this phase that haven't happened yet. */
|
|
129
|
+
scheduledEncounters: number;
|
|
130
|
+
target: number;
|
|
131
|
+
};
|
|
132
|
+
export type BenchmarkData = {
|
|
133
|
+
academicYear: string;
|
|
134
|
+
rosterTotal: number;
|
|
135
|
+
/** Year groups present in the roster, ascending. */
|
|
136
|
+
yearGroups: number[];
|
|
137
|
+
byBenchmark: {
|
|
138
|
+
[benchmark: number]: BenchmarkStat;
|
|
139
|
+
};
|
|
140
|
+
pal: PalPhaseStat[];
|
|
141
|
+
/** BM3: aspiration-survey response coverage (data-collection health). */
|
|
142
|
+
destinationsHealth: {
|
|
143
|
+
surveyed: number;
|
|
144
|
+
responded: number;
|
|
145
|
+
responseRate: number;
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Everything the aggregation reads, as the collections come back.
|
|
150
|
+
*
|
|
151
|
+
* Keyed maps rather than arrays because that is the shape both callers already have:
|
|
152
|
+
* the client's `getDocsWhere` returns one, and the cron builds one off its snapshot.
|
|
153
|
+
* All seven are scoped to a single school by `oId` before they get here.
|
|
154
|
+
*/
|
|
155
|
+
export type BenchmarkEvidenceInput = {
|
|
156
|
+
/** The year the ANNUAL benchmarks are measured over. The cumulative ones (BM6/7/8)
|
|
157
|
+
* read every year, which is why most of these collections arrive unfiltered. */
|
|
158
|
+
academicYear: string;
|
|
159
|
+
events: {
|
|
160
|
+
[id: string]: ExternalEvent;
|
|
161
|
+
};
|
|
162
|
+
/** One doc per pupil per event, carrying their uid. NOT externalEventAttendees,
|
|
163
|
+
* which is the employer side and holds document ids rather than uids. */
|
|
164
|
+
eventStudents: {
|
|
165
|
+
[id: string]: ExternalEventStudent;
|
|
166
|
+
};
|
|
167
|
+
activities: {
|
|
168
|
+
[id: string]: StudentActivity;
|
|
169
|
+
};
|
|
170
|
+
placements: {
|
|
171
|
+
[id: string]: StudentPlacementData;
|
|
172
|
+
};
|
|
173
|
+
/** On-roll, active students only. This is the denominator of every figure here. */
|
|
174
|
+
students: {
|
|
175
|
+
[uid: string]: UserData;
|
|
176
|
+
};
|
|
177
|
+
/** This year's aspiration cycles, for the BM3 data-collection health figure. */
|
|
178
|
+
aspirationCycles: {
|
|
179
|
+
[id: string]: SurveyCycle;
|
|
180
|
+
};
|
|
181
|
+
/** Deliberately unfiltered by year: BM8 is a cumulative "by 16 / by 18" duty, so a
|
|
182
|
+
* meeting held in Year 9 still evidences the duty in Year 11. */
|
|
183
|
+
careersRecord: {
|
|
184
|
+
[id: string]: CareersRecordEntry;
|
|
185
|
+
};
|
|
186
|
+
/** The instant "scheduled" is measured against. Defaults to now; passed explicitly
|
|
187
|
+
* by the cron so one run stamps every school with the same clock, and by tests. */
|
|
188
|
+
now?: Date | string;
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* One school's evidence coverage, from the documents behind it.
|
|
192
|
+
*
|
|
193
|
+
* Pure: same input, same output. Nothing in here reads a clock except through `now`,
|
|
194
|
+
* which is why a rollup can be trusted to say what the browser would have said at the
|
|
195
|
+
* moment it was written.
|
|
196
|
+
*
|
|
197
|
+
* @param {BenchmarkEvidenceInput} input the school's documents and the year to measure.
|
|
198
|
+
* @return {BenchmarkData} the coverage.
|
|
199
|
+
*/
|
|
200
|
+
export declare function computeBenchmarkData(input: BenchmarkEvidenceInput): BenchmarkData;
|
|
201
|
+
/** The collection the rollups live in. */
|
|
202
|
+
export declare const BENCHMARK_AGGREGATES = "benchmarkAggregates";
|
|
203
|
+
/**
|
|
204
|
+
* A benchmark's stat as the rollup stores it: everything except the two per-pupil
|
|
205
|
+
* lists.
|
|
206
|
+
*
|
|
207
|
+
* `missing` is a name per uncovered pupil and `eventRefs` one per contributing event,
|
|
208
|
+
* so on a large secondary they are most of the payload — and they are only ever read
|
|
209
|
+
* on a school's own page, which computes live anyway. Dropping them is what keeps a
|
|
210
|
+
* school's year comfortably inside the 1MB document limit.
|
|
211
|
+
*/
|
|
212
|
+
export type BenchmarkAggregateStat = Omit<BenchmarkStat, "eventRefs" | "byPeriod"> & {
|
|
213
|
+
byPeriod?: Omit<NonNullable<BenchmarkStat["byPeriod"]>[number], "missing">[];
|
|
214
|
+
};
|
|
215
|
+
/** One school-year in the rollup. */
|
|
216
|
+
export type BenchmarkAggregateYear = Omit<BenchmarkData, "byBenchmark"> & {
|
|
217
|
+
byBenchmark: {
|
|
218
|
+
[benchmark: number]: BenchmarkAggregateStat;
|
|
219
|
+
};
|
|
220
|
+
/** When this year was last recomputed, ISO. The UI shows it: a figure from a
|
|
221
|
+
* rollup has to say how old it is, or it reads as live and isn't. */
|
|
222
|
+
computedAt: string;
|
|
223
|
+
};
|
|
224
|
+
export type BenchmarkAggregateDoc = {
|
|
225
|
+
oId: string;
|
|
226
|
+
years: {
|
|
227
|
+
[academicYear: string]: BenchmarkAggregateYear;
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
/**
|
|
231
|
+
* A computed year, trimmed to what the rollup stores.
|
|
232
|
+
*
|
|
233
|
+
* Drops the per-pupil lists, and drops `undefined` outright rather than writing it:
|
|
234
|
+
* Firestore rejects an undefined field value, and `biggestGap`, `byPeriod` and an
|
|
235
|
+
* event's `startDate` are all legitimately absent.
|
|
236
|
+
*
|
|
237
|
+
* @param {BenchmarkData} data the computed year.
|
|
238
|
+
* @param {string} computedAt when it was computed, ISO.
|
|
239
|
+
* @return {BenchmarkAggregateYear} the document body for this year.
|
|
240
|
+
*/
|
|
241
|
+
export declare function benchmarkRollupYear(data: BenchmarkData, computedAt: string): BenchmarkAggregateYear;
|
|
242
|
+
/**
|
|
243
|
+
* How stale a rollup year is, in hours.
|
|
244
|
+
*
|
|
245
|
+
* @param {BenchmarkAggregateYear} [year] the stored year.
|
|
246
|
+
* @param {Date|string} [now] the instant to measure from.
|
|
247
|
+
* @return {number|undefined} hours since it was computed, absent when it never was.
|
|
248
|
+
*/
|
|
249
|
+
export declare function benchmarkRollupAgeHours(year?: Pick<BenchmarkAggregateYear, "computedAt">, now?: Date | string): number | undefined;
|