mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
Merge branch 'feat/w-g-screenings' into release/v1.27.10
This commit is contained in:
commit
6e59fe5ce0
40 files changed
+2180
-352
No files matched your search
@@ -37,10 +37,26 @@ describe("measurementReminderTypeEnum (V3 — completeness)", () => {
|
||||
// v1.27.6 — plannable mental-wellbeing screenings
|
||||
"PHQ9_SCORE",
|
||||
"GAD7_SCORE",
|
||||
// v1.27.9 — WHO-5 + SCI join on the identical contract
|
||||
"WHO5_SCORE",
|
||||
"SCI_SCORE",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts the WHO-5 / SCI screenings with the 4-week default cadence", () => {
|
||||
for (const measurementType of ["WHO5_SCORE", "SCI_SCORE"]) {
|
||||
const parsed = createMeasurementReminderSchema.safeParse({
|
||||
label: "Screening check-in",
|
||||
measurementType,
|
||||
intervalDays: 28,
|
||||
});
|
||||
expect(parsed.success, `expected ${measurementType} to be allowed`).toBe(
|
||||
true,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts a plannable screening reminder with the 4-week default cadence", () => {
|
||||
const parsed = createMeasurementReminderSchema.safeParse({
|
||||
label: "PHQ-9 Check-in",
|
||||
|
||||
@@ -473,9 +473,15 @@ describe("measurement validation", () => {
|
||||
it("rejects a COMPUTED-attributed screener score row on a client write", () => {
|
||||
// v1.27.6 — the mental-wellbeing screener projects each completed
|
||||
// administration onto a COMPUTED-source PHQ9_SCORE / GAD7_SCORE row
|
||||
// (the RECOVERY_SCORE precedent). COMPUTED is not client-attributable,
|
||||
// so a client can never mint a server-owned score trend point.
|
||||
for (const type of ["PHQ9_SCORE", "GAD7_SCORE"]) {
|
||||
// (the RECOVERY_SCORE precedent; v1.27.9 adds WHO5_SCORE / SCI_SCORE).
|
||||
// COMPUTED is not client-attributable, so a client can never mint a
|
||||
// server-owned score trend point.
|
||||
for (const type of [
|
||||
"PHQ9_SCORE",
|
||||
"GAD7_SCORE",
|
||||
"WHO5_SCORE",
|
||||
"SCI_SCORE",
|
||||
]) {
|
||||
const parsed = createMeasurementSchema.safeParse({
|
||||
type,
|
||||
value: 5,
|
||||
|
||||
@@ -64,11 +64,16 @@ export const measurementReminderTypeEnum = z
|
||||
// the dispatcher (see `moduleForMeasurementType`).
|
||||
"PHQ9_SCORE",
|
||||
"GAD7_SCORE",
|
||||
// v1.27.9 — WHO-5 + SCI join the plannable screenings on the identical
|
||||
// contract (auto-resolve on the server-owned COMPUTED score row, module-
|
||||
// gated in the dispatcher).
|
||||
"WHO5_SCORE",
|
||||
"SCI_SCORE",
|
||||
])
|
||||
.meta({
|
||||
id: "MeasurementReminderType",
|
||||
description:
|
||||
"Auto-resolve target metric (vitals + body-composition family + the PHQ-9 / GAD-7 screenings). BP resolves on BLOOD_PRESSURE_SYS (the SYS row is the 'a BP was measured' sentinel); a screening reminder resolves on the server-written PHQ9_SCORE / GAD7_SCORE row when a check-in completes. Omit for a free-text Vorsorge that resolves only on a manual satisfy or a matching lab result.",
|
||||
"Auto-resolve target metric (vitals + body-composition family + the PHQ-9 / GAD-7 / WHO-5 / SCI screenings). BP resolves on BLOOD_PRESSURE_SYS (the SYS row is the 'a BP was measured' sentinel); a screening reminder resolves on the server-written *_SCORE row when a check-in completes. Omit for a free-text Vorsorge that resolves only on a manual satisfy or a matching lab result.",
|
||||
});
|
||||
|
||||
export type MeasurementReminderType = z.infer<
|
||||
@@ -85,6 +90,8 @@ export type MeasurementReminderType = z.infer<
|
||||
export const SCREENING_REMINDER_TYPES: ReadonlySet<string> = new Set([
|
||||
"PHQ9_SCORE",
|
||||
"GAD7_SCORE",
|
||||
"WHO5_SCORE",
|
||||
"SCI_SCORE",
|
||||
]);
|
||||
|
||||
/** Whether a reminder's `measurementType` targets a screening check-in. */
|
||||
|
||||
@@ -135,6 +135,11 @@ export const measurementTypeEnum = z.enum([
|
||||
"PAIN_NRS",
|
||||
"WAIST_CIRCUMFERENCE",
|
||||
"WAIST_TO_HEIGHT",
|
||||
// ── v1.27.9 — screening scores (same assessment contract as PHQ-9/GAD-7,
|
||||
// inverted direction: HIGHER is better). WHO-5 stores the reported 0–100
|
||||
// percentage (raw sum × 4); SCI stores the 0–32 total.
|
||||
"WHO5_SCORE",
|
||||
"SCI_SCORE",
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -379,6 +384,9 @@ const unitMap: Record<string, string> = {
|
||||
// ── v1.25 — clinical-signals wave ──
|
||||
PHQ9_SCORE: "score",
|
||||
GAD7_SCORE: "score",
|
||||
// ── v1.27.9 — screening scores ──
|
||||
WHO5_SCORE: "score",
|
||||
SCI_SCORE: "score",
|
||||
GRIP_STRENGTH: "kg",
|
||||
PAIN_NRS: "score",
|
||||
WAIST_CIRCUMFERENCE: "cm",
|
||||
@@ -581,6 +589,10 @@ export const VALUE_RANGES: Record<string, { min: number; max: number }> = {
|
||||
// PHQ-9 total 0–27, GAD-7 total 0–21 — server-derived from the screener.
|
||||
PHQ9_SCORE: { min: 0, max: 27 },
|
||||
GAD7_SCORE: { min: 0, max: 21 },
|
||||
// ── v1.27.9 — screening scores. WHO-5 reported percentage 0–100 (raw × 4),
|
||||
// SCI total 0–32 — both server-derived, higher = better.
|
||||
WHO5_SCORE: { min: 0, max: 100 },
|
||||
SCI_SCORE: { min: 0, max: 32 },
|
||||
// Grip strength kg — a child can read in the single digits, an elite athlete
|
||||
// tops ~90 kg; 120 is a generous ceiling that still rejects sensor noise.
|
||||
GRIP_STRENGTH: { min: 0, max: 120 },
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/**
|
||||
* v1.25 — request/response validation for the opt-in mental-health screeners
|
||||
* (PHQ-9 / GAD-7). The item answers are 0–3 integers; the array length must
|
||||
* match the instrument (PHQ-9 = 9, GAD-7 = 7). `userId` is NEVER a body field —
|
||||
* it is narrowed from the session in the handler (CLAUDE.md convention).
|
||||
* (PHQ-9 / GAD-7; v1.27.9 adds WHO-5 + SCI). The item answers are small
|
||||
* integers whose ceiling is instrument-specific (PHQ/GAD 0–3, SCI 0–4,
|
||||
* WHO-5 0–5); the array length must match the instrument. `userId` is NEVER
|
||||
* a body field — it is narrowed from the session in the handler (CLAUDE.md
|
||||
* convention).
|
||||
*/
|
||||
import { z } from "zod/v4";
|
||||
import {
|
||||
@@ -10,7 +12,7 @@ import {
|
||||
type InstrumentId,
|
||||
} from "@/lib/mental-health/instruments";
|
||||
|
||||
export const assessmentInstrumentEnum = z.enum(["PHQ9", "GAD7"]);
|
||||
export const assessmentInstrumentEnum = z.enum(["PHQ9", "GAD7", "WHO5", "SCI"]);
|
||||
|
||||
/**
|
||||
* Client provenance for an administration. Closed enum so the column can never
|
||||
@@ -21,12 +23,15 @@ export const assessmentInstrumentEnum = z.enum(["PHQ9", "GAD7"]);
|
||||
*/
|
||||
export const assessmentSourceEnum = z.enum(["WEB", "IOS"]);
|
||||
|
||||
const itemAnswer = z.number().int().min(0).max(3);
|
||||
// Structural ceiling — the WHO-5's 0–5 scale is the widest. The exact
|
||||
// per-instrument ceiling (PHQ/GAD 3, SCI 4) is enforced in the superRefine
|
||||
// below, where the instrument is known.
|
||||
const itemAnswer = z.number().int().min(0).max(5);
|
||||
|
||||
export const createAssessmentSchema = z
|
||||
.object({
|
||||
instrument: assessmentInstrumentEnum,
|
||||
/** Per-item answers, each 0–3, ordered as the instrument presents them. */
|
||||
/** Per-item answers, ordered as the instrument presents them. */
|
||||
items: z.array(itemAnswer).min(1).max(12),
|
||||
/** Optional functional-impairment follow-up (not scored into the total). */
|
||||
functionalDifficulty: z.number().int().min(0).max(3).optional(),
|
||||
@@ -55,6 +60,15 @@ export const createAssessmentSchema = z
|
||||
message: `${val.instrument} expects exactly ${def.itemCount} item answers`,
|
||||
});
|
||||
}
|
||||
val.items.forEach((answer, index) => {
|
||||
if (answer > def.itemMax) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
path: ["items", index],
|
||||
message: `${val.instrument} answers range 0–${def.itemMax}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export const listAssessmentsSchema = z.object({
|
||||
|
||||
Reference in new issue
Block a user