mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
Review follow-up to the shared entry-instant guard. The past-floor refine re-ran the full plausibility predicate, so a future-dated instant tripped both refines and the 422 carried a nonsensical "too far in the past" message alongside the correct future one. The floor refine now checks the past bound alone; a future value emits only the future message, matching the pre-refactor single-message contract. Also harden the Health-Score BP inputs and de-duplicate the tile-window derivation surfaced in the same review: - the prior-week graded score now keys off the prior-week window's own sufficiency rather than the current window's, so the week-over-week graded delta never pairs a confident current value against a thin-sample prior one; - the BD-Zielbereich 90-day count + effective span derive through one shared `deriveBpWindow90` helper, so the dashboard snapshot and the analytics thick slice can never compute the gate or the label span differently.
114 lines
4.5 KiB
TypeScript
114 lines
4.5 KiB
TypeScript
/**
|
|
* Shared window-confidence helper (v1.17 W1b).
|
|
*
|
|
* Several insight tiles narrate a metric over a fixed trailing window —
|
|
* the BD-Zielbereich tile reports the BP in-target share over the last
|
|
* 90 days, and future tiles (glucose TIR / GMI, sleep efficiency, …)
|
|
* will do the same over their own windows. A static "· 90 T" label is
|
|
* dishonest for a user who has only a few weeks of history or only a
|
|
* handful of readings: the headline reads like a stable quarter-long
|
|
* average when it is really a thin sample.
|
|
*
|
|
* This helper turns a window length + the readings observed inside it
|
|
* into two truthful signals every windowed tile can reuse:
|
|
*
|
|
* 1. `effectiveSpanDays` — the real calendar span of the data, capped
|
|
* at the window. A user with 23 days of history sees "· 23 T"; once
|
|
* ~90 days of history exist the span saturates at the window and the
|
|
* label reads "· 90 T". `null` when the window holds no readings.
|
|
*
|
|
* 2. `sufficient` — `false` when fewer than `MIN_READINGS_FOR_CONFIDENCE`
|
|
* readings fall in the window. The caller renders a "collecting data"
|
|
* placeholder instead of a confident percentage, and the Health-Score
|
|
* pillar treats the metric as absent rather than grading a thin sample.
|
|
*
|
|
* Pure & deterministic so the unit suite can pin both outputs.
|
|
*/
|
|
|
|
/**
|
|
* Minimum readings inside the window before a percentage is trustworthy.
|
|
* Below this the tile narrates "collecting data" and the score pillar is
|
|
* suppressed. Five is the same floor the trend cards use before they draw
|
|
* a first slope — a sample small enough to be noise.
|
|
*/
|
|
export const MIN_READINGS_FOR_CONFIDENCE = 5;
|
|
|
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
|
|
/**
|
|
* `true` once a window holds enough readings to narrate a confident
|
|
* percentage. Shared by the server (Health-Score pillar gate) and the
|
|
* client (tile placeholder) so both apply the identical floor.
|
|
*/
|
|
export function isWindowSufficient(readingCount: number): boolean {
|
|
return readingCount >= MIN_READINGS_FOR_CONFIDENCE;
|
|
}
|
|
|
|
export interface WindowConfidence {
|
|
/**
|
|
* Real calendar span of the in-window data, in days, capped at
|
|
* `windowDays`. `null` when the window holds no readings (caller renders
|
|
* the "—" / placeholder rather than "· 0 T").
|
|
*/
|
|
effectiveSpanDays: number | null;
|
|
/** `true` once at least `MIN_READINGS_FOR_CONFIDENCE` readings exist. */
|
|
sufficient: boolean;
|
|
}
|
|
|
|
/**
|
|
* v1.17 W1b — derive the BD-Zielbereich tile's confidence inputs (90-day
|
|
* pair count + effective label span) from a BP envelope's 90-day window.
|
|
* Shared by the dashboard snapshot and the `/api/analytics` thick slice so
|
|
* the two surfaces can never compute the tile's gate or span differently.
|
|
*/
|
|
export function deriveBpWindow90(
|
|
last90: { pairs: number } | null,
|
|
last90EarliestAt: Date | null,
|
|
now: Date,
|
|
): { count: number; spanDays: number | null } {
|
|
const count = last90?.pairs ?? 0;
|
|
const spanDays = computeWindowConfidence({
|
|
windowDays: 90,
|
|
readingCount: count,
|
|
earliestReadingAt: last90EarliestAt,
|
|
now,
|
|
}).effectiveSpanDays;
|
|
return { count, spanDays };
|
|
}
|
|
|
|
/**
|
|
* Compute the effective label span + sufficiency flag for a windowed
|
|
* metric.
|
|
*
|
|
* @param windowDays the configured trailing window (e.g. 90).
|
|
* @param readingCount number of readings counted inside the window.
|
|
* @param earliestReadingAt timestamp of the oldest in-window reading, or
|
|
* `null` when the window is empty. The span is
|
|
* `now - earliest`, ceil-rounded to whole days,
|
|
* and never exceeds `windowDays`. At least 1 day
|
|
* so a same-day-only sample still reads "· 1 T".
|
|
* @param now clock anchor (injectable for deterministic tests).
|
|
*/
|
|
export function computeWindowConfidence(input: {
|
|
windowDays: number;
|
|
readingCount: number;
|
|
earliestReadingAt: Date | null;
|
|
now?: Date;
|
|
}): WindowConfidence {
|
|
const { windowDays, readingCount, earliestReadingAt } = input;
|
|
const now = input.now ?? new Date();
|
|
|
|
const sufficient = isWindowSufficient(readingCount);
|
|
|
|
if (readingCount <= 0 || earliestReadingAt === null) {
|
|
return { effectiveSpanDays: null, sufficient };
|
|
}
|
|
|
|
const rawSpanMs = now.getTime() - earliestReadingAt.getTime();
|
|
// A reading logged moments ago still spans one calendar day of history.
|
|
const rawSpanDays = Math.max(1, Math.ceil(rawSpanMs / DAY_MS));
|
|
const effectiveSpanDays = Math.min(rawSpanDays, windowDays);
|
|
|
|
return { effectiveSpanDays, sufficient };
|
|
}
|