fix(ai): keep lab values out of every model prompt while Labs is off

The Coach snapshot attached its labs block regardless of the module, a
decision from before Labs could be switched off, while the snapshot's own
prelude says a disabled domain never reaches the model. With Labs off, the
Coach prompt, its get_labs tool, the daily briefing's flagged-marker block,
the biomarker status note, the reaction line to a new panel and the MCP
lab_trend_brief prompt all still read lab values for a model.

Each of those reads now asks the labs module first and reads nothing when it
is off: the Coach tool answers module_disabled, the briefing omits the
block, the status note neither writes nor serves text, the reaction line
gets no value, and the MCP prompt refuses like get_labs and the doctor-visit
prompt already did. The data routes stay as they are. A structural guard
holds every lab-table reader under the model-input paths to the gate or to
a listed reason its rows never reach a model.
This commit is contained in:
Marc-André Bombeck committed 2026-10-25 13:52:04 +01:00
1 parent 6fbfe71955
commit fa50bcd1cb
20 files changed
+390 -41

No files matched your search

@@ -68,7 +68,7 @@
"lib/jobs/insight-pregenerate.ts": 1189,
"lib/jobs/medication-intake-import.ts": 819,
"lib/jobs/offhost-backup.ts": 1147,
"lib/jobs/reaction-line.ts": 824,
"lib/jobs/reaction-line.ts": 803,
"lib/jobs/reminder/medication-reminder-check.ts": 816,
"lib/managed-profiles/handover.ts": 986,
"lib/mcp/record-search.ts": 1467,
@@ -0,0 +1,112 @@
/**
* Structural guard: no lab value reaches a model while the labs module is off.
*
* ## The failure this freezes
*
* The Coach snapshot carried a labs block "intentionally not module-gated", a
* decision from before Labs could be switched off. Once it could, the Coach
* prompt, its `get_labs` tool, the daily briefing, the biomarker status note,
* the reaction line to a new panel and the MCP lab-trend prompt all kept
* reading lab values for the model after the owner had turned the module
* off, while the snapshot's own prelude promised that "the model never sees
* a disabled domain". Each read was a separate module with its own
* `prisma.labResult` query, and nothing tied them to the switch.
*
* ## What it proves and what it does not
*
* Every module under the paths that build model input (`src/lib/ai`,
* `src/lib/insights`, `src/lib/jobs`, `src/lib/mcp`) that reads the lab
* table either asks the labs module itself (`isModuleEnabled(…, "labs")`,
* the `correlation:LAB_DRAWS` surface, or the MCP `whenModuleOn` wrapper) or
* is listed below with the reason its read cannot reach a prompt. A new
* reader fails here until it does one or the other. The data routes are not
* in scope: a module decides what a model is given, not what the owner can
* read (`CLAUDE.md`, "An AI capability gates model work").
*
* It is grep-shaped: it sees a read, not where the rows go, and a reader in
* another directory is outside it. The behaviour is pinned per path by the
* module-off cases in the suites of the readers themselves.
*/
import { describe, expect, it } from "vitest";
import { readSourceFolder, stripComments } from "./helpers/source-files";
const ROOTS = ["src/lib/ai", "src/lib/insights", "src/lib/jobs", "src/lib/mcp"];
/** A read of the lab table: the Prisma delegate or raw SQL naming it. */
const LAB_READ =
/\.labResult\s*\.\s*(?:findMany|findFirst|findUnique|count|aggregate|groupBy)\s*\(|\blab_results\b/;
/** What counts as asking the labs module. */
const LABS_GATE =
/isModuleEnabled\(\s*[\w.]+\s*,\s*"labs"\s*\)|"correlation:LAB_DRAWS"|whenModuleOn\(/;
/** Readers whose rows never reach a model, each with the reason. */
const NOT_MODEL_INPUT: Record<string, string> = {
"src/lib/jobs/lab-biomarker-backfill.ts":
"links stored rows to their biomarker; no model is involved",
"src/lib/jobs/soft-delete-retention.ts":
"purges rows past the trash window; no model is involved",
"src/lib/insights/correlation-channel-series.ts":
"`fetchLabDraws`: both callers gate it, the Coach on correlation:LAB_DRAWS and the correlations data route on the same surface",
"src/lib/mcp/rich-reads.ts":
"`getLabHistory`: reached only through the MCP get_labs tool, which runs behind whenModuleOn",
"src/lib/ai/coach/tools/availability.ts":
"counts lab rows for the Coach's empty-read answer; the only lab subject comes from get_labs, which answers module_disabled first",
};
function labReaders(): Array<{ path: string; gated: boolean }> {
const out: Array<{ path: string; gated: boolean }> = [];
for (const root of ROOTS) {
for (const [rel, source] of readSourceFolder(root, {
floor: 20,
include: (r) =>
!r.split("/").includes("__tests__") &&
!/\.(?:test|spec)\.tsx?$/.test(r),
})) {
const code = stripComments(source);
if (!LAB_READ.test(code)) continue;
out.push({ path: `${root}/${rel}`, gated: LABS_GATE.test(code) });
}
}
return out.sort((a, b) => a.path.localeCompare(b.path));
}
describe("labs reach a model only with the labs module on", () => {
const readers = labReaders();
it("finds the readers it exists to check", () => {
// An empty match set would pass every assertion below vacuously.
expect(readers.length).toBeGreaterThanOrEqual(8);
expect(readers.filter((r) => r.gated).length).toBeGreaterThanOrEqual(6);
});
it("every reader asks the labs module or is listed as not model input", () => {
const open = readers
.filter((r) => !r.gated && !(r.path in NOT_MODEL_INPUT))
.map((r) => r.path);
expect(open).toEqual([]);
});
it("lists no reader that has since gained the gate or gone", () => {
const paths = new Map(readers.map((r) => [r.path, r.gated]));
const stale = Object.keys(NOT_MODEL_INPUT).filter(
(path) => paths.get(path) !== false,
);
expect(stale).toEqual([]);
});
it("the matchers catch what they exist to catch", () => {
expect(LAB_READ.test("await prisma.labResult.findMany({")).toBe(true);
expect(LAB_READ.test("tx.labResult\n .count({")).toBe(true);
expect(LAB_READ.test("SELECT 1 FROM lab_results")).toBe(true);
expect(LAB_READ.test("prisma.labResultDocumentLink.findMany(")).toBe(false);
expect(LABS_GATE.test('await isModuleEnabled(userId, "labs")')).toBe(true);
expect(LABS_GATE.test('await isModuleEnabled(ctx.userId, "labs")')).toBe(
true,
);
expect(LABS_GATE.test('isModuleEnabled(userId, "medications")')).toBe(
false,
);
});
});