mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
fix(labs): stop projecting lab trends from stale or short series
A marker whose newest reading is more than a year old is now reported as of that reading (projection "stale"), with no exit date and no already-outside verdict. The projection horizon is capped at the span the readings cover, so a line is never carried further ahead than the data reaches back. The detail read keeps the newest 2000 readings rather than the oldest.
This commit is contained in:
1 parent
5f563623d7
commit
e482f76508
16 files changed
+165
-30
No files matched your search
+10
-3
@@ -74758,7 +74758,11 @@ components:
|
||||
description: The line's value on `asOf`.
|
||||
asOf:
|
||||
type: string
|
||||
description: YYYY-MM-DD the line was evaluated on.
|
||||
description: "YYYY-MM-DD the line was evaluated on: today, or `lastAt` when `projection` is `stale`."
|
||||
horizonYears:
|
||||
type: number
|
||||
description: "How far ahead a bound may be met and still count as `exits`: three years, or the readings' span when
|
||||
shorter."
|
||||
projection:
|
||||
type: string
|
||||
enum:
|
||||
@@ -74768,6 +74772,7 @@ components:
|
||||
- already_outside
|
||||
- no_clear_trend
|
||||
- no_band
|
||||
- stale
|
||||
exitDate:
|
||||
description: "Only for `exits`: the day the straight line meets `exitBound`. A projection, never a forecast."
|
||||
type:
|
||||
@@ -74791,6 +74796,7 @@ components:
|
||||
- pValue
|
||||
- fittedNow
|
||||
- asOf
|
||||
- horizonYears
|
||||
- projection
|
||||
- exitDate
|
||||
- exitBound
|
||||
@@ -74822,8 +74828,9 @@ components:
|
||||
description: "v1.44 — the least-squares line through every numeric reading of the marker (all stored in the marker's
|
||||
unit). Needs at least three readings spanning at least 365 days, else `insufficient`. `projection` places the
|
||||
line against the marker's band: `exits` (with `exitDate`) only when the slope's 95 % interval excludes zero, the
|
||||
line is inside the band today and reaches a bound within three years. Server-computed; clients render it as
|
||||
given."
|
||||
line is inside the band today and reaches a bound within `horizonYears` (three years, or the readings' span when
|
||||
shorter). `stale` when the newest reading is more than 365 days old: the line is then reported as of `lastAt`,
|
||||
with no projection. Server-computed; clients render it as given."
|
||||
UpdateBiomarkerResponse:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
+3
-2
@@ -10207,10 +10207,11 @@
|
||||
"high": "Liefe diese Gerade so weiter, erreichte sie um den {date} das obere Ende deines Referenzbereichs. Das ist eine Fortschreibung, keine Vorhersage.",
|
||||
"low": "Liefe diese Gerade so weiter, erreichte sie um den {date} das untere Ende deines Referenzbereichs. Das ist eine Fortschreibung, keine Vorhersage."
|
||||
},
|
||||
"beyond_horizon": "In diesem Tempo bleibt die Gerade noch mindestens drei Jahre in deinem Referenzbereich.",
|
||||
"beyond_horizon": "In diesem Tempo bleibt die Gerade länger als {years} Jahre in deinem Referenzbereich, so weit reichen deine Werte zurück.",
|
||||
"no_exit": "Die Gerade läuft von den Grenzen deines Referenzbereichs weg.",
|
||||
"already_outside": "Die Gerade liegt bereits außerhalb deines Referenzbereichs.",
|
||||
"no_clear_trend": "Über diese Werte ist die Veränderung nicht klar von keiner Veränderung zu unterscheiden, deshalb wird nichts fortgeschrieben."
|
||||
"no_clear_trend": "Über diese Werte ist die Veränderung nicht klar von keiner Veränderung zu unterscheiden, deshalb wird nichts fortgeschrieben.",
|
||||
"stale": "Der neueste Wert ist vom {date}, also älter als ein Jahr. Die Gerade gilt für diesen Stand, ohne Projektion."
|
||||
},
|
||||
"interval": "95-%-Intervall der jährlichen Veränderung: {lo} bis {hi} {unit}."
|
||||
}
|
||||
|
||||
+3
-2
@@ -10207,10 +10207,11 @@
|
||||
"high": "If this straight line continued, it would reach the upper end of your reference range around {date}. That is a projection, not a forecast.",
|
||||
"low": "If this straight line continued, it would reach the lower end of your reference range around {date}. That is a projection, not a forecast."
|
||||
},
|
||||
"beyond_horizon": "At this pace the line stays inside your reference range for at least three more years.",
|
||||
"beyond_horizon": "At this pace the line stays inside your reference range for more than {years} years, the span your readings cover.",
|
||||
"no_exit": "The line heads away from the edges of your reference range.",
|
||||
"already_outside": "The line already sits outside your reference range.",
|
||||
"no_clear_trend": "Across these readings the change is not clearly different from no change, so no projection is drawn."
|
||||
"no_clear_trend": "Across these readings the change is not clearly different from no change, so no projection is drawn.",
|
||||
"stale": "The newest reading is from {date}, more than a year ago. The line is shown as of then, without a projection."
|
||||
},
|
||||
"interval": "95 % interval of the yearly change: {lo} to {hi} {unit}."
|
||||
}
|
||||
|
||||
+3
-2
@@ -10207,10 +10207,11 @@
|
||||
"high": "Si esta línea recta continuara, llegaría al límite superior de tu rango de referencia hacia el {date}. Es una proyección, no un pronóstico.",
|
||||
"low": "Si esta línea recta continuara, llegaría al límite inferior de tu rango de referencia hacia el {date}. Es una proyección, no un pronóstico."
|
||||
},
|
||||
"beyond_horizon": "A este ritmo, la línea se mantiene dentro de tu rango de referencia al menos tres años más.",
|
||||
"beyond_horizon": "A este ritmo, la línea se mantiene dentro de tu rango de referencia más de {years} años, el periodo que cubren tus valores.",
|
||||
"no_exit": "La línea se aleja de los límites de tu rango de referencia.",
|
||||
"already_outside": "La línea ya está fuera de tu rango de referencia.",
|
||||
"no_clear_trend": "En estos valores, el cambio no se distingue con claridad de la ausencia de cambio, así que no se proyecta nada."
|
||||
"no_clear_trend": "En estos valores, el cambio no se distingue con claridad de la ausencia de cambio, así que no se proyecta nada.",
|
||||
"stale": "El valor más reciente es del {date}, hace más de un año. La línea se muestra a esa fecha, sin proyección."
|
||||
},
|
||||
"interval": "Intervalo del 95 % del cambio anual: de {lo} a {hi} {unit}."
|
||||
}
|
||||
|
||||
+3
-2
@@ -10207,10 +10207,11 @@
|
||||
"high": "Si cette droite se prolongeait, elle atteindrait la limite haute de ta plage de référence vers le {date}. C'est une projection, pas une prévision.",
|
||||
"low": "Si cette droite se prolongeait, elle atteindrait la limite basse de ta plage de référence vers le {date}. C'est une projection, pas une prévision."
|
||||
},
|
||||
"beyond_horizon": "À ce rythme, la droite reste dans ta plage de référence pendant au moins trois ans encore.",
|
||||
"beyond_horizon": "À ce rythme, la droite reste dans ta plage de référence plus de {years} ans, la durée couverte par tes valeurs.",
|
||||
"no_exit": "La droite s'éloigne des limites de ta plage de référence.",
|
||||
"already_outside": "La droite est déjà hors de ta plage de référence.",
|
||||
"no_clear_trend": "Sur ces valeurs, la variation ne se distingue pas clairement d'une absence de variation, donc aucune projection n'est tracée."
|
||||
"no_clear_trend": "Sur ces valeurs, la variation ne se distingue pas clairement d'une absence de variation, donc aucune projection n'est tracée.",
|
||||
"stale": "La valeur la plus récente date du {date}, il y a plus d’un an. La droite est montrée à cette date, sans projection."
|
||||
},
|
||||
"interval": "Intervalle à 95 % de la variation annuelle : de {lo} à {hi} {unit}."
|
||||
}
|
||||
|
||||
+3
-2
@@ -10207,10 +10207,11 @@
|
||||
"high": "Se questa retta proseguisse, raggiungerebbe il limite superiore del tuo intervallo di riferimento verso il {date}. È una proiezione, non una previsione.",
|
||||
"low": "Se questa retta proseguisse, raggiungerebbe il limite inferiore del tuo intervallo di riferimento verso il {date}. È una proiezione, non una previsione."
|
||||
},
|
||||
"beyond_horizon": "A questo ritmo la retta resta nel tuo intervallo di riferimento per almeno altri tre anni.",
|
||||
"beyond_horizon": "A questo ritmo la retta resta nel tuo intervallo di riferimento per più di {years} anni, il periodo coperto dai tuoi valori.",
|
||||
"no_exit": "La retta si allontana dai limiti del tuo intervallo di riferimento.",
|
||||
"already_outside": "La retta è già fuori dal tuo intervallo di riferimento.",
|
||||
"no_clear_trend": "Su questi valori la variazione non si distingue chiaramente dall'assenza di variazione, quindi non si proietta nulla."
|
||||
"no_clear_trend": "Su questi valori la variazione non si distingue chiaramente dall'assenza di variazione, quindi non si proietta nulla.",
|
||||
"stale": "Il valore più recente è del {date}, più di un anno fa. La retta è mostrata a quella data, senza proiezione."
|
||||
},
|
||||
"interval": "Intervallo al 95 % della variazione annua: da {lo} a {hi} {unit}."
|
||||
}
|
||||
|
||||
+3
-2
@@ -10207,10 +10207,11 @@
|
||||
"high": "이 직선이 그대로 이어진다면 {date}쯤 참고 범위의 상한에 닿습니다. 이는 연장선일 뿐 예측이 아닙니다.",
|
||||
"low": "이 직선이 그대로 이어진다면 {date}쯤 참고 범위의 하한에 닿습니다. 이는 연장선일 뿐 예측이 아닙니다."
|
||||
},
|
||||
"beyond_horizon": "이 속도라면 적어도 3년 동안은 참고 범위 안에 머뭅니다.",
|
||||
"beyond_horizon": "이 속도라면 {years}년 넘게 참고 범위 안에 머뭅니다. 측정값이 다루는 기간입니다.",
|
||||
"no_exit": "직선이 참고 범위의 경계에서 멀어지고 있습니다.",
|
||||
"already_outside": "직선이 이미 참고 범위 밖에 있습니다.",
|
||||
"no_clear_trend": "이 값들에서는 변화가 '변화 없음'과 뚜렷이 구별되지 않아 연장선을 그리지 않습니다."
|
||||
"no_clear_trend": "이 값들에서는 변화가 '변화 없음'과 뚜렷이 구별되지 않아 연장선을 그리지 않습니다.",
|
||||
"stale": "가장 최근 값은 {date}로 1년이 넘었습니다. 직선은 그 시점 기준으로 표시하며 예측은 하지 않습니다."
|
||||
},
|
||||
"interval": "연간 변화의 95% 구간: {lo}~{hi} {unit}."
|
||||
}
|
||||
|
||||
+3
-2
@@ -10207,10 +10207,11 @@
|
||||
"high": "Als deze rechte lijn zo doorliep, zou ze rond {date} de bovengrens van je referentiebereik bereiken. Dat is een doortrekking, geen voorspelling.",
|
||||
"low": "Als deze rechte lijn zo doorliep, zou ze rond {date} de ondergrens van je referentiebereik bereiken. Dat is een doortrekking, geen voorspelling."
|
||||
},
|
||||
"beyond_horizon": "In dit tempo blijft de lijn nog minstens drie jaar binnen je referentiebereik.",
|
||||
"beyond_horizon": "In dit tempo blijft de lijn langer dan {years} jaar binnen je referentiebereik, de periode die je waarden beslaan.",
|
||||
"no_exit": "De lijn loopt weg van de grenzen van je referentiebereik.",
|
||||
"already_outside": "De lijn ligt al buiten je referentiebereik.",
|
||||
"no_clear_trend": "Over deze waarden is de verandering niet duidelijk te onderscheiden van geen verandering, dus er wordt niets doorgetrokken."
|
||||
"no_clear_trend": "Over deze waarden is de verandering niet duidelijk te onderscheiden van geen verandering, dus er wordt niets doorgetrokken.",
|
||||
"stale": "De nieuwste waarde is van {date}, meer dan een jaar geleden. De lijn staat op die datum, zonder projectie."
|
||||
},
|
||||
"interval": "95%-interval van de jaarlijkse verandering: {lo} tot {hi} {unit}."
|
||||
}
|
||||
|
||||
+3
-2
@@ -10207,10 +10207,11 @@
|
||||
"high": "Gdyby ta prosta biegła dalej, osiągnęłaby górną granicę twojego zakresu referencyjnego około {date}. To ekstrapolacja, nie prognoza.",
|
||||
"low": "Gdyby ta prosta biegła dalej, osiągnęłaby dolną granicę twojego zakresu referencyjnego około {date}. To ekstrapolacja, nie prognoza."
|
||||
},
|
||||
"beyond_horizon": "W tym tempie prosta pozostaje w twoim zakresie referencyjnym jeszcze co najmniej trzy lata.",
|
||||
"beyond_horizon": "W tym tempie prosta pozostaje w twoim zakresie referencyjnym dłużej niż {years} lat, czyli okres objęty twoimi wynikami.",
|
||||
"no_exit": "Prosta oddala się od granic twojego zakresu referencyjnego.",
|
||||
"already_outside": "Prosta jest już poza twoim zakresem referencyjnym.",
|
||||
"no_clear_trend": "Na podstawie tych wyników zmiana nie różni się wyraźnie od braku zmiany, więc nie jest wyznaczana ekstrapolacja."
|
||||
"no_clear_trend": "Na podstawie tych wyników zmiana nie różni się wyraźnie od braku zmiany, więc nie jest wyznaczana ekstrapolacja.",
|
||||
"stale": "Najnowszy wynik pochodzi z {date}, sprzed ponad roku. Prosta jest pokazana na ten dzień, bez projekcji."
|
||||
},
|
||||
"interval": "95% przedział rocznej zmiany: od {lo} do {hi} {unit}."
|
||||
}
|
||||
|
||||
@@ -74,7 +74,9 @@ export const GET = apiHandler(
|
||||
deletedAt: null,
|
||||
value: { not: null },
|
||||
},
|
||||
orderBy: { takenAt: "asc" },
|
||||
// Newest first, so a capped read keeps the recent end of the series
|
||||
// (the staleness check reads the newest reading); the fit sorts.
|
||||
orderBy: { takenAt: "desc" },
|
||||
take: LAB_TREND_READ_CAP,
|
||||
select: { takenAt: true, value: true },
|
||||
}),
|
||||
|
||||
@@ -20,6 +20,7 @@ const OK: BiomarkerTrendDto = {
|
||||
pValue: 0.02,
|
||||
fittedNow: 130,
|
||||
asOf: "2026-10-21",
|
||||
horizonYears: 3,
|
||||
projection: "exits",
|
||||
exitDate: "2027-11-02",
|
||||
exitBound: "high",
|
||||
@@ -71,4 +72,25 @@ describe("<LabTrendCard>", () => {
|
||||
).toBe("");
|
||||
expect(render(undefined)).toBe("");
|
||||
});
|
||||
|
||||
it("words a stale trend as of its newest reading, and a span-bound horizon", () => {
|
||||
const stale = render({
|
||||
...OK,
|
||||
asOf: "2026-01-10",
|
||||
projection: "stale",
|
||||
exitDate: null,
|
||||
exitBound: null,
|
||||
});
|
||||
expect(stale).toContain('data-projection="stale"');
|
||||
expect(stale).toContain("more than a year ago");
|
||||
expect(stale).not.toContain("forecast");
|
||||
const beyond = render({
|
||||
...OK,
|
||||
horizonYears: 1.5,
|
||||
projection: "beyond_horizon",
|
||||
exitDate: null,
|
||||
exitBound: null,
|
||||
});
|
||||
expect(beyond).toContain("for more than 1.5 years");
|
||||
});
|
||||
});
|
||||
@@ -48,7 +48,15 @@ export function LabTrendCard({
|
||||
})
|
||||
: trend.projection === "no_band"
|
||||
? null
|
||||
: t(`labs.trend.projection.${trend.projection}`);
|
||||
: trend.projection === "stale"
|
||||
? t("labs.trend.projection.stale", {
|
||||
date: fmt.dateShortSmartCalendar(trend.lastAt),
|
||||
})
|
||||
: trend.projection === "beyond_horizon"
|
||||
? t("labs.trend.projection.beyond_horizon", {
|
||||
years: fmt.number(trend.horizonYears, 1),
|
||||
})
|
||||
: t(`labs.trend.projection.${trend.projection}`);
|
||||
|
||||
return (
|
||||
<Card data-slot="lab-trend">
|
||||
|
||||
@@ -125,13 +125,15 @@ export type BiomarkerTrendDto =
|
||||
pValue: number;
|
||||
fittedNow: number;
|
||||
asOf: string;
|
||||
horizonYears: number;
|
||||
projection:
|
||||
| "exits"
|
||||
| "beyond_horizon"
|
||||
| "no_exit"
|
||||
| "already_outside"
|
||||
| "no_clear_trend"
|
||||
| "no_band";
|
||||
| "no_band"
|
||||
| "stale";
|
||||
exitDate: string | null;
|
||||
exitBound: "low" | "high" | null;
|
||||
}
|
||||
|
||||
@@ -128,4 +128,50 @@ describe("computeLabTrend", () => {
|
||||
computeLabTrend(falling, { low: 90, high: 140 }, atYears(2)),
|
||||
).toMatchObject({ projection: "exits", exitBound: "low" });
|
||||
});
|
||||
|
||||
it("never carries the line further ahead than the readings reach back", () => {
|
||||
// 10 a year over one year (span 1.0); band top 125 sits 1.5 years past
|
||||
// the last reading. Inside the three-year cap, outside the one-year span.
|
||||
const short = readings([
|
||||
[0, 100],
|
||||
[0.5, 105],
|
||||
[1, 110],
|
||||
]);
|
||||
expect(
|
||||
computeLabTrend(short, { low: 70, high: 125 }, atYears(1)),
|
||||
).toMatchObject({ projection: "beyond_horizon", horizonYears: 1 });
|
||||
// The same line with a top of 118 meets it 0.8 years out: projected.
|
||||
expect(
|
||||
computeLabTrend(short, { low: 70, high: 118 }, atYears(1)),
|
||||
).toMatchObject({ projection: "exits", exitBound: "high" });
|
||||
});
|
||||
|
||||
it("reports a stale series as of its last reading, without a projection", () => {
|
||||
const line = readings([
|
||||
[0, 100],
|
||||
[1, 110],
|
||||
[2, 120],
|
||||
]);
|
||||
// Last reading two years before "now": evaluated on that day.
|
||||
const stale = computeLabTrend(line, { low: 70, high: 125 }, atYears(4));
|
||||
expect(stale).toMatchObject({
|
||||
status: "ok",
|
||||
projection: "stale",
|
||||
exitDate: null,
|
||||
exitBound: null,
|
||||
fittedNow: 120,
|
||||
slopePerYear: 10,
|
||||
});
|
||||
if (stale.status !== "ok") throw new Error("expected ok");
|
||||
expect(stale.asOf).toBe(stale.lastAt);
|
||||
// Carried to today the line would read 140, above the band: a stale
|
||||
// series never says "already outside".
|
||||
expect(
|
||||
computeLabTrend(line, { low: 70, high: 115 }, atYears(4)),
|
||||
).toMatchObject({ projection: "stale" });
|
||||
// Eleven months after the last reading it is still current.
|
||||
expect(
|
||||
computeLabTrend(line, { low: 70, high: 200 }, atYears(2.9)),
|
||||
).toMatchObject({ projection: "beyond_horizon" });
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,14 @@
|
||||
* The slope carries its 95 % confidence interval (Student t at n − 2 degrees
|
||||
* of freedom). A projection is offered only when that interval excludes zero,
|
||||
* the line is inside the band today and heads toward a bound it reaches
|
||||
* within {@link LAB_PROJECTION_HORIZON_YEARS} years. It is a straight-line
|
||||
* within the horizon: {@link LAB_PROJECTION_HORIZON_YEARS} years, or the span
|
||||
* the readings cover when that is shorter, so the line is never carried
|
||||
* further ahead than the data reaches back.
|
||||
*
|
||||
* A marker whose newest reading is older than {@link LAB_TREND_STALE_DAYS}
|
||||
* days gets no projection at all (`stale`): the line is evaluated on the day
|
||||
* of that reading and reported as of then, with no exit date and no
|
||||
* "already outside" verdict about a today the readings say nothing about. It is a straight-line
|
||||
* extrapolation, labelled as one wherever it is shown, never a forecast of
|
||||
* the body and never a risk statement: lab values plateau, respond to
|
||||
* treatment and carry assay noise a line does not know about. With three
|
||||
@@ -29,6 +36,8 @@ import { dateOnlyKey } from "@/lib/tz/date-only";
|
||||
export const LAB_TREND_MIN_VALUES = 3;
|
||||
export const LAB_TREND_MIN_SPAN_DAYS = 365;
|
||||
export const LAB_PROJECTION_HORIZON_YEARS = 3;
|
||||
/** Newest reading older than this: the trend is reported as of that reading. */
|
||||
export const LAB_TREND_STALE_DAYS = 365;
|
||||
|
||||
const DAY_MS = 86_400_000;
|
||||
const YEAR_DAYS = 365.25;
|
||||
@@ -45,7 +54,12 @@ export type LabTrendProjection =
|
||||
/** The slope's interval includes zero: no projection is drawn. */
|
||||
| "no_clear_trend"
|
||||
/** The marker has no reference band to leave. */
|
||||
| "no_band";
|
||||
| "no_band"
|
||||
/**
|
||||
* The newest reading is older than {@link LAB_TREND_STALE_DAYS} days: the
|
||||
* line is reported as of that reading (`asOf` = `lastAt`), not projected.
|
||||
*/
|
||||
| "stale";
|
||||
|
||||
export interface LabTrendOk {
|
||||
status: "ok";
|
||||
@@ -62,7 +76,13 @@ export interface LabTrendOk {
|
||||
pValue: number;
|
||||
/** The line's value on `asOf`. */
|
||||
fittedNow: number;
|
||||
/** Today, or the newest reading's day when the trend is `stale`. */
|
||||
asOf: string;
|
||||
/**
|
||||
* How far ahead a bound may be met and still count as `exits`: three years,
|
||||
* or the readings' span when shorter.
|
||||
*/
|
||||
horizonYears: number;
|
||||
projection: LabTrendProjection;
|
||||
/** Set only for `exits`: the day the line meets `exitBound`. */
|
||||
exitDate: string | null;
|
||||
@@ -136,14 +156,23 @@ export function computeLabTrend(
|
||||
: 0
|
||||
: twoSidedPFromT(Math.abs(fit.slope / se), df);
|
||||
|
||||
const xNow = (now.getTime() - t0) / DAY_MS / YEAR_DAYS;
|
||||
const tLast = points[n - 1].t;
|
||||
const stale = now.getTime() - tLast > LAB_TREND_STALE_DAYS * DAY_MS;
|
||||
// A stale series is read on its own last day, never carried to today.
|
||||
const tAsOf = stale ? tLast : now.getTime();
|
||||
const xNow = (tAsOf - t0) / DAY_MS / YEAR_DAYS;
|
||||
const fittedNow = fit.intercept + fit.slope * xNow;
|
||||
const horizonYears = Math.min(
|
||||
LAB_PROJECTION_HORIZON_YEARS,
|
||||
(tLast - t0) / DAY_MS / YEAR_DAYS,
|
||||
);
|
||||
|
||||
let projection: LabTrendProjection;
|
||||
let exitDate: string | null = null;
|
||||
let exitBound: "low" | "high" | null = null;
|
||||
const hasBand = band.low != null || band.high != null;
|
||||
if (!hasBand) projection = "no_band";
|
||||
if (stale) projection = "stale";
|
||||
else if (!hasBand) projection = "no_band";
|
||||
else if (!(lo > 0 || hi < 0)) projection = "no_clear_trend";
|
||||
else if (
|
||||
(band.low != null && fittedNow < band.low) ||
|
||||
@@ -156,7 +185,7 @@ export function computeLabTrend(
|
||||
else {
|
||||
const xCross = (bound - fit.intercept) / fit.slope;
|
||||
const yearsAhead = xCross - xNow;
|
||||
if (yearsAhead > LAB_PROJECTION_HORIZON_YEARS) {
|
||||
if (yearsAhead > horizonYears) {
|
||||
projection = "beyond_horizon";
|
||||
} else {
|
||||
projection = "exits";
|
||||
@@ -176,7 +205,8 @@ export function computeLabTrend(
|
||||
slopeCi95: [round(lo, 4), round(hi, 4)],
|
||||
pValue: round(pValue, 6),
|
||||
fittedNow: round(fittedNow, 4),
|
||||
asOf: dayOf(now.getTime()),
|
||||
asOf: dayOf(tAsOf),
|
||||
horizonYears: round(horizonYears, 2),
|
||||
projection,
|
||||
exitDate,
|
||||
exitBound,
|
||||
|
||||
@@ -93,7 +93,16 @@ const labTrend = z
|
||||
.describe("95 % interval of the slope (Student t at n − 2 df)."),
|
||||
pValue: z.number(),
|
||||
fittedNow: z.number().describe("The line's value on `asOf`."),
|
||||
asOf: z.string().describe("YYYY-MM-DD the line was evaluated on."),
|
||||
asOf: z
|
||||
.string()
|
||||
.describe(
|
||||
"YYYY-MM-DD the line was evaluated on: today, or `lastAt` when `projection` is `stale`.",
|
||||
),
|
||||
horizonYears: z
|
||||
.number()
|
||||
.describe(
|
||||
"How far ahead a bound may be met and still count as `exits`: three years, or the readings' span when shorter.",
|
||||
),
|
||||
projection: z.enum([
|
||||
"exits",
|
||||
"beyond_horizon",
|
||||
@@ -101,6 +110,7 @@ const labTrend = z
|
||||
"already_outside",
|
||||
"no_clear_trend",
|
||||
"no_band",
|
||||
"stale",
|
||||
]),
|
||||
exitDate: z
|
||||
.string()
|
||||
@@ -120,7 +130,7 @@ const labTrend = z
|
||||
.meta({
|
||||
id: "BiomarkerTrend",
|
||||
description:
|
||||
"v1.44 — the least-squares line through every numeric reading of the marker (all stored in the marker's unit). Needs at least three readings spanning at least 365 days, else `insufficient`. `projection` places the line against the marker's band: `exits` (with `exitDate`) only when the slope's 95 % interval excludes zero, the line is inside the band today and reaches a bound within three years. Server-computed; clients render it as given.",
|
||||
"v1.44 — the least-squares line through every numeric reading of the marker (all stored in the marker's unit). Needs at least three readings spanning at least 365 days, else `insufficient`. `projection` places the line against the marker's band: `exits` (with `exitDate`) only when the slope's 95 % interval excludes zero, the line is inside the band today and reaches a bound within `horizonYears` (three years, or the readings' span when shorter). `stale` when the newest reading is more than 365 days old: the line is then reported as of `lastAt`, with no projection. Server-computed; clients render it as given.",
|
||||
});
|
||||
|
||||
const biomarkerDetail = biomarkerRow
|
||||
|
||||
Reference in new issue
Block a user