diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 5e38a7de8..4fad898e2 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -14734,10 +14734,17 @@ paths: tags: - Mental health summary: Record a completed screener - description: Stores one administration (item answers encrypted) and writes the derived total as a server-owned - PHQ9_SCORE / GAD7_SCORE / WHO5_SCORE / SCI_SCORE measurement (source COMPUTED — never client-writable). WHO-5 - and SCI totals run HIGHER = better; their follow-up thresholds point downwards (≤ 50 / ≤ 16). On a positive - PHQ-9 item-9 the response carries the locale-aware crisis-resource set. + description: >- + Stores one administration (item answers encrypted) and writes the derived total as a server-owned PHQ9_SCORE / + GAD7_SCORE / WHO5_SCORE / SCI_SCORE measurement (source COMPUTED — never client-writable). WHO-5 and SCI totals + run HIGHER = better; their follow-up thresholds point downwards (≤ 50 / ≤ 16). On a positive PHQ-9 item-9 the + response carries the locale-aware crisis-resource set. + + + The projection row is the one write this operation makes outside its declared section: the operation is fenced + on `mind`, and the measurement it derives lands in `measurements`. It is deliberate and unconditional — a + screener whose own total is missing from the record would be an inconsistency inside the section the grant does + open. The consent copy for the mood-and-mind section names it. parameters: - *a6 requestBody: diff --git a/messages/de.json b/messages/de.json index fca6667b9..d2eb66a83 100644 --- a/messages/de.json +++ b/messages/de.json @@ -10484,7 +10484,7 @@ }, "mind": { "label": "Stimmung und Psyche", - "detail": "Stimmungseinträge und der Verlauf der psychologischen Fragebögen." + "detail": "Stimmungseinträge und der Verlauf der psychologischen Fragebögen. Ein ausgefüllter Fragebogen legt seinen Summenwert zusätzlich als Messwert ab." }, "cycle": { "label": "Zyklus", diff --git a/messages/en.json b/messages/en.json index 5891d7ddf..e3d0d808e 100644 --- a/messages/en.json +++ b/messages/en.json @@ -10484,7 +10484,7 @@ }, "mind": { "label": "Mood and mind", - "detail": "Mood entries and the history of the mental-health questionnaires." + "detail": "Mood entries and the history of the mental-health questionnaires. A completed questionnaire also files its total score as a reading." }, "cycle": { "label": "Cycle", diff --git a/messages/es.json b/messages/es.json index 990f5c414..49f719a61 100644 --- a/messages/es.json +++ b/messages/es.json @@ -10484,7 +10484,7 @@ }, "mind": { "label": "Ánimo y bienestar mental", - "detail": "Las entradas de ánimo y el historial de los cuestionarios de salud mental." + "detail": "Las entradas de ánimo y el historial de los cuestionarios de salud mental. Un cuestionario completado también registra su puntuación total como una medición." }, "cycle": { "label": "Ciclo", diff --git a/messages/fr.json b/messages/fr.json index eee2b3cbd..182fbec05 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -10484,7 +10484,7 @@ }, "mind": { "label": "Humeur et santé mentale", - "detail": "Les entrées d'humeur et l'historique des questionnaires de santé mentale." + "detail": "Les entrées d'humeur et l'historique des questionnaires de santé mentale. Un questionnaire rempli enregistre aussi son score total comme une mesure." }, "cycle": { "label": "Cycle", diff --git a/messages/it.json b/messages/it.json index f550c93a0..7eba6de70 100644 --- a/messages/it.json +++ b/messages/it.json @@ -10484,7 +10484,7 @@ }, "mind": { "label": "Umore e benessere mentale", - "detail": "Le voci sull'umore e lo storico dei questionari sulla salute mentale." + "detail": "Le voci sull'umore e lo storico dei questionari sulla salute mentale. Un questionario compilato registra anche il suo punteggio totale come una misurazione." }, "cycle": { "label": "Ciclo", diff --git a/messages/ko.json b/messages/ko.json index 641082b67..aaa7194fc 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -10484,7 +10484,7 @@ }, "mind": { "label": "기분과 마음", - "detail": "기분 기록과 정신건강 설문 이력이에요." + "detail": "기분 기록과 정신건강 설문 이력이에요. 작성한 설문은 총점도 측정값으로 함께 저장돼요." }, "cycle": { "label": "생리 주기", diff --git a/messages/pl.json b/messages/pl.json index 1caaad0a3..10a28fb95 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -10484,7 +10484,7 @@ }, "mind": { "label": "Nastrój i zdrowie psychiczne", - "detail": "Wpisy o nastroju i historia kwestionariuszy zdrowia psychicznego." + "detail": "Wpisy o nastroju i historia kwestionariuszy zdrowia psychicznego. Wypełniony kwestionariusz zapisuje też swój wynik łączny jako pomiar." }, "cycle": { "label": "Cykl", diff --git a/src/app/api/mental-health/assessments/route.ts b/src/app/api/mental-health/assessments/route.ts index 4229ecfd3..74c81f181 100644 --- a/src/app/api/mental-health/assessments/route.ts +++ b/src/app/api/mental-health/assessments/route.ts @@ -159,6 +159,17 @@ async function postAssessment(request: NextRequest): Promise { // into the record: the ordinary way this instrument is used when the person // it is about cannot run their own record. The row records what the person // in the room reported; the audit row says who typed it. + // + // It writes ONE row outside its declared section, and the crossing is + // deliberate rather than an oversight: the `*_SCORE` Measurement below is a + // projection of this administration, minted `COMPUTED`, keyed on + // `assessment:`, and it exists only so the trend reads through the chart + // and rollup tier without ever touching the encrypted item content. Fencing + // it on `measurements` the way the booster mint is fenced would file a + // screener whose own total is missing from the record — an inconsistency + // inside the section the grant DOES open, to avoid a derived point in one it + // does not. The consent copy for this section says so, which is where a + // widening belongs. const { user, actor } = await requireRecordAuth("manage", "mind"); // Opt-in module (default OFF) — same gate as GET. diff --git a/src/lib/openapi/routes/mental-health.ts b/src/lib/openapi/routes/mental-health.ts index 6e9080da6..6687f11c6 100644 --- a/src/lib/openapi/routes/mental-health.ts +++ b/src/lib/openapi/routes/mental-health.ts @@ -135,7 +135,8 @@ export const mentalHealthPaths: NonNullable = { tags: ["Mental health"], summary: "Record a completed screener", description: - "Stores one administration (item answers encrypted) and writes the derived total as a server-owned PHQ9_SCORE / GAD7_SCORE / WHO5_SCORE / SCI_SCORE measurement (source COMPUTED — never client-writable). WHO-5 and SCI totals run HIGHER = better; their follow-up thresholds point downwards (≤ 50 / ≤ 16). On a positive PHQ-9 item-9 the response carries the locale-aware crisis-resource set.", + "Stores one administration (item answers encrypted) and writes the derived total as a server-owned PHQ9_SCORE / GAD7_SCORE / WHO5_SCORE / SCI_SCORE measurement (source COMPUTED — never client-writable). WHO-5 and SCI totals run HIGHER = better; their follow-up thresholds point downwards (≤ 50 / ≤ 16). On a positive PHQ-9 item-9 the response carries the locale-aware crisis-resource set.\n\n" + + "The projection row is the one write this operation makes outside its declared section: the operation is fenced on `mind`, and the measurement it derives lands in `measurements`. It is deliberate and unconditional — a screener whose own total is missing from the record would be an inconsistency inside the section the grant does open. The consent copy for the mood-and-mind section names it.", parameters: [idempotencyKeyParameter], requestBody: { content: { diff --git a/src/lib/sharing/scope.ts b/src/lib/sharing/scope.ts index 784297cf4..5ea941070 100644 --- a/src/lib/sharing/scope.ts +++ b/src/lib/sharing/scope.ts @@ -68,7 +68,16 @@ export const SHARE_DOMAINS = [ "profile", /** Illness episodes and the day logs under them. */ "illness", - /** Mood entries and mental-health screener history. */ + /** + * Mood entries and mental-health screener history. + * + * One write in this section lands outside it, and the consent copy names it: + * completing a screener also files the instrument's total as a `COMPUTED` + * measurement, so the trend reads through the chart and rollup tier without + * the encrypted item content going anywhere. It is a projection of the + * administration rather than a second reading, and withholding it would file + * a screener whose own total is missing from the record. + */ "mind", /** Cycle tracking. */ "cycle",