From 45bbfe4c9b4bf102ba1e7af15b39af10e4e33781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 14 May 2026 18:09:26 +0200 Subject: [PATCH] test(medications): drift-guard for glp1-knowledge.ts vs EMA + psp4.13099 citations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single-source-of-truth gate between the GLP-1 knowledge layer and the research file (`.planning/research/glp1-feature-inspiration.md` §1 + §2.6). Two layers: 1) Hard pins on every cited numeric — half-life, Ka, CL, F, bioavailability, titration ladder, post-opening window — against the EMA EPAR / psp4.13099 values. A silent mutation in the TS module surfaces here before reaching the Coach or inventory surfaces. 2) Soft check that the research file still carries the underlying citations (EMA EPAR URLs, Schneck/Urva DOI, the N7 retatrutide exclusion clause). A research-file revision that drops a citation is caught immediately. 30 tests, all green. Future Coach grounding (deferred to W14c) can trust the values it reads. --- .../__tests__/glp1-knowledge-drift.test.ts | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 src/lib/medications/__tests__/glp1-knowledge-drift.test.ts diff --git a/src/lib/medications/__tests__/glp1-knowledge-drift.test.ts b/src/lib/medications/__tests__/glp1-knowledge-drift.test.ts new file mode 100644 index 000000000..c0eced312 --- /dev/null +++ b/src/lib/medications/__tests__/glp1-knowledge-drift.test.ts @@ -0,0 +1,225 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { GLP1_DRUGS } from "@/lib/medications/glp1-knowledge"; + +/** + * Drift guard for the GLP-1 knowledge layer. + * + * The research file `.planning/research/glp1-feature-inspiration.md` + * §1 + §2.6 is the single source of truth — every numeric parameter + * in `glp1-knowledge.ts` is cited there against an EMA EPAR PDF or + * the peer-reviewed pharmacometric paper (Schneck & Urva 2024, + * DOI 10.1002/psp4.13099). + * + * Two layers of guarantee: + * 1) Hard-coded expected values that match the cited primary + * regulatory document — break if anyone edits the TS module + * without a matching research-file update. + * 2) Soft pin against the research file itself — the test reads + * the markdown and asserts the citation strings still appear, + * so a research-file revision that *removes* a citation + * surfaces immediately. + * + * Together: a refactor that mutates a half-life or shifts a Ka by + * a percent without a regulatory rationale fails the test before + * it reaches production. The clinical reading layer (Coach, + * inventory countdown, titration card) can trust the catalog. + */ + +const RESEARCH_PATH = join( + __dirname, + "../../../../.planning/research/glp1-feature-inspiration.md", +); + +const RESEARCH_TEXT = readFileSync(RESEARCH_PATH, "utf8"); + +/** Hard pins — these are the exact values the research file cites + * against EMA EPAR / psp4.13099. If you change the TS module, you + * must update both halves: this test and the research file. */ +const HARD_PINS = [ + { + id: "tirzepatide", + halfLifeDays: 5.0, + bioavailability: 0.8, + absorptionRateHourlyKa: 0.0373, + clearanceLPerHour70kg: 0.0329, + compartmentModel: "two-compartment", + citation: "Schneck", + citationDoi: "10.1002/psp4.13099", + titrationStepsMg: [2.5, 5, 7.5, 10, 12.5, 15], + inUseMaxDays: 30, + }, + { + id: "semaglutide", + halfLifeDays: 7.0, + bioavailability: 0.89, + compartmentModel: "two-compartment", + inUseMaxDays: 56, + }, + { + id: "liraglutide", + // 13 h per EMA EPAR §5.2 — stored as fractional days. + halfLifeHoursApprox: 13, + bioavailability: 0.55, + }, + { + id: "dulaglutide", + halfLifeDays: 5.0, + // 47–65 % range; record carries the lower bound. + bioavailability: 0.47, + }, + { + id: "exenatide", + // Byetta IR ~2.4 h per EMA EPAR §5.2. + halfLifeHoursApprox: 2.4, + bioavailability: 0.65, + }, +] as const; + +describe("glp1-knowledge drift guard", () => { + describe("TS module values vs cited primary sources", () => { + for (const pin of HARD_PINS) { + describe(pin.id, () => { + const record = GLP1_DRUGS[pin.id as keyof typeof GLP1_DRUGS]; + + if ("halfLifeDays" in pin && pin.halfLifeDays !== undefined) { + it(`pins half-life to ${pin.halfLifeDays} d`, () => { + expect(record.pharmacology.halfLifeDays).toBe(pin.halfLifeDays); + }); + } + + if ("halfLifeHoursApprox" in pin) { + it(`pins half-life within rounding of ${pin.halfLifeHoursApprox} h`, () => { + expect(record.pharmacology.halfLifeDays * 24).toBeCloseTo( + pin.halfLifeHoursApprox, + 1, + ); + }); + } + + if ("bioavailability" in pin) { + it(`pins bioavailability to ${pin.bioavailability}`, () => { + expect(record.pharmacology.bioavailability).toBe(pin.bioavailability); + }); + } + + if ("absorptionRateHourlyKa" in pin && pin.absorptionRateHourlyKa !== undefined) { + it(`pins Ka to ${pin.absorptionRateHourlyKa} h⁻¹ (psp4.13099 Table 3)`, () => { + expect(record.pharmacology.absorptionRateHourlyKa).toBe( + pin.absorptionRateHourlyKa, + ); + }); + } + + if ("clearanceLPerHour70kg" in pin && pin.clearanceLPerHour70kg !== undefined) { + it(`pins CL to ${pin.clearanceLPerHour70kg} L/h per 70 kg`, () => { + expect(record.pharmacology.clearanceLPerHour70kg).toBe( + pin.clearanceLPerHour70kg, + ); + }); + } + + if ("compartmentModel" in pin && pin.compartmentModel !== undefined) { + it(`pins compartment model to ${pin.compartmentModel}`, () => { + expect(record.pharmacology.compartmentModel).toBe(pin.compartmentModel); + }); + } + + if ("titrationStepsMg" in pin && pin.titrationStepsMg !== undefined) { + it("pins titration ladder to EMA EPAR §4.2 sequence", () => { + expect(record.titrationStepsMg).toEqual(pin.titrationStepsMg); + }); + } + + if ("inUseMaxDays" in pin && pin.inUseMaxDays !== undefined) { + it(`pins post-opening in-use window to ${pin.inUseMaxDays} d`, () => { + expect(record.storage.inUse.maxDays).toBe(pin.inUseMaxDays); + }); + } + + if ("citation" in pin && pin.citation !== undefined) { + it(`carries the ${pin.citation} citation`, () => { + expect(record.sourceJournal?.citation).toContain(pin.citation); + }); + } + + if ("citationDoi" in pin && pin.citationDoi !== undefined) { + it(`carries the DOI ${pin.citationDoi}`, () => { + expect(record.sourceJournal?.doi).toBe(pin.citationDoi); + }); + } + }); + } + }); + + describe("research-file citation presence", () => { + it("references the EMA Mounjaro EPAR for tirzepatide", () => { + expect(RESEARCH_TEXT).toMatch(/mounjaro-epar-product-information/); + }); + + it("references the Schneck/Urva 2024 paper for tirzepatide PK", () => { + expect(RESEARCH_TEXT).toMatch(/Schneck/); + expect(RESEARCH_TEXT).toMatch(/psp4\.13099/); + }); + + it("cites tirzepatide Ka 0.0373 h⁻¹ in §1.1 / §2.6", () => { + expect(RESEARCH_TEXT).toMatch(/0\.0373/); + }); + + it("cites tirzepatide CL 0.0329 L/h in §1.1 / §2.6", () => { + expect(RESEARCH_TEXT).toMatch(/0\.0329/); + }); + + it("cites the 30-day in-use KwikPen window in §1.1", () => { + expect(RESEARCH_TEXT).toMatch(/30 days/); + }); + + it("references the Ozempic EPAR for semaglutide", () => { + expect(RESEARCH_TEXT).toMatch(/ozempic-epar/); + }); + + it("references the Saxenda EPAR for liraglutide", () => { + expect(RESEARCH_TEXT).toMatch(/saxenda-epar/); + }); + + it("declares the N7 retatrutide exclusion explicitly", () => { + // Two phrasings appear in the research file; either is enough. + expect( + /retatrutide.*not approved|EMA approval|including it implies endorsement/i.test( + RESEARCH_TEXT, + ), + ).toBe(true); + }); + }); + + describe("research-file presence checks for module values", () => { + it("agrees with the research file on tirzepatide half-life ≈ 5 days", () => { + // Research file's §1.1 + §2.6 both say "≈ 5 days" / "5.4 days" + // for tirzepatide. TS module pins 5.0. The numbers reconcile + // within psp4.13099's IIV (5.4 d journal vs ≈ 5 d EMA EPAR). + expect(GLP1_DRUGS.tirzepatide.pharmacology.halfLifeDays).toBeGreaterThanOrEqual( + 5.0, + ); + expect(GLP1_DRUGS.tirzepatide.pharmacology.halfLifeDays).toBeLessThanOrEqual( + 5.5, + ); + expect(RESEARCH_TEXT).toMatch(/(≈ 5 d|5 days|5\.4 d|5\.4 days)/); + }); + + it("agrees with the research file on semaglutide half-life ≈ 1 week", () => { + expect(GLP1_DRUGS.semaglutide.pharmacology.halfLifeDays).toBe(7); + expect(RESEARCH_TEXT).toMatch(/(≈ 1 week|half-life ≈ 1 week|7 days)/); + }); + + it("agrees with the research file on liraglutide half-life ≈ 13 h", () => { + expect(GLP1_DRUGS.liraglutide.pharmacology.halfLifeDays * 24).toBeCloseTo( + 13, + 1, + ); + expect(RESEARCH_TEXT).toMatch(/(≈ 13 hours|13 h|half-life ≈ 13)/); + }); + }); +});