marmar-context / web / context / test / fixture / clinical.ts
clinical.ts
Raw
// The fixture's reading and check: the preview's real local reader and the real engine,
// wrapped in the /api/clinician shapes. Clinician wording for the film's two rules is the PRD's.
import type * as T from "../../src/api/types";
import type { Source } from "../../src/types";
import { assess } from "../../server/context";
import { extract, modelReady } from "../../server/model";
import { findingEvidence, patientProfile } from "../../src/flow";
import { notCheckedLines } from "../../src/logic/checks";
import { withEngineOptions } from "./engine-options";
import type { StoredRecord } from "./data";
import { nextId } from "./data";

export type StoredExtraction = T.Extraction & { sources: Source[] };

export const toSources = (records: StoredRecord[]): Source[] =>
  records
    .filter((r) => r.reading === "read" && r.text)
    .map((r) => ({ id: r.id, title: r.title, kind: r.type, date: r.recordDate ?? "", text: r.text!, ...(r.clarifies ? { clarification: r.clarifies } : {}) }));

/** Reads with the configured model (Meta Muse Spark) when there is one, else the local structured reader. */
export async function readRecords(patientId: string, records: StoredRecord[]): Promise<StoredExtraction> {
  const sources = toSources(records);
  const draft = await extract(sources, modelReady());
  return {
    ...draft,
    id: nextId("ext"),
    patientId,
    revision: 0,
    createdAt: new Date().toISOString(),
    records: records.filter((r) => !r.clarifies).map((r) => ({
      recordId: r.id,
      title: r.title,
      status: r.heldBack ? "held_back" : r.reading === "read" ? "read" : r.reading === "failed" ? "failed" : "queued",
      error: r.reading === "failed" ? r.readingErrorCode ?? "READ_FAILED" : null,
    })),
    titles: { [`profile-${patientId}`]: "Patient profile", ...Object.fromEntries(records.map((r) => [r.id, r.title])) },
    recordsNotReady: records.filter((r) => r.heldBack || r.reading !== "read").map((r) => ({ recordId: r.id, title: r.title, status: r.heldBack ? "held_back" : r.reading })),
    decisions: {},
    coverageReview: {},
    acknowledgedOriginals: false,
    confirmedHardToRead: [],
    stale: false,
    sources,
  };
}

const day = (iso: string) => new Date(`${iso}T12:00:00Z`).toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric", timeZone: "UTC" });
const CLINICIAN_WORDING: Record<string, (facts: T.FactRef[], placeOf: (recordId: string) => string) => { title: string; explanation: string }> = {
  "allergy-penicillin": (facts, placeOf) => {
    const medicine = facts.find((f) => f.factId && /amoxicillin|ampicillin|augmentin|ampiclox/i.test(f.label));
    const allergy = facts.find((f) => /penicillin/i.test(f.label) && !/amoxicillin/i.test(f.label));
    return {
      title: `${medicine?.label ?? "Amoxicillin"} and a recorded Penicillin allergy`,
      explanation: `${medicine?.label ?? "Amoxicillin"} is in the penicillin class. A Penicillin allergy is recorded${allergy ? ` (${placeOf(allergy.recordId)})` : ""}. Its reaction and severity are not in a form the rules read.`,
    };
  },
  "lab-metformin-egfr-45": (facts, placeOf) => {
    const egfr = facts.find((f) => /egfr/i.test(f.label));
    const date = egfr?.detail.split(" · ")[1];
    return {
      title: "Metformin and the latest eGFR result",
      explanation: `${egfr ? `${egfr.label} (${placeOf(egfr.recordId)}, ${date ? day(date) : "date not recorded"})` : "The latest eGFR"} is in the range where the Metformin label limits the dose to 1,000 mg a day.`,
    };
  },
};

const plainTitle = (title: string) => title.replace(/^(Call today|Get help now|Ask this week|Good to know):\s*/i, "").replace(/\btheir\b/g, "a recorded");

export function runCheckRun(
  ex: StoredExtraction,
  records: StoredRecord[],
  ranBy: string,
  withheldSome: boolean,
  newMedicineNames: string[],
  reactions: Record<string, { reaction: string | null; onset: string | null; year: number | null }> = {},
): T.CheckRun {
  const assessment = assess(ex.sources, ex.decisions, ex.fingerprint, ex.acknowledgedOriginals, ex, ex.coverageReview);
  const title = (id: string) => records.find((r) => r.id === id)?.title ?? "Record";
  const refs = assessment.contextual.what_we_checked.references;
  const details: Record<string, T.FindingDetail> = {};
  for (const finding of assessment.contextual.findings) {
    const facts: T.FactRef[] = findingEvidence(assessment, finding).map((f) => ({
      factId: f.id,
      label: f.label,
      detail: f.detail,
      recordId: f.sourceId,
      recordTitle: title(f.sourceId),
      line: f.line,
      quote: f.quote,
    }));
    const placeOf = (id: string) => records.find((r) => r.id === id)?.place ?? title(id);
    const wording = CLINICIAN_WORDING[finding.ruleId]?.(facts, placeOf) ?? { title: plainTitle(finding.title), explanation: finding.clinical || finding.why };
    details[finding.id] = {
      findingId: finding.id,
      ruleId: finding.ruleId,
      ruleReviewed: assessment.contextual.rulesReviewed,
      // As the backend: the engine's clinician option wrote the film's two, the rest come from the rule's note.
      wording: CLINICIAN_WORDING[finding.ruleId] ? "engine" : "derived",
      clinicianTitle: wording.title,
      clinicianExplanation: wording.explanation,
      references: finding.source_ids.map((id) => {
        const ref = refs.find((r) => r.id === id);
        return { name: ref?.name ?? id, version: null, section: null, date: null, url: ref?.url ?? null };
      }),
      factsUsed: facts,
      questions: [],
    };
  }
  const profile = patientProfile(assessment);
  const missingFactors: T.MissingFactor[] = profile
    .filter((p) => p.status === "missing")
    .map((p) => ({ id: p.id, label: p.label, reason: p.note, questions: [] }));
  const meds = assessment.includedFacts.filter((f) => f.kind === "medicine");
  const monitoring: T.CheckRun["monitoring"] = assessment.contextual.tests_to_ask_about.map((t) => ({
    test: t.test,
    medicines: [...new Set(meds.filter((m) => t.why.toLowerCase().includes(m.label.toLowerCase())).map((m) => m.label))],
    onFile: t.last_result_at,
    status: t.status,
  }));
  if (missingFactors.some((m) => m.id === "weight"))
    monitoring.push({ test: "Weight", medicines: [], onFile: null, status: "no_result" });
  const products = Object.values(assessment.coverageReview ?? {}).length
    ? (assessment.coverage ?? []).filter((c) => assessment.coverageReview?.[c.id]).map((c) => c.text)
    : [];
  const notChecked: T.NotChecked = {
    productsWithoutRules: [...new Set(products)],
    unconfirmedFacts: assessment.excludedFacts.length + ex.unparsed.length,
    rulesAwaitingReview: assessment.contextual.rulesReviewed ? 0 : 8,
    checkInAnswers: true,
    other: assessment.contextual.what_we_couldnt_check.some((c) => c.reason === "condition")
      ? [`Conditions are recorded but no rule reads them: ${assessment.contextual.what_we_couldnt_check.filter((c) => c.reason === "condition").map((c) => c.item).join(", ")}.`]
      : [],
  };
  const egfrFact = assessment.includedFacts.find((f) => f.kind === "lab" && /^egfr /i.test(f.label));
  const egfr = egfrFact ? { value: Number(egfrFact.label.split(" ")[1]), date: egfrFact.detail.split(" · ")[1] ?? "" } : null;
  assessment.contextual = withEngineOptions(
    assessment.contextual,
    Object.fromEntries(Object.values(details).map((d) => [d.findingId, { title: d.clinicianTitle, explanation: d.clinicianExplanation }])),
    reactions,
    meds.map((m) => ({ id: m.id, label: m.label, detail: m.detail })),
    notCheckedLines(notChecked, assessment.contextual.rulesReviewed),
    egfr && Number.isFinite(egfr.value) ? egfr : null,
  );
  const before = assessment.baseline.findings.length;
  const after = assessment.contextual.findings.length;
  const added = assessment.addedRuleIds.length;
  const urgentIds = assessment.contextual.findings.filter((f) => f.tier === "get_help_now").map((f) => f.id);
  return {
    id: nextId("chk"),
    patientId: ex.patientId,
    extractionId: ex.id,
    revision: ex.revision,
    ranAt: new Date().toISOString(),
    ranBy,
    checkedOn: new Date(Date.now() + 3_600_000).toISOString().slice(0, 10),
    summary: {
      before,
      after,
      added,
      headline: added ? `Personalized: ${added} ${added === 1 ? "finding" : "findings"} the medication list alone did not show.` : after ? `${after} findings for clinician review.` : "No findings. This does not mean the prescription is safe.",
      subline: "Each finding links back to the record lines it came from. The clinician makes every decision.",
    },
    urgentReview: { show: urgentIds.length > 0, findingIds: urgentIds, line: urgentIds.length ? "Urgent review: phone the patient before the next dose." : null },
    latest: true,
    newInformation: null,
    informationTool: "MARMAR Context shows information and where it came from. The clinician makes every decision.",
    assessment,
    recordTitles: Object.fromEntries(records.map((r) => [r.id, r.title])),
    details,
    marks: [],
    missingFactors,
    newMedicineIds: meds.filter((m) => newMedicineNames.includes(m.label.toLowerCase())).map((m) => m.id),
    monitoring,
    notChecked,
    withheldSome,
    versions: {
      engine: assessment.contextual.engineVersion,
      rules: assessment.contextual.rulesVersion,
      rulesReviewed: assessment.contextual.rulesReviewed,
      signedOffOnly: false,
      optionsVersion: null,
    },
  };
}

/** The printable page the backend answers for format=pdf (it has no PDF library): escaped text, no scripts. */
export function printableHtml(title: string, lines: string[]): string {
  const esc = (s: string) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${esc(title)}</title></head><body><h1>${esc(title)}</h1>${lines
    .map((l) => (l ? `<p>${esc(l)}</p>` : ""))
    .join("")}</body></html>`;
}

/** The readable export: patient, sources, decisions, both results, findings, marks, versions. */
export function exportLines(check: T.CheckRun, patientName: string, clinicianName: string, records: StoredRecord[]): string[] {
  const a = check.assessment;
  const lines = [
    `Patient: ${patientName}`,
    `Checked: ${check.ranAt} by ${clinicianName}`,
    `Engine ${check.versions.engine}, rules ${check.versions.rules}${check.versions.rulesReviewed ? "" : " (rules not yet reviewed)"}`,
    "",
    `The medication list alone: ${a.baseline.findings.length} findings. Personalized: ${a.contextual.findings.length} findings.`,
    a.contextual.findings.length ? "" : "No findings. This does not mean the prescription is safe.",
    "Findings, for clinician review:",
    ...a.contextual.findings.flatMap((f) => {
      const d = check.details[f.id];
      const mark = check.marks.filter((m) => m.findingId === f.id).at(-1);
      return [
        `- ${d?.clinicianTitle ?? f.title} (${f.tier})`,
        `  ${d?.clinicianExplanation ?? f.clinical}`,
        ...(d?.factsUsed ?? []).map((x) => `  Based on: ${x.label}, ${x.recordTitle} line ${x.line}: "${x.quote}"`),
        ...(d?.references ?? []).map((r) => `  Reference: ${r.name}${r.version ? `, ${r.version}` : ", version not recorded"}`),
        mark ? `  Marked ${mark.kind.replace("_", " ")} by ${mark.by}${mark.reason ? `: ${mark.reason}` : ""}` : "  Not marked",
      ];
    }),
    "",
    "Records and review decisions:",
    ...records.map((r) => `- ${r.title}${r.place ? `, ${r.place}` : ""}${r.recordDate ? `, ${r.recordDate}` : ""}`),
    ...a.includedFacts.map((f) => `  Included: ${f.label} (${f.detail})`),
    ...a.excludedFacts.map((f) => `  Excluded: ${f.label} (${f.detail})`),
    "",
    "Not checked:",
    ...check.notChecked.productsWithoutRules.map((p) => `- ${p}: recorded, no rules for this product yet`),
    `- ${check.notChecked.unconfirmedFacts} facts or lines not confirmed for the check`,
    `- ${check.notChecked.rulesAwaitingReview} rule changes waiting for clinical review`,
    "- Answers from check-ins: no rule reads them",
    ...check.notChecked.other.map((o) => `- ${o}`),
    "",
    "MARMAR Context shows information and where it came from. The clinician makes every decision.",
  ];
  return lines;
}