diff --git a/src/lib/analytics/score/composite.ts b/src/lib/analytics/score/composite.ts index c47bf3781..6dde302a5 100644 --- a/src/lib/analytics/score/composite.ts +++ b/src/lib/analytics/score/composite.ts @@ -6,7 +6,7 @@ import { import type { Derived } from "@/lib/insights/derived/types"; import { - SCORE_PILLAR_IDS, + orderedUniquePillars, SCORE_VERSION, type CompositeValue, type HealthScoreReport, @@ -36,15 +36,10 @@ export interface CompositeInput { asOf: Date; } -function orderedUnique(ids: readonly ScorePillarId[]): ScorePillarId[] { - const present = new Set(ids); - return SCORE_PILLAR_IDS.filter((id) => present.has(id)); -} - export function computeComposite( input: CompositeInput, ): Derived { - const available = orderedUnique(input.availablePillars); + const available = orderedUniquePillars(input.availablePillars); const byId = new Map(input.pillars.map((pillar) => [pillar.id, pillar])); const eligible = available .map((id) => byId.get(id)) diff --git a/src/lib/analytics/score/config.ts b/src/lib/analytics/score/config.ts index 8842b08ee..015796fc0 100644 --- a/src/lib/analytics/score/config.ts +++ b/src/lib/analytics/score/config.ts @@ -34,13 +34,11 @@ */ import { z } from "zod"; -import { SCORE_PILLAR_IDS, type ScorePillarId } from "./types"; - -/** Ordered, de-duplicated, unknown ids dropped. Registry order is the score's own. */ -export function orderedUniquePillars(ids: readonly unknown[]): ScorePillarId[] { - const present = new Set(ids); - return SCORE_PILLAR_IDS.filter((id) => present.has(id)); -} +import { + orderedUniquePillars, + SCORE_PILLAR_IDS, + type ScorePillarId, +} from "./types"; export const scorePillarIdSchema = z.enum(SCORE_PILLAR_IDS); diff --git a/src/lib/analytics/score/types.ts b/src/lib/analytics/score/types.ts index 83ee83941..6cf094734 100644 --- a/src/lib/analytics/score/types.ts +++ b/src/lib/analytics/score/types.ts @@ -20,6 +20,18 @@ export const SCORE_PILLAR_IDS = [ ] as const; export type ScorePillarId = (typeof SCORE_PILLAR_IDS)[number]; + +/** + * Sanitise a list of pillar ids: registry order, no duplicates, and + * anything outside the catalogue dropped. Everything that narrows the + * score's composition goes through here, so a stored blob, a caller's + * argument and the scorer itself cannot disagree about what a valid + * pillar set looks like. + */ +export function orderedUniquePillars(ids: readonly unknown[]): ScorePillarId[] { + const present = new Set(ids); + return SCORE_PILLAR_IDS.filter((id) => present.has(id)); +} export type ScoreBand = "green" | "yellow" | "red"; export type ScoreDomain = | "cardiometabolic"