placementt-core 1.400.1120 → 1.400.1121

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,18 +1,4 @@
1
1
  import { z } from "zod";
2
- /**
3
- * Contracts for the `he` backend module
4
- * (`Placementt-Backend/functions/src/heImport.ts`).
5
- *
6
- * Admin-only controls for the weekly mirror of the HESA Discover Uni dataset
7
- * into `heCourses` / `heProviders`. The status document (`heMeta/status`) is
8
- * the source of truth the admin card reads live, so the responses only echo
9
- * enough to confirm the call landed - an import can outlive the client's
10
- * callable deadline and the card still resolves.
11
- */
12
- /** Every enrichment source, in the order the admin card lists them. The
13
- * orchestrator runs "areas" first (the city sources key on its output) and
14
- * "finalise" last. */
15
- export declare const HE_ENRICHMENT_SOURCES: readonly ["areas", "hesaSsr", "hesaFinance", "hesaStudents", "wideningParticipation", "openAlex", "rent", "crime", "localJobs", "weather", "amenities", "population", "finalise"];
16
2
  export declare const heCallables: {
17
3
  /** Run a full import now. `storagePath` names a zip already uploaded to
18
4
  * Cloud Storage (`heImports/<file>.zip`) to use instead of the URL in
@@ -93,33 +79,6 @@ export declare const heCallables: {
93
79
  };
94
80
  }>;
95
81
  };
96
- /**
97
- * Run the university enrichment: one status entry per source in
98
- * `heMeta/enrichmentStatus`, one Cloud Task per source, then a finalise
99
- * task that computes provider / area percentiles and reapplies course
100
- * percentiles. `sources` limits the run to those sources (a retry of the
101
- * ones that failed); omitted, every source runs. Returns as soon as the
102
- * tasks are queued; the admin card follows the status document.
103
- */
104
- "he-runEnrichment": {
105
- request: z.ZodObject<{
106
- sources: z.ZodOptional<z.ZodArray<z.ZodEnum<["areas", "hesaSsr", "hesaFinance", "hesaStudents", "wideningParticipation", "openAlex", "rent", "crime", "localJobs", "weather", "amenities", "population", "finalise"]>, "many">>;
107
- }, "strip", z.ZodTypeAny, {
108
- sources?: ("areas" | "hesaSsr" | "hesaFinance" | "hesaStudents" | "wideningParticipation" | "openAlex" | "rent" | "crime" | "localJobs" | "weather" | "amenities" | "population" | "finalise")[] | undefined;
109
- }, {
110
- sources?: ("areas" | "hesaSsr" | "hesaFinance" | "hesaStudents" | "wideningParticipation" | "openAlex" | "rent" | "crime" | "localJobs" | "weather" | "amenities" | "population" | "finalise")[] | undefined;
111
- }>;
112
- response: z.ZodObject<{
113
- runId: z.ZodString;
114
- queued: z.ZodArray<z.ZodEnum<["areas", "hesaSsr", "hesaFinance", "hesaStudents", "wideningParticipation", "openAlex", "rent", "crime", "localJobs", "weather", "amenities", "population", "finalise"]>, "many">;
115
- }, "strip", z.ZodTypeAny, {
116
- runId: string;
117
- queued: ("areas" | "hesaSsr" | "hesaFinance" | "hesaStudents" | "wideningParticipation" | "openAlex" | "rent" | "crime" | "localJobs" | "weather" | "amenities" | "population" | "finalise")[];
118
- }, {
119
- runId: string;
120
- queued: ("areas" | "hesaSsr" | "hesaFinance" | "hesaStudents" | "wideningParticipation" | "openAlex" | "rent" | "crime" | "localJobs" | "weather" | "amenities" | "population" | "finalise")[];
121
- }>;
122
- };
123
82
  /** Clear a stuck "running" state. The mirror and its manifest are left
124
83
  * alone. */
125
84
  "he-resetRun": {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.heCallables = exports.HE_ENRICHMENT_SOURCES = void 0;
3
+ exports.heCallables = void 0;
4
4
  const zod_1 = require("zod");
5
5
  /**
6
6
  * Contracts for the `he` backend module
@@ -12,11 +12,6 @@ const zod_1 = require("zod");
12
12
  * enough to confirm the call landed - an import can outlive the client's
13
13
  * callable deadline and the card still resolves.
14
14
  */
15
- /** Every enrichment source, in the order the admin card lists them. The
16
- * orchestrator runs "areas" first (the city sources key on its output) and
17
- * "finalise" last. */
18
- exports.HE_ENRICHMENT_SOURCES = ["areas", "hesaSsr", "hesaFinance", "hesaStudents", "wideningParticipation",
19
- "openAlex", "rent", "crime", "localJobs", "weather", "amenities", "population", "finalise"];
20
15
  const importCounts = zod_1.z.object({
21
16
  coursesAssembled: zod_1.z.number(),
22
17
  coursesWritten: zod_1.z.number(),
@@ -44,23 +39,6 @@ exports.heCallables = {
44
39
  counts: importCounts,
45
40
  }),
46
41
  },
47
- /**
48
- * Run the university enrichment: one status entry per source in
49
- * `heMeta/enrichmentStatus`, one Cloud Task per source, then a finalise
50
- * task that computes provider / area percentiles and reapplies course
51
- * percentiles. `sources` limits the run to those sources (a retry of the
52
- * ones that failed); omitted, every source runs. Returns as soon as the
53
- * tasks are queued; the admin card follows the status document.
54
- */
55
- "he-runEnrichment": {
56
- request: zod_1.z.object({
57
- sources: zod_1.z.array(zod_1.z.enum(exports.HE_ENRICHMENT_SOURCES)).optional(),
58
- }),
59
- response: zod_1.z.object({
60
- runId: zod_1.z.string(),
61
- queued: zod_1.z.array(zod_1.z.enum(exports.HE_ENRICHMENT_SOURCES)),
62
- }),
63
- },
64
42
  /** Clear a stuck "running" state. The mirror and its manifest are left
65
43
  * alone. */
66
44
  "he-resetRun": {
@@ -1 +1 @@
1
- {"version":3,"file":"he.js","sourceRoot":"","sources":["../../src/callables/he.ts"],"names":[],"mappings":";;;AAAA,6BAAsB;AAGtB;;;;;;;;;GASG;AAEH;;sBAEsB;AACT,QAAA,qBAAqB,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,cAAc,EAAE,uBAAuB;IAC5G,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,CAAU,CAAC;AAEzG,MAAM,YAAY,GAAG,OAAC,CAAC,MAAM,CAAC;IAC1B,gBAAgB,EAAE,OAAC,CAAC,MAAM,EAAE;IAC5B,cAAc,EAAE,OAAC,CAAC,MAAM,EAAE;IAC1B,gBAAgB,EAAE,OAAC,CAAC,MAAM,EAAE;IAC5B,cAAc,EAAE,OAAC,CAAC,MAAM,EAAE;IAC1B,gBAAgB,EAAE,OAAC,CAAC,MAAM,EAAE;IAC5B,kBAAkB,EAAE,OAAC,CAAC,MAAM,EAAE;CACjC,CAAC,CAAC;AAEU,QAAA,WAAW,GAAG;IACvB;;;4BAGwB;IACxB,cAAc,EAAE;QACZ,OAAO,EAAE,OAAC,CAAC,MAAM,CAAC;YACd,WAAW,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;SACrC,CAAC;QACF,QAAQ,EAAE,OAAC,CAAC,MAAM,CAAC;YACf,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE;YACjB,cAAc,EAAE,OAAC,CAAC,MAAM,EAAE;YAC1B,MAAM,EAAE,OAAC,CAAC,MAAM,CAAC;gBACb,IAAI,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;gBAChC,GAAG,EAAE,OAAC,CAAC,MAAM,EAAE;aAClB,CAAC;YACF,MAAM,EAAE,YAAY;SACvB,CAAC;KACL;IACD;;;;;;;OAOG;IACH,kBAAkB,EAAE;QAChB,OAAO,EAAE,OAAC,CAAC,MAAM,CAAC;YACd,OAAO,EAAE,OAAC,CAAC,KAAK,CAAC,OAAC,CAAC,IAAI,CAAC,6BAAqB,CAAC,CAAC,CAAC,QAAQ,EAAE;SAC7D,CAAC;QACF,QAAQ,EAAE,OAAC,CAAC,MAAM,CAAC;YACf,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE;YACjB,MAAM,EAAE,OAAC,CAAC,KAAK,CAAC,OAAC,CAAC,IAAI,CAAC,6BAAqB,CAAC,CAAC;SACjD,CAAC;KACL;IACD;gBACY;IACZ,aAAa,EAAE;QACX,OAAO,EAAE,OAAC,CAAC,MAAM,CAAC,EAAE,CAAC;QACrB,QAAQ,EAAE,OAAC,CAAC,MAAM,CAAC;YACf,EAAE,EAAE,OAAC,CAAC,OAAO,CAAC,IAAI,CAAC;SACtB,CAAC;KACL;CACoB,CAAC"}
1
+ {"version":3,"file":"he.js","sourceRoot":"","sources":["../../src/callables/he.ts"],"names":[],"mappings":";;;AAAA,6BAAsB;AAGtB;;;;;;;;;GASG;AAEH,MAAM,YAAY,GAAG,OAAC,CAAC,MAAM,CAAC;IAC1B,gBAAgB,EAAE,OAAC,CAAC,MAAM,EAAE;IAC5B,cAAc,EAAE,OAAC,CAAC,MAAM,EAAE;IAC1B,gBAAgB,EAAE,OAAC,CAAC,MAAM,EAAE;IAC5B,cAAc,EAAE,OAAC,CAAC,MAAM,EAAE;IAC1B,gBAAgB,EAAE,OAAC,CAAC,MAAM,EAAE;IAC5B,kBAAkB,EAAE,OAAC,CAAC,MAAM,EAAE;CACjC,CAAC,CAAC;AAEU,QAAA,WAAW,GAAG;IACvB;;;4BAGwB;IACxB,cAAc,EAAE;QACZ,OAAO,EAAE,OAAC,CAAC,MAAM,CAAC;YACd,WAAW,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;SACrC,CAAC;QACF,QAAQ,EAAE,OAAC,CAAC,MAAM,CAAC;YACf,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE;YACjB,cAAc,EAAE,OAAC,CAAC,MAAM,EAAE;YAC1B,MAAM,EAAE,OAAC,CAAC,MAAM,CAAC;gBACb,IAAI,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;gBAChC,GAAG,EAAE,OAAC,CAAC,MAAM,EAAE;aAClB,CAAC;YACF,MAAM,EAAE,YAAY;SACvB,CAAC;KACL;IACD;gBACY;IACZ,aAAa,EAAE;QACX,OAAO,EAAE,OAAC,CAAC,MAAM,CAAC,EAAE,CAAC;QACrB,QAAQ,EAAE,OAAC,CAAC,MAAM,CAAC;YACf,EAAE,EAAE,OAAC,CAAC,OAAO,CAAC,IAAI,CAAC;SACtB,CAAC;KACL;CACoB,CAAC"}
@@ -618,25 +618,6 @@ export declare const allCallables: {
618
618
  };
619
619
  }>;
620
620
  };
621
- "he-runEnrichment": {
622
- request: z.ZodObject<{
623
- sources: z.ZodOptional<z.ZodArray<z.ZodEnum<["areas", "hesaSsr", "hesaFinance", "hesaStudents", "wideningParticipation", "openAlex", "rent", "crime", "localJobs", "weather", "amenities", "population", "finalise"]>, "many">>;
624
- }, "strip", z.ZodTypeAny, {
625
- sources?: ("areas" | "hesaSsr" | "hesaFinance" | "hesaStudents" | "wideningParticipation" | "openAlex" | "rent" | "crime" | "localJobs" | "weather" | "amenities" | "population" | "finalise")[] | undefined;
626
- }, {
627
- sources?: ("areas" | "hesaSsr" | "hesaFinance" | "hesaStudents" | "wideningParticipation" | "openAlex" | "rent" | "crime" | "localJobs" | "weather" | "amenities" | "population" | "finalise")[] | undefined;
628
- }>;
629
- response: z.ZodObject<{
630
- runId: z.ZodString;
631
- queued: z.ZodArray<z.ZodEnum<["areas", "hesaSsr", "hesaFinance", "hesaStudents", "wideningParticipation", "openAlex", "rent", "crime", "localJobs", "weather", "amenities", "population", "finalise"]>, "many">;
632
- }, "strip", z.ZodTypeAny, {
633
- runId: string;
634
- queued: ("areas" | "hesaSsr" | "hesaFinance" | "hesaStudents" | "wideningParticipation" | "openAlex" | "rent" | "crime" | "localJobs" | "weather" | "amenities" | "population" | "finalise")[];
635
- }, {
636
- runId: string;
637
- queued: ("areas" | "hesaSsr" | "hesaFinance" | "hesaStudents" | "wideningParticipation" | "openAlex" | "rent" | "crime" | "localJobs" | "weather" | "amenities" | "population" | "finalise")[];
638
- }>;
639
- };
640
621
  "he-resetRun": {
641
622
  request: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
642
623
  response: z.ZodObject<{
@@ -2373,25 +2354,6 @@ export declare const callableGroups: {
2373
2354
  };
2374
2355
  }>;
2375
2356
  };
2376
- "he-runEnrichment": {
2377
- request: z.ZodObject<{
2378
- sources: z.ZodOptional<z.ZodArray<z.ZodEnum<["areas", "hesaSsr", "hesaFinance", "hesaStudents", "wideningParticipation", "openAlex", "rent", "crime", "localJobs", "weather", "amenities", "population", "finalise"]>, "many">>;
2379
- }, "strip", z.ZodTypeAny, {
2380
- sources?: ("areas" | "hesaSsr" | "hesaFinance" | "hesaStudents" | "wideningParticipation" | "openAlex" | "rent" | "crime" | "localJobs" | "weather" | "amenities" | "population" | "finalise")[] | undefined;
2381
- }, {
2382
- sources?: ("areas" | "hesaSsr" | "hesaFinance" | "hesaStudents" | "wideningParticipation" | "openAlex" | "rent" | "crime" | "localJobs" | "weather" | "amenities" | "population" | "finalise")[] | undefined;
2383
- }>;
2384
- response: z.ZodObject<{
2385
- runId: z.ZodString;
2386
- queued: z.ZodArray<z.ZodEnum<["areas", "hesaSsr", "hesaFinance", "hesaStudents", "wideningParticipation", "openAlex", "rent", "crime", "localJobs", "weather", "amenities", "population", "finalise"]>, "many">;
2387
- }, "strip", z.ZodTypeAny, {
2388
- runId: string;
2389
- queued: ("areas" | "hesaSsr" | "hesaFinance" | "hesaStudents" | "wideningParticipation" | "openAlex" | "rent" | "crime" | "localJobs" | "weather" | "amenities" | "population" | "finalise")[];
2390
- }, {
2391
- runId: string;
2392
- queued: ("areas" | "hesaSsr" | "hesaFinance" | "hesaStudents" | "wideningParticipation" | "openAlex" | "rent" | "crime" | "localJobs" | "weather" | "amenities" | "population" | "finalise")[];
2393
- }>;
2394
- };
2395
2357
  "he-resetRun": {
2396
2358
  request: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
2397
2359
  response: z.ZodObject<{
@@ -1,15 +1,33 @@
1
- import { HeArea, HeCourse, HePriority, HeProvider } from "./typeDefinitions";
2
- /** Metrics with fewer respondents than this get no percentile. */
1
+ /**
2
+ * University priorities: the NSS questions that matter most to a student,
3
+ * course percentiles within a subject, ranking and comparison helpers.
4
+ *
5
+ * A student picks up to five NSS questions (HE_PRIORITIES, reworded for a 17
6
+ * year old with the original wording kept for tooltips). Every course carries
7
+ * `pct`: the percentile of each NSS question's agreement within its CAH
8
+ * subject, written by the Discover Uni import. A course's score is the mean
9
+ * percentile of the picks it has data for; a pick with no data (suppressed,
10
+ * or too few respondents) is left out of the mean and counted, never treated
11
+ * as zero, so missing data cannot drag a course down.
12
+ *
13
+ * Everything here is pure: the backend uses `coursePercentiles` when it
14
+ * writes `pct`, and the web ranks Algolia hits with `rankCourses`.
15
+ *
16
+ * Data is © HESA, licensed under CC BY 4.0: every surface showing it carries
17
+ * HE_ATTRIBUTION.
18
+ */
19
+ import { HeCourse, HePriority, HePriorityGroup } from "./typeDefinitions";
20
+ /** NSS questions with fewer respondents than this get no percentile. */
3
21
  export declare const HE_MIN_RESPONSES = 20;
4
- /** A CAH group needs at least this many courses carrying a metric before
5
- * courses are ranked within it; smaller groups fall back to the parent. */
22
+ /** A CAH group needs at least this many courses with an answer before courses
23
+ * are ranked within it; smaller groups fall back to the parent. */
6
24
  export declare const HE_MIN_COHORT = 10;
7
25
  /** How many priorities a student can pick. */
8
26
  export declare const HE_MAX_PRIORITIES = 5;
9
27
  /** How many courses a comparison holds. */
10
28
  export declare const HE_MAX_COMPARE = 3;
11
- /** OpenStreetMap counts are taken within this radius of an area's centre. */
12
- export declare const HE_AMENITY_RADIUS_KM = 2;
29
+ /** Attribution for every surface showing Discover Uni data. */
30
+ export declare const HE_ATTRIBUTION: string;
13
31
  /** CAH level-1 subjects, for the subject picker and the `subjectGroups`
14
32
  * facet. */
15
33
  export declare const CAH_SUBJECTS: {
@@ -36,6 +54,14 @@ export declare function subjectsForCategories(categories: string[]): string[];
36
54
  * @return {string[]} sorted, de-duplicated codes.
37
55
  */
38
56
  export declare function subjectGroupsFor(subjects: string[]): string[];
57
+ /**
58
+ * The CAH level-1, level-2 and level-3 codes of a subject, most specific
59
+ * first. A malformed code yields just itself.
60
+ *
61
+ * @param {string} cah a CAH code at any level, e.g. "CAH10-01-02".
62
+ * @return {string[]} e.g. ["CAH10-01-02", "CAH10-01", "CAH10"].
63
+ */
64
+ export declare function cahAncestors(cah: string): string[];
39
65
  /** Original NSS question text, for tooltips. Mirrors the importer's list. */
40
66
  export declare const NSS_QUESTION_TEXT: {
41
67
  [code: string]: string;
@@ -48,12 +74,8 @@ export declare const NSS_QUESTION_TEXT: {
48
74
  */
49
75
  export declare const nssMetricId: (code: string) => string;
50
76
  /** Groups in picker order. */
51
- export declare const HE_PRIORITY_GROUPS: HePriority["group"][];
52
- /**
53
- * Every priority a student can pick. Research has no rankable entry yet: REF
54
- * 2021 results need UKRI's written permission for commercial use, and
55
- * OpenAlex research profiles are shown, never ranked.
56
- */
77
+ export declare const HE_PRIORITY_GROUPS: HePriorityGroup[];
78
+ /** Every NSS question a student can pick, in question order. */
57
79
  export declare const HE_PRIORITIES: HePriority[];
58
80
  /** Priorities by id. */
59
81
  export declare const HE_PRIORITY_BY_ID: {
@@ -62,53 +84,27 @@ export declare const HE_PRIORITY_BY_ID: {
62
84
  /**
63
85
  * Keep only known priority ids, de-duplicated, capped at HE_MAX_PRIORITIES,
64
86
  * in the order given. Used before saving and before ranking, so a stale id
65
- * (a priority since retired) is dropped rather than breaking the page.
87
+ * is dropped rather than breaking the page.
66
88
  *
67
89
  * @param {string[]|undefined} ids what the student picked.
68
90
  * @return {string[]} the valid picks.
69
91
  */
70
92
  export declare function cleanPriorities(ids: string[] | undefined): string[];
71
- /** One metric value and the respondents behind it (null when unknown). */
93
+ /** One question's agreement and the respondents behind it (null when unknown). */
72
94
  export type HeMetricValue = {
73
95
  value: number;
74
96
  n: number | null;
75
97
  };
76
98
  /**
77
- * The raw value of every course-level metric on a course, with its
78
- * respondent count. Metrics with no value are absent.
99
+ * Each NSS question's agreement on a course, keyed by metric id, with the
100
+ * respondent count. Questions with no value are absent.
79
101
  *
80
- * @param {object} course the course fields the metrics read.
102
+ * @param {object} course the course's NSS fields.
81
103
  * @return {object} metric id -> value and respondents.
82
104
  */
83
- export declare function courseMetricValues(course: Partial<HeCourse>): {
105
+ export declare function nssMetricValues(course: Pick<Partial<HeCourse>, "nss" | "nssResponses" | "nssPopulation" | "nssResponseRate">): {
84
106
  [metricId: string]: HeMetricValue;
85
107
  };
86
- /**
87
- * The TEF award as a number (Gold 3 ... Requires improvement 0).
88
- *
89
- * @param {string|undefined} outcome the award text.
90
- * @return {number|null} the score, or null when there is no award.
91
- */
92
- export declare function tefScore(outcome: string | undefined): number | null;
93
- /**
94
- * The raw value of every university-level metric on a provider.
95
- *
96
- * @param {object} provider the provider with its enrichment.
97
- * @return {object} metric id -> raw value (absent when unknown).
98
- */
99
- export declare function providerMetricValues(provider: Partial<HeProvider>): {
100
- [metricId: string]: number;
101
- };
102
- /**
103
- * The raw value of every city-level metric on an area. Local jobs are one
104
- * metric per BRES sector, `jobs_<sector>`.
105
- *
106
- * @param {object} area the area.
107
- * @return {object} metric id -> raw value (absent when unknown).
108
- */
109
- export declare function areaMetricValues(area: Partial<HeArea>): {
110
- [metricId: string]: number;
111
- };
112
108
  /**
113
109
  * Mid-rank percentile of `v` within `sorted`: the share of values below it
114
110
  * plus half the share equal to it, 0-100, rounded. Ties share a percentile.
@@ -118,17 +114,6 @@ export declare function areaMetricValues(area: Partial<HeArea>): {
118
114
  * @return {number} the percentile.
119
115
  */
120
116
  export declare function percentileOf(sorted: number[], v: number): number;
121
- /**
122
- * Percentiles of a flat set of values (providers or areas), ascending.
123
- *
124
- * @param {object} values id -> raw value; null / undefined are skipped.
125
- * @return {object} id -> percentile, only for ids with a value.
126
- */
127
- export declare function percentileMap(values: {
128
- [id: string]: number | null | undefined;
129
- }): {
130
- [id: string]: number;
131
- };
132
117
  /** The input `coursePercentiles` ranks. */
133
118
  export type HeCourseMetricRow = {
134
119
  courseId: string;
@@ -138,20 +123,19 @@ export type HeCourseMetricRow = {
138
123
  };
139
124
  };
140
125
  /**
141
- * Percentile of every course-level metric within the course's CAH subject.
126
+ * Percentile of every NSS question within the course's CAH subject.
142
127
  *
143
- * Each course is ranked on its first (primary) CAH subject. For each metric
128
+ * Each course is ranked on its first (primary) CAH subject. For each question
144
129
  * the cohort is the most specific of that subject's level-3, level-2 and
145
- * level-1 groups holding at least `minCohort` courses with the metric,
146
- * falling back to every course with it. Values from fewer than
147
- * `minResponses` respondents are left out entirely: they neither get a
148
- * percentile nor count in anyone's cohort. A null respondent count (a metric
149
- * with no population, like the cost-centre figures) is not filtered.
130
+ * level-1 groups holding at least `minCohort` courses with an answer, falling
131
+ * back to every course with one. Answers from fewer than `minResponses`
132
+ * respondents are left out entirely: they neither get a percentile nor count
133
+ * in anyone's cohort. An unknown respondent count is not filtered.
150
134
  *
151
135
  * @param {HeCourseMetricRow[]} rows every course's metrics.
152
136
  * @param {object} options thresholds, defaulting to the constants above.
153
- * @return {Map} courseId -> {pct, cohort}; courses with no rankable metric
154
- * get empty objects.
137
+ * @return {Map} courseId -> {pct, cohort}; courses with nothing rankable get
138
+ * empty objects.
155
139
  */
156
140
  export declare function coursePercentiles(rows: HeCourseMetricRow[], options?: {
157
141
  minResponses?: number;
@@ -164,28 +148,6 @@ export declare function coursePercentiles(rows: HeCourseMetricRow[], options?: {
164
148
  [metricId: string]: string;
165
149
  };
166
150
  }>;
167
- /** Prefix on the course `pct` keys that come from the provider / area, so a
168
- * course-level and a university-level metric can never collide. */
169
- export declare const HE_UNI_PCT_PREFIX = "u_";
170
- export declare const HE_CITY_PCT_PREFIX = "c_";
171
- /**
172
- * A course's full `pct`: its own course-level percentiles plus its
173
- * provider's and area's, prefixed.
174
- *
175
- * @param {object} own course-level percentiles.
176
- * @param {object|undefined} provider the provider's percentiles.
177
- * @param {object|undefined} area the area's percentiles.
178
- * @return {object} the merged map, keys sorted so the content hash is stable.
179
- */
180
- export declare function mergeCoursePct(own: {
181
- [id: string]: number;
182
- }, provider?: {
183
- [id: string]: number;
184
- }, area?: {
185
- [id: string]: number;
186
- }): {
187
- [id: string]: number;
188
- };
189
151
  /** What `rankCourses` needs from a course (an Algolia hit is enough). */
190
152
  export type HeRankable = {
191
153
  courseId: string;
@@ -193,104 +155,36 @@ export type HeRankable = {
193
155
  pct?: {
194
156
  [metricId: string]: number;
195
157
  };
196
- _geoloc?: {
197
- lat: number;
198
- lng: number;
199
- }[];
200
- locations?: {
201
- lat: number;
202
- lng: number;
203
- }[];
204
- };
205
- /** Context the student-specific priorities need. */
206
- export type HeRankContext = {
207
- /** BRES sector ids from the student's career interests, for "jobs". */
208
- sectors?: string[];
209
- /** The student's home, for "distance". */
210
- home?: {
211
- lat: number;
212
- lng: number;
213
- };
214
158
  };
215
159
  /** One pick's contribution to a course's score. */
216
160
  export type HeRankPart = {
217
161
  priorityId: string;
218
- /** 0-100, higher is better for the student, or null without data. */
219
- goodness: number | null;
162
+ /** The course's percentile in subject for this question, or null without data. */
163
+ percentile: number | null;
220
164
  };
221
165
  /** A ranked course. */
222
166
  export type HeRanked<T extends HeRankable> = {
223
167
  course: T;
224
- /** Mean goodness of the picks with data, or null when none had any. */
168
+ /** Mean percentile of the picks with data, or null when none had any. */
225
169
  score: number | null;
226
170
  /** How many picks had data, out of how many were made. */
227
171
  picksWithData: number;
228
172
  picksTotal: number;
229
173
  parts: HeRankPart[];
230
- /** Straight-line km to the nearest location, when a home was given. */
231
- distanceKm?: number;
232
174
  };
233
- /**
234
- * Great-circle distance in km.
235
- *
236
- * @param {object} a a point.
237
- * @param {object} b a point.
238
- * @return {number} the distance.
239
- */
240
- export declare function haversineKm(a: {
241
- lat: number;
242
- lng: number;
243
- }, b: {
244
- lat: number;
245
- lng: number;
246
- }): number;
247
- /**
248
- * The course's nearest teaching location to `home`, in km.
249
- *
250
- * @param {HeRankable} course the course.
251
- * @param {object} home the student's home.
252
- * @return {number|undefined} the distance, or undefined without locations.
253
- */
254
- export declare function courseDistanceKm(course: HeRankable, home: {
255
- lat: number;
256
- lng: number;
257
- }): number | undefined;
258
- /**
259
- * The raw (ascending) percentile a priority reads from a course's `pct`, or
260
- * null without data. "jobs" is the mean over the student's sectors that have
261
- * data; "distance" is handled by `rankCourses`.
262
- *
263
- * @param {HePriority} priority the priority.
264
- * @param {object|undefined} pct the course's `pct`.
265
- * @param {HeRankContext} ctx the student context.
266
- * @return {number|null} the percentile.
267
- */
268
- export declare function priorityPercentile(priority: HePriority, pct: {
269
- [id: string]: number;
270
- } | undefined, ctx?: HeRankContext): number | null;
271
- /**
272
- * The percentile turned the student's way: 100 is best for them.
273
- *
274
- * @param {HePriority} priority the priority.
275
- * @param {number|null} percentile the raw ascending percentile.
276
- * @return {number|null} the goodness.
277
- */
278
- export declare const goodnessOf: (priority: HePriority, percentile: number | null) => number | null;
279
175
  /**
280
176
  * Rank courses on the student's picks.
281
177
  *
282
- * Score = mean goodness over the picks the course has data for; picks
178
+ * Score = mean percentile over the picks the course has data for; picks
283
179
  * without data are counted in `picksWithData` and otherwise ignored. Courses
284
180
  * with no data for any pick sort last. Ties go to more picks with data, then
285
- * title. "Close to home" is ranked within the list itself, since it depends
286
- * on the student.
181
+ * title.
287
182
  *
288
183
  * @param {HeRankable[]} courses the candidates.
289
184
  * @param {string[]} priorityIds the student's picks.
290
- * @param {HeRankContext} ctx sectors and home.
291
185
  * @return {HeRanked[]} the courses, best first.
292
186
  */
293
- export declare function rankCourses<T extends HeRankable>(courses: T[], priorityIds: string[], ctx?: HeRankContext): HeRanked<T>[];
187
+ export declare function rankCourses<T extends HeRankable>(courses: T[], priorityIds: string[]): HeRanked<T>[];
294
188
  /**
295
189
  * The "why" chips for a ranked course: its strongest picks, phrased for a
296
190
  * student ("Top 10% for feedback on time"). Only picks in the top quarter
@@ -302,36 +196,51 @@ export declare function rankCourses<T extends HeRankable>(courses: T[], priority
302
196
  */
303
197
  export declare function whyChips(parts: HeRankPart[], max?: number): string[];
304
198
  /**
305
- * The raw value behind a priority for display on the compare page.
199
+ * A question's agreement on a course, for display.
306
200
  *
307
- * @param {HePriority} priority the priority.
308
- * @param {object} data the course, its provider and area, and the student
309
- * context.
310
- * @return {number|null} the value in the priority's format, or null.
201
+ * @param {HePriority} priority the question.
202
+ * @param {object} course the course's NSS answers.
203
+ * @return {number|null} % agreeing, or null without data.
311
204
  */
312
- export declare function priorityRawValue(priority: HePriority, data: {
313
- course?: Partial<HeCourse>;
314
- provider?: Partial<HeProvider>;
315
- area?: Partial<HeArea>;
316
- ctx?: HeRankContext;
317
- distanceKm?: number;
318
- }): number | null;
205
+ export declare function priorityValue(priority: HePriority, course: Pick<Partial<HeCourse>, "nss">): number | null;
319
206
  /**
320
- * Format a metric value for display, "No data" when null.
207
+ * Format an agreement for display, "No data" when null.
321
208
  *
322
- * @param {HePriority} priority the priority whose format applies.
323
- * @param {number|null} value the raw value.
324
- * @return {string} the formatted value.
209
+ * @param {number|null} value % agreeing.
210
+ * @return {string} e.g. "82%".
325
211
  */
326
- export declare function formatHeValue(priority: HePriority, value: number | null): string;
327
- /** Attribution line per source, shown wherever its figures appear. */
328
- export declare const HE_ATTRIBUTIONS: {
329
- [source in HePriority["source"]]?: string;
330
- };
212
+ export declare const formatAgree: (value: number | null) => string;
331
213
  /**
332
- * The attribution lines for a set of priorities, de-duplicated, HESA first.
214
+ * Great-circle distance in km, for the ranked list's distance filter.
333
215
  *
334
- * @param {HePriority[]} priorities what the surface shows.
335
- * @return {string[]} attribution lines.
216
+ * @param {object} a a point.
217
+ * @param {object} b a point.
218
+ * @return {number} the distance.
336
219
  */
337
- export declare function attributionsFor(priorities: HePriority[]): string[];
220
+ export declare function haversineKm(a: {
221
+ lat: number;
222
+ lng: number;
223
+ }, b: {
224
+ lat: number;
225
+ lng: number;
226
+ }): number;
227
+ /**
228
+ * The course's nearest teaching location to `home`, in km.
229
+ *
230
+ * @param {object} course its locations (Algolia `_geoloc` or `locations`).
231
+ * @param {object} home the point to measure from.
232
+ * @return {number|undefined} the distance, or undefined without locations.
233
+ */
234
+ export declare function courseDistanceKm(course: {
235
+ _geoloc?: {
236
+ lat: number;
237
+ lng: number;
238
+ }[];
239
+ locations?: {
240
+ lat: number;
241
+ lng: number;
242
+ }[];
243
+ }, home: {
244
+ lat: number;
245
+ lng: number;
246
+ }): number | undefined;