test(labs): check the labs gate per read, and only the labs module's

The guard asked whether a file that reads the lab table mentions the gate
anywhere, so one ungated read in a file with a gated one passed, and
whenModuleOn for any module counted as the labs gate. It now walks the syntax
tree: each read must sit in a function that asks the labs module, or in a
private helper whose every caller in the file does, or be listed by file and
function with its reason. whenModuleOn counts only with "labs".
This commit is contained in:
Marc-André Bombeck committed 2026-10-25 17:44:31 +01:00
1 parent 70df2009d9
commit 698312863d
1 file changed
+228 -45
+228 -45
View File
@@ -14,49 +14,197 @@
*
* ## 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").
* Every lab read under the paths that build model input (`src/lib/ai`,
* `src/lib/insights`, `src/lib/jobs`, `src/lib/mcp`) either sits in a
* function that asks the labs module itself (`isModuleEnabled(…, "labs")`,
* the `correlation:LAB_DRAWS` surface, or the MCP `whenModuleOn(…, "labs")`
* wrapper), or sits in a private helper of its file whose every caller in
* that file does, or is listed below by file and function with the reason
* its rows cannot reach a prompt. A new reader fails here until it does one
* of those. 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.
* The check is per read, not per file. It used to ask whether a file that
* reads the table mentions the gate anywhere, so one ungated read in a file
* with a gated one passed, and `whenModuleOn` for any module counted as the
* labs gate. It still sees a read and the function around it, not where the
* rows go: a gate in the function is taken to cover the read, 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 ts from "typescript";
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/;
const READ_METHODS = new Set([
"findMany",
"findFirst",
"findUnique",
"count",
"aggregate",
"groupBy",
]);
/** Raw SQL naming the lab table. */
const LAB_TABLE = /\blab_results\b/;
/** What counts as asking the labs module. */
const LABS_GATE =
/isModuleEnabled\(\s*[\w.]+\s*,\s*"labs"\s*\)|"correlation:LAB_DRAWS"|whenModuleOn\(/;
/isModuleEnabled\(\s*[\w.]+\s*,\s*"labs"\s*\)|"correlation:LAB_DRAWS"|whenModuleOn\(\s*[\w.]+\s*,\s*"labs"/;
/** Readers whose rows never reach a model, each with the reason. */
/**
* Reads whose rows never reach a model, by `file#top-level name`, each with
* the reason.
*/
const NOT_MODEL_INPUT: Record<string, string> = {
"src/lib/jobs/lab-biomarker-backfill.ts":
"src/lib/jobs/lab-biomarker-backfill.ts#runLabBiomarkerBackfillForUser":
"links stored rows to their biomarker; no model is involved",
"src/lib/jobs/soft-delete-retention.ts":
"src/lib/jobs/lab-biomarker-backfill.ts#enqueueBootTimeLabBiomarkerBackfill":
"finds the accounts whose rows need linking; no model is involved",
"src/lib/jobs/soft-delete-retention.ts#SOFT_DELETE_RETENTION":
"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":
"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(ctx, "labs")',
"src/lib/ai/coach/tools/availability.ts#readTableBounds":
"counts lab rows for the Coach's empty-read answer; the only lab subject comes from get_labs, which answers module_disabled first",
"src/lib/ai/coach/tools/availability.ts#holdsRowsBefore":
"whether lab rows predate the lookback, for the same empty-read answer and the same get_labs subject",
};
function labReaders(): Array<{ path: string; gated: boolean }> {
const out: Array<{ path: string; gated: boolean }> = [];
type FunctionNode = ts.SignatureDeclaration & { body?: ts.Node };
function isReadCall(node: ts.Node): boolean {
if (!ts.isCallExpression(node)) return false;
const callee = node.expression;
if (!ts.isPropertyAccessExpression(callee)) return false;
if (!READ_METHODS.has(callee.name.text)) return false;
const target = callee.expression;
return (
ts.isPropertyAccessExpression(target) && target.name.text === "labResult"
);
}
function isTableLiteral(node: ts.Node): boolean {
return (
(ts.isStringLiteral(node) ||
ts.isNoSubstitutionTemplateLiteral(node) ||
ts.isTemplateHead(node) ||
ts.isTemplateMiddle(node) ||
ts.isTemplateTail(node)) &&
LAB_TABLE.test(node.text)
);
}
function enclosingFunctions(node: ts.Node): FunctionNode[] {
const out: FunctionNode[] = [];
for (let at = node.parent; at; at = at.parent) {
if (ts.isFunctionLike(at)) out.push(at as FunctionNode);
}
return out;
}
/** The statement directly under the file that holds `node`. */
function topLevelStatement(node: ts.Node): ts.Statement | null {
let at: ts.Node = node;
while (at.parent && !ts.isSourceFile(at.parent)) at = at.parent;
return at.parent ? (at as ts.Statement) : null;
}
function declaredName(statement: ts.Statement): string | null {
if (ts.isFunctionDeclaration(statement)) return statement.name?.text ?? null;
if (ts.isVariableStatement(statement)) {
const [first] = statement.declarationList.declarations;
return first && ts.isIdentifier(first.name) ? first.name.text : null;
}
return null;
}
function isExported(statement: ts.Statement): boolean {
return (
ts.canHaveModifiers(statement) &&
(ts.getModifiers(statement) ?? []).some(
(m) => m.kind === ts.SyntaxKind.ExportKeyword,
)
);
}
/**
* Whether the read at `node` is behind the labs gate: a function around it
* asks, or it sits in a private top-level helper every one of whose
* references in the file is itself behind the gate.
*/
function gated(
file: ts.SourceFile,
node: ts.Node,
seen: Set<string> = new Set(),
): boolean {
for (const fn of enclosingFunctions(node)) {
if (LABS_GATE.test(stripComments(fn.getText(file)))) return true;
}
const statement = topLevelStatement(node);
const name = statement ? declaredName(statement) : null;
if (!statement || !name || isExported(statement) || seen.has(name)) {
return false;
}
seen.add(name);
const references: ts.Node[] = [];
const visit = (at: ts.Node) => {
if (
ts.isIdentifier(at) &&
at.text === name &&
topLevelStatement(at) !== statement
) {
references.push(at);
}
ts.forEachChild(at, visit);
};
visit(file);
return (
references.length > 0 &&
references.every((ref) => gated(file, ref, new Set(seen)))
);
}
interface LabRead {
/** `file#top-level name`. */
site: string;
line: number;
gated: boolean;
}
function readsIn(path: string, source: string): LabRead[] {
const file = ts.createSourceFile(
path,
source,
ts.ScriptTarget.Latest,
true,
path.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
);
const out: LabRead[] = [];
const visit = (node: ts.Node) => {
if (isReadCall(node) || isTableLiteral(node)) {
const statement = topLevelStatement(node);
const name = (statement && declaredName(statement)) ?? "(top level)";
out.push({
site: `${path}#${name}`,
line: file.getLineAndCharacterOfPosition(node.getStart(file)).line + 1,
gated: gated(file, node),
});
}
ts.forEachChild(node, visit);
};
visit(file);
return out;
}
function labReads(): LabRead[] {
const out: LabRead[] = [];
for (const root of ROOTS) {
for (const [rel, source] of readSourceFolder(root, {
floor: 20,
@@ -64,49 +212,84 @@ function labReaders(): Array<{ path: string; gated: boolean }> {
!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) });
out.push(...readsIn(`${root}/${rel}`, source));
}
}
return out.sort((a, b) => a.path.localeCompare(b.path));
return out.sort((a, b) => a.site.localeCompare(b.site) || a.line - b.line);
}
describe("labs reach a model only with the labs module on", () => {
const readers = labReaders();
const reads = labReads();
it("finds the readers it exists to check", () => {
it("finds the reads 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);
expect(reads.length).toBeGreaterThanOrEqual(14);
expect(reads.filter((r) => r.gated).length).toBeGreaterThanOrEqual(8);
});
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);
it("every read asks the labs module or is listed as not model input", () => {
const open = reads
.filter((r) => !r.gated && !(r.site in NOT_MODEL_INPUT))
.map((r) => `${r.site}:${r.line}`);
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]));
it("lists no site that has since gained the gate or gone", () => {
const stale = Object.keys(NOT_MODEL_INPUT).filter(
(path) => paths.get(path) !== false,
(site) => !reads.some((r) => r.site === site && !r.gated),
);
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);
it("judges each read on its own, not the file it is in", () => {
const source = `
export async function gatedRead(userId: string) {
if (!(await isModuleEnabled(userId, "labs"))) return null;
return prisma.labResult.findMany({ where: { userId } });
}
export async function openRead(userId: string) {
return prisma.labResult.count({ where: { userId } });
}
async function helper(userId: string) {
return prisma.labResult.findFirst({ where: { userId } });
}
export async function callsHelperGated(userId: string) {
if (!(await isModuleEnabled(userId, "labs"))) return null;
return helper(userId);
}
async function helperTwo(userId: string) {
return prisma.$queryRaw\`SELECT 1 FROM lab_results\`;
}
export async function callsHelperTwo(userId: string) {
return helperTwo(userId);
}
export function medsOnly(ctx: Ctx) {
return whenModuleOn(ctx, "medications", "t", () =>
prisma.labResult.findMany({}),
);
}
`;
const verdicts = readsIn("fixture.ts", source).map(
(r) => `${r.site}:${r.gated}`,
);
expect(verdicts).toEqual([
"fixture.ts#gatedRead:true",
"fixture.ts#openRead:false",
"fixture.ts#helper:true",
"fixture.ts#helperTwo:false",
"fixture.ts#medsOnly:false",
]);
});
it("the gate matcher takes the labs module only", () => {
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('whenModuleOn(ctx, "labs", "get_labs"')).toBe(true);
expect(LABS_GATE.test('isModuleEnabled(userId, "medications")')).toBe(
false,
);
expect(LABS_GATE.test('whenModuleOn(ctx, "environment", "x"')).toBe(false);
});
});