mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
fix(labs): word a value without a unit the same on every remaining surface
The briefing's lab input passed the stored "1" to the model, the unit mismatch refusal read "is in 1", and the trend card left a gap where a language puts the unit before a particle. get_labs says that a value without a unit has an empty unit, and a lab link id is scrubbed like its label. The review row is back at its baseline length.
This commit is contained in:
1 parent
6a8ac98613
commit
49a0eb92ff
9 files changed
+48
-18
No files matched your search
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
|
||||
"count": 345,
|
||||
"total": 111930,
|
||||
"total": 111929,
|
||||
"entries": {
|
||||
"app/achievements/page.tsx :: AchievementsPage": 206,
|
||||
"app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 275,
|
||||
@@ -93,7 +93,7 @@
|
||||
"components/documents/document-card.tsx :: DocumentCard": 354,
|
||||
"components/documents/document-detail-sheet.tsx :: DocumentDetailSheet": 325,
|
||||
"components/documents/document-facts-review.tsx :: DocumentFactsSection": 242,
|
||||
"components/documents/document-facts-review.tsx :: FactRow": 263,
|
||||
"components/documents/document-facts-review.tsx :: FactRow": 262,
|
||||
"components/documents/document-filter-bar.tsx :: DocumentFilterBar": 348,
|
||||
"components/documents/document-record-links.tsx :: DocumentRecordLinks": 202,
|
||||
"components/documents/document-timeline.tsx :: DocumentTimeline": 314,
|
||||
|
||||
@@ -17,6 +17,7 @@ import { withIdempotency } from "@/lib/idempotency";
|
||||
import { enqueueReminderSatisfy } from "@/lib/jobs/reminder-satisfy";
|
||||
import { emitDataArrival } from "@/lib/arrivals/emit-shared";
|
||||
import { resolveOrMintBiomarker } from "@/lib/labs/biomarker-store";
|
||||
import { isDimensionlessUnit } from "@/lib/labs/dimensionless";
|
||||
import { readingInMarkerUnit } from "@/lib/labs/reading-conversion";
|
||||
import {
|
||||
type ResolvedBiomarker,
|
||||
@@ -270,7 +271,7 @@ async function postLabResult(request: NextRequest) {
|
||||
meta: { biomarkerId: biomarker.id, reason: reading.reason },
|
||||
});
|
||||
return apiError(
|
||||
`The reading is in ${unit}, but ${biomarker.name} is tracked in ${biomarker.unit}.`,
|
||||
unitMismatchMessage(unit, biomarker.name, biomarker.unit),
|
||||
422,
|
||||
{
|
||||
errorCode: "labs.unit.mismatch",
|
||||
@@ -392,3 +393,19 @@ async function postLabResult(request: NextRequest) {
|
||||
|
||||
return apiSuccess(dto, 201);
|
||||
}
|
||||
|
||||
/** The refusal for a reading whose unit differs from its marker's. */
|
||||
function unitMismatchMessage(
|
||||
readingUnit: string | undefined,
|
||||
markerName: string,
|
||||
markerUnit: string,
|
||||
): string {
|
||||
const readingSide =
|
||||
!readingUnit || isDimensionlessUnit(readingUnit)
|
||||
? "The reading has no unit"
|
||||
: `The reading is in ${readingUnit}`;
|
||||
const markerSide = isDimensionlessUnit(markerUnit)
|
||||
? "tracked without a unit"
|
||||
: `tracked in ${markerUnit}`;
|
||||
return `${readingSide}, but ${markerName} is ${markerSide}.`;
|
||||
}
|
||||
@@ -223,7 +223,7 @@ export function FactRow({
|
||||
onCheckedChange: (checked: boolean) => void;
|
||||
/** A per-fact commit refusal from the last confirm attempt, or null. */
|
||||
failure: string | null;
|
||||
/** v1.43 — the suggestion the person picked for a lab value, or null. */
|
||||
/** The suggestion the person picked for a lab value, or null. */
|
||||
chosenBiomarkerId: string | null;
|
||||
onChooseBiomarker: (biomarkerId: string | null) => void;
|
||||
/** The account's marker name by id, for the matched marker. */
|
||||
@@ -252,8 +252,7 @@ export function FactRow({
|
||||
{
|
||||
onSuccess: () => {
|
||||
setVerifying(false);
|
||||
// The refetched fact arrives with needsReview cleared; include it —
|
||||
// the person just asserted these values.
|
||||
// Include it: the refetched fact clears needsReview, as asserted.
|
||||
onCheckedChange(true);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ import { primeLocales } from "@/__tests__/helpers/prime-locales";
|
||||
import { LabTrendCard } from "../lab-trend-card";
|
||||
import type { BiomarkerTrendDto } from "../types";
|
||||
|
||||
primeLocales("de");
|
||||
primeLocales("de", "ko");
|
||||
|
||||
const OK: BiomarkerTrendDto = {
|
||||
status: "ok",
|
||||
@@ -29,11 +29,12 @@ const OK: BiomarkerTrendDto = {
|
||||
|
||||
function render(
|
||||
trend: BiomarkerTrendDto | undefined,
|
||||
locale: "en" | "de" = "en",
|
||||
locale: "en" | "de" | "ko" = "en",
|
||||
unit = "mg/dL",
|
||||
) {
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale={locale}>
|
||||
<LabTrendCard trend={trend} unit="mg/dL" />
|
||||
<LabTrendCard trend={trend} unit={unit} />
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
@@ -48,6 +49,16 @@ describe("<LabTrendCard>", () => {
|
||||
expect(html).toContain("4.25 to 20.75 mg/dL");
|
||||
});
|
||||
|
||||
it("leaves no gap where a value without a unit has none, in any language", () => {
|
||||
const en = render(OK, "en", "1");
|
||||
expect(en).toContain("Rising by about 13 a year across 4 readings");
|
||||
expect(en).toContain("4.25 to 20.75.");
|
||||
const ko = render(OK, "ko", "1");
|
||||
expect(ko).toContain("약 13씩");
|
||||
expect(ko).not.toMatch(/ 씩/);
|
||||
expect(ko).not.toContain("\u0000");
|
||||
});
|
||||
|
||||
it("speaks German with du", () => {
|
||||
const html = render(OK, "de");
|
||||
expect(html).toContain("deines Referenzbereichs");
|
||||
|
||||
@@ -37,14 +37,16 @@ export function LabTrendCard({
|
||||
? "falling"
|
||||
: "flat";
|
||||
const shownUnit = labUnitText(unit);
|
||||
// A value without a unit leaves the {unit} slot empty; close the gap it
|
||||
// leaves rather than print a space before the full stop.
|
||||
// A value without a unit fills the {unit} slot with a marker and drops it
|
||||
// together with the space before it, so no language keeps the gap ("0.3 .",
|
||||
// "0.3 씩").
|
||||
const SLOT = "\u0000";
|
||||
const tidy = (text: string) =>
|
||||
shownUnit ? text : text.replace(/ +([.,])/g, "$1").replace(/ {2,}/g, " ");
|
||||
shownUnit ? text : text.replace(new RegExp(` ?${SLOT}`, "g"), "");
|
||||
const slopeLine = tidy(
|
||||
t(`labs.trend.slope.${direction}`, {
|
||||
amount: per(trend.slopePerYear),
|
||||
unit: shownUnit,
|
||||
unit: shownUnit || SLOT,
|
||||
n: trend.n,
|
||||
since: fmt.dateShortSmartCalendar(trend.firstAt),
|
||||
}),
|
||||
@@ -97,7 +99,7 @@ export function LabTrendCard({
|
||||
t("labs.trend.interval", {
|
||||
lo: fmt.number(trend.slopeCi95[0], 2),
|
||||
hi: fmt.number(trend.slopeCi95[1], 2),
|
||||
unit: shownUnit,
|
||||
unit: shownUnit || SLOT,
|
||||
}),
|
||||
)}
|
||||
</p>
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
canonicalWorkoutsInRange,
|
||||
readWorkoutsAroundRange,
|
||||
} from "@/lib/workouts/canonical-read";
|
||||
import { labUnitText } from "@/lib/labs/dimensionless";
|
||||
import { deriveBucketedTypes } from "@/lib/signals/adapters/correlation";
|
||||
import type {
|
||||
AggregatedFeatures,
|
||||
@@ -434,7 +435,7 @@ export async function readLabsBriefingBlock(
|
||||
analyte: resolved.analyte,
|
||||
value: latest.value,
|
||||
valueText: latest.valueText ? sanitizeLabel(latest.valueText, 60) : null,
|
||||
unit: resolved.unit,
|
||||
unit: labUnitText(resolved.unit),
|
||||
rangeStatus,
|
||||
trend,
|
||||
takenAt: latest.takenAt.toISOString(),
|
||||
|
||||
@@ -378,7 +378,7 @@
|
||||
{
|
||||
"name": "get_labs",
|
||||
"title": "Get lab results",
|
||||
"description": "Fetch the user's lab results. By default returns the latest reading per biomarker over the whole record. Filtered to one named analyte, each matching marker also carries a summarised history (the newest earlier values in full, older ones as one mean / min / max / count per year). Pass history:true with an analyte to return that analyte's reading TRAJECTORY (newest first, paginated). Each reading carries its value, unit, reference range, and in-range/below/above status. Each reading (the latest, an earlier value in full, a trajectory reading; not a per-year summary) carries links: the ids of the records it is linked to (the report it was read from, the visits it is filed against), [] when none, at most 25 with linksTruncated; every linked record is described once in linkedRecords (id for fetch, kind, label, date, and for a document imported from Paperless-ngx or Papra its sourceSystem and sourceId). A document is listed only while the Documents module is on. Returns { present: false } when no labs match, and { present: false, reason: \"module_disabled\" } when the labs module is switched off.",
|
||||
"description": "Fetch the user's lab results. By default returns the latest reading per biomarker over the whole record. Filtered to one named analyte, each matching marker also carries a summarised history (the newest earlier values in full, older ones as one mean / min / max / count per year). Pass history:true with an analyte to return that analyte's reading TRAJECTORY (newest first, paginated). Each reading carries its value, unit, reference range, and in-range/below/above status; a ratio or an index recorded without a unit has an empty unit. Each reading (the latest, an earlier value in full, a trajectory reading; not a per-year summary) carries links: the ids of the records it is linked to (the report it was read from, the visits it is filed against), [] when none, at most 25 with linksTruncated; every linked record is described once in linkedRecords (id for fetch, kind, label, date, and for a document imported from Paperless-ngx or Papra its sourceSystem and sourceId). A document is listed only while the Documents module is on. Returns { present: false } when no labs match, and { present: false, reason: \"module_disabled\" } when the labs module is switched off.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -104,7 +104,7 @@ async function labLinks(
|
||||
const name = analyte.get(target.id);
|
||||
if (!name) continue;
|
||||
out.set(target.id, {
|
||||
id: `lab:${name}`,
|
||||
id: `lab:${scrubFenceMarkers(name)}`,
|
||||
kind: "lab",
|
||||
label: scrubFenceMarkers(target.label),
|
||||
date: linkDate(target.date),
|
||||
|
||||
@@ -140,7 +140,7 @@ export const getLabsTool: McpToolDefinition = {
|
||||
name: "get_labs",
|
||||
title: "Get lab results",
|
||||
description:
|
||||
"Fetch the user's lab results. By default returns the latest reading per biomarker over the whole record. Filtered to one named analyte, each matching marker also carries a summarised history (the newest earlier values in full, older ones as one mean / min / max / count per year). Pass history:true with an analyte to return that analyte's reading TRAJECTORY (newest first, paginated). Each reading carries its value, unit, reference range, and in-range/below/above status. Each reading (the latest, an earlier value in full, a trajectory reading; not a per-year summary) carries links: the ids of the records it is linked to (the report it was read from, the visits it is filed against), [] when none, at most 25 with linksTruncated; every linked record is described once in linkedRecords (id for fetch, kind, label, date, and for a document imported from Paperless-ngx or Papra its sourceSystem and sourceId). A document is listed only while the Documents module is on. Returns { present: false } when no labs match, and { present: false, reason: \"module_disabled\" } when the labs module is switched off.",
|
||||
"Fetch the user's lab results. By default returns the latest reading per biomarker over the whole record. Filtered to one named analyte, each matching marker also carries a summarised history (the newest earlier values in full, older ones as one mean / min / max / count per year). Pass history:true with an analyte to return that analyte's reading TRAJECTORY (newest first, paginated). Each reading carries its value, unit, reference range, and in-range/below/above status; a ratio or an index recorded without a unit has an empty unit. Each reading (the latest, an earlier value in full, a trajectory reading; not a per-year summary) carries links: the ids of the records it is linked to (the report it was read from, the visits it is filed against), [] when none, at most 25 with linksTruncated; every linked record is described once in linkedRecords (id for fetch, kind, label, date, and for a document imported from Paperless-ngx or Papra its sourceSystem and sourceId). A document is listed only while the Documents module is on. Returns { present: false } when no labs match, and { present: false, reason: \"module_disabled\" } when the labs module is switched off.",
|
||||
inputShape: {
|
||||
analyte: z.string().min(1).max(80).optional(),
|
||||
history: z
|
||||
|
||||
Reference in new issue
Block a user