mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
get_labs readings (the latest, earlier values in full, trajectory readings) carry the fetch ids of what they link to, with linksTruncated; each linked record is described once per answer in linkedRecords, because one report usually holds a whole panel and would otherwise repeat on every reading. fetch lab: and fetch med: list their links like the other record kinds, and get_medication_schedule rows carry their id and links inline. get_visits and get_illness_recovery rows carry visit: and condition: ids, so a row's links are one fetch away. The labs and illness blocks gain an opt-in to carry row ids; the Coach never asks for them.
108 lines
3.8 KiB
TypeScript
108 lines
3.8 KiB
TypeScript
/**
|
|
* The latest-per-marker labs read behind `get_labs` and `fetch lab:`, with
|
|
* what each reading is linked to.
|
|
*
|
|
* A present read builds the same block the Coach's `get_labs` returns
|
|
* (`buildLabsSnapshotBlock`, the whole record, as an MCP read has no lookback
|
|
* limit), asking it to carry each reading's lab result id. A miss falls back
|
|
* to the caller's Coach read, which owns the grounded empty answer (what the
|
|
* record holds, `analyte_not_found`).
|
|
*
|
|
* The links of a reading are the report it was read from (a document, behind
|
|
* the Documents module) and the visits it is filed against. A labs answer
|
|
* lists many readings, and one report usually holds a whole panel, so the
|
|
* answer names each linked record once, in `linkedRecords`, and a reading
|
|
* carries the ids of its links only. Every id there is one `fetch` accepts.
|
|
*/
|
|
import { buildLabsSnapshotBlock } from "@/lib/ai/coach/labs-snapshot";
|
|
import { UNBOUNDED_REACH } from "@/lib/ai/coach/history-reach";
|
|
import { boundLinks, linksBySource, type RecordLink } from "./record-links";
|
|
|
|
/** What a lab result lists as linked, in this order. */
|
|
export const LAB_LINK_TARGETS = ["document", "encounter"] as const;
|
|
|
|
/** The latest reading per marker with ids, or `fallback()` on a miss. */
|
|
export async function readLatestLabs(
|
|
userId: string,
|
|
analyte: string | undefined,
|
|
fallback: () => Promise<unknown>,
|
|
): Promise<unknown> {
|
|
const data = await buildLabsSnapshotBlock(
|
|
userId,
|
|
new Date(),
|
|
UNBOUNDED_REACH,
|
|
{ analyte, withIds: true },
|
|
);
|
|
return data ? { present: true, data } : fallback();
|
|
}
|
|
|
|
type IdReading = Record<string, unknown> & { id: string };
|
|
|
|
/** Every reading of a labs answer that carries its lab result id. */
|
|
function idReadings(result: unknown): IdReading[] {
|
|
const r = result as {
|
|
readings?: unknown[];
|
|
data?: { recent?: Array<{ history?: { earlier?: unknown[] } }> };
|
|
} | null;
|
|
const list: unknown[] = [
|
|
...(Array.isArray(r?.readings) ? r.readings : []),
|
|
...(Array.isArray(r?.data?.recent) ? r.data.recent : []).flatMap(
|
|
(entry) => [entry, ...(entry?.history?.earlier ?? [])],
|
|
),
|
|
];
|
|
return list.filter(
|
|
(x): x is IdReading =>
|
|
typeof x === "object" &&
|
|
x !== null &&
|
|
typeof (x as { id?: unknown }).id === "string",
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Replace each reading's lab result id with the ids of what it links to
|
|
* (`links`, bounded, with `linksTruncated`), and add the linked records once
|
|
* each as `linkedRecords`. A reading linked to nothing carries `links: []`.
|
|
* An answer without ids (a miss) comes back unchanged. `result` must be the
|
|
* caller's own copy: its readings are rewritten in place.
|
|
*/
|
|
export async function attachLabLinks(
|
|
userId: string,
|
|
result: unknown,
|
|
): Promise<unknown> {
|
|
const readings = idReadings(result);
|
|
if (readings.length === 0) return result;
|
|
const bySource = await linksBySource(
|
|
userId,
|
|
"labResult",
|
|
[...new Set(readings.map((r) => r.id))],
|
|
LAB_LINK_TARGETS,
|
|
);
|
|
const table = new Map<string, RecordLink>();
|
|
for (const reading of readings) {
|
|
const { links, linksTruncated } = boundLinks(
|
|
bySource.get(reading.id) ?? [],
|
|
);
|
|
delete (reading as { id?: string }).id;
|
|
reading.links = links.flatMap((l) => (l.id ? [l.id] : []));
|
|
reading.linksTruncated = linksTruncated;
|
|
for (const link of links) {
|
|
if (link.id && !table.has(link.id)) table.set(link.id, link);
|
|
}
|
|
}
|
|
return { ...(result as object), linkedRecords: [...table.values()] };
|
|
}
|
|
|
|
/** The links of one lab result, bounded, for a single-record `fetch`. */
|
|
export async function labResultLinks(
|
|
userId: string,
|
|
labResultId: string,
|
|
): Promise<{ links: RecordLink[]; linksTruncated: boolean }> {
|
|
const bySource = await linksBySource(
|
|
userId,
|
|
"labResult",
|
|
[labResultId],
|
|
LAB_LINK_TARGETS,
|
|
);
|
|
return boundLinks(bySource.get(labResultId) ?? []);
|
|
}
|