mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
feat(mental-health): opt-in PHQ-9 / GAD-7 screeners with item-9 crisis signposting
Add the validated mental-health self-assessment surface, beside mood tracking (never replacing it) and off by default: - MentalHealthAssessment stores per-item answers in a single AES-256-GCM Bytes blob (registered for key rotation); only the server-authoritative total, severity band, and item-9 flag are denormalised. The total projects to a PHQ9_SCORE / GAD7_SCORE measurement row so the trend rides existing infra without ever exposing item content. - Item-9 safety: any non-zero PHQ-9 self-harm answer surfaces a calm, locale-aware crisis-resource set (DE: TelefonSeelsorge 0800 111 0 111 / 112; international fallback) and stamps crisisShownAt. No clinical interpretation, no AI ingestion of item content, no third-party alerting. - API + OpenAPI: POST/GET /api/mental-health/assessments; the response excludes raw item answers by construction. Required Pfizer/PHQ attribution lives in the instrument module, rendered verbatim. The screener scores are kept off the Coach snapshot and MCP surfaces, and out of exports by default — item content never reaches the AI pipeline.
This commit is contained in:
1 parent
e4875db93e
commit
7575275d7e
11 files changed
+655
No files matched your search
@@ -44,6 +44,7 @@ import { labsPaths } from "./labs";
|
||||
import { ocrPaths } from "./ocr";
|
||||
import { measurementPaths } from "./measurements";
|
||||
import { measurementReminderPaths } from "./measurement-reminders";
|
||||
import { mentalHealthPaths } from "./mental-health";
|
||||
import { medicationPaths, medicationResource } from "./medications";
|
||||
import { metaPaths } from "./meta";
|
||||
import { moodPaths } from "./mood";
|
||||
@@ -83,6 +84,7 @@ export const openApiPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
...ocrPaths,
|
||||
...biomarkerPaths,
|
||||
...illnessPaths,
|
||||
...mentalHealthPaths,
|
||||
...onboardingPaths,
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* OpenAPI route table for the opt-in mental-health screeners
|
||||
* (`/api/mental-health/assessments`). PHQ-9 / GAD-7.
|
||||
*
|
||||
* Part of the OpenAPI route table; aggregated in `./index.ts`. The request body
|
||||
* reuses the runtime Zod schema from `@/lib/validations/mental-health` so the
|
||||
* wire contract stays single-source. The response deliberately excludes the
|
||||
* raw item answers — only the server-authoritative total / band / safety flag
|
||||
* are exposed. The iOS client mirrors this DTO and MUST surface its own
|
||||
* crisis-resource card on a positive item-9 flag (never rely on server text
|
||||
* alone).
|
||||
*/
|
||||
import type { ZodOpenApiObject } from "zod-openapi";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import {
|
||||
createAssessmentSchema,
|
||||
listAssessmentsSchema,
|
||||
} from "@/lib/validations/mental-health";
|
||||
|
||||
import { dataEnvelope, stdResponses } from "./shared";
|
||||
|
||||
createAssessmentSchema.meta({
|
||||
id: "CreateMentalHealthAssessmentRequest",
|
||||
description:
|
||||
"Record one completed PHQ-9 (9 items) / GAD-7 (7 items) administration. Each item answer is 0–3; the array length must match the instrument. The item answers are encrypted at rest and never returned. The server computes the total + severity band + item-9 safety flag. Opt-in, beside mood tracking — this is a screening surface, not a diagnosis.",
|
||||
});
|
||||
|
||||
listAssessmentsSchema.meta({
|
||||
id: "ListMentalHealthAssessmentsQuery",
|
||||
description:
|
||||
"Filter the caller's screener history by instrument; paginate with limit/offset.",
|
||||
});
|
||||
|
||||
const assessmentRow = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
instrument: z.enum(["PHQ9", "GAD7"]),
|
||||
locale: z.string(),
|
||||
version: z.string(),
|
||||
totalScore: z.number(),
|
||||
severityBand: z.string(),
|
||||
item9Flagged: z.boolean(),
|
||||
crisisShownAt: z.string().nullable(),
|
||||
takenAt: z.string(),
|
||||
createdAt: z.string(),
|
||||
})
|
||||
.meta({
|
||||
id: "MentalHealthAssessment",
|
||||
description:
|
||||
"One completed screener administration. `severityBand` is a descriptive label for the screen (never a diagnosis). `item9Flagged` is true when the PHQ-9 self-harm item was answered > 0 — the client MUST show crisis resources when it is. Raw per-item answers are intentionally absent.",
|
||||
});
|
||||
|
||||
const crisisResource = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
contacts: z.array(z.string()),
|
||||
})
|
||||
.meta({ id: "CrisisResource" });
|
||||
|
||||
const crisisSet = z
|
||||
.object({
|
||||
emergencyNumber: z.string(),
|
||||
resources: z.array(crisisResource),
|
||||
})
|
||||
.meta({
|
||||
id: "CrisisResourceSet",
|
||||
description:
|
||||
"Locale-aware crisis-resource signposting, present only when item-9 is flagged. Names resolve via i18n on the client; contacts are literal.",
|
||||
});
|
||||
|
||||
const createResponse = z
|
||||
.object({
|
||||
assessment: assessmentRow,
|
||||
actionThreshold: z.number(),
|
||||
crisis: crisisSet.nullable(),
|
||||
})
|
||||
.meta({ id: "CreateMentalHealthAssessmentResponse" });
|
||||
|
||||
const listResponse = z
|
||||
.object({ assessments: z.array(assessmentRow) })
|
||||
.meta({ id: "ListMentalHealthAssessmentsResponse" });
|
||||
|
||||
export const mentalHealthPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
"/api/mental-health/assessments": {
|
||||
get: {
|
||||
tags: ["Mental health"],
|
||||
summary: "List the caller's screener history",
|
||||
description:
|
||||
"Returns PHQ-9 / GAD-7 administrations (newest first). Totals + bands + flags only; raw item answers are never returned.",
|
||||
requestParams: { query: listAssessmentsSchema },
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Screener history.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
listResponse,
|
||||
"ListMentalHealthAssessmentsEnvelope",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
post: {
|
||||
tags: ["Mental health"],
|
||||
summary: "Record a completed PHQ-9 / GAD-7 screener",
|
||||
description:
|
||||
"Stores one administration (item answers encrypted) and writes the derived total as a PHQ9_SCORE / GAD7_SCORE measurement. On a positive PHQ-9 item-9 the response carries the locale-aware crisis-resource set.",
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": { schema: createAssessmentSchema },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
"201": {
|
||||
description: "Assessment recorded.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
createResponse,
|
||||
"CreateMentalHealthAssessmentEnvelope",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user