Merge branch 'feat/v144-report' into release/v1.44.0

This commit is contained in:
Marc-André Bombeck committed 2026-10-21 22:18:50 +02:00
commit 4e71cf4dc2
58 files changed
+5703 -91

No files matched your search

@@ -74,9 +74,9 @@ describe("report scope gating — structural guard", () => {
it("reads a plausible catalogue", () => {
// Sanity floor: a silently degraded catalogue would pass everything below
// vacuously.
expect(ALL_LEAF_IDS.length).toBe(104);
expect(ALL_LEAF_IDS.length).toBe(106);
expect(Object.keys(MEASUREMENT_LEAF_GROUP)).toHaveLength(86);
expect(Object.keys(STRUCTURED_LEAF_GROUP)).toHaveLength(18);
expect(Object.keys(STRUCTURED_LEAF_GROUP)).toHaveLength(20);
});
it("places every leaf in exactly one group", () => {
@@ -98,6 +98,8 @@ const LEGIT_IDENTICAL = new Set<string>([
"insights.derived.composite.READINESS.component.hrv",
"labs.catalog.hs-crp",
"measurements.typeHrvRmssd",
// The report's SDNN row, named the way the RMSSD row above is.
"reportSelection.leafHrvSdnn",
"illness.vital.HRV_RMSSD",
"settings.sections.sources.metrics.hrvRmssd",
"measurements.typeVo2Max",
@@ -13,7 +13,7 @@
* is `STRUCTURED_LEAF_GROUP`, the other is HTML rendered by the real
* `<ClinicianView>` over a real fixture. It proves four things per leaf.
*
* 1. `DISPOSITIONS` is `Record<StructuredLeafId, …>`, so a nineteenth
* 1. `DISPOSITIONS` is `Record<StructuredLeafId, …>`, so a new
* structured leaf cannot be added to the catalogue without deciding here
* whether it renders — and saying so — or `pnpm typecheck` fails. That is
* the lock, and it is the compiler's, not this suite's.
@@ -291,6 +291,69 @@ const DISPOSITIONS: Record<StructuredLeafId, Disposition> = {
},
marker: "Semaglutide",
},
MEDICATION_EFFECT: {
kind: "rendered",
data: {
medicationEffects: [
{
medicationName: "Effect-marker drug",
startsOn: "2026-01-02",
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: [],
},
},
],
},
],
},
marker: "Effect-marker drug",
},
WORKOUTS: {
kind: "rendered",
data: {
workouts: {
sessions: 3,
totalDurationSec: 7200,
sessionsPerWeek: 0.7,
minutesPerWeek: 28,
sportCount: 1,
bySport: [
{
sportType: "rowing",
sessions: 3,
durationSec: 7200,
distanceM: 15000,
distanceSessions: 3,
energyKcal: null,
avgHeartRate: null,
},
],
},
},
marker: "15.0 km",
},
ALLERGIES: {
kind: "rendered",
data: {
@@ -444,7 +507,7 @@ describe("clinician view — every catalogue leaf has a renderer or a reason", (
it("reads a plausible catalogue", () => {
// Sanity floor: a degraded import would satisfy every loop below
// vacuously, which is the failure mode a pair guard dies of.
expect(STRUCTURED_LEAF_IDS).toHaveLength(18);
expect(STRUCTURED_LEAF_IDS).toHaveLength(20);
expect(Object.keys(DISPOSITIONS).sort()).toEqual(
[...STRUCTURED_LEAF_IDS].sort(),
);
@@ -30,6 +30,8 @@ const ALLOWED: Record<string, string> = {
"The correlation engine's centred sums and the Welch t-test variances.",
"lib/insights/mood-tag-influence.ts": "Welch t-test variances.",
"lib/insights/mood-pattern-cards.ts": "A pooled variance for a t statistic.",
"lib/stats/segmented-regression.ts":
"The total sum of squares behind R² and the HAC standard errors of a segmented fit; neither is reported as a spread.",
"lib/insights/derived/trajectory.ts":
"The residual standard error of a regression fit (÷ n − 2).",
"lib/insights/derived/sleep-score.ts":
@@ -361,6 +361,16 @@ const UNIT_LITERAL_ALLOWLIST: Record<
reason:
"SI by policy: the clinician artefacts keep mass in kg (glucose follows the owner)",
},
"lib/doctor-report-pdf/activity-effect-sections.ts": {
lines: 2,
reason:
"SI by policy: the report's workout distance is in km, like every clinician artefact",
},
"components/clinician/activity-effect-sections.tsx": {
lines: 1,
reason:
"SI by policy: the clinician page's workout distance is in km, like the PDF",
},
"components/insights/workout-detail/splits.tsx": {
lines: 1,
reason: "kilometre splits are cut server-side; their pace is per km",
@@ -43,6 +43,9 @@ const IDENTITY_OWNER: Record<string, string> = {
"lib/jobs/workout-insight-generate.ts",
"lib/workouts/list-read.ts": "lib/workouts/list-read.ts",
"lib/workouts/sport-context.ts": "lib/workouts/sport-context.ts",
// The doctor report's per-sport block (v1.44): sums sessions, so a session
// two sources reported must count once, as it does on the list.
"lib/doctor-report/workouts.ts": "lib/doctor-report/workouts.ts",
// The only indirection in the set. The Coach block is handed rows it does not
// read itself; the snapshot builder fetches them in parallel with every other
// block's data. Recording the owner here rather than assuming the call site