fix(report): release the medication effect only with the medication list

The effect block names every medication it describes and its
confounders, so a report or share link that withheld the medication
list still carried the names. The aggregator now loads the block only
when both leaves are admitted, the clinician view checks the list leaf
as well, and the leaf label says it needs the list.
This commit is contained in:
Marc-André Bombeck committed 2026-10-21 23:31:58 +02:00
1 parent 733b00ee1a
commit cfe5e2d3c6
13 files changed
+95 -12

No files matched your search

+1 -1
View File
@@ -12427,7 +12427,7 @@
"leafSurgicalHistory": "Operationen und Eingriffe",
"leafImmunizations": "Impfungen",
"leafEmergency": "Notfalldaten (Blutgruppe, Kontakte, Verfügungen)",
"leafMedicationEffect": "Veränderung rund um den Start jedes Medikaments",
"leafMedicationEffect": "Veränderung rund um den Start jedes Medikaments (braucht die Medikamentenliste)",
"leafWorkouts": "Trainings (Übersicht je Sportart)",
"leafHrvSdnn": "HRV (SDNN)"
},
+1 -1
View File
@@ -12427,7 +12427,7 @@
"leafSurgicalHistory": "Surgical history",
"leafImmunizations": "Immunizations",
"leafEmergency": "Emergency information (blood type, contacts, directives)",
"leafMedicationEffect": "Change around each medication start",
"leafMedicationEffect": "Change around each medication start (needs the medication list)",
"leafWorkouts": "Workouts (summary per sport)",
"leafHrvSdnn": "HRV (SDNN)"
},
+1 -1
View File
@@ -12427,7 +12427,7 @@
"leafSurgicalHistory": "Antecedentes quirúrgicos",
"leafImmunizations": "Vacunas",
"leafEmergency": "Información de emergencia (grupo sanguíneo, contactos, voluntades)",
"leafMedicationEffect": "Cambio en torno al inicio de cada medicamento",
"leafMedicationEffect": "Cambio en torno al inicio de cada medicamento (requiere la lista de medicamentos)",
"leafWorkouts": "Entrenamientos (resumen por deporte)",
"leafHrvSdnn": "VFC (SDNN)"
},
+1 -1
View File
@@ -12427,7 +12427,7 @@
"leafSurgicalHistory": "Antécédents chirurgicaux",
"leafImmunizations": "Vaccinations",
"leafEmergency": "Informations d'urgence (groupe sanguin, contacts, directives)",
"leafMedicationEffect": "Changement autour du démarrage de chaque médicament",
"leafMedicationEffect": "Changement autour du démarrage de chaque médicament (nécessite la liste des médicaments)",
"leafWorkouts": "Entraînements (résumé par sport)",
"leafHrvSdnn": "VFC (SDNN)"
},
+1 -1
View File
@@ -12427,7 +12427,7 @@
"leafSurgicalHistory": "Anamnesi chirurgica",
"leafImmunizations": "Vaccinazioni",
"leafEmergency": "Informazioni di emergenza (gruppo sanguigno, contatti, disposizioni)",
"leafMedicationEffect": "Variazione intorno all'inizio di ogni farmaco",
"leafMedicationEffect": "Variazione intorno all'inizio di ogni farmaco (richiede l'elenco dei farmaci)",
"leafWorkouts": "Allenamenti (riepilogo per sport)",
"leafHrvSdnn": "HRV (SDNN)"
},
+1 -1
View File
@@ -12427,7 +12427,7 @@
"leafSurgicalHistory": "수술 이력",
"leafImmunizations": "예방접종",
"leafEmergency": "응급 정보 (혈액형, 연락처, 사전지시)",
"leafMedicationEffect": "각 약 시작 전후의 변화",
"leafMedicationEffect": "각 약 시작 전후의 변화 (약 목록 필요)",
"leafWorkouts": "운동(종목별 요약)",
"leafHrvSdnn": "심박변이도 (SDNN)"
},
+1 -1
View File
@@ -12427,7 +12427,7 @@
"leafSurgicalHistory": "Operatiegeschiedenis",
"leafImmunizations": "Vaccinaties",
"leafEmergency": "Noodinformatie (bloedgroep, contactpersonen, wilsverklaringen)",
"leafMedicationEffect": "Verandering rond de start van elk medicijn",
"leafMedicationEffect": "Verandering rond de start van elk medicijn (vereist de medicatielijst)",
"leafWorkouts": "Trainingen (overzicht per sport)",
"leafHrvSdnn": "HRV (SDNN)"
},
+1 -1
View File
@@ -12427,7 +12427,7 @@
"leafSurgicalHistory": "Historia operacji",
"leafImmunizations": "Szczepienia",
"leafEmergency": "Informacje na wypadek nagły (grupa krwi, kontakty, oświadczenia)",
"leafMedicationEffect": "Zmiana wokół rozpoczęcia każdego leku",
"leafMedicationEffect": "Zmiana wokół rozpoczęcia każdego leku (wymaga listy leków)",
"leafWorkouts": "Treningi (podsumowanie według dyscypliny)",
"leafHrvSdnn": "HRV (SDNN)"
},
@@ -163,6 +163,53 @@ describe("<ClinicianView>", () => {
expect(html).not.toContain("Current smoker");
});
it("shows the medication effect only beside the medication list", () => {
const report = makeReport({
medicationEffects: [
{
medicationName: "Named effect drug",
startsOn: "2026-01-05",
targets: [
{
type: "BLOOD_PRESSURE_SYS",
effect: {
present: false,
reason: "insufficient_after",
level: 0.95,
window: null,
days: {
before: 40,
after: 3,
excludedLowAdherence: 0,
excludedNotTaking: 0,
},
adherence: {
status: "tracked",
floorPct: 80,
lookbackDays: 14,
},
levelChange: null,
slopeChangePerWeek: null,
preSlopePerWeek: null,
fit: null,
confounders: [],
},
},
],
},
],
});
const leaves = ["MEDICATION_EFFECT", "BLOOD_PRESSURE_SYS"] as const;
expect(
render(report, {
selection: selectionFromLeaves([...leaves, "MEDICATION_LIST"]),
}),
).toContain("Named effect drug");
expect(
render(report, { selection: selectionFromLeaves([...leaves]) }),
).not.toContain("Named effect drug");
});
it("names both windows when a lab's report and the saved band disagree", () => {
// Two windows that disagree about the same number, and a card showing one
// of them is a partial answer. The PDF settles this with a footnote under
@@ -113,7 +113,11 @@ export function MedicationEffectSection({
/** A stated calendar day (YYYY-MM-DD) in the owner's date order. */
fmtDay: (day: string) => string;
}) {
const effects = report.medicationEffects ?? [];
// The block names medications: without the list leaf it shows nothing,
// whatever the payload carries.
const effects = scope.admits("MEDICATION_LIST")
? (report.medicationEffects ?? [])
: [];
const glucoseUnit = resolveGlucoseUnit(report.glucoseUnit ?? null);
return (
<LeafSection
+3 -2
View File
@@ -509,8 +509,9 @@ export interface DoctorReportData {
* metric(s), around the medication's start (v1.44). Computed without AI from
* the same day values and dose ledger the efficacy tab reads. A target rides
* only when its own measurement leaf is admitted too, so the block never
* carries a figure derived from a series the owner withheld. Null when the
* MEDICATION_EFFECT leaf was not selected or nothing qualified.
* carries a figure derived from a series the owner withheld. It names the
* medications, so it rides only when MEDICATION_LIST is admitted as well.
* Null when either leaf was not selected or nothing qualified.
*/
medicationEffects?: DoctorReportMedicationEffect[] | null;
}
@@ -409,6 +409,33 @@ describe("the derived figures ride their own leaf", () => {
expect(off.medications.length).toBeGreaterThan(0);
});
it("withholds the medication effect without the medication list", async () => {
// The effect block names every medication it describes, so withholding
// the list must withhold the names here too.
const leaves: ReportLeafId[] = [
"MEDICATION_EFFECT",
...MEASUREMENT_LEAF_IDS,
];
const withList = await collectDoctorReportData(
"u1",
RANGE,
selectionFromLeaves([...leaves, "MEDICATION_LIST"]),
{ moduleMap: moduleMap() },
);
expect(withList.medicationEffects?.length ?? 0).toBeGreaterThan(0);
const withoutList = await collectDoctorReportData(
"u1",
RANGE,
selectionFromLeaves(leaves),
{ moduleMap: moduleMap() },
);
expect(withoutList.medicationEffects ?? null).toBeNull();
expect(JSON.stringify(withoutList)).not.toContain(
withList.medicationEffects![0].medicationName,
);
});
it("withholds the glucose panel while the raw series stays", async () => {
const withPanel = await collectDoctorReportData(
"u1",
+5 -1
View File
@@ -674,7 +674,11 @@ export async function collectDoctorReportData(
: Promise.resolve(null),
// The effect around each listed medication's start. Its own leaf AND the
// target's measurement leaf must both admit it; the loader asks the gate.
gate.admits("MEDICATION_EFFECT")
// The block names every medication it describes (and the confounders),
// so it also needs the medication list: an owner who withheld the list
// never releases the names through the effect block. Same rule as the
// clinician view's compliance rows, which are named from the list.
gate.admits("MEDICATION_EFFECT") && gate.admits("MEDICATION_LIST")
? loadReportMedicationEffects({
userId,
medicationIds: medications.map((m) => m.id),