style: apply prettier across the tree

One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
This commit is contained in:
Marc-André Bombeck committed 2026-06-19 22:20:31 +02:00
1 parent b3fea2ef59
commit 1f4ba5939d
1000 files changed
+12130 -8385

No files matched your search

@@ -90,9 +90,9 @@ describe("isTolerableRequestProbeError — narrow-catch classifier", () => {
expect(isTolerableRequestProbeError(new Error("private member #x"))).toBe(
false,
);
expect(
isTolerableRequestProbeError(new RangeError("private member")),
).toBe(false);
expect(isTolerableRequestProbeError(new RangeError("private member"))).toBe(
false,
);
expect(isTolerableRequestProbeError("private member string")).toBe(false);
expect(isTolerableRequestProbeError(null)).toBe(false);
expect(isTolerableRequestProbeError(undefined)).toBe(false);
+4 -5
View File
@@ -1,9 +1,6 @@
import { describe, it, expect } from "vitest";
import { z } from "zod/v4";
import {
returnAllZodIssues,
sanitiseZodIssues,
} from "../api-response";
import { returnAllZodIssues, sanitiseZodIssues } from "../api-response";
// v1.4.42 W2 — every Zod-validated route historically returned
// `parsed.error.issues[0].message`, dropping every issue past the first.
@@ -28,7 +25,9 @@ describe("returnAllZodIssues", () => {
const body = (await response.json()) as {
data: null;
error: string;
details: { issues: Array<{ path: string; code: string; message: string }> };
details: {
issues: Array<{ path: string; code: string; message: string }>;
};
};
expect(body.data).toBeNull();
expect(body.error).toBe("Validation failed");
+35 -8
View File
@@ -15,14 +15,41 @@ const PNG_1X1 = Buffer.from(
/** Minimal JPEG: SOI + APP0 + SOF0(1×1) + EOI. */
const JPEG_1X1 = Buffer.from([
0xff, 0xd8, // SOI
0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00,
0x01, 0x00, 0x01, 0x00, 0x00,
0xff, 0xc0, 0x00, 0x0b, 0x08,
0x00, 0x01, // height = 1
0x00, 0x01, // width = 1
0x01, 0x01, 0x11, 0x00,
0xff, 0xd9, // EOI
0xff,
0xd8, // SOI
0xff,
0xe0,
0x00,
0x10,
0x4a,
0x46,
0x49,
0x46,
0x00,
0x01,
0x01,
0x00,
0x00,
0x01,
0x00,
0x01,
0x00,
0x00,
0xff,
0xc0,
0x00,
0x0b,
0x08,
0x00,
0x01, // height = 1
0x00,
0x01, // width = 1
0x01,
0x01,
0x11,
0x00,
0xff,
0xd9, // EOI
]);
/**
Binary file not shown.
@@ -10,14 +10,7 @@
* `window` shape the helper actually reads, so the test runs under
* the default node environment without pulling in `jsdom`.
*/
import {
afterEach,
beforeEach,
describe,
expect,
it,
vi,
} from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
interface Globals {
document?: { cookie: string };
@@ -106,9 +106,9 @@ describe("formatDateOrRelative", () => {
});
it("accepts a Date instance as well as ISO string", () => {
expect(
formatDateOrRelative(new Date(NOW - 30 * MINUTE), fakeT, NOW),
).toBe("insights.relativeMinutesAgoOther:count=30");
expect(formatDateOrRelative(new Date(NOW - 30 * MINUTE), fakeT, NOW)).toBe(
"insights.relativeMinutesAgoOther:count=30",
);
});
// v1.4.49.2 — regression guard. The helper's `t()` calls must dispatch
@@ -121,16 +121,39 @@ describe("formatDateOrRelative", () => {
it("emits only keys that exist in the translation bundle", async () => {
const en = await import("../../../messages/en.json");
const bundleKeysCalled = new Set<string>();
const collectingT = (key: string, params?: Record<string, string | number>) => {
const collectingT = (
key: string,
params?: Record<string, string | number>,
) => {
bundleKeysCalled.add(key);
return params ? `${key}:${JSON.stringify(params)}` : key;
};
// Drive every relative bucket so we hit `t()` on each branch.
formatDateOrRelative(new Date(NOW - 30 * SECOND).toISOString(), collectingT, NOW);
formatDateOrRelative(new Date(NOW - 1 * MINUTE).toISOString(), collectingT, NOW);
formatDateOrRelative(new Date(NOW - 30 * MINUTE).toISOString(), collectingT, NOW);
formatDateOrRelative(new Date(NOW - 1 * HOUR).toISOString(), collectingT, NOW);
formatDateOrRelative(new Date(NOW - 12 * HOUR).toISOString(), collectingT, NOW);
formatDateOrRelative(
new Date(NOW - 30 * SECOND).toISOString(),
collectingT,
NOW,
);
formatDateOrRelative(
new Date(NOW - 1 * MINUTE).toISOString(),
collectingT,
NOW,
);
formatDateOrRelative(
new Date(NOW - 30 * MINUTE).toISOString(),
collectingT,
NOW,
);
formatDateOrRelative(
new Date(NOW - 1 * HOUR).toISOString(),
collectingT,
NOW,
);
formatDateOrRelative(
new Date(NOW - 12 * HOUR).toISOString(),
collectingT,
NOW,
);
expect(bundleKeysCalled.size).toBeGreaterThan(0);
// `insights.*` carries a mix of string and nested-object children
// (e.g. `personalRecord: { badge, tooltip }`), so the bundle type
+7 -5
View File
@@ -75,10 +75,8 @@ vi.mock("node:fs", async () => {
return false;
};
const readFileSync = (p: string) => {
if (p.endsWith("GeoLite2-City.mmdb"))
return Buffer.from("city-mmdb-stub");
if (p.endsWith("GeoLite2-ASN.mmdb"))
return Buffer.from("asn-mmdb-stub");
if (p.endsWith("GeoLite2-City.mmdb")) return Buffer.from("city-mmdb-stub");
if (p.endsWith("GeoLite2-ASN.mmdb")) return Buffer.from("asn-mmdb-stub");
throw new Error(`unexpected read ${p}`);
};
return {
@@ -219,7 +217,11 @@ describe("lookupIpLocation — offline-first city resolver (v1.4.27 B3)", () =>
const fetchSpy = vi.fn().mockResolvedValue(
new Response(
new TextEncoder().encode(
JSON.stringify({ success: true, city: "Hamburg", country_code: "DE" }),
JSON.stringify({
success: true,
city: "Hamburg",
country_code: "DE",
}),
).buffer as ArrayBuffer,
{
status: 200,
@@ -98,7 +98,10 @@ describe("offline-geo runtime detection (v1.4.27 R5)", () => {
});
it("offlineGeoReady() is true when the City MMDB is present and no marker", async () => {
writeFileSync(join(tmpRoot, "GeoLite2-City.mmdb"), Buffer.from("city-stub"));
writeFileSync(
join(tmpRoot, "GeoLite2-City.mmdb"),
Buffer.from("city-stub"),
);
const { offlineGeoReady } = await import("../geo");
expect(offlineGeoReady()).toBe(true);
});
@@ -130,7 +133,10 @@ describe("offline-geo runtime detection (v1.4.27 R5)", () => {
});
it("does NOT fire the notification when the City MMDB is present", async () => {
writeFileSync(join(tmpRoot, "GeoLite2-City.mmdb"), Buffer.from("city-stub"));
writeFileSync(
join(tmpRoot, "GeoLite2-City.mmdb"),
Buffer.from("city-stub"),
);
// The stubbed reader returns null for every IP, which is fine — the
// lookup will still go through the online path, but the offline
// check sees a healthy directory so the notification stays silent.
+5 -15
View File
@@ -107,16 +107,12 @@ describe("getClientIp trusted-proxy semantics (V3 audit)", () => {
// pollute the rate-limit / audit log with non-IP strings; they are
// now rejected at the helper.
it("rejects structurally invalid IPv4 candidates (1.2)", () => {
const ip = getClientIp(
makeRequest({ "x-forwarded-for": "1.2, 5.6.7.8" }),
);
const ip = getClientIp(makeRequest({ "x-forwarded-for": "1.2, 5.6.7.8" }));
expect(ip).toBe("5.6.7.8");
});
it("rejects structurally invalid IPv6 candidates (:::)", () => {
const ip = getClientIp(
makeRequest({ "x-forwarded-for": ":::, 5.6.7.8" }),
);
const ip = getClientIp(makeRequest({ "x-forwarded-for": ":::, 5.6.7.8" }));
expect(ip).toBe("5.6.7.8");
});
@@ -129,9 +125,7 @@ describe("getClientIp trusted-proxy semantics (V3 audit)", () => {
it("accepts well-formed IPv6 addresses", () => {
process.env.TRUST_PROXY_HOPS = "1";
const ip = getClientIp(
makeRequest({ "x-forwarded-for": "2001:db8::1" }),
);
const ip = getClientIp(makeRequest({ "x-forwarded-for": "2001:db8::1" }));
expect(ip).toBe("2001:db8::1");
});
@@ -294,12 +288,8 @@ describe("getClientIpOrTrustWarning shape (F-6, 2026-05-16)", () => {
process.env.TRUST_PROXY_HOPS = "2";
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
try {
getClientIpOrTrustWarning(
makeRequest({ "x-forwarded-for": "5.6.7.8" }),
);
getClientIpOrTrustWarning(
makeRequest({ "x-forwarded-for": "5.6.7.8" }),
);
getClientIpOrTrustWarning(makeRequest({ "x-forwarded-for": "5.6.7.8" }));
getClientIpOrTrustWarning(makeRequest({ "x-forwarded-for": "5.6.7.8" }));
expect(warn).toHaveBeenCalledTimes(1);
} finally {
warn.mockRestore();
+8 -2
View File
@@ -189,7 +189,9 @@ describe("serializeInsightsLayout — defaults + dense order", () => {
tiles: [{ id: "overview", visible: true, order: 0 }],
});
// Sorted by input order, then re-numbered 0,1,2.
expect(serialized.sections.map((s) => ({ id: s.id, order: s.order }))).toEqual([
expect(
serialized.sections.map((s) => ({ id: s.id, order: s.order })),
).toEqual([
{ id: "trends", order: 0 },
{ id: "vitals", order: 1 },
{ id: "signals", order: 2 },
@@ -333,7 +335,11 @@ describe("orderedVisibleSectionIds — W2 render-order helper", () => {
const visible = orderedVisibleSectionIds(layout);
// The three explicit sections lead in their saved order; the rest
// auto-merge invisible (new-id semantics), so they do NOT appear.
expect(visible.slice(0, 3)).toEqual(["trends", "vitals", "wellness-scores"]);
expect(visible.slice(0, 3)).toEqual([
"trends",
"vitals",
"wellness-scores",
]);
});
});
+9 -3
View File
@@ -33,7 +33,10 @@ describe("queryKeys factory", () => {
});
it("threads the slim slice through queryKeys.analytics", () => {
expect(queryKeys.analytics("summaries")).toEqual(["analytics", "summaries"]);
expect(queryKeys.analytics("summaries")).toEqual([
"analytics",
"summaries",
]);
});
it("keeps the no-arg call byte-identical to the legacy literal", () => {
@@ -238,7 +241,9 @@ describe("dependent-key bundles", () => {
// (audit-L4).
it("medicationDependentKeys bundles the dashboard-medication-compliance prefix (v1.4.40)", () => {
const keyStrings = medicationDependentKeys.map((k) => JSON.stringify(k));
expect(keyStrings).toContain(JSON.stringify(["dashboard-medication-compliance"]));
expect(keyStrings).toContain(
JSON.stringify(["dashboard-medication-compliance"]),
);
});
// v1.5.5 D-3 §10 invariant 20 — the per-medication inline compliance
@@ -334,7 +339,8 @@ describe("queryKey factory enforcement", () => {
}
it("no guarded file declares a bare-literal `queryKey:` (factory enforcement)", () => {
const offenders: Array<{ file: string; line: number; snippet: string }> = [];
const offenders: Array<{ file: string; line: number; snippet: string }> =
[];
const files = collect();
for (const file of files) {
const text = readFileSync(file, "utf8");
+24 -7
View File
@@ -17,13 +17,22 @@ describe("reference-ranges backbone", () => {
it("every band + conflict + headline resolves a real citation", () => {
for (const metric of REFERENCE_METRICS) {
const range = REFERENCE_RANGES[metric];
expect(CITATIONS[range.referenceId], `${metric} referenceId`).toBeTruthy();
expect(
CITATIONS[range.referenceId],
`${metric} referenceId`,
).toBeTruthy();
expect(range.unit, `${metric} unit`).toBeTruthy();
expect(range.guidanceCaveat, `${metric} guidanceCaveat`).toBeTruthy();
for (const band of range.bands) {
expect(CITATIONS[band.citation], `${metric} band ${band.label}`).toBeTruthy();
expect(
CITATIONS[band.citation],
`${metric} band ${band.label}`,
).toBeTruthy();
// A band must pin at least one bound.
expect(band.low != null || band.high != null, `${metric} ${band.label}`).toBe(true);
expect(
band.low != null || band.high != null,
`${metric} ${band.label}`,
).toBe(true);
}
for (const conflict of range.conflicts ?? []) {
expect(CITATIONS[conflict.citation], `${metric} conflict`).toBeTruthy();
@@ -62,14 +71,18 @@ describe("reference-ranges backbone", () => {
const marked = bands.filter((b) => b.normal === true);
expect(marked, `${metric} normal-band count`).toHaveLength(1);
// `normalBandIndex` points at that marked band.
expect(bands[normalBandIndex(metric)].normal, `${metric} index`).toBe(true);
expect(bands[normalBandIndex(metric)].normal, `${metric} index`).toBe(
true,
);
}
});
it("resolves the normal band by marker, not by index 0", () => {
// BMI authors Underweight FIRST; the normal band is the 18.5–24.9 entry.
expect(REFERENCE_RANGES.BMI.bands[0].label).toBe("Underweight");
expect(REFERENCE_RANGES.BMI.bands[normalBandIndex("BMI")].label).toBe("Normal");
expect(REFERENCE_RANGES.BMI.bands[normalBandIndex("BMI")].label).toBe(
"Normal",
);
// BMI 22 is healthy → within (the index-0 bug reported it slightly-outside).
expect(classifyReference("BMI", 22)).toBe("within");
// BMI 17 is underweight → one band below normal → slightly-outside
@@ -93,10 +106,14 @@ describe("reference-ranges backbone", () => {
it("returns insufficient for absent values and band-less metrics", () => {
expect(classifyReference("RESTING_HEART_RATE", null)).toBe("insufficient");
expect(classifyReference("RESTING_HEART_RATE", Number.NaN)).toBe("insufficient");
expect(classifyReference("RESTING_HEART_RATE", Number.NaN)).toBe(
"insufficient",
);
// HRV carries no fixed population band — baseline-only.
expect(REFERENCE_RANGES.HEART_RATE_VARIABILITY.bands).toHaveLength(0);
expect(classifyReference("HEART_RATE_VARIABILITY", 42)).toBe("insufficient");
expect(classifyReference("HEART_RATE_VARIABILITY", 42)).toBe(
"insufficient",
);
});
it("steps backbone uses the canonical 8,000 green floor", () => {
+33 -39
View File
@@ -67,18 +67,16 @@ describe("pinnedPublicDispatcher", () => {
it("refuses a hostname that resolves to a private IP", async () => {
// Mock dns.lookup to return 169.254.169.254 (cloud metadata).
const lookupSpy = vi.spyOn(dns, "lookup");
lookupSpy.mockImplementation(
((
_hostname: string,
_opts: dns.LookupAllOptions,
callback: (
err: NodeJS.ErrnoException | null,
addresses: dns.LookupAddress[],
) => void,
) => {
callback(null, [{ address: "169.254.169.254", family: 4 }]);
}) as unknown as typeof dns.lookup,
);
lookupSpy.mockImplementation(((
_hostname: string,
_opts: dns.LookupAllOptions,
callback: (
err: NodeJS.ErrnoException | null,
addresses: dns.LookupAddress[],
) => void,
) => {
callback(null, [{ address: "169.254.169.254", family: 4 }]);
}) as unknown as typeof dns.lookup);
const dispatcher = getPinnedPublicDispatcher();
// Drive the dispatcher through `fetch` so the integration mirrors
@@ -100,18 +98,16 @@ describe("pinnedPublicDispatcher", () => {
// The expected outcome is therefore NOT ENOTFOUND but a connect
// failure further down the stack (the IP refuses or times out).
const lookupSpy = vi.spyOn(dns, "lookup");
lookupSpy.mockImplementation(
((
_hostname: string,
_opts: dns.LookupAllOptions,
callback: (
err: NodeJS.ErrnoException | null,
addresses: dns.LookupAddress[],
) => void,
) => {
callback(null, [{ address: "192.0.2.1", family: 4 }]);
}) as unknown as typeof dns.lookup,
);
lookupSpy.mockImplementation(((
_hostname: string,
_opts: dns.LookupAllOptions,
callback: (
err: NodeJS.ErrnoException | null,
addresses: dns.LookupAddress[],
) => void,
) => {
callback(null, [{ address: "192.0.2.1", family: 4 }]);
}) as unknown as typeof dns.lookup);
const dispatcher = getPinnedPublicDispatcher();
let caught: unknown;
@@ -135,21 +131,19 @@ describe("pinnedPublicDispatcher", () => {
// Mock dns.lookup to return one private + one public address. The
// dispatcher must drop the private one and pin to the public one.
const lookupSpy = vi.spyOn(dns, "lookup");
lookupSpy.mockImplementation(
((
_hostname: string,
_opts: dns.LookupAllOptions,
callback: (
err: NodeJS.ErrnoException | null,
addresses: dns.LookupAddress[],
) => void,
) => {
callback(null, [
{ address: "10.0.0.5", family: 4 },
{ address: "192.0.2.1", family: 4 },
]);
}) as unknown as typeof dns.lookup,
);
lookupSpy.mockImplementation(((
_hostname: string,
_opts: dns.LookupAllOptions,
callback: (
err: NodeJS.ErrnoException | null,
addresses: dns.LookupAddress[],
) => void,
) => {
callback(null, [
{ address: "10.0.0.5", family: 4 },
{ address: "192.0.2.1", family: 4 },
]);
}) as unknown as typeof dns.lookup);
const dispatcher = getPinnedPublicDispatcher();
let caught: unknown;
@@ -365,7 +365,8 @@ describe("Codex slug fallback chain", () => {
} finally {
if (origModel === undefined) delete process.env.CODEX_MODEL;
else process.env.CODEX_MODEL = origModel;
if (origChain === undefined) delete process.env.CODEX_MODEL_FALLBACK_CHAIN;
if (origChain === undefined)
delete process.env.CODEX_MODEL_FALLBACK_CHAIN;
else process.env.CODEX_MODEL_FALLBACK_CHAIN = origChain;
}
});
+4 -1
View File
@@ -49,7 +49,10 @@ describe("chainRequiresServerManagedConsent", () => {
true,
);
expect(
chainRequiresServerManagedConsent([entry("openai"), entry("admin-openai")]),
chainRequiresServerManagedConsent([
entry("openai"),
entry("admin-openai"),
]),
).toBe(true);
});
@@ -28,10 +28,7 @@ describe("isLocalAiHostAllowed", () => {
});
it("permits ONLY the listed hosts when given a comma-separated allowlist", () => {
vi.stubEnv(
"ALLOW_LOCAL_AI_PRIVATE_HOSTS",
"ollama.lan, 10.0.0.5",
);
vi.stubEnv("ALLOW_LOCAL_AI_PRIVATE_HOSTS", "ollama.lan, 10.0.0.5");
expect(isLocalAiHostAllowed("http://ollama.lan:11434/v1")).toBe(true);
expect(isLocalAiHostAllowed("http://10.0.0.5:11434/v1")).toBe(true);
// A different private host is still rejected — the metadata endpoint and
+7 -1
View File
@@ -57,7 +57,13 @@ describe("coach clusters", () => {
it("expands the default clusters to the legacy core sources + additive members", () => {
const sources = clusterSourcesFromPrefs(undefined);
// Legacy five domains' sources are all present.
for (const core of ["bp", "weight", "pulse", "mood", "compliance"] as const) {
for (const core of [
"bp",
"weight",
"pulse",
"mood",
"compliance",
] as const) {
expect(sources.has(core)).toBe(true);
}
// Additive members ride inside cardio/body.
@@ -121,7 +121,8 @@ describe("refreshConversationSummary", () => {
});
it("(c) merge path passes the decrypted prior summary into the user prompt", async () => {
const prior = "Earlier: user wants better sleep and dislikes evening caffeine.";
const prior =
"Earlier: user wants better sleep and dislikes evening caffeine.";
// 30 turns, no prior summaryTurnCount → forces a generation.
const { client } = makePrisma(
makeConversation(30, {
@@ -162,7 +163,9 @@ describe("refreshConversationSummary", () => {
it("(e) success path encrypts and persists summaryEncrypted + turn count + updatedAt", async () => {
const now = new Date("2026-06-04T08:00:00.000Z");
const { client, update } = makePrisma(makeConversation(30, { summaryTurnCount: 0 }));
const { client, update } = makePrisma(
makeConversation(30, { summaryTurnCount: 0 }),
);
const runCompletion = vi.fn().mockResolvedValue(okCompletion);
const result = await refreshConversationSummary("conv-1", "user-1", {
@@ -185,9 +188,7 @@ describe("refreshConversationSummary", () => {
describe("buildSummaryUserPrompt", () => {
it("uses (none) when no prior summary is supplied", () => {
const out = buildSummaryUserPrompt(null, [
{ role: "user", content: "hi" },
]);
const out = buildSummaryUserPrompt(null, [{ role: "user", content: "hi" }]);
expect(out).toContain("PRIOR SUMMARY\n(none)");
expect(out).toContain("user: hi");
});
@@ -73,7 +73,9 @@ function rhrMeasurements() {
out.push({
type: "RESTING_HEART_RATE",
value: v + (i % 3) * 0.5,
measuredAt: new Date(`2026-03-${String(i).padStart(2, "0")}T12:00:00.000Z`),
measuredAt: new Date(
`2026-03-${String(i).padStart(2, "0")}T12:00:00.000Z`,
),
source: "APPLE_HEALTH",
deviceType: null,
});
@@ -176,6 +178,8 @@ describe("buildCycleSnapshotBlock", () => {
expect(block!.phaseInsight).not.toBeNull();
expect(block!.phaseInsight!.metric).toBe("resting heart rate");
expect(block!.phaseInsight!.delta).toBeGreaterThan(0);
expect(block!.phaseInsight!.interpretation).toMatch(/descriptive pattern, not a cause/);
expect(block!.phaseInsight!.interpretation).toMatch(
/descriptive pattern, not a cause/,
);
});
});
@@ -1,7 +1,8 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("@/lib/insights/derived", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/insights/derived")>();
const actual =
await importOriginal<typeof import("@/lib/insights/derived")>();
return { ...actual, computeDerivedMetric: vi.fn() };
});
@@ -14,8 +15,9 @@ import { prisma } from "@/lib/db";
import { buildDerivedSnapshotBlock } from "../derived-snapshot";
const compute = computeDerivedMetric as unknown as ReturnType<typeof vi.fn>;
const whoopFindFirst = prisma.measurement
.findFirst as unknown as ReturnType<typeof vi.fn>;
const whoopFindFirst = prisma.measurement.findFirst as unknown as ReturnType<
typeof vi.fn
>;
/** Make the WHOOP-native recovery probe report present / absent. */
function setWhoopNativePresent(present: boolean) {
whoopFindFirst.mockResolvedValue(present ? { id: "whoop-1" } : null);
@@ -29,13 +31,28 @@ function ok(value: Record<string, unknown>, historyDays = 14) {
value,
coverage: { requiredInputs: 1, presentInputs: 1, historyDays, missing: [] },
confidence: { score: 88, band: "high" as const },
provenance: { inputs: [], source: "DAY" as const, windowDays: 14, computedAt: "x" },
provenance: {
inputs: [],
source: "DAY" as const,
windowDays: 14,
computedAt: "x",
},
};
}
const insufficient = {
status: "insufficient" as const,
coverage: { requiredInputs: 1, presentInputs: 0, historyDays: 0, missing: [] },
provenance: { inputs: [], source: "none" as const, windowDays: 0, computedAt: "x" },
coverage: {
requiredInputs: 1,
presentInputs: 0,
historyDays: 0,
missing: [],
},
provenance: {
inputs: [],
source: "none" as const,
windowDays: 0,
computedAt: "x",
},
reason: "no_score_in_window",
};
@@ -59,8 +76,10 @@ describe("buildDerivedSnapshotBlock", () => {
// alongside readiness (the dedup only drops the COMPUTED proxy).
setWhoopNativePresent(true);
compute.mockImplementation(async (args?: { metric?: string }) => {
if (args?.metric === "READINESS") return ok({ score: 64, band: "yellow" });
if (args?.metric === "RECOVERY_SCORE") return ok({ score: 72, band: "green" });
if (args?.metric === "READINESS")
return ok({ score: 64, band: "yellow" });
if (args?.metric === "RECOVERY_SCORE")
return ok({ score: 72, band: "green" });
return insufficient;
});
const block = await buildDerivedSnapshotBlock("u1", PROFILE, NOW);
@@ -87,8 +106,10 @@ describe("buildDerivedSnapshotBlock", () => {
// same number twice, so the recovery line is dropped.
setWhoopNativePresent(false);
compute.mockImplementation(async (args?: { metric?: string }) => {
if (args?.metric === "READINESS") return ok({ score: 64, band: "yellow" });
if (args?.metric === "RECOVERY_SCORE") return ok({ score: 64, band: "yellow" });
if (args?.metric === "READINESS")
return ok({ score: 64, band: "yellow" });
if (args?.metric === "RECOVERY_SCORE")
return ok({ score: 64, band: "yellow" });
return insufficient;
});
const block = await buildDerivedSnapshotBlock("u1", PROFILE, NOW);
@@ -103,8 +124,10 @@ describe("buildDerivedSnapshotBlock", () => {
// source-aware drop keeps it because a WHOOP-native row exists.
setWhoopNativePresent(true);
compute.mockImplementation(async (args?: { metric?: string }) => {
if (args?.metric === "READINESS") return ok({ score: 64, band: "yellow" });
if (args?.metric === "RECOVERY_SCORE") return ok({ score: 64, band: "yellow" });
if (args?.metric === "READINESS")
return ok({ score: 64, band: "yellow" });
if (args?.metric === "RECOVERY_SCORE")
return ok({ score: 64, band: "yellow" });
return insufficient;
});
const block = await buildDerivedSnapshotBlock("u1", PROFILE, NOW);
@@ -115,8 +138,10 @@ describe("buildDerivedSnapshotBlock", () => {
it("keeps RECOVERY_SCORE when a WHOOP-native value differs from READINESS", async () => {
setWhoopNativePresent(true);
compute.mockImplementation(async (args?: { metric?: string }) => {
if (args?.metric === "READINESS") return ok({ score: 64, band: "yellow" });
if (args?.metric === "RECOVERY_SCORE") return ok({ score: 80, band: "green" });
if (args?.metric === "READINESS")
return ok({ score: 64, band: "yellow" });
if (args?.metric === "RECOVERY_SCORE")
return ok({ score: 80, band: "green" });
return insufficient;
});
const block = await buildDerivedSnapshotBlock("u1", PROFILE, NOW);
@@ -127,12 +152,18 @@ describe("buildDerivedSnapshotBlock", () => {
it("isolates a per-metric compute failure", async () => {
compute.mockImplementation(async (args?: { metric?: string }) => {
if (args?.metric === "READINESS") throw new Error("boom");
if (args?.metric === "SLEEP_SCORE") return ok({ score: 80, band: "green" });
if (args?.metric === "SLEEP_SCORE")
return ok({ score: 80, band: "green" });
return insufficient;
});
const block = await buildDerivedSnapshotBlock("u1", PROFILE, NOW);
expect(block).toEqual({
SLEEP_SCORE: { value: 80, band: "green", confidence: 88, historyDays: 14 },
SLEEP_SCORE: {
value: 80,
band: "green",
confidence: 88,
historyDays: 14,
},
});
});
});
+25 -8
View File
@@ -59,15 +59,22 @@ describe("buildCoachFactsBlock", () => {
});
expect(block).not.toBeNull();
expect(block!.facts.map((f) => f.text)).toEqual(["newer high", "older high", "low"]);
expect(block!.facts.map((f) => f.text)).toEqual([
"newer high",
"older high",
"low",
]);
});
it("takes only the top N", async () => {
const seed: SeedRow[] = Array.from({ length: FACTS_INJECT_TOP_N + 4 }, (_, i) => ({
text: `fact ${i}`,
confidence: 100 - i,
updatedAt: new Date(2026, 0, 1 + i),
}));
const seed: SeedRow[] = Array.from(
{ length: FACTS_INJECT_TOP_N + 4 },
(_, i) => ({
text: `fact ${i}`,
confidence: 100 - i,
updatedAt: new Date(2026, 0, 1 + i),
}),
);
const { prisma } = makeFakePrisma(seed);
const block = await buildCoachFactsBlock("user-1", {
@@ -92,8 +99,18 @@ describe("buildCoachFactsBlock", () => {
it("skips undecryptable rows fault-isolated (never throws)", async () => {
// One row whose codec throws on decrypt; the helper must skip it.
const findMany = vi.fn(async () => [
{ factEncrypted: bytes("good fact"), category: "goal", confidence: 80, updatedAt: new Date() },
{ factEncrypted: bytes("__throw__"), category: "goal", confidence: 70, updatedAt: new Date() },
{
factEncrypted: bytes("good fact"),
category: "goal",
confidence: 80,
updatedAt: new Date(),
},
{
factEncrypted: bytes("__throw__"),
category: "goal",
confidence: 70,
updatedAt: new Date(),
},
]);
// Re-point the codec mock to throw for the sentinel payload.
const codec = await import("../bytes-codec");
@@ -85,8 +85,10 @@ describe("extractDeterministicFacts", () => {
it("matches German intolerance statements", () => {
expect(
extractDeterministicFacts("ich habe eine Laktoseunverträglichkeit", "de")[0]
?.fact,
extractDeterministicFacts(
"ich habe eine Laktoseunverträglichkeit",
"de",
)[0]?.fact,
).toContain("Laktose");
expect(
extractDeterministicFacts("ich bin laktoseintolerant", "de")[0]?.fact,
@@ -139,7 +141,10 @@ describe("extractDeterministicFacts", () => {
[],
);
expect(
extractDeterministicFacts("meine Schwester hat eine Katzenhaarallergie", "de"),
extractDeterministicFacts(
"meine Schwester hat eine Katzenhaarallergie",
"de",
),
).toEqual([]);
expect(
extractDeterministicFacts("what is an allergy exactly?", "en"),
+55 -11
View File
@@ -47,7 +47,12 @@ function bytes(s: string): Uint8Array {
}
function makeFakePrisma(opts: {
activeFacts?: Array<{ id?: string; text: string; category?: string; confidence?: number }>;
activeFacts?: Array<{
id?: string;
text: string;
category?: string;
confidence?: number;
}>;
turns?: Array<{ role: string; content: string }>;
conversationExists?: boolean;
}) {
@@ -87,7 +92,13 @@ function makeFakePrisma(opts: {
}
function ok(content: string): StatusProviderResult {
return { kind: "ok", content, providerType: "mock", model: "m", tokensUsed: 1 };
return {
kind: "ok",
content,
providerType: "mock",
model: "m",
tokensUsed: 1,
};
}
beforeEach(() => {
@@ -97,12 +108,21 @@ beforeEach(() => {
describe("extractAndStoreFacts", () => {
it("(a) parses durable facts and persists them field-by-field", async () => {
const { prisma, createCalls } = makeFakePrisma({
turns: [{ role: "user", content: "I prefer morning workouts and I'm vegetarian." }],
turns: [
{
role: "user",
content: "I prefer morning workouts and I'm vegetarian.",
},
],
});
const runCompletion = vi.fn(async () =>
ok(
JSON.stringify([
{ category: "preference", fact: "Prefers morning workouts", confidence: 90 },
{
category: "preference",
fact: "Prefers morning workouts",
confidence: 90,
},
{ category: "preference", fact: "Is vegetarian", confidence: 80 },
]),
),
@@ -120,7 +140,13 @@ describe("extractAndStoreFacts", () => {
// Field-by-field — exact key set, no spread of the parsed object.
for (const data of createCalls) {
expect(Object.keys(data).sort()).toEqual(
["category", "confidence", "factEncrypted", "sourceConversationId", "userId"].sort(),
[
"category",
"confidence",
"factEncrypted",
"sourceConversationId",
"userId",
].sort(),
);
expect(data.userId).toBe("user-1");
expect(data.sourceConversationId).toBe("conv-1");
@@ -128,7 +154,9 @@ describe("extractAndStoreFacts", () => {
}
// Annotation carries counts/ids only.
const extracted = annotateMock.mock.calls.find(
(c) => (c[0] as { action?: { name?: string } }).action?.name === "coach.facts.extracted",
(c) =>
(c[0] as { action?: { name?: string } }).action?.name ===
"coach.facts.extracted",
);
expect(extracted).toBeTruthy();
expect((extracted![0] as { meta: Record<string, unknown> }).meta).toEqual({
@@ -181,7 +209,9 @@ describe("extractAndStoreFacts", () => {
expect(res).toEqual({ status: "none", count: 0 });
expect(createCalls).toHaveLength(0);
const failed = annotateMock.mock.calls.find(
(c) => (c[0] as { action?: { name?: string } }).action?.name === "coach.facts.parse_failed",
(c) =>
(c[0] as { action?: { name?: string } }).action?.name ===
"coach.facts.parse_failed",
);
expect(failed).toBeTruthy();
expect((failed![0] as { meta: Record<string, unknown> }).meta).toEqual({
@@ -197,8 +227,16 @@ describe("extractAndStoreFacts", () => {
const runCompletion = vi.fn(async () =>
ok(
JSON.stringify([
{ category: "preference", fact: "prefers morning workouts", confidence: 90 }, // dup
{ category: "goal", fact: "Wants to run a 10k in autumn", confidence: 85 }, // new
{
category: "preference",
fact: "prefers morning workouts",
confidence: 90,
}, // dup
{
category: "goal",
fact: "Wants to run a 10k in autumn",
confidence: 85,
}, // new
]),
),
);
@@ -229,7 +267,11 @@ describe("extractAndStoreFacts", () => {
ok(
JSON.stringify([
{ category: "goal", fact: "low confidence newcomer", confidence: 20 }, // ≤30 → skip
{ category: "goal", fact: "high confidence newcomer", confidence: 95 }, // >30 → store
{
category: "goal",
fact: "high confidence newcomer",
confidence: 95,
}, // >30 → store
]),
),
);
@@ -251,7 +293,9 @@ describe("extractAndStoreFacts", () => {
const { prisma, createCalls } = makeFakePrisma({
turns: [{ role: "user", content: "talk" }],
});
const runCompletion = vi.fn(async () => ({ kind }) as StatusProviderResult);
const runCompletion = vi.fn(
async () => ({ kind }) as StatusProviderResult,
);
const res = await extractAndStoreFacts("conv-1", "user-1", {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
prisma: prisma as any,
@@ -93,7 +93,11 @@ describe("buildCoachMemoryBlock", () => {
expect(block?.priorNarrative?.headline).toContain("Ruhepuls");
expect(block?.priorNarrative?.drivers).toEqual([]);
expect(block?.trendMemory).toEqual({
HEART_RATE: { priorBand: "in", currentBand: "above", priorPeriod: "month" },
HEART_RATE: {
priorBand: "in",
currentBand: "above",
priorPeriod: "month",
},
WEIGHT: { priorBand: "in", currentBand: "in", priorPeriod: "month" },
});
});
@@ -9,8 +9,8 @@ const txCreate = {
};
vi.mock("@/lib/db", () => ({
prisma: {
$transaction: vi.fn(
async (cb: (tx: typeof txCreate) => Promise<unknown>) => cb(txCreate),
$transaction: vi.fn(async (cb: (tx: typeof txCreate) => Promise<unknown>) =>
cb(txCreate),
),
},
}));
@@ -40,7 +40,9 @@ const BRAND_TOKENS = [
describe("buildReferenceGroundingBlock", () => {
it("returns null when no metric is covered by the backbone", () => {
expect(buildReferenceGroundingBlock({ metrics: [], hasDiabetes: false })).toBeNull();
expect(
buildReferenceGroundingBlock({ metrics: [], hasDiabetes: false }),
).toBeNull();
});
it("cites the ESH 2023 source for blood pressure and never the US/ESC line", () => {
@@ -74,9 +76,7 @@ describe("buildReferenceGroundingBlock", () => {
})!;
expect(block).toContain("general guidance");
expect(block).toContain("not a diagnosis");
expect(block.toLowerCase()).toContain(
"not personal medical advice",
);
expect(block.toLowerCase()).toContain("not personal medical advice");
// The closing reminder line is present.
expect(block).toMatch(/general guidance, not personal medical advice\.$/);
});
@@ -150,7 +150,9 @@ describe("buildReferenceGroundingBlock", () => {
metrics: [{ metric: "PULSE_PRESSURE", value: 40 }],
hasDiabetes: false,
})!;
const line = block.split("\n").find((l) => l.startsWith("- pulse pressure:"))!;
const line = block
.split("\n")
.find((l) => l.startsWith("- pulse pressure:"))!;
// The normal "Typical at rest" band (25–60), not the first "Narrow" band.
expect(line).toContain("25–60 mmHg");
expect(line).toContain("sits inside the general reference band");
@@ -33,9 +33,9 @@ vi.mock("@/lib/modules/gate", () => ({
// with the same resolved map the tests drive via `resolveModuleMap`.
isModuleEnabled: vi.fn(async (...args: [string, ModuleKey]) => {
const key = args[1];
const map = await (resolveModuleMap as unknown as () => Promise<
Record<ModuleKey, boolean>
>)();
const map = await (
resolveModuleMap as unknown as () => Promise<Record<ModuleKey, boolean>>
)();
return map[key] !== false;
}),
}));
@@ -44,9 +44,9 @@ vi.mock("@/lib/modules/gate", () => ({
// with the same resolved map the tests drive via `resolveModuleMap`.
isModuleEnabled: vi.fn(async (...args: [string, ModuleKey]) => {
const key = args[1];
const map = await (resolveModuleMap as unknown as () => Promise<
Record<ModuleKey, boolean>
>)();
const map = await (
resolveModuleMap as unknown as () => Promise<Record<ModuleKey, boolean>>
)();
return map[key] !== false;
}),
}));
@@ -406,7 +406,10 @@ describe("buildCoachSnapshot — WHOOP-native day strain (v1.17.0)", () => {
mockByType({
HEART_RATE_VARIABILITY: [daysAgo(2, 64, "HEART_RATE_VARIABILITY")],
// Ascending (oldest first), mirroring the `orderBy: measuredAt asc` read.
DAY_STRAIN: [daysAgo(3, 11.8, "DAY_STRAIN"), daysAgo(1, 14.2, "DAY_STRAIN")],
DAY_STRAIN: [
daysAgo(3, 11.8, "DAY_STRAIN"),
daysAgo(1, 14.2, "DAY_STRAIN"),
],
});
const out = await buildCoachSnapshot("user-1", {
+17 -15
View File
@@ -46,9 +46,9 @@ vi.mock("@/lib/modules/gate", () => ({
// cycle block (operator/user kill-switch parity) without a DB read.
isModuleEnabled: vi.fn(async (...args: [string, ModuleKey]) => {
const key = args[1];
const map = await (resolveModuleMap as unknown as () => Promise<
Record<ModuleKey, boolean>
>)();
const map = await (
resolveModuleMap as unknown as () => Promise<Record<ModuleKey, boolean>>
)();
return map[key] !== false;
}),
}));
@@ -319,12 +319,12 @@ describe("buildCoachSnapshot", () => {
// The compliance query and the GLP-1 query share this mock; only the
// compliance one (no `treatmentClass` filter) gets the fixture so the
// GLP-1 block — which selects `doseChanges` etc. — sees no rows.
prismaMock.medication.findMany.mockImplementation((args?: {
where?: { treatmentClass?: string };
}) => {
if (args?.where?.treatmentClass === "GLP1") return Promise.resolve([]);
return Promise.resolve([medication]);
});
prismaMock.medication.findMany.mockImplementation(
(args?: { where?: { treatmentClass?: string } }) => {
if (args?.where?.treatmentClass === "GLP1") return Promise.resolve([]);
return Promise.resolve([medication]);
},
);
prismaMock.user.findUnique.mockResolvedValue({
coachPrefsJson: null,
timezone: "Europe/Berlin",
@@ -339,7 +339,11 @@ describe("buildCoachSnapshot", () => {
// Independent authority over the same window the coach uses (30-day
// default scope), same ledger context — the coach rate must equal it.
const now = new Date();
const ctx = buildComplianceMedicationContext(medication, null, "Europe/Berlin");
const ctx = buildComplianceMedicationContext(
medication,
null,
"Europe/Berlin",
);
const authority = calculateCompliance(
intakeEvents,
[schedule],
@@ -672,9 +676,8 @@ describe("buildCoachSnapshot", () => {
recovery: false,
});
const { buildDerivedSnapshotBlock } = await import("../derived-snapshot");
const { buildTrajectorySnapshotBlock } = await import(
"../trajectory-snapshot"
);
const { buildTrajectorySnapshotBlock } =
await import("../trajectory-snapshot");
prismaMock.measurement.findMany.mockResolvedValue([]);
featuresMock.mockResolvedValue({
@@ -850,8 +853,7 @@ describe("Coach snapshot — free-text fields wrap through sanitizeForPrompt (L-
if (violations.length > 0) {
const formatted = violations
.map(
(v) =>
` ${filePath}:${v.line} — \`${v.fieldName}\`: ${v.snippet}`,
(v) => ` ${filePath}:${v.line} — \`${v.fieldName}\`: ${v.snippet}`,
)
.join("\n");
throw new Error(
@@ -42,7 +42,10 @@ describe("decideFromPrefs", () => {
it("suppresses a dismissed cadence (dismissal memory)", () => {
const d = decideFromPrefs(
{ ...DEFAULT_REMINDER_SUGGESTION_PREFS, dismissedCadences: ["weight_daily"] },
{
...DEFAULT_REMINDER_SUGGESTION_PREFS,
dismissedCadences: ["weight_daily"],
},
"weight_daily",
NOW,
);
@@ -131,8 +134,11 @@ describe("gateSuggestion", () => {
now: NOW,
});
expect(
(prisma as unknown as { measurementReminder: { findFirst: ReturnType<typeof vi.fn> } })
.measurementReminder.findFirst,
(
prisma as unknown as {
measurementReminder: { findFirst: ReturnType<typeof vi.fn> };
}
).measurementReminder.findFirst,
).not.toHaveBeenCalled();
});
});
@@ -61,8 +61,7 @@ describe("parseSuggestReminder", () => {
});
it("flags malformed and strips the block on an unknown cadence id", () => {
const raw =
"text\n---SUGGEST-REMINDER---\ncadence: bp_hourly\n---END---";
const raw = "text\n---SUGGEST-REMINDER---\ncadence: bp_hourly\n---END---";
const r = parseSuggestReminder(raw);
expect(r.cadence).toBeNull();
expect(r.malformed).toBe(true);
@@ -1,7 +1,8 @@
import { describe, it, expect, vi } from "vitest";
vi.mock("@/lib/insights/derived", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/insights/derived")>();
const actual =
await importOriginal<typeof import("@/lib/insights/derived")>();
return { ...actual, computeTrajectory: vi.fn() };
});
@@ -26,20 +27,52 @@ function okTrajectory(type: string) {
sampleDays: 20,
lastValue: 61.44,
projection: [
{ dayOffset: 1, date: "2026-06-03", projected: 61.5, bandLow: 60.1, bandHigh: 62.9 },
{ dayOffset: 14, date: "2026-06-16", projected: 63.16, bandLow: 60.02, bandHigh: 66.34 },
{
dayOffset: 1,
date: "2026-06-03",
projected: 61.5,
bandLow: 60.1,
bandHigh: 62.9,
},
{
dayOffset: 14,
date: "2026-06-16",
projected: 63.16,
bandLow: 60.02,
bandHigh: 66.34,
},
],
method: "ols" as const,
},
coverage: { requiredInputs: 1, presentInputs: 1, historyDays: 20, missing: [] },
coverage: {
requiredInputs: 1,
presentInputs: 1,
historyDays: 20,
missing: [],
},
confidence: { score: 71, band: "moderate" as const },
provenance: { inputs: [type], source: "DAY" as const, windowDays: 30, computedAt: "x" },
provenance: {
inputs: [type],
source: "DAY" as const,
windowDays: 30,
computedAt: "x",
},
};
}
const insufficient = {
status: "insufficient" as const,
coverage: { requiredInputs: 1, presentInputs: 0, historyDays: 4, missing: [] },
provenance: { inputs: [], source: "none" as const, windowDays: 30, computedAt: "x" },
coverage: {
requiredInputs: 1,
presentInputs: 0,
historyDays: 4,
missing: [],
},
provenance: {
inputs: [],
source: "none" as const,
windowDays: 30,
computedAt: "x",
},
reason: "insufficient_fit_for_projection",
};
+8 -9
View File
@@ -27,10 +27,7 @@
* Server-only — reads `@/lib/db`.
*/
import { prisma } from "@/lib/db";
import {
decryptFromBytes,
encryptToBytes,
} from "@/lib/ai/coach/bytes-codec";
import { decryptFromBytes, encryptToBytes } from "@/lib/ai/coach/bytes-codec";
import {
SELF_REPORT_FENCE_START,
SELF_REPORT_FENCE_END,
@@ -87,11 +84,15 @@ export async function getSelfContextForUser(
coachFocus: decryptOrNull(row?.coachFocusEncrypted ?? null),
};
} catch {
return { aboutMe: null, conditions: null, allergies: null, coachFocus: null };
return {
aboutMe: null,
conditions: null,
allergies: null,
coachFocus: null,
};
}
}
/** Whole years between `dateOfBirth` and `now`; null when unknown. */
export function deriveAgeYears(
dateOfBirth: Date | null,
@@ -142,9 +143,7 @@ export function composeSelfContextText(
}
}
if (facts.length > 0) {
lines.push(
(de ? "Profil: " : "Profile: ") + facts.join(" · "),
);
lines.push((de ? "Profil: " : "Profile: ") + facts.join(" · "));
}
if (ctx.conditions) {
+5 -7
View File
@@ -84,13 +84,13 @@ export function buildSummaryUserPrompt(
foldedTurns: SummaryFoldTurn[],
locale: "de" | "en" = "en",
): string {
const priorLabel = locale === "de" ? "FRÜHERE ZUSAMMENFASSUNG" : "PRIOR SUMMARY";
const priorLabel =
locale === "de" ? "FRÜHERE ZUSAMMENFASSUNG" : "PRIOR SUMMARY";
const turnsLabel = locale === "de" ? "NEUE WORTWECHSEL" : "NEW TURNS";
const none = locale === "de" ? "(keine)" : "(none)";
const priorBlock = priorSummary && priorSummary.trim().length > 0
? priorSummary.trim()
: none;
const priorBlock =
priorSummary && priorSummary.trim().length > 0 ? priorSummary.trim() : none;
const turnsBlock = foldedTurns
.map((t) => `${t.role}: ${t.content}`)
@@ -182,9 +182,7 @@ export async function refreshConversationSummary(
// fail-closed like every other decrypt in the Coach stack.
return [];
}
return [
{ role: m.role === "assistant" ? "assistant" : "user", content },
];
return [{ role: m.role === "assistant" ? "assistant" : "user", content }];
});
// 5. Run the user's provider chain. Best-effort: non-ok leaves the old
+1 -4
View File
@@ -21,10 +21,7 @@
* day" claim.
*/
import { prisma } from "@/lib/db";
import {
predictCycle,
type NightlyTempInput,
} from "@/lib/cycle";
import { predictCycle, type NightlyTempInput } from "@/lib/cycle";
import { LUTEAL_DEFAULT, type CyclePhase } from "@/lib/cycle/types";
import {
phaseForDate,
+12 -3
View File
@@ -68,11 +68,17 @@ function summariseValue(
case "STRESS_SCORE":
case "STRAIN_SCORE":
return typeof v.score === "number"
? { value: v.score, band: typeof v.band === "string" ? v.band : undefined }
? {
value: v.score,
band: typeof v.band === "string" ? v.band : undefined,
}
: null;
case "HRV_BALANCE":
return typeof v.recentAvg === "number"
? { value: Math.round(v.recentAvg), band: typeof v.band === "string" ? v.band : undefined }
? {
value: Math.round(v.recentAvg),
band: typeof v.band === "string" ? v.band : undefined,
}
: null;
case "FITNESS_AGE":
return typeof v.vo2Max === "number"
@@ -119,7 +125,10 @@ export async function buildDerivedSnapshotBlock(
const computed = await Promise.all(
SNAPSHOT_METRICS.map(async (metric) => {
try {
return { metric, derived: await computeDerivedMetric({ metric, userId, profile, now }) };
return {
metric,
derived: await computeDerivedMetric({ metric, userId, profile, now }),
};
} catch {
return { metric, derived: null };
}
+38 -14
View File
@@ -89,11 +89,7 @@ function extractionSystemPrompt(locale: string | undefined): string {
const rawFactSchema = z.object({
category: z.enum(COACH_FACT_CATEGORIES),
fact: z
.string()
.trim()
.min(1)
.max(FACT_MAX_CHARS),
fact: z.string().trim().min(1).max(FACT_MAX_CHARS),
confidence: z.coerce.number().int().min(0).max(100),
});
@@ -110,7 +106,9 @@ interface ParsedFact {
* top-level JSON is unparseable (the caller then annotates `parse_failed`);
* individual malformed items are dropped silently, not fatal.
*/
function parseFacts(content: string): { facts: ParsedFact[]; dropped: number } | null {
function parseFacts(
content: string,
): { facts: ParsedFact[]; dropped: number } | null {
let json: unknown;
try {
json = JSON.parse(content.trim());
@@ -129,7 +127,10 @@ function parseFacts(content: string): { facts: ParsedFact[]; dropped: number } |
facts.push({
category: parsed.data.category,
fact: parsed.data.fact,
confidence: Math.max(0, Math.min(100, Math.round(parsed.data.confidence))),
confidence: Math.max(
0,
Math.min(100, Math.round(parsed.data.confidence)),
),
});
} else {
dropped += 1;
@@ -169,7 +170,8 @@ function isNearDuplicate(candidate: string, existing: string[]): boolean {
const cNorm = candidate.trim().toLowerCase();
for (const ex of existing) {
if (ex.trim().toLowerCase() === cNorm) return true;
if (tokenOverlap(cTokens, tokenSet(ex)) >= DEDUP_OVERLAP_THRESHOLD) return true;
if (tokenOverlap(cTokens, tokenSet(ex)) >= DEDUP_OVERLAP_THRESHOLD)
return true;
}
return false;
}
@@ -197,17 +199,29 @@ interface ActiveFactRow {
async function loadActiveFacts(
db: PrismaLike,
userId: string,
): Promise<Array<{ id: string; text: string; category: string; confidence: number }>> {
): Promise<
Array<{ id: string; text: string; category: string; confidence: number }>
> {
const rows = (await db.coachFact.findMany({
where: { userId, deletedAt: null },
select: { id: true, factEncrypted: true, category: true, confidence: true },
})) as ActiveFactRow[];
const out: Array<{ id: string; text: string; category: string; confidence: number }> = [];
const out: Array<{
id: string;
text: string;
category: string;
confidence: number;
}> = [];
for (const r of rows) {
const text = decryptOrNull(r.factEncrypted);
if (text === null) continue;
out.push({ id: r.id, text, category: r.category, confidence: r.confidence });
out.push({
id: r.id,
text,
category: r.category,
confidence: r.confidence,
});
}
return out;
}
@@ -341,7 +355,9 @@ export async function extractAndStoreFacts(
const toStore: ParsedFact[] = [];
// Highest-confidence candidates first so a tight cap admits the best.
for (const cand of [...accepted].sort((a, b) => b.confidence - a.confidence)) {
for (const cand of [...accepted].sort(
(a, b) => b.confidence - a.confidence,
)) {
if (remainingCapacity > 0) {
toStore.push(cand);
remainingCapacity -= 1;
@@ -529,7 +545,10 @@ const DETERMINISTIC_PATTERNS: DeterministicPattern[] = [
];
function cleanCapture(raw: string): string | null {
const cleaned = raw.replace(/\s+/g, " ").replace(/[.,;:!?]+$/u, "").trim();
const cleaned = raw
.replace(/\s+/g, " ")
.replace(/[.,;:!?]+$/u, "")
.trim();
if (cleaned.length < 2 || cleaned.length > 60) return null;
return cleaned;
}
@@ -653,7 +672,12 @@ export async function buildCoachFactsBlock(
const rows = (await db.coachFact.findMany({
where: { userId, deletedAt: null },
orderBy: [{ confidence: "desc" }, { updatedAt: "desc" }],
select: { factEncrypted: true, category: true, confidence: true, updatedAt: true },
select: {
factEncrypted: true,
category: true,
confidence: true,
updatedAt: true,
},
})) as RankFactRow[];
const facts: Array<{ category: string; text: string }> = [];
-2
View File
@@ -70,7 +70,6 @@ function deriveDrugNames(raw: string): { display: string; generic: string } {
return { display, generic };
}
function parseDaysOfWeek(raw: string | null): number[] {
if (!raw) return [];
return raw
@@ -437,4 +436,3 @@ export async function buildGlp1SnapshotBlock(
medications,
};
}
+6 -1
View File
@@ -72,7 +72,12 @@ export async function buildIllnessSnapshotBlock(
const [activeRows, resolvedRows] = await Promise.all([
prisma.illnessEpisode.findMany({
where: { userId, deletedAt: null, resolvedAt: null, onsetAt: { lte: now } },
where: {
userId,
deletedAt: null,
resolvedAt: null,
onsetAt: { lte: now },
},
orderBy: { onsetAt: "asc" },
select: { label: true, type: true, lifecycle: true, onsetAt: true },
}),
+18 -20
View File
@@ -78,10 +78,7 @@ export const medicationExtractionSchema = z
intervalMonths: z.number().int().min(1).max(12).optional(),
dayOfMonth: z.number().int().min(1).max(31).optional(),
rollingIntervalDays: z.number().int().min(1).max(365).optional(),
timesOfDay: z
.array(z.string().regex(HH_MM_RE, "HH:MM"))
.max(12)
.optional(),
timesOfDay: z.array(z.string().regex(HH_MM_RE, "HH:MM")).max(12).optional(),
startsOn: z.string().regex(ISO_DATE_RE, "YYYY-MM-DD").optional(),
endsOn: z.string().regex(ISO_DATE_RE, "YYYY-MM-DD").optional(),
oneShot: z.boolean().optional(),
@@ -120,7 +117,7 @@ export function buildMedicationExtractionSystemPrompt(): string {
"Every field is optional — omit a field rather than guess. Keys:",
"",
" name — medication brand or active substance, verbatim",
" dose — numeric portion only, as a string: \"5\", \"7.5\"",
' dose — numeric portion only, as a string: "5", "7.5"',
" doseUnit — one of: mg | ml | iu | tablets | drops | puffs | sprays",
" cadenceKind — one of: daily | weekdays | everyNWeeks |",
" monthly | everyNMonths | yearly | rolling |",
@@ -130,9 +127,9 @@ export function buildMedicationExtractionSystemPrompt(): string {
" intervalWeeks — 1..52, used with everyNWeeks",
" intervalMonths — 1..12, used with everyNMonths",
" dayOfMonth — 1..31, used with monthly / everyNMonths",
" rollingIntervalDays — 1..365, used with rolling (\"every N days",
" from the last intake\")",
" timesOfDay — array of \"HH:MM\" wall-clock strings (24h)",
' rollingIntervalDays — 1..365, used with rolling ("every N days',
' from the last intake")',
' timesOfDay — array of "HH:MM" wall-clock strings (24h)',
" startsOn — ISO YYYY-MM-DD, course start date",
" endsOn — ISO YYYY-MM-DD, course end date",
" oneShot — true when the user describes a single-time",
@@ -147,26 +144,26 @@ export function buildMedicationExtractionSystemPrompt(): string {
" not state a numeric dose, omit `dose` and `doseUnit`.",
"",
" 2. Map vague time-of-day words to canonical 24h slots:",
" morning → \"08:00\", noon / midday → \"12:00\",",
" afternoon → \"14:00\", evening → \"18:00\", night → \"22:00\".",
' morning → "08:00", noon / midday → "12:00",',
' afternoon → "14:00", evening → "18:00", night → "22:00".',
" Multiple times in one description → emit each as its own entry.",
"",
" 3. \"Every day\" / \"daily\" / \"once a day\" → `cadenceKind:",
" \"daily\"`. \"Twice a day\" / \"3 times a day\" → still",
' 3. "Every day" / "daily" / "once a day" → `cadenceKind:',
' "daily"`. "Twice a day" / "3 times a day" → still',
" `daily` with the right `timesOfDay[]` count; never invent a",
" weekly cadence.",
"",
" 4. \"Every N days from my last injection / dose\" /",
" \"7 days after the previous one\" → `rolling` with",
' 4. "Every N days from my last injection / dose" /',
' "7 days after the previous one" → `rolling` with',
" `rollingIntervalDays = N`. This is distinct from",
" `everyNWeeks` (calendar-anchored).",
"",
" 5. Single-shot wording (\"single flu shot\", \"one-off dose\",",
" \"on October 15 only\") → `oneShot: true` and a `startsOn`",
' 5. Single-shot wording ("single flu shot", "one-off dose",',
' "on October 15 only") → `oneShot: true` and a `startsOn`',
" date. Do not also emit `cadenceKind`.",
"",
" 6. Resolve relative dates against today's date supplied in the",
" CONTEXT block. \"Next Monday\", \"tomorrow\", \"in 2 weeks\"",
' CONTEXT block. "Next Monday", "tomorrow", "in 2 weeks"',
" become absolute ISO dates.",
"",
" 7. If a field is ambiguous, leave it empty. The wizard prefers",
@@ -206,9 +203,10 @@ export function buildMedicationExtractionUserPrompt(
* payload without having to glue the two helpers together at every call
* site.
*/
export function buildMedicationExtractionPrompt(
args: BuildUserPromptArgs,
): { systemPrompt: string; userPrompt: string } {
export function buildMedicationExtractionPrompt(args: BuildUserPromptArgs): {
systemPrompt: string;
userPrompt: string;
} {
return {
systemPrompt: buildMedicationExtractionSystemPrompt(),
userPrompt: buildMedicationExtractionUserPrompt(args),
+1 -5
View File
@@ -166,11 +166,7 @@ export async function buildCoachMemoryBlock(
facts = undefined;
}
if (
!priorNarrative &&
Object.keys(trendMemory).length === 0 &&
!facts
) {
if (!priorNarrative && Object.keys(trendMemory).length === 0 && !facts) {
return null;
}
-1
View File
@@ -360,4 +360,3 @@ export async function deleteConversation(
await prisma.coachConversation.delete({ where: { id: row.id } });
return true;
}
+2 -2
View File
@@ -249,8 +249,8 @@ export function buildReferenceGroundingBlock(
"These are published POPULATION reference bands for context only — they",
"are not personal medical advice, not a diagnosis, and not a target set",
"for this user. The user's own baseline always leads the read. Cite a",
"band only to orient a reply (\"the general reference range is …\"); never",
"tell the user they \"have\" or \"don't have\" a condition from a band.",
'band only to orient a reply ("the general reference range is …"); never',
'tell the user they "have" or "don\'t have" a condition from a band.',
"Blood pressure follows the European ESH 2023 line; US (ACC/AHA) and",
"ESC framings differ and are context, not the call. This block does not",
"change any safety rule above — keep deferring dose / diagnosis / drug-",
+1 -4
View File
@@ -21,10 +21,7 @@
*
* Server-only — reads `@/lib/db` through the provider resolver.
*/
import {
hasAnyConfiguredProvider,
resolveProvider,
} from "@/lib/ai/provider";
import { hasAnyConfiguredProvider, resolveProvider } from "@/lib/ai/provider";
import {
buildDateKey,
getDailyTokenSpend,
+220 -53
View File
@@ -201,13 +201,14 @@ const CORE_CLUSTERS: ReadonlySet<CoachDataCluster> = new Set<CoachDataCluster>([
* a data domain. `labs` / `achievements` / `insights` / `doctorReport`
* own no coach-snapshot data domain.
*/
const MODULE_EXCLUDED_SOURCES: Partial<Record<ModuleKey, CoachScopeSource[]>> = {
mood: ["mood"],
sleep: ["sleep"],
glucose: ["glucose"],
workouts: ["workouts"],
recovery: ["hrv", "resting_hr", "vo2_max"],
};
const MODULE_EXCLUDED_SOURCES: Partial<Record<ModuleKey, CoachScopeSource[]>> =
{
mood: ["mood"],
sleep: ["sleep"],
glucose: ["glucose"],
workouts: ["workouts"],
recovery: ["hrv", "resting_hr", "vo2_max"],
};
const WEEKDAY_KEYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const;
@@ -420,12 +421,18 @@ async function buildCoarseTimelineTail(
return undefined;
}
const monthly = series.monthBand.map(
(b) => [b.bucketStart, b.mean, b.min, b.max] as [string, number, number, number],
(b) =>
[b.bucketStart, b.mean, b.min, b.max] as [string, number, number, number],
);
const yearly = series.yearBand.map(
(b) => [b.bucketStart, b.mean, b.min, b.max] as [string, number, number, number],
(b) =>
[b.bucketStart, b.mean, b.min, b.max] as [string, number, number, number],
);
if (monthly.length === 0 && yearly.length === 0 && series.anomalies.length === 0) {
if (
monthly.length === 0 &&
yearly.length === 0 &&
series.anomalies.length === 0
) {
return undefined;
}
return {
@@ -523,7 +530,10 @@ const snapshotCache = new Map<
{ expiresAt: number; result: CoachSnapshotResult }
>();
function snapshotCacheKey(userId: string, scope: CoachScope | undefined): string {
function snapshotCacheKey(
userId: string,
scope: CoachScope | undefined,
): string {
// v1.7.0 — when the request pins an explicit source list, key on it.
// Otherwise the source set is derived from the user's saved
// `dataClusters`, which we don't read here (the cache must stay
@@ -1523,42 +1533,202 @@ async function buildCoachSnapshotImpl(
// so accounts without that data never see a void section.
const valueBlocks: ValueBlock[] = [
// ── cardio ──
{ metric: "hrv", source: "hrv", snapshotKey: "heartRateVariability", type: "HEART_RATE_VARIABILITY" },
{ metric: "resting_hr", source: "resting_hr", snapshotKey: "restingHeartRate", type: "RESTING_HEART_RATE" },
{ metric: "walking_hr", source: "walking_hr", snapshotKey: "walkingHeartRateAverage", type: "WALKING_HEART_RATE_AVERAGE" },
{ metric: "respiratory_rate", source: "respiratory_rate", snapshotKey: "respiratoryRate", type: "RESPIRATORY_RATE" },
{ metric: "spo2", source: "spo2", snapshotKey: "oxygenSaturation", type: "OXYGEN_SATURATION" },
{ metric: "pulse_wave_velocity", source: "pulse_wave_velocity", snapshotKey: "pulseWaveVelocity", type: "PULSE_WAVE_VELOCITY" },
{ metric: "vascular_age", source: "vascular_age", snapshotKey: "vascularAge", type: "VASCULAR_AGE" },
{
metric: "hrv",
source: "hrv",
snapshotKey: "heartRateVariability",
type: "HEART_RATE_VARIABILITY",
},
{
metric: "resting_hr",
source: "resting_hr",
snapshotKey: "restingHeartRate",
type: "RESTING_HEART_RATE",
},
{
metric: "walking_hr",
source: "walking_hr",
snapshotKey: "walkingHeartRateAverage",
type: "WALKING_HEART_RATE_AVERAGE",
},
{
metric: "respiratory_rate",
source: "respiratory_rate",
snapshotKey: "respiratoryRate",
type: "RESPIRATORY_RATE",
},
{
metric: "spo2",
source: "spo2",
snapshotKey: "oxygenSaturation",
type: "OXYGEN_SATURATION",
},
{
metric: "pulse_wave_velocity",
source: "pulse_wave_velocity",
snapshotKey: "pulseWaveVelocity",
type: "PULSE_WAVE_VELOCITY",
},
{
metric: "vascular_age",
source: "vascular_age",
snapshotKey: "vascularAge",
type: "VASCULAR_AGE",
},
// ── body composition ──
{ metric: "body_fat", source: "body_fat", snapshotKey: "bodyFat", type: "BODY_FAT" },
{ metric: "fat_mass", source: "fat_mass", snapshotKey: "fatMass", type: "FAT_MASS" },
{ metric: "fat_free_mass", source: "fat_free_mass", snapshotKey: "fatFreeMass", type: "FAT_FREE_MASS" },
{ metric: "muscle_mass", source: "muscle_mass", snapshotKey: "muscleMass", type: "MUSCLE_MASS" },
{ metric: "lean_body_mass", source: "lean_body_mass", snapshotKey: "leanBodyMass", type: "LEAN_BODY_MASS" },
{ metric: "bone_mass", source: "bone_mass", snapshotKey: "boneMass", type: "BONE_MASS" },
{ metric: "total_body_water", source: "total_body_water", snapshotKey: "totalBodyWater", type: "TOTAL_BODY_WATER" },
{ metric: "bmi", source: "bmi", snapshotKey: "bodyMassIndex", type: "BODY_MASS_INDEX" },
{ metric: "visceral_fat", source: "visceral_fat", snapshotKey: "visceralFat", type: "VISCERAL_FAT" },
{
metric: "body_fat",
source: "body_fat",
snapshotKey: "bodyFat",
type: "BODY_FAT",
},
{
metric: "fat_mass",
source: "fat_mass",
snapshotKey: "fatMass",
type: "FAT_MASS",
},
{
metric: "fat_free_mass",
source: "fat_free_mass",
snapshotKey: "fatFreeMass",
type: "FAT_FREE_MASS",
},
{
metric: "muscle_mass",
source: "muscle_mass",
snapshotKey: "muscleMass",
type: "MUSCLE_MASS",
},
{
metric: "lean_body_mass",
source: "lean_body_mass",
snapshotKey: "leanBodyMass",
type: "LEAN_BODY_MASS",
},
{
metric: "bone_mass",
source: "bone_mass",
snapshotKey: "boneMass",
type: "BONE_MASS",
},
{
metric: "total_body_water",
source: "total_body_water",
snapshotKey: "totalBodyWater",
type: "TOTAL_BODY_WATER",
},
{
metric: "bmi",
source: "bmi",
snapshotKey: "bodyMassIndex",
type: "BODY_MASS_INDEX",
},
{
metric: "visceral_fat",
source: "visceral_fat",
snapshotKey: "visceralFat",
type: "VISCERAL_FAT",
},
// ── activity ──
{ metric: "steps", source: "steps", snapshotKey: "steps", type: "ACTIVITY_STEPS" },
{ metric: "active_energy", source: "active_energy", snapshotKey: "activeEnergy", type: "ACTIVE_ENERGY_BURNED" },
{ metric: "flights", source: "flights", snapshotKey: "flightsClimbed", type: "FLIGHTS_CLIMBED" },
{ metric: "distance", source: "distance", snapshotKey: "walkingRunningDistance", type: "WALKING_RUNNING_DISTANCE" },
{ metric: "vo2_max", source: "vo2_max", snapshotKey: "vo2Max", type: "VO2_MAX" },
{
metric: "steps",
source: "steps",
snapshotKey: "steps",
type: "ACTIVITY_STEPS",
},
{
metric: "active_energy",
source: "active_energy",
snapshotKey: "activeEnergy",
type: "ACTIVE_ENERGY_BURNED",
},
{
metric: "flights",
source: "flights",
snapshotKey: "flightsClimbed",
type: "FLIGHTS_CLIMBED",
},
{
metric: "distance",
source: "distance",
snapshotKey: "walkingRunningDistance",
type: "WALKING_RUNNING_DISTANCE",
},
{
metric: "vo2_max",
source: "vo2_max",
snapshotKey: "vo2Max",
type: "VO2_MAX",
},
// ── mobility & gait ──
{ metric: "walking_steadiness", source: "walking_steadiness", snapshotKey: "walkingSteadiness", type: "WALKING_STEADINESS" },
{ metric: "walking_asymmetry", source: "walking_asymmetry", snapshotKey: "walkingAsymmetry", type: "WALKING_ASYMMETRY" },
{ metric: "walking_double_support", source: "walking_double_support", snapshotKey: "walkingDoubleSupport", type: "WALKING_DOUBLE_SUPPORT" },
{ metric: "walking_step_length", source: "walking_step_length", snapshotKey: "walkingStepLength", type: "WALKING_STEP_LENGTH" },
{ metric: "walking_speed", source: "walking_speed", snapshotKey: "walkingSpeed", type: "WALKING_SPEED" },
{
metric: "walking_steadiness",
source: "walking_steadiness",
snapshotKey: "walkingSteadiness",
type: "WALKING_STEADINESS",
},
{
metric: "walking_asymmetry",
source: "walking_asymmetry",
snapshotKey: "walkingAsymmetry",
type: "WALKING_ASYMMETRY",
},
{
metric: "walking_double_support",
source: "walking_double_support",
snapshotKey: "walkingDoubleSupport",
type: "WALKING_DOUBLE_SUPPORT",
},
{
metric: "walking_step_length",
source: "walking_step_length",
snapshotKey: "walkingStepLength",
type: "WALKING_STEP_LENGTH",
},
{
metric: "walking_speed",
source: "walking_speed",
snapshotKey: "walkingSpeed",
type: "WALKING_SPEED",
},
// ── environment / exposure ──
{ metric: "audio_env", source: "audio_env", snapshotKey: "audioExposureEnvironment", type: "AUDIO_EXPOSURE_ENV" },
{ metric: "audio_headphone", source: "audio_headphone", snapshotKey: "audioExposureHeadphone", type: "AUDIO_EXPOSURE_HEADPHONE" },
{ metric: "audio_event", source: "audio_event", snapshotKey: "audioExposureEvent", type: "AUDIO_EXPOSURE_EVENT" },
{ metric: "daylight", source: "daylight", snapshotKey: "timeInDaylight", type: "TIME_IN_DAYLIGHT" },
{ metric: "skin_temp", source: "skin_temp", snapshotKey: "skinTemperature", type: "SKIN_TEMPERATURE" },
{ metric: "body_temp", source: "body_temp", snapshotKey: "bodyTemperature", type: "BODY_TEMPERATURE" },
{
metric: "audio_env",
source: "audio_env",
snapshotKey: "audioExposureEnvironment",
type: "AUDIO_EXPOSURE_ENV",
},
{
metric: "audio_headphone",
source: "audio_headphone",
snapshotKey: "audioExposureHeadphone",
type: "AUDIO_EXPOSURE_HEADPHONE",
},
{
metric: "audio_event",
source: "audio_event",
snapshotKey: "audioExposureEvent",
type: "AUDIO_EXPOSURE_EVENT",
},
{
metric: "daylight",
source: "daylight",
snapshotKey: "timeInDaylight",
type: "TIME_IN_DAYLIGHT",
},
{
metric: "skin_temp",
source: "skin_temp",
snapshotKey: "skinTemperature",
type: "SKIN_TEMPERATURE",
},
{
metric: "body_temp",
source: "body_temp",
snapshotKey: "bodyTemperature",
type: "BODY_TEMPERATURE",
},
];
for (const block of valueBlocks) {
if (!sources.has(block.source)) continue;
@@ -1765,10 +1935,11 @@ async function buildCoachSnapshotImpl(
// then convert each resulting figure ONCE (parity with the series
// DTO + detail page + FHIR). mg/dL users stay byte-identical because
// the conversion is skipped entirely.
const recent = buildDailyValueRows(rows, recentCutoff, userTz).map((d) =>
glucoseUnit === "mmol/L"
? { ...d, value: convertGlucose(d.value, glucoseUnit) }
: d,
const recent = buildDailyValueRows(rows, recentCutoff, userTz).map(
(d) =>
glucoseUnit === "mmol/L"
? { ...d, value: convertGlucose(d.value, glucoseUnit) }
: d,
);
const weekly = bucketWeekly(
rows.filter((r) => r.measuredAt < recentCutoff),
@@ -1960,9 +2131,7 @@ async function buildCoachSnapshotImpl(
const ctx = features.context;
if (
ctx &&
(ctx.heightCm !== null ||
ctx.ageYears !== null ||
ctx.gender !== null)
(ctx.heightCm !== null || ctx.ageYears !== null || ctx.gender !== null)
) {
snapshot.anthropometrics = {
heightCm: ctx.heightCm,
@@ -2293,9 +2462,7 @@ function degradeToBudget(
}
if (degraded.length > 0) {
const droppedClusters = Array.from(
new Set(degraded.map((d) => d.cluster)),
);
const droppedClusters = Array.from(new Set(degraded.map((d) => d.cluster)));
annotate({
action: { name: "coach.snapshot.truncated" },
meta: {
+2 -5
View File
@@ -171,8 +171,7 @@ export function parseSuggestReminder(raw: string): SuggestReminderParseResult {
const closeRel = afterOpen.indexOf(CLOSE_SENTINEL);
// Cap the payload before parsing regardless of whether the close marker
// is present (a missing close marker treats the rest as the body).
const bodyRaw =
closeRel === -1 ? afterOpen : afterOpen.slice(0, closeRel);
const bodyRaw = closeRel === -1 ? afterOpen : afterOpen.slice(0, closeRel);
const body =
bodyRaw.length > SENTINEL_BYTE_CAP
? bodyRaw.slice(0, SENTINEL_BYTE_CAP)
@@ -183,9 +182,7 @@ export function parseSuggestReminder(raw: string): SuggestReminderParseResult {
// it. Only the sentinel block itself is stripped; when the close marker is
// missing there is no trailing text to recover.
const after =
closeRel === -1
? ""
: afterOpen.slice(closeRel + CLOSE_SENTINEL.length);
closeRel === -1 ? "" : afterOpen.slice(closeRel + CLOSE_SENTINEL.length);
const prose = `${before}${after}`.replace(/^\s+|\s+$/gu, "");
const cadenceId = extractCadenceId(body);
+4 -1
View File
@@ -101,7 +101,10 @@ export async function buildTrajectorySnapshotBlock(
continue;
}
if (!isDerivedOk(derived)) continue; // omit insufficient — no projection noise
const summary = summariseTrajectory(derived.value, derived.confidence.score);
const summary = summariseTrajectory(
derived.value,
derived.confidence.score,
);
if (!summary) continue;
block[type] = summary;
}
+44 -40
View File
@@ -291,47 +291,51 @@ export class CodexClient implements AIProvider {
): Promise<Response> {
const sessionId = randomUUID();
const threadId = randomUUID();
return safeFetch(CODEX_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "text/event-stream",
Authorization: `Bearer ${this.accessToken}`,
"ChatGPT-Account-ID": this.accountId,
originator: ORIGINATOR,
"User-Agent": USER_AGENT,
session_id: sessionId,
"session-id": sessionId,
thread_id: threadId,
"thread-id": threadId,
return safeFetch(
CODEX_ENDPOINT,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "text/event-stream",
Authorization: `Bearer ${this.accessToken}`,
"ChatGPT-Account-ID": this.accountId,
originator: ORIGINATOR,
"User-Agent": USER_AGENT,
session_id: sessionId,
"session-id": sessionId,
thread_id: threadId,
"thread-id": threadId,
},
body: JSON.stringify({
model: slug,
instructions: params.systemPrompt,
input: [
{
type: "message",
role: "user",
content: [{ type: "input_text", text: params.userPrompt }],
},
],
tools: [],
tool_choice: "auto",
parallel_tool_calls: false,
// Reasoning is required-but-nullable on the wire. We don't ask
// for reasoning summaries — emit `null` so the JSON has the
// field present and the server stops complaining about a
// missing key (it returned 400 in earlier iterations).
reasoning: null,
store: false,
stream: true,
// Required field; empty array when not asking for reasoning.
include: [],
}),
// SSE streaming completion — match the 60 s budget the other AI
// clients use so a long generation is not clipped by the 15 s
// default while still bounding a tar-pit upstream.
},
body: JSON.stringify({
model: slug,
instructions: params.systemPrompt,
input: [
{
type: "message",
role: "user",
content: [{ type: "input_text", text: params.userPrompt }],
},
],
tools: [],
tool_choice: "auto",
parallel_tool_calls: false,
// Reasoning is required-but-nullable on the wire. We don't ask
// for reasoning summaries — emit `null` so the JSON has the
// field present and the server stops complaining about a
// missing key (it returned 400 in earlier iterations).
reasoning: null,
store: false,
stream: true,
// Required field; empty array when not asking for reasoning.
include: [],
}),
// SSE streaming completion — match the 60 s budget the other AI
// clients use so a long generation is not clipped by the 15 s
// default while still bounding a tar-pit upstream.
}, { timeoutMs: 60_000 });
{ timeoutMs: 60_000 },
);
}
/**
+1 -4
View File
@@ -106,10 +106,7 @@ export interface CoachFeedbackAttribution extends FeedbackAttribution {
metricSourceType: string;
}
function buildCoachMetricSourceType(
tone: string,
verbosity: string,
): string {
function buildCoachMetricSourceType(tone: string, verbosity: string): string {
return `coach:tone=${tone}:verbosity=${verbosity}`;
}
@@ -72,25 +72,28 @@ describe("getCoachSystemPrompt — native AI-initial locales", () => {
expect(prompt).toMatch(/BLOK DOWODÓW/);
});
it.each(NATIVE_LOCALES)("%s carries every GROUND RULE body verbatim", (locale) => {
const matrix = loadSafetyContracts(locale);
const prompt = getCoachSystemPrompt(locale);
for (const key of Object.keys(matrix.ground_rules) as Array<
keyof typeof matrix.ground_rules
>) {
const rule = matrix.ground_rules[key];
// Coach uses surface = "coach" or "both" rules.
if (rule.surface === "insights") continue;
const body = rule.locale ?? rule.en;
expect(body).toBeDefined();
// Match on a representative substring (first 80 chars of body).
const sample = body!.trim().slice(0, 80);
expect(
prompt,
`coach prompt for ${locale} dropped ${key} (looking for "${sample}")`,
).toContain(sample);
}
});
it.each(NATIVE_LOCALES)(
"%s carries every GROUND RULE body verbatim",
(locale) => {
const matrix = loadSafetyContracts(locale);
const prompt = getCoachSystemPrompt(locale);
for (const key of Object.keys(matrix.ground_rules) as Array<
keyof typeof matrix.ground_rules
>) {
const rule = matrix.ground_rules[key];
// Coach uses surface = "coach" or "both" rules.
if (rule.surface === "insights") continue;
const body = rule.locale ?? rule.en;
expect(body).toBeDefined();
// Match on a representative substring (first 80 chars of body).
const sample = body!.trim().slice(0, 80);
expect(
prompt,
`coach prompt for ${locale} dropped ${key} (looking for "${sample}")`,
).toContain(sample);
}
},
);
it.each(NATIVE_LOCALES)(
"%s preserves the ---KEYVALUES--- / ---END--- sentinels",
@@ -52,38 +52,10 @@ const DRUG_LEVEL_RULE_KEY = "ground_rule_15_drug_level_refusal" as const;
const DRUG_LEVEL_CONCEPT_TOKENS: Record<Locale, readonly string[]> = {
en: ["level", "concentration", "peak", "trough", "Cmax", "therapeutic"],
de: ["Spiegel", "Konzentration", "Peak", "Trough", "Cmax", "therapeutisch"],
fr: [
"niveau",
"concentration",
"pic",
"creux",
"Cmax",
"thérapeutique",
],
es: [
"nivel",
"concentración",
"pico",
"valle",
"Cmax",
"terapéutica",
],
it: [
"livello",
"concentrazione",
"picco",
"valle",
"Cmax",
"terapeutica",
],
pl: [
"poziom",
"stężeni",
"szczyt",
"minimum",
"Cmax",
"terapeutyczn",
],
fr: ["niveau", "concentration", "pic", "creux", "Cmax", "thérapeutique"],
es: ["nivel", "concentración", "pico", "valle", "Cmax", "terapéutica"],
it: ["livello", "concentrazione", "picco", "valle", "Cmax", "terapeutica"],
pl: ["poziom", "stężeni", "szczyt", "minimum", "Cmax", "terapeutyczn"],
};
/**
@@ -196,9 +168,10 @@ describe("drug-level refusal — adversarial trigger probes survive assembly", (
// this locale. Today the prompt is static per locale, so this
// is structurally equivalent across probes; the loop pins the
// invariant against future per-turn prompt mutation.
const matched = matrix.drug_level_refusal.expected_refusal_keywords.some(
(keyword) => prompt.includes(keyword),
);
const matched =
matrix.drug_level_refusal.expected_refusal_keywords.some((keyword) =>
prompt.includes(keyword),
);
expect(
matched,
`${locale} probe="${probe.slice(0, 40)}..." found no expected refusal keyword in the Coach prompt`,
@@ -24,8 +24,12 @@ describe("base-system assessment prompt — locale skeleton parity", () => {
it("every section carries both EN and DE fragments, none blank", () => {
expect(ASSESSMENT_SECTION_PAIRS.length).toBeGreaterThan(0);
for (const s of ASSESSMENT_SECTION_PAIRS) {
expect(s.en.trim().length, `EN fragment for "${s.id}"`).toBeGreaterThan(0);
expect(s.de.trim().length, `DE fragment for "${s.id}"`).toBeGreaterThan(0);
expect(s.en.trim().length, `EN fragment for "${s.id}"`).toBeGreaterThan(
0,
);
expect(s.de.trim().length, `DE fragment for "${s.id}"`).toBeGreaterThan(
0,
);
}
});
@@ -49,8 +53,12 @@ describe("insight system prompt — locale skeleton parity", () => {
it("every section carries both EN and DE fragments, none blank", () => {
expect(INSIGHT_PROMPT_SECTION_PAIRS.length).toBeGreaterThan(0);
for (const s of INSIGHT_PROMPT_SECTION_PAIRS) {
expect(s.en.trim().length, `EN fragment for "${s.id}"`).toBeGreaterThan(0);
expect(s.de.trim().length, `DE fragment for "${s.id}"`).toBeGreaterThan(0);
expect(s.en.trim().length, `EN fragment for "${s.id}"`).toBeGreaterThan(
0,
);
expect(s.de.trim().length, `DE fragment for "${s.id}"`).toBeGreaterThan(
0,
);
}
});
-1
View File
@@ -57,4 +57,3 @@ Schreibe eine kurze Gesamteinschätzung über die verfügbaren Gesundheitsdaten:
${snapshotJson}`;
}
+2 -8
View File
@@ -27,10 +27,7 @@ import type {
MetricStatusMeta,
} from "@/lib/insights/metric-status-registry";
function directionPhrase(
meta: MetricStatusMeta,
locale: Locale,
): string {
function directionPhrase(meta: MetricStatusMeta, locale: Locale): string {
if (locale === "en") {
switch (meta.direction) {
case "higher-better":
@@ -78,10 +75,7 @@ function metaBlock(meta: MetricStatusMeta, locale: Locale): string {
].join("\n");
}
const ARCHETYPE_SECTION: Record<
MetricArchetype,
{ de: string; en: string }
> = {
const ARCHETYPE_SECTION: Record<MetricArchetype, { de: string; en: string }> = {
"physiological-vital": {
en: "ARCHETYPE — PHYSIOLOGICAL VITAL:\n- This is a point-in-time physiological reading. Name the recent level, place it against both the user's own baseline (recent vs monthly/yearly mean) and, when a reference band is given, that band.\n- A single out-of-band reading is rarely a finding; a sustained shift across the recent window against the user's baseline is. Note an out-of-band trend without alarm.\n- Frame any direction (rising/falling) as a possible explanation (fitness, stress, sleep, illness, medication), never a diagnosis.",
de: "ARCHETYP — PHYSIOLOGISCHER VITALWERT:\n- Das ist ein Momentanwert. Benenne das aktuelle Niveau und ordne es sowohl gegen die eigene Baseline (recent vs. Monats-/Jahresmittel) als auch — falls ein Referenzband vorliegt — gegen dieses Band ein.\n- Ein einzelner Wert außerhalb des Bands ist selten ein Befund; eine anhaltende Verschiebung über das recent-Fenster gegen die Baseline schon. Erwähne einen Trend außerhalb des Bands sachlich, ohne zu alarmieren.\n- Formuliere jede Richtung (steigend/fallend) als mögliche Erklärung (Fitness, Stress, Schlaf, Krankheit, Medikament), nie als Diagnose.",
+1 -4
View File
@@ -195,10 +195,7 @@ export function loadSafetyContracts(locale: Locale): SafetyContractMatrix {
* `.locale`. Throws if the rule has no body for the requested locale
* (this indicates a translation gap and is caught by the parity test).
*/
export function getGroundRuleBody(
locale: Locale,
key: GroundRuleKey,
): string {
export function getGroundRuleBody(locale: Locale, key: GroundRuleKey): string {
const matrix = loadSafetyContracts(locale);
const rule = matrix.ground_rules[key];
const body = locale === "en" ? rule.en : rule.locale;
+2 -6
View File
@@ -51,16 +51,12 @@ export function getStatusBatchSystemPrompt(
const en = `BATCHED OUTPUT — you are assessing SEVERAL of this user's metrics in one pass. Each metric below carries its OWN graded snapshot under a "### <key>" heading; assess each one STRICTLY from its own snapshot block, applying every rule above per metric. Do NOT let one metric's finding leak into another. Do NOT invent a metric: assess ONLY the keys present below.
OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. "perMetric" carries one short assessment (2-4 sentences each, the same contract as a single card) per present key:
{ "perMetric": { ${presentKeys
.map((k) => `"${k}": "..."`)
.join(", ")} } }
{ "perMetric": { ${presentKeys.map((k) => `"${k}": "..."`).join(", ")} } }
Include a key ONLY if its snapshot block is present below (keys present: ${keyList}). Omit any metric whose block is absent — never fabricate one.`;
const de = `GEBÜNDELTE AUSGABE — du bewertest in einem Durchgang MEHRERE Metriken dieses Nutzers. Jede Metrik unten trägt ihren EIGENEN graded snapshot unter einer Überschrift "### <key>"; bewerte jede ausschließlich aus ihrem eigenen Snapshot-Block und wende alle obigen Regeln pro Metrik an. Lass den Befund einer Metrik NICHT in eine andere überlaufen. Erfinde KEINE Metrik: bewerte NUR die unten vorhandenen Keys.
AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. "perMetric" enthält je vorhandenem Key eine kurze Einschätzung (je 2-4 Sätze, derselbe Vertrag wie eine Einzelkarte):
{ "perMetric": { ${presentKeys
.map((k) => `"${k}": "..."`)
.join(", ")} } }
{ "perMetric": { ${presentKeys.map((k) => `"${k}": "..."`).join(", ")} } }
Nimm einen Key NUR auf, wenn sein Snapshot-Block unten vorhanden ist (vorhandene Keys: ${keyList}). Lass jede Metrik ohne Block weg — erfinde niemals eine.`;
return `${base}\n\n${locale === "en" ? en : de}`;
}
+5 -2
View File
@@ -72,7 +72,9 @@ export interface ProviderSkipHint {
export interface ProviderHealthLedger {
/** Providers to skip right now for this user, keyed by chain type.
* Fails open (empty) on any error. */
getSkipHints(userId: string): Promise<Map<ProviderChainType, ProviderSkipHint>>;
getSkipHints(
userId: string,
): Promise<Map<ProviderChainType, ProviderSkipHint>>;
/** Record a successful generation — clears any negative cache. */
recordSuccess(userId: string, providerType: ProviderChainType): Promise<void>;
/** Record a hard failure. `httpStatus` null = network/transport. */
@@ -285,7 +287,8 @@ export function createInMemoryProviderHealthLedger(): ProviderHealthLedger & {
async recordFailure(userId, providerType, httpStatus) {
const { result, cooldownMs } = classifyFailure(httpStatus);
const prev = rowsFor(userId).get(providerType);
const priorCount = prev && prev.result !== "ok" ? prev.consecutiveFailures : 0;
const priorCount =
prev && prev.result !== "ok" ? prev.consecutiveFailures : 0;
rowsFor(userId).set(providerType, {
result,
status: httpStatus,
+1 -4
View File
@@ -418,10 +418,7 @@ export async function hasAnyConfiguredProvider(
where: { id: "singleton" },
select: { adminAiKeyEncrypted: true },
});
return userRowHasProviderCredential(
userRow,
!!settings?.adminAiKeyEncrypted,
);
return userRowHasProviderCredential(userRow, !!settings?.adminAiKeyEncrypted);
}
/**
+10 -8
View File
@@ -89,8 +89,8 @@ describe("gradeBpScore", () => {
// floor and descends smoothly. Walk across the systolic floor (90):
// dia 66 sits on the optimal plateau (offset −13 → 100) so the
// systolic axis is the worst-of(sys,dia) winner across the walk.
const sysWalk = [92, 91, 90, 89, 88].map(
(sys) => gradeBpScore({ sys, dia: 66, target: UNDER65 }),
const sysWalk = [92, 91, 90, 89, 88].map((sys) =>
gradeBpScore({ sys, dia: 66, target: UNDER65 }),
);
// AT and ABOVE the floor sit on the plateau (100).
expect(sysWalk[2]).toBe(100); // sys 90
@@ -108,8 +108,8 @@ describe("gradeBpScore", () => {
// the diastolic axis is the worst-of(sys,dia) winner throughout.
// sys 117 sits on the optimal plateau (offset −12 → 100) so the
// diastolic axis is the worst-of(sys,dia) winner across the walk.
const diaWalk = [52, 51, 50, 49, 48].map(
(dia) => gradeBpScore({ sys: 117, dia, target: UNDER65 }),
const diaWalk = [52, 51, 50, 49, 48].map((dia) =>
gradeBpScore({ sys: 117, dia, target: UNDER65 }),
);
expect(diaWalk[2]).toBe(100); // dia 50
expect(diaWalk[1]).toBe(100); // dia 51
@@ -177,9 +177,9 @@ describe("gradeBpScoreFromSeries", () => {
{ at: NOW, sys: 134, dia: 87 },
{ at: NOW, sys: 134, dia: 87 },
];
expect(
gradeBpScoreFromSeries({ pairs, target: UNDER65, now: NOW }),
).toBe(gradeBpScore({ sys: 134, dia: 87, target: UNDER65 }));
expect(gradeBpScoreFromSeries({ pairs, target: UNDER65, now: NOW })).toBe(
gradeBpScore({ sys: 134, dia: 87, target: UNDER65 }),
);
});
it("rollup (per-day-mean + count) and live (per-event) agree on the same data", () => {
@@ -218,6 +218,8 @@ describe("gradeBpScoreFromSeries", () => {
});
expect(live).not.toBeNull();
expect(rollup).not.toBeNull();
expect(Math.abs((live as number) - (rollup as number))).toBeLessThanOrEqual(1);
expect(Math.abs((live as number) - (rollup as number))).toBeLessThanOrEqual(
1,
);
});
});
@@ -35,8 +35,10 @@ import {
} from "@/lib/rollups/measurement-coverage";
import { computeBpInTargetFastPath } from "../bp-in-target-fast-path";
const MEASUREMENT_FIND_MANY = prisma.measurement.findMany as unknown as ReturnType<typeof vi.fn>;
const ROLLUP_FIND_MANY = prisma.measurementRollup.findMany as unknown as ReturnType<typeof vi.fn>;
const MEASUREMENT_FIND_MANY = prisma.measurement
.findMany as unknown as ReturnType<typeof vi.fn>;
const ROLLUP_FIND_MANY = prisma.measurementRollup
.findMany as unknown as ReturnType<typeof vi.fn>;
const PROBE = probeRollupCoverage as unknown as ReturnType<typeof vi.fn>;
const FULLY_COVERED = isFullyCovered as unknown as ReturnType<typeof vi.fn>;
const ANNOTATE = annotate as unknown as ReturnType<typeof vi.fn>;
@@ -223,11 +225,18 @@ describe("computeBpInTargetFastPath", () => {
// First findMany = SYS rows, second = DIA rows.
MEASUREMENT_FIND_MANY.mockResolvedValueOnce([
{ id: "1", measuredAt: new Date("2026-05-15T08:00:00.000Z"), value: 122 },
])
.mockResolvedValueOnce([
{ id: "2", measuredAt: new Date("2026-05-15T08:00:00.000Z"), value: 76 },
]);
{
id: "1",
measuredAt: new Date("2026-05-15T08:00:00.000Z"),
value: 122,
},
]).mockResolvedValueOnce([
{
id: "2",
measuredAt: new Date("2026-05-15T08:00:00.000Z"),
value: 76,
},
]);
const result = await computeBpInTargetFastPath({
userId: "user-partial",
@@ -94,13 +94,29 @@ interface FixtureEvent {
function dailyFixtureEvents(): FixtureEvent[] {
return [
// Taken on time across several days.
{ takenAt: slotAt(1, "08:05"), skipped: false, scheduledFor: slotAt(1, "08:00") },
{ takenAt: slotAt(2, "08:10"), skipped: false, scheduledFor: slotAt(2, "08:00") },
{ takenAt: slotAt(4, "07:55"), skipped: false, scheduledFor: slotAt(4, "08:00") },
{
takenAt: slotAt(1, "08:05"),
skipped: false,
scheduledFor: slotAt(1, "08:00"),
},
{
takenAt: slotAt(2, "08:10"),
skipped: false,
scheduledFor: slotAt(2, "08:00"),
},
{
takenAt: slotAt(4, "07:55"),
skipped: false,
scheduledFor: slotAt(4, "08:00"),
},
// A deliberate skip (excluded from the denominator).
{ takenAt: null, skipped: true, scheduledFor: slotAt(3, "08:00") },
// A taken-late dose (inside the late tail).
{ takenAt: slotAt(5, "10:30"), skipped: false, scheduledFor: slotAt(5, "08:00") },
{
takenAt: slotAt(5, "10:30"),
skipped: false,
scheduledFor: slotAt(5, "08:00"),
},
// Day 6 has no event → the unfilled slot reads missed.
// An auto-missed forgotten dose.
{
@@ -254,7 +270,8 @@ describe("dailyComplianceRatesFromLedger — cadence-aware per-day series", () =
// counted as a miss.
const from = new Date(NOW.getTime() - 30 * DAY_MS);
const windowRows = bundle.ledgerRows.filter(
(r) => r.at.getTime() >= from.getTime() && r.at.getTime() <= NOW.getTime(),
(r) =>
r.at.getTime() >= from.getTime() && r.at.getTime() <= NOW.getTime(),
);
const series = dailyComplianceRatesFromLedger(windowRows);
@@ -269,8 +286,7 @@ describe("dailyComplianceRatesFromLedger — cadence-aware per-day series", () =
// The series' mean rate equals the canonical compliance30 rate: the two
// are computed from the SAME ledger and cannot contradict each other.
const meanRate =
series.reduce((sum, d) => sum + d.rate, 0) / series.length;
const meanRate = series.reduce((sum, d) => sum + d.rate, 0) / series.length;
expect(Math.round(meanRate)).toBe(bundle.compliance30.rate);
});
@@ -300,7 +316,8 @@ describe("dailyComplianceRatesFromLedger — cadence-aware per-day series", () =
const from = new Date(NOW.getTime() - 30 * DAY_MS);
const windowRows = bundle.ledgerRows.filter(
(r) => r.at.getTime() >= from.getTime() && r.at.getTime() <= NOW.getTime(),
(r) =>
r.at.getTime() >= from.getTime() && r.at.getTime() <= NOW.getTime(),
);
const series = dailyComplianceRatesFromLedger(windowRows);
@@ -331,7 +348,14 @@ describe("tallyLedgerRows — windowed tallies over one shared ledger", () => {
);
const from = new Date(NOW.getTime() - 30 * DAY_MS);
const rows = buildComplianceLedgerRows(events, schedules, ctx, from, NOW, NOW);
const rows = buildComplianceLedgerRows(
events,
schedules,
ctx,
from,
NOW,
NOW,
);
expect(tallyLedgerRows(rows)).toEqual(
tallyComplianceFromLedger(events, schedules, ctx, from, NOW, NOW),
);
+127 -62
View File
@@ -194,10 +194,7 @@ describe("calculateCompliance — cadence-aware adapter", () => {
// `deletedAt: null` filter) and re-added as a fresh taken event.
const morning = new Date("2025-01-15T07:05:00Z");
const eveningReAdded = new Date("2025-01-15T19:10:00Z");
const events = [
eventAt(morning, true),
eventAt(eveningReAdded, true),
];
const events = [eventAt(morning, true), eventAt(eveningReAdded, true)];
const result = calculateCompliance(events, schedules, 1, undefined, {
now: lateNow,
});
@@ -429,14 +426,16 @@ describe("calculateCompliance — engine-routed (medicationContext)", () => {
vi.useRealTimers();
});
function ctx(overrides: Partial<{
startsOn: Date | null;
endsOn: Date | null;
oneShot: boolean;
createdAt: Date;
lastIntakeAt: Date | null;
timeZone: string;
}> = {}) {
function ctx(
overrides: Partial<{
startsOn: Date | null;
endsOn: Date | null;
oneShot: boolean;
createdAt: Date;
lastIntakeAt: Date | null;
timeZone: string;
}> = {},
) {
return {
startsOn: null,
endsOn: null,
@@ -457,7 +456,12 @@ describe("calculateCompliance — engine-routed (medicationContext)", () => {
// tail ends at 12:00) so it reads upcoming, not missed → a clean 100%.
// (The off-time / stricter behaviour is pinned in the next test.)
const schedules: ComplianceSchedule[] = [
{ windowStart: "08:00", windowEnd: "09:00", daysOfWeek: null, timesOfDay: ["08:00"] },
{
windowStart: "08:00",
windowEnd: "09:00",
daysOfWeek: null,
timesOfDay: ["08:00"],
},
];
const onTime = (d: Date) => ({
scheduledFor: d,
@@ -489,7 +493,12 @@ describe("calculateCompliance — engine-routed (medicationContext)", () => {
// percentage now agrees with the history view (the dose cannot read
// "taken" in the % while the ledger calls it ad-hoc).
const schedules: ComplianceSchedule[] = [
{ windowStart: "08:00", windowEnd: "09:00", daysOfWeek: null, timesOfDay: ["08:00"] },
{
windowStart: "08:00",
windowEnd: "09:00",
daysOfWeek: null,
timesOfDay: ["08:00"],
},
];
// takenAt = 08:00 slot + 4.5h = 12:30 (eventAt adds 30min onto a 12:00
// scheduledFor; here we pin the off-time take explicitly).
@@ -1186,7 +1195,12 @@ describe("expectedSlotCountForDay + lastNonSkippedTakenAt", () => {
// the 14th is 19:00 UTC on the 13th — a UTC slice would mislabel it.
expect(userDayKey(start, tz)).toBe("2025-01-14");
const count = expectedSlotCountForDay(schedules, start, dayEnd, ctx({ timeZone: tz }));
const count = expectedSlotCountForDay(
schedules,
start,
dayEnd,
ctx({ timeZone: tz }),
);
expect(count).toBe(1);
});
@@ -1302,7 +1316,7 @@ describe("classifyIntakeTiming", () => {
);
});
it('respects the configurable `lateMinutes` tail', () => {
it("respects the configurable `lateMinutes` tail", () => {
// With lateMinutes=30 the late tail collapses to 30 min after the
// 3h on-time grace. windowEnd is 09:00 so `on_time` extends to
// 12:00 and `late` extends to 12:30. A dose at 12:15 falls in
@@ -1733,11 +1747,21 @@ describe("buildComplianceDisplay — two rows, cadence-scaled windows", () => {
const slot1900 = new Date("2025-06-15T19:00:00Z");
const events = [
// 07:00 dose logged on time.
{ scheduledFor: slot0700, takenAt: new Date("2025-06-15T07:05:00Z"), skipped: false },
{
scheduledFor: slot0700,
takenAt: new Date("2025-06-15T07:05:00Z"),
skipped: false,
},
// 19:00 dose logged EARLY (before its slot) — snapped onto the 19:00 row.
{ scheduledFor: slot1900, takenAt: new Date("2025-06-15T11:30:00Z"), skipped: false },
{
scheduledFor: slot1900,
takenAt: new Date("2025-06-15T11:30:00Z"),
skipped: false,
},
];
const display = buildComplianceDisplay(events, schedules, ctx(), { now: NOW });
const display = buildComplianceDisplay(events, schedules, ctx(), {
now: NOW,
});
expect(display.currentCycle.nextDueAt).not.toBeNull();
// Next open slot is tomorrow 07:00 UTC, not today's resolved 19:00.
expect(display.currentCycle.nextDueAt!.toISOString()).toBe(
@@ -1756,9 +1780,15 @@ describe("buildComplianceDisplay — two rows, cadence-scaled windows", () => {
];
const slot0700 = new Date("2025-06-15T07:00:00Z");
const events = [
{ scheduledFor: slot0700, takenAt: new Date("2025-06-15T07:05:00Z"), skipped: false },
{
scheduledFor: slot0700,
takenAt: new Date("2025-06-15T07:05:00Z"),
skipped: false,
},
];
const display = buildComplianceDisplay(events, schedules, ctx(), { now: NOW });
const display = buildComplianceDisplay(events, schedules, ctx(), {
now: NOW,
});
expect(display.currentCycle.nextDueAt).not.toBeNull();
// Today's 19:00 is still open.
expect(display.currentCycle.nextDueAt!.toISOString()).toBe(
@@ -2152,16 +2182,25 @@ describe("calculateCompliance — autoMissed forgotten doses (BUG #2 / #3)", ()
},
];
const ctxVal = buildComplianceMedicationContext(
{ startsOn: null, endsOn: null, oneShot: false, createdAt: new Date("2025-01-08T00:00:00Z") },
{
startsOn: null,
endsOn: null,
oneShot: false,
createdAt: new Date("2025-01-08T00:00:00Z"),
},
lastNonSkippedTakenAt(events),
"UTC",
);
const display = buildComplianceDisplay(events, daily, ctxVal, { now: NOW });
// `expected` is the rate denominator (taken + missed), `missed` counts the
// forgotten doses, `taken` the numerator — the "taken / expected" triple.
expect(display.short.expected).toBe(display.short.taken + display.short.missed);
expect(display.short.expected).toBe(
display.short.taken + display.short.missed,
);
expect(display.short.missed).toBeGreaterThanOrEqual(1);
expect(display.long.expected).toBe(display.long.taken + display.long.missed);
expect(display.long.expected).toBe(
display.long.taken + display.long.missed,
);
});
});
@@ -2223,7 +2262,11 @@ describe("calculateCompliance — irregular twice-daily taker (BUG 3 verify)", (
// Cover EVERY slot the window emits so there is no stray uncovered slot:
// a 3-day window (NOW − 3d = Jan-12 12:00 .. Jan-15 12:00) holds Jan-12
// 19:00, Jan-13 07:00+19:00, Jan-14 07:00+19:00, Jan-15 07:00 = 6 slots.
const onTime = (s: Date) => ({ scheduledFor: s, takenAt: s, skipped: false });
const onTime = (s: Date) => ({
scheduledFor: s,
takenAt: s,
skipped: false,
});
const events = [
onTime(slot(12, "19:00")),
onTime(slot(13, "07:00")),
@@ -2231,7 +2274,9 @@ describe("calculateCompliance — irregular twice-daily taker (BUG 3 verify)", (
// 07:00 slot, logged at 08:13 (within the late tail → taken_late).
{
scheduledFor: slot(14, "07:00"),
takenAt: new Date(slot(14, "07:00").getTime() + (1 * 60 + 13) * 60 * 1000),
takenAt: new Date(
slot(14, "07:00").getTime() + (1 * 60 + 13) * 60 * 1000,
),
skipped: false,
},
onTime(slot(14, "19:00")),
@@ -2304,7 +2349,11 @@ describe("calculateCompliance — irregular twice-daily taker (BUG 3 verify)", (
skipped: false,
autoMissed: true,
});
const extra = (s: Date) => ({ scheduledFor: s, takenAt: s, skipped: false });
const extra = (s: Date) => ({
scheduledFor: s,
takenAt: s,
skipped: false,
});
const events = [
taken(slot(8, "19:00"), 20, 10),
@@ -2355,35 +2404,35 @@ describe("deriveDoseStatus — daily / intraday windows", () => {
const target = new Date("2025-01-15T08:00:00Z");
it("on-time within ±60 min of target", () => {
expect(deriveDoseStatus(target, "daily", new Date("2025-01-15T08:30:00Z"))).toBe(
"on_time_window",
);
expect(deriveDoseStatus(target, "daily", new Date("2025-01-15T07:15:00Z"))).toBe(
"on_time_window",
);
expect(
deriveDoseStatus(target, "daily", new Date("2025-01-15T08:30:00Z")),
).toBe("on_time_window");
expect(
deriveDoseStatus(target, "daily", new Date("2025-01-15T07:15:00Z")),
).toBe("on_time_window");
});
it("upcoming before the on-time window opens", () => {
expect(deriveDoseStatus(target, "daily", new Date("2025-01-15T06:30:00Z"))).toBe(
"upcoming",
);
expect(
deriveDoseStatus(target, "daily", new Date("2025-01-15T06:30:00Z")),
).toBe("upcoming");
});
it("overdue between +60 min and +240 min", () => {
// +90 min → past on-time (60), before miss cutoff (240).
expect(deriveDoseStatus(target, "daily", new Date("2025-01-15T09:30:00Z"))).toBe(
"overdue",
);
expect(
deriveDoseStatus(target, "daily", new Date("2025-01-15T09:30:00Z")),
).toBe("overdue");
// +239 min → still overdue (takeable).
expect(deriveDoseStatus(target, "daily", new Date("2025-01-15T11:59:00Z"))).toBe(
"overdue",
);
expect(
deriveDoseStatus(target, "daily", new Date("2025-01-15T11:59:00Z")),
).toBe("overdue");
});
it("missed past +240 min", () => {
expect(deriveDoseStatus(target, "daily", new Date("2025-01-15T12:30:00Z"))).toBe(
"missed",
);
expect(
deriveDoseStatus(target, "daily", new Date("2025-01-15T12:30:00Z")),
).toBe("missed");
});
it("never overlaps the next dose's on-time window", () => {
@@ -2424,19 +2473,19 @@ describe("deriveDoseStatus — weekly GLP-1 (4-day rule)", () => {
const target = new Date("2025-01-13T08:00:00Z"); // a Monday shot
it("on-time within ±1 day of the target day", () => {
expect(deriveDoseStatus(target, "weekly", new Date("2025-01-13T20:00:00Z"))).toBe(
"on_time_window",
);
expect(deriveDoseStatus(target, "weekly", new Date("2025-01-14T07:00:00Z"))).toBe(
"on_time_window",
);
expect(
deriveDoseStatus(target, "weekly", new Date("2025-01-13T20:00:00Z")),
).toBe("on_time_window");
expect(
deriveDoseStatus(target, "weekly", new Date("2025-01-14T07:00:00Z")),
).toBe("on_time_window");
});
it("overdue up to +4 days (still counts when taken — the clinical rule)", () => {
// +3 days from target → past the ±1-day on-time window, before +4 days.
expect(deriveDoseStatus(target, "weekly", new Date("2025-01-16T12:00:00Z"))).toBe(
"overdue",
);
expect(
deriveDoseStatus(target, "weekly", new Date("2025-01-16T12:00:00Z")),
).toBe("overdue");
// Taken on day +3 still counts as late, not missed.
expect(
deriveDoseStatus(target, "weekly", new Date("2025-01-20T00:00:00Z"), {
@@ -2446,15 +2495,16 @@ describe("deriveDoseStatus — weekly GLP-1 (4-day rule)", () => {
});
it("missed past +4 days", () => {
expect(deriveDoseStatus(target, "weekly", new Date("2025-01-18T12:00:00Z"))).toBe(
"missed",
);
expect(
deriveDoseStatus(target, "weekly", new Date("2025-01-18T12:00:00Z")),
).toBe("missed");
});
it("defaults match the documented 60-min / 240-min / 4-day boundaries", () => {
expect(DOSE_WINDOW_DEFAULTS.dailyOnTimeMinutes).toBe(60);
expect(
DOSE_WINDOW_DEFAULTS.dailyOnTimeMinutes + DOSE_WINDOW_DEFAULTS.dailyOverdueMinutes,
DOSE_WINDOW_DEFAULTS.dailyOnTimeMinutes +
DOSE_WINDOW_DEFAULTS.dailyOverdueMinutes,
).toBe(240);
expect(DOSE_WINDOW_DEFAULTS.weeklyOverdueDays).toBe(4);
});
@@ -2463,7 +2513,11 @@ describe("deriveDoseStatus — weekly GLP-1 (4-day rule)", () => {
describe("doseCadenceFamily", () => {
it("rolling ≥ 2 days → weekly", () => {
expect(
doseCadenceFamily({ windowStart: "08:00", windowEnd: "09:00", rollingIntervalDays: 7 }),
doseCadenceFamily({
windowStart: "08:00",
windowEnd: "09:00",
rollingIntervalDays: 7,
}),
).toBe("weekly");
});
it("WEEKLY rrule → weekly", () => {
@@ -2477,12 +2531,20 @@ describe("doseCadenceFamily", () => {
});
it("daily rrule → daily", () => {
expect(
doseCadenceFamily({ windowStart: "08:00", windowEnd: "09:00", rrule: "FREQ=DAILY" }),
doseCadenceFamily({
windowStart: "08:00",
windowEnd: "09:00",
rrule: "FREQ=DAILY",
}),
).toBe("daily");
});
it("plain legacy daily → daily", () => {
expect(
doseCadenceFamily({ windowStart: "08:00", windowEnd: "09:00", daysOfWeek: null }),
doseCadenceFamily({
windowStart: "08:00",
windowEnd: "09:00",
daysOfWeek: null,
}),
).toBe("daily");
});
});
@@ -2506,7 +2568,10 @@ import {
reconstructDoseHistory,
type HistoryIntake,
} from "@/lib/medications/scheduling/dose-history";
import type { CanonicalSchedule, RecurrenceContext } from "@/lib/medications/scheduling/recurrence";
import type {
CanonicalSchedule,
RecurrenceContext,
} from "@/lib/medications/scheduling/recurrence";
describe("tallyComplianceFromLedger — the unified % keystone", () => {
const TZ = "Europe/Berlin";
@@ -40,10 +40,15 @@ import {
computeCorrelationHypothesesFastPath,
} from "../correlations-fast-path";
const MEASUREMENT_FIND_MANY = prisma.measurement.findMany as unknown as ReturnType<typeof vi.fn>;
const ROLLUP_FIND_MANY = prisma.measurementRollup.findMany as unknown as ReturnType<typeof vi.fn>;
const MOOD_FIND_MANY = prisma.moodEntry.findMany as unknown as ReturnType<typeof vi.fn>;
const INTAKE_FIND_MANY = prisma.medicationIntakeEvent.findMany as unknown as ReturnType<typeof vi.fn>;
const MEASUREMENT_FIND_MANY = prisma.measurement
.findMany as unknown as ReturnType<typeof vi.fn>;
const ROLLUP_FIND_MANY = prisma.measurementRollup
.findMany as unknown as ReturnType<typeof vi.fn>;
const MOOD_FIND_MANY = prisma.moodEntry.findMany as unknown as ReturnType<
typeof vi.fn
>;
const INTAKE_FIND_MANY = prisma.medicationIntakeEvent
.findMany as unknown as ReturnType<typeof vi.fn>;
const PROBE = probeRollupCoverage as unknown as ReturnType<typeof vi.fn>;
const FULLY_COVERED = isFullyCovered as unknown as ReturnType<typeof vi.fn>;
const ANNOTATE = annotate as unknown as ReturnType<typeof vi.fn>;
@@ -82,8 +87,7 @@ describe("computeCorrelationHypothesesFastPath", () => {
FULLY_COVERED.mockReturnValue(true);
PROBE.mockResolvedValue(coverage);
ROLLUP_FIND_MANY
.mockResolvedValueOnce([]) // SYS buckets
ROLLUP_FIND_MANY.mockResolvedValueOnce([]) // SYS buckets
.mockResolvedValueOnce([]) // PULSE buckets
.mockResolvedValueOnce([]); // WEIGHT buckets
@@ -141,8 +145,7 @@ describe("computeCorrelationHypothesesFastPath", () => {
FULLY_COVERED.mockReturnValue(false);
PROBE.mockResolvedValue(coverage);
MEASUREMENT_FIND_MANY
.mockResolvedValueOnce([]) // SYS rows
MEASUREMENT_FIND_MANY.mockResolvedValueOnce([]) // SYS rows
.mockResolvedValueOnce([]) // PULSE rows
.mockResolvedValueOnce([]); // WEIGHT rows
@@ -250,7 +253,9 @@ describe("computeCorrelationHypothesesFastPath", () => {
const calls = ANNOTATE.mock.calls.map((c) => c[0]);
const corrCall = calls.find((c) => c?.meta?.correlations !== undefined);
expect(corrCall?.meta.correlations.tz_guard).toBe("non-utc-live-fallback");
expect(corrCall?.meta.correlations.tz_guard).toBe(
"non-utc-live-fallback",
);
expect(corrCall?.meta.correlations.path).toBe("live");
});
@@ -102,4 +102,3 @@ describe("pearsonCorrelation", () => {
expect(result.strength).toBe("keine");
});
});
@@ -254,13 +254,10 @@ describe("computeGlucoseClinicalMetrics — window + adequacy", () => {
// 30/day. That clears the bar legitimately, but a handful in a day must
// not: 10 readings in one day = 10/day < 24 → still a spot estimate.
const HOUR = 60 * 60 * 1000;
const readings: GlucoseReading[] = Array.from(
{ length: 10 },
(_, i) => ({
mgdl: 115,
measuredAt: new Date(now.getTime() - i * HOUR),
}),
);
const readings: GlucoseReading[] = Array.from({ length: 10 }, (_, i) => ({
mgdl: 115,
measuredAt: new Date(now.getTime() - i * HOUR),
}));
const m = computeGlucoseClinicalMetrics(readings, { now });
expect(m.isSpotEstimate).toBe(true);
});
@@ -309,10 +306,9 @@ describe("computeGlucoseClinicalMetrics — window + adequacy", () => {
it("clears above both thresholds and asserts the panel", () => {
// 14 readings, one per day → 13-day span, count 14
const values = Array.from({ length: 14 }, () => 154);
const m = computeGlucoseClinicalMetrics(
dailyReadings(values, now),
{ now },
);
const m = computeGlucoseClinicalMetrics(dailyReadings(values, now), {
now,
});
expect(m.readingCount).toBe(14);
expect(m.actualSpanDays).toBeCloseTo(13, 6);
expect(m.stillLearning).toBe(false);
@@ -38,9 +38,7 @@ function readings() {
}
/** Coach projection — mirrors `snapshot.glucose.clinical` (asserted branch). */
function coachProjection(
m: ReturnType<typeof computeGlucoseClinicalMetrics>,
) {
function coachProjection(m: ReturnType<typeof computeGlucoseClinicalMetrics>) {
return {
tirPercent: m.distribution ? Math.round(m.distribution.tir * 100) : null,
gmi: m.gmi !== null ? Math.round(m.gmi * 10) / 10 : null,
@@ -52,9 +50,7 @@ function coachProjection(
}
/** Panel projection — mirrors the panel's display rounding. */
function panelProjection(
m: ReturnType<typeof computeGlucoseClinicalMetrics>,
) {
function panelProjection(m: ReturnType<typeof computeGlucoseClinicalMetrics>) {
return {
tirPercent: m.distribution ? Math.round(m.distribution.tir * 100) : null,
gmi: m.gmi !== null ? Number(m.gmi.toFixed(1)) : null,
@@ -12,7 +12,10 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { DEFAULT_WINDOW_DAYS, GLUCOSE_PANEL_WINDOW_DAYS } from "../glucose-metrics";
import {
DEFAULT_WINDOW_DAYS,
GLUCOSE_PANEL_WINDOW_DAYS,
} from "../glucose-metrics";
const ROOT = join(__dirname, "..", "..", "..", "..");
@@ -43,7 +46,9 @@ describe("glucose panel window constant", () => {
// It must NOT be a bare `windowDays: 30` glucose literal.
const usesConstantDirectly = /GLUCOSE_PANEL_WINDOW_DAYS/.test(src);
const aliasesConstant =
/GLUCOSE_CLINICAL_WINDOW_DAYS\s*=\s*GLUCOSE_PANEL_WINDOW_DAYS/.test(src);
/GLUCOSE_CLINICAL_WINDOW_DAYS\s*=\s*GLUCOSE_PANEL_WINDOW_DAYS/.test(
src,
);
expect(usesConstantDirectly || aliasesConstant).toBe(true);
},
);
@@ -37,11 +37,17 @@ import {
} from "@/lib/rollups/measurement-coverage";
import { computeUserHealthScoreFastPath } from "../health-score-fast-path";
const MEASUREMENT_FIND_MANY = prisma.measurement.findMany as unknown as ReturnType<typeof vi.fn>;
const ROLLUP_FIND_MANY = prisma.measurementRollup.findMany as unknown as ReturnType<typeof vi.fn>;
const MOOD_FIND_MANY = prisma.moodEntry.findMany as unknown as ReturnType<typeof vi.fn>;
const MEDICATION_FIND_MANY = prisma.medication.findMany as unknown as ReturnType<typeof vi.fn>;
const INTAKE_FIND_MANY = prisma.medicationIntakeEvent.findMany as unknown as ReturnType<typeof vi.fn>;
const MEASUREMENT_FIND_MANY = prisma.measurement
.findMany as unknown as ReturnType<typeof vi.fn>;
const ROLLUP_FIND_MANY = prisma.measurementRollup
.findMany as unknown as ReturnType<typeof vi.fn>;
const MOOD_FIND_MANY = prisma.moodEntry.findMany as unknown as ReturnType<
typeof vi.fn
>;
const MEDICATION_FIND_MANY = prisma.medication
.findMany as unknown as ReturnType<typeof vi.fn>;
const INTAKE_FIND_MANY = prisma.medicationIntakeEvent
.findMany as unknown as ReturnType<typeof vi.fn>;
const PROBE = probeRollupCoverage as unknown as ReturnType<typeof vi.fn>;
const FULLY_COVERED = isFullyCovered as unknown as ReturnType<typeof vi.fn>;
const ANNOTATE = annotate as unknown as ReturnType<typeof vi.fn>;
@@ -110,17 +116,16 @@ describe("computeUserHealthScoreFastPath", () => {
// null` and the score shape is unchanged.
ROLLUP_FIND_MANY.mockResolvedValueOnce([]);
// Weight source-attribution read (narrow 2-column).
MEASUREMENT_FIND_MANY
.mockResolvedValueOnce([
{
measuredAt: new Date("2026-05-10T08:00:00.000Z"),
source: "WITHINGS",
},
{
measuredAt: new Date("2026-05-14T08:00:00.000Z"),
source: "WITHINGS",
},
])
MEASUREMENT_FIND_MANY.mockResolvedValueOnce([
{
measuredAt: new Date("2026-05-10T08:00:00.000Z"),
source: "WITHINGS",
},
{
measuredAt: new Date("2026-05-14T08:00:00.000Z"),
source: "WITHINGS",
},
])
// BP-SYS source attribution read (parallel after rollup).
.mockResolvedValueOnce([]);
@@ -311,18 +316,14 @@ describe("computeUserHealthScoreFastPath", () => {
// raw weight findMany is already doing the work.
expect(ROLLUP_FIND_MANY).not.toHaveBeenCalled();
const calls = ANNOTATE.mock.calls.map((c) => c[0]);
const pathCall = calls.find(
(c) => c?.meta?.healthScore?.path === "live",
);
const pathCall = calls.find((c) => c?.meta?.healthScore?.path === "live");
expect(pathCall?.meta?.healthScore?.weightLongWindow).toBeNull();
});
});
describe("live fallback — WEIGHT not covered", () => {
it("reads raw weight rows and pins path:live", async () => {
const coverage = new Map<string, boolean>([
["WEIGHT", false],
]);
const coverage = new Map<string, boolean>([["WEIGHT", false]]);
FULLY_COVERED.mockReturnValue(false);
PROBE.mockResolvedValue(coverage);
@@ -354,9 +355,7 @@ describe("computeUserHealthScoreFastPath", () => {
expect(result).not.toBeNull();
const calls = ANNOTATE.mock.calls.map((c) => c[0]);
const pathCall = calls.find(
(c) => c?.meta?.healthScore?.path === "live",
);
const pathCall = calls.find((c) => c?.meta?.healthScore?.path === "live");
expect(pathCall).toBeDefined();
// Live path read raw weight (value selected).
const weightCall = MEASUREMENT_FIND_MANY.mock.calls[0][0];
@@ -491,9 +490,9 @@ describe("computeUserHealthScoreFastPath", () => {
// care here that the two windows are NOT pinned to the same
// value any more.
const componentDelta = result?.delta;
expect(typeof componentDelta === "number" || componentDelta === null).toBe(
true,
);
expect(
typeof componentDelta === "number" || componentDelta === null,
).toBe(true);
});
it("falls back to bpInTargetPct when bpInTargetPctPriorWeek is omitted", async () => {
@@ -802,8 +801,7 @@ describe("computeUserHealthScoreFastPath", () => {
const now = new Date("2026-06-07T12:00:00.000Z");
// Live fallback — no weight/mood/med data; only BP signal present.
MEASUREMENT_FIND_MANY
.mockResolvedValueOnce([]) // weight raw read (live fallback)
MEASUREMENT_FIND_MANY.mockResolvedValueOnce([]) // weight raw read (live fallback)
.mockResolvedValueOnce([]); // BP-SYS source attribution
const result = await computeUserHealthScoreFastPath({
@@ -828,8 +826,7 @@ describe("computeUserHealthScoreFastPath", () => {
PROBE.mockResolvedValue(coverage);
const now = new Date("2026-06-07T12:00:00.000Z");
MEASUREMENT_FIND_MANY
.mockResolvedValueOnce([]) // weight raw read
MEASUREMENT_FIND_MANY.mockResolvedValueOnce([]) // weight raw read
.mockResolvedValueOnce([]); // BP-SYS source attribution
// Mood present so the score is computable even without BP.
MOOD_FIND_MANY.mockResolvedValue(
@@ -76,7 +76,11 @@ describe("buildHealthScoreBpInputs — single Health-Score BP input builder", ()
// path), but here it is genuinely thin so the rate is the all-time pct.
// Pin the stricter case: when all-time is also null the pillar collapses.
const thin = buildHealthScoreBpInputs(
env({ last90Days: { pct: 100, pairs: 4 }, allTime: null, gradedScore: 95 }),
env({
last90Days: { pct: 100, pairs: 4 },
allTime: null,
gradedScore: 95,
}),
null,
);
expect(thin.bpInTargetPct).toBeNull();
@@ -101,7 +101,10 @@ describe("weightTrendAlignment", () => {
it("scores the bare trend (non-null) when the target is missing but ≥2 readings exist", () => {
// No target → fall back to trend-only scoring instead of returning
// null, so a height-less user with weight data gets a populated pillar.
const result = weightTrendAlignment(weightSeries([80, 80.5, 81, 81.5]), null);
const result = weightTrendAlignment(
weightSeries([80, 80.5, 81, 81.5]),
null,
);
expect(result).not.toBeNull();
expect(result!).toBeGreaterThanOrEqual(0);
expect(result!).toBeLessThanOrEqual(100);
@@ -112,13 +115,19 @@ describe("weightTrendAlignment", () => {
});
it("rewards a gentle decline (>75) when there is no target", () => {
const result = weightTrendAlignment(weightSeries([84, 83.5, 83, 82.5]), null);
const result = weightTrendAlignment(
weightSeries([84, 83.5, 83, 82.5]),
null,
);
expect(result).not.toBeNull();
expect(result!).toBeGreaterThan(75);
});
it("penalises a sustained rise (<75) when there is no target", () => {
const result = weightTrendAlignment(weightSeries([80, 81, 82, 83, 84]), null);
const result = weightTrendAlignment(
weightSeries([80, 81, 82, 83, 84]),
null,
);
expect(result).not.toBeNull();
expect(result!).toBeLessThan(75);
});
@@ -23,7 +23,9 @@ describe("deriveRestingProxyFromPulse", () => {
// One Berlin day: 10 resting reads ~70, 500 workout reads ~150.
const samples: PulseSample[] = [];
for (let i = 0; i < 10; i++) {
samples.push(day(`2026-06-01T06:${String(i).padStart(2, "0")}:00`, 68 + i));
samples.push(
day(`2026-06-01T06:${String(i).padStart(2, "0")}:00`, 68 + i),
);
}
for (let i = 0; i < 500; i++) {
samples.push(
@@ -48,7 +50,12 @@ describe("deriveRestingProxyFromPulse", () => {
// Realistic day: 30 waking/spot reads 70-95, 10 workout reads ~150.
const samples: PulseSample[] = [];
for (let i = 0; i < 30; i++) {
samples.push(day(`2026-06-02T08:${String(i % 60).padStart(2, "0")}:00`, 70 + (i % 25)));
samples.push(
day(
`2026-06-02T08:${String(i % 60).padStart(2, "0")}:00`,
70 + (i % 25),
),
);
}
for (let i = 0; i < 10; i++) {
samples.push(day(`2026-06-02T18:${String(i).padStart(2, "0")}:00`, 150));
@@ -107,7 +114,10 @@ describe("deriveRestingProxyFromPulse", () => {
describe("resolveRestingPulseSeries", () => {
it("prefers RESTING_HEART_RATE — a clean ~72 series reads as resting", () => {
const restingSamples: PulseSample[] = Array.from({ length: 30 }, (_v, i) =>
day(`2026-06-${String((i % 28) + 1).padStart(2, "0")}T06:00:00`, 70 + (i % 5)),
day(
`2026-06-${String((i % 28) + 1).padStart(2, "0")}T06:00:00`,
70 + (i % 5),
),
);
// Workout-polluted PULSE that should be IGNORED in favour of resting.
const pulseSamples: PulseSample[] = Array.from({ length: 500 }, () =>
@@ -486,9 +486,7 @@ describe("reconstructSleepSessions", () => {
expect(sessions[0].source).toBe("APPLE_HEALTH");
// The surviving segments carry a real timeline: more than one
// distinct end instant.
const ends = new Set(
sessions[0].segments.map((seg) => seg.end.getTime()),
);
const ends = new Set(sessions[0].segments.map((seg) => seg.end.getTime()));
expect(ends.size).toBeGreaterThan(1);
});
@@ -243,12 +243,7 @@ describe("pickCanonicalSourceRows — v1.4.25 W8c two-axis device-type picker",
default: ["phone", "watch"],
},
};
const out = pickCanonicalSourceRows(
rows,
"steps",
userPriority,
isoDayKey,
);
const out = pickCanonicalSourceRows(rows, "steps", userPriority, isoDayKey);
expect(out.canonicalRows).toHaveLength(1);
expect(out.canonicalRows[0].deviceType).toBe("phone");
expect(out.canonicalRows[0].value).toBe(4100);
@@ -280,12 +275,7 @@ describe("pickCanonicalSourceRows — v1.4.25 W8c two-axis device-type picker",
ACTIVITY_STEPS: ["phone", "watch"],
},
};
const out = pickCanonicalSourceRows(
rows,
"steps",
userPriority,
isoDayKey,
);
const out = pickCanonicalSourceRows(rows, "steps", userPriority, isoDayKey);
expect(out.canonicalRows).toHaveLength(1);
expect(out.canonicalRows[0].deviceType).toBe("phone");
});
@@ -365,12 +355,7 @@ describe("pickCanonicalSourceRows — v1.4.25 W8c two-axis device-type picker",
default: ["ring"],
},
};
const out = pickCanonicalSourceRows(
rows,
"steps",
userPriority,
isoDayKey,
);
const out = pickCanonicalSourceRows(rows, "steps", userPriority, isoDayKey);
expect(out.canonicalRows).toHaveLength(2);
});
@@ -425,12 +410,7 @@ describe("pickCanonicalSourceRows — v1.4.25 W8c two-axis device-type picker",
ACTIVITY_STEPS: ["watch", "phone"],
},
};
const out = pickCanonicalSourceRows(
rows,
"steps",
userPriority,
isoDayKey,
);
const out = pickCanonicalSourceRows(rows, "steps", userPriority, isoDayKey);
// Expect: WEIGHT's scale row + ACTIVITY_STEPS' watch row. Both
// types' losers (WEIGHT's watch row, ACTIVITY_STEPS' phone row)
// are dropped against their OWN ladder, not the bucket's first
@@ -485,12 +465,7 @@ describe("pickCanonicalSourceRows — v1.4.25 W8c two-axis device-type picker",
WEIGHT: ["ring"],
},
};
const out = pickCanonicalSourceRows(
rows,
"steps",
userPriority,
isoDayKey,
);
const out = pickCanonicalSourceRows(rows, "steps", userPriority, isoDayKey);
// 2 WEIGHT rows kept (per-type fall-through) + 1 ACTIVITY_STEPS
// watch row (phone dropped against default ladder).
expect(out.canonicalRows).toHaveLength(3);
@@ -69,16 +69,18 @@ import { computeSummariesSlice } from "../summaries-slice";
const RAW = prisma.$queryRaw as unknown as ReturnType<typeof vi.fn>;
const UNSAFE = prisma.$queryRawUnsafe as unknown as ReturnType<typeof vi.fn>;
const USER_FIND_UNIQUE =
prisma.user.findUnique as unknown as ReturnType<typeof vi.fn>;
const MEASUREMENT_FIND_FIRST =
prisma.measurement.findFirst as unknown as ReturnType<typeof vi.fn>;
const ROLLUP_FIND_MANY =
prisma.measurementRollup.findMany as unknown as ReturnType<typeof vi.fn>;
const ROLLUP_FIND_FIRST =
prisma.measurementRollup.findFirst as unknown as ReturnType<typeof vi.fn>;
const CANONICAL_FROM =
canonicalMeasurementsFrom as unknown as ReturnType<typeof vi.fn>;
const USER_FIND_UNIQUE = prisma.user.findUnique as unknown as ReturnType<
typeof vi.fn
>;
const MEASUREMENT_FIND_FIRST = prisma.measurement
.findFirst as unknown as ReturnType<typeof vi.fn>;
const ROLLUP_FIND_MANY = prisma.measurementRollup
.findMany as unknown as ReturnType<typeof vi.fn>;
const ROLLUP_FIND_FIRST = prisma.measurementRollup
.findFirst as unknown as ReturnType<typeof vi.fn>;
const CANONICAL_FROM = canonicalMeasurementsFrom as unknown as ReturnType<
typeof vi.fn
>;
beforeEach(() => {
RAW.mockReset();
@@ -603,4 +605,3 @@ describe("computeSummariesSlice", () => {
});
});
});
@@ -6,7 +6,8 @@ import {
} from "../window-confidence";
const NOW = new Date("2026-06-14T12:00:00.000Z");
const daysAgo = (n: number) => new Date(NOW.getTime() - n * 24 * 60 * 60 * 1000);
const daysAgo = (n: number) =>
new Date(NOW.getTime() - n * 24 * 60 * 60 * 1000);
describe("computeWindowConfidence", () => {
it("flags a sample below the floor as insufficient", () => {
+1 -4
View File
@@ -51,10 +51,7 @@
* Pure & deterministic — fully unit-tested in `__tests__/bp-grade.test.ts`.
*/
import type { BpTargets } from "./bp-targets";
import {
SYS_HYPOTENSION_FLOOR,
DIA_HYPOTENSION_FLOOR,
} from "./bp-in-target";
import { SYS_HYPOTENSION_FLOOR, DIA_HYPOTENSION_FLOOR } from "./bp-in-target";
/**
* Offset-from-ceiling → axis-score anchors. Offsets are in mmHg; the
+6 -1
View File
@@ -310,7 +310,12 @@ async function computeFromRollups(
// All-time on the rollup path is bounded to the 395-day read window.
// Documented above — same trade-off the per-event path makes when the
// user's history extends beyond 365 days.
const allTime = bucketWindow(pairsByDay, threeNinetyFiveDaysAgo, now, targets);
const allTime = bucketWindow(
pairsByDay,
threeNinetyFiveDaysAgo,
now,
targets,
);
const priorMonth = bucketWindow(
pairsByDay,
sixtyDaysAgo,
+79 -34
View File
@@ -328,8 +328,13 @@ export function deriveDoseStatus(
* the 4-day rule); everything denser (daily, intraday multi-dose) is
* `daily` (minute-scale windows).
*/
export function doseCadenceFamily(schedule: ComplianceSchedule): DoseCadenceFamily {
if (schedule.rollingIntervalDays != null && schedule.rollingIntervalDays >= 2) {
export function doseCadenceFamily(
schedule: ComplianceSchedule,
): DoseCadenceFamily {
if (
schedule.rollingIntervalDays != null &&
schedule.rollingIntervalDays >= 2
) {
return "weekly";
}
const rrule = schedule.rrule ?? "";
@@ -496,7 +501,9 @@ function rollingIntakeInstants(
return events
.filter(
(e): e is { takenAt: Date; skipped: boolean } =>
!e.skipped && e.takenAt !== null && e.takenAt.getTime() <= now.getTime(),
!e.skipped &&
e.takenAt !== null &&
e.takenAt.getTime() <= now.getTime(),
)
.map((e) => e.takenAt)
.sort((a, b) => a.getTime() - b.getTime());
@@ -584,7 +591,9 @@ function intakeInstantsAtOrBefore(
return intakes
.filter(
(e): e is { takenAt: Date; skipped: boolean } =>
!e.skipped && e.takenAt !== null && e.takenAt.getTime() <= now.getTime(),
!e.skipped &&
e.takenAt !== null &&
e.takenAt.getTime() <= now.getTime(),
)
.map((e) => e.takenAt)
.sort((a, b) => a.getTime() - b.getTime());
@@ -658,7 +667,13 @@ export function expectedSlotCountForDay(
ctx.scheduleRevisions ?? [],
schedules.map((s, i) => toCanonicalSchedule(s, `compliance-daily-${i}`)),
(schedule, eraFrom, eraTo) =>
expandComplianceOccurrences(schedule, recurrenceCtx, eraFrom, eraTo, retro),
expandComplianceOccurrences(
schedule,
recurrenceCtx,
eraFrom,
eraTo,
retro,
),
{ oneShot: ctx.oneShot },
).length;
}
@@ -712,7 +727,13 @@ export function expectedSlotsBetween(
ctx.scheduleRevisions ?? [],
schedules.map((s, i) => toCanonicalSchedule(s, `compliance-slots-${i}`)),
(schedule, eraFrom, eraTo) =>
expandComplianceOccurrences(schedule, recurrenceCtx, eraFrom, eraTo, retro),
expandComplianceOccurrences(
schedule,
recurrenceCtx,
eraFrom,
eraTo,
retro,
),
{ oneShot: ctx.oneShot },
);
}
@@ -735,12 +756,13 @@ export const MIN_STABLE_DOSES = 4;
* expected doses to mean something, up to a 12-month long window for very
* rare meds.
*/
export const COMPLIANCE_WINDOW_LADDER: ReadonlyArray<readonly [number, number]> =
[
[7, 30],
[30, 90],
[90, 365],
];
export const COMPLIANCE_WINDOW_LADDER: ReadonlyArray<
readonly [number, number]
> = [
[7, 30],
[30, 90],
[90, 365],
];
/**
* v1.8.6 — the day-counts of the two compliance windows a medication's
@@ -803,10 +825,7 @@ export function selectComplianceWindows(
for (const [shortDays, longDays] of COMPLIANCE_WINDOW_LADDER) {
const expectedShort = expected(shortDays);
const expectedLong = expected(longDays);
if (
expectedShort >= MIN_STABLE_DOSES &&
expectedLong >= MIN_STABLE_DOSES
) {
if (expectedShort >= MIN_STABLE_DOSES && expectedLong >= MIN_STABLE_DOSES) {
return { shortDays, longDays, expectedShort, expectedLong };
}
}
@@ -886,7 +905,13 @@ export interface ComplianceDisplay {
* `missed` the count counting against the rate. The card can render
* "26 / 30 · 87%" from `taken` / `expected`.
*/
short: { rate: number; taken: number; expected: number; missed: number; streak: number };
short: {
rate: number;
taken: number;
expected: number;
missed: number;
streak: number;
};
/** Compliance percentage + counts over the long window. */
long: { rate: number; taken: number; expected: number; missed: number };
/**
@@ -967,8 +992,7 @@ export function buildCurrentCycle(
const recurrenceCtx = toRecurrenceCtx(ctx, "compliance-cycle");
const DAY_MS = 24 * 60 * 60 * 1000;
const allIntakes = intakes ?? [];
const lastIntakeAt =
lastNonSkippedTakenAt(allIntakes) ?? ctx.lastIntakeAt;
const lastIntakeAt = lastNonSkippedTakenAt(allIntakes) ?? ctx.lastIntakeAt;
// The open cycle's due instant + grace, picked as the SOONEST across every
// schedule. Two paths:
@@ -999,10 +1023,9 @@ export function buildCurrentCycle(
const anchor =
lastIntakeAt !== null
? new Date(lastIntakeAt.getTime() + n * DAY_MS)
: ctx.startsOn ?? ctx.createdAt;
: (ctx.startsOn ?? ctx.createdAt);
if (ctx.endsOn && anchor.getTime() > ctx.endsOn.getTime()) continue;
const graceMs =
(canonical.reminderGraceMinutes ?? 60) * 60 * 1000;
const graceMs = (canonical.reminderGraceMinutes ?? 60) * 60 * 1000;
consider(anchor, new Date(anchor.getTime() + graceMs));
continue;
}
@@ -1032,7 +1055,12 @@ export function buildCurrentCycle(
const hasClosedCycles = expectedShort > 0;
if (dueAt === null || graceUntil === null) {
return { state: "none", nextDueAt: null, graceUntil: null, hasClosedCycles };
return {
state: "none",
nextDueAt: null,
graceUntil: null,
hasClosedCycles,
};
}
// Narrow the union-mutated locals for the comparisons below.
@@ -1073,10 +1101,16 @@ export function buildComplianceDisplay(
const { shortDays, longDays, expectedShort, expectedLong } =
selectComplianceWindows(schedules, ctx, { now, intakes: events });
const short = calculateCompliance(events, schedules, shortDays, ctx.createdAt, {
now,
medicationContext: ctx,
});
const short = calculateCompliance(
events,
schedules,
shortDays,
ctx.createdAt,
{
now,
medicationContext: ctx,
},
);
const long = calculateCompliance(events, schedules, longDays, ctx.createdAt, {
now,
medicationContext: ctx,
@@ -1468,9 +1502,12 @@ export function calculateCompliance(
// legacy timeline tally byte-stable — they have no engine context to mint
// bands from.
const ledgerCtx = options?.medicationContext;
let ledgerCounts:
| { taken: number; skipped: number; missed: number; rate: number }
| null = null;
let ledgerCounts: {
taken: number;
skipped: number;
missed: number;
rate: number;
} | null = null;
if (ledgerCtx) {
const tally = tallyComplianceFromLedger(
events,
@@ -1598,8 +1635,7 @@ export function calculateCompliance(
// Skipped doses are excluded from the denominator — they represent a
// deliberate user decision rather than a missed dose.
const denom = taken + missed;
rate =
denom > 0 ? Math.min(100, Math.round((taken / denom) * 100)) : 100;
rate = denom > 0 ? Math.min(100, Math.round((taken / denom) * 100)) : 100;
}
const totalExpected = taken + skipped + missed;
@@ -1726,7 +1762,10 @@ export function buildMedicationComplianceBundle(
1,
Math.ceil((now.getTime() - effectiveStart.getTime()) / ONE_DAY_MS),
);
const tally = tallyLedgerRows(ledgerRows, { from: effectiveStart, to: now });
const tally = tallyLedgerRows(ledgerRows, {
from: effectiveStart,
to: now,
});
const { current: streak } = streaksFromTimeline(
timeline,
now,
@@ -1834,7 +1873,13 @@ export function buildMedicationComplianceBundle(
currentDose,
};
return { compliance7, compliance30, complianceDisplay, ledgerRows, ledgerFrom };
return {
compliance7,
compliance30,
complianceDisplay,
ledgerRows,
ledgerFrom,
};
}
/**
+8 -1
View File
@@ -174,7 +174,14 @@ export async function computeCorrelationHypothesesFastPath(
// reader so the per-type fan-out doesn't re-query the user 3×.
const priority = await loadUserSourcePriority(userId);
const [sysBuckets, pulseBuckets, weightBuckets] = await Promise.all([
readRollupBuckets(userId, "BLOOD_PRESSURE_SYS", "DAY", since, now, priority),
readRollupBuckets(
userId,
"BLOOD_PRESSURE_SYS",
"DAY",
since,
now,
priority,
),
readRollupBuckets(userId, "PULSE", "DAY", since, now, priority),
readRollupBuckets(userId, "WEIGHT", "DAY", since, now, priority),
]);
-1
View File
@@ -104,4 +104,3 @@ export function pearsonCorrelation(
return { r, strength, n };
}
+4 -5
View File
@@ -146,7 +146,6 @@ export interface TimeInRangeDistribution {
};
}
/** Variability summary: SD + CV% with the Monnier stability flag. */
export interface GlucoseVariability {
/** Sample standard deviation (n−1), mg/dL. */
@@ -245,7 +244,9 @@ export function glucoseSD(values: number[]): number | null {
* Monnier 2017, Diabetes Care 40(7):832-838, DOI 10.2337/dc16-1769;
* endorsed in Battelino 2019.
*/
export function glucoseVariability(values: number[]): GlucoseVariability | null {
export function glucoseVariability(
values: number[],
): GlucoseVariability | null {
const sd = glucoseSD(values);
if (sd === null) return null;
const m = mean(values);
@@ -344,9 +345,7 @@ export function bloodGlucoseRiskIndices(
* level-1), so the five fractions are NOT mutually exclusive by design.
* Battelino 2019, Diabetes Care 42(8):1593-1603, DOI 10.2337/dci19-0028.
*/
export function timeInRange(
values: number[],
): TimeInRangeDistribution | null {
export function timeInRange(values: number[]): TimeInRangeDistribution | null {
const n = values.length;
if (n === 0) return null;
+12 -10
View File
@@ -235,7 +235,9 @@ export async function computeUserHealthScoreFastPath(
kg: b.mean,
}));
weightSeriesPrev30d = dayBuckets
.filter((b) => b.bucketStart >= prevSince30d && b.bucketStart <= prevUntil)
.filter(
(b) => b.bucketStart >= prevSince30d && b.bucketStart <= prevUntil,
)
.map((b) => ({
date: b.bucketStart.toISOString(),
kg: b.mean,
@@ -266,7 +268,9 @@ export async function computeUserHealthScoreFastPath(
select: { measuredAt: true, source: true },
orderBy: { measuredAt: "asc" },
});
weightSourcesIn30d = uniqueComponentSources(sourceRows.map((r) => r.source));
weightSourcesIn30d = uniqueComponentSources(
sourceRows.map((r) => r.source),
);
latestWeightAsOf = sourceRows.at(-1)?.measuredAt.toISOString() ?? null;
} else {
// Live fallback — raw rows over the 37-day window. Mirrors the
@@ -288,13 +292,13 @@ export async function computeUserHealthScoreFastPath(
.filter((r) => r.measuredAt >= prevSince30d && r.measuredAt <= prevUntil)
.map((r) => ({ date: r.measuredAt.toISOString(), kg: r.value }));
weightSourcesIn30d = uniqueComponentSources(
weightRows
.filter((r) => r.measuredAt >= since30d)
.map((r) => r.source),
weightRows.filter((r) => r.measuredAt >= since30d).map((r) => r.source),
);
latestWeightAsOf =
weightRows.filter((r) => r.measuredAt >= since30d).at(-1)?.measuredAt
.toISOString() ?? null;
weightRows
.filter((r) => r.measuredAt >= since30d)
.at(-1)
?.measuredAt.toISOString() ?? null;
}
// Remaining reads — mood, BP-SYS source attribution, medications +
@@ -327,9 +331,7 @@ export async function computeUserHealthScoreFastPath(
select: { score: true, moodLoggedAt: true },
orderBy: { moodLoggedAt: "asc" },
})
: Promise.resolve(
[] as Array<{ score: number; moodLoggedAt: Date }>,
),
: Promise.resolve([] as Array<{ score: number; moodLoggedAt: Date }>),
prisma.medication.findMany({
// v1.16.11 — as-needed (PRN) medications carry no expected doses
// and are excluded from every compliance rate: they must
+1 -2
View File
@@ -78,8 +78,7 @@ export function buildHealthScoreBpInputs(
// 90-day window is itself sparse so a deep-history account survives a quiet
// quarter, but never grade off a sub-floor 90-day sample.
const last90Sufficient =
current.last90Days !== null &&
isWindowSufficient(current.last90Days.pairs);
current.last90Days !== null && isWindowSufficient(current.last90Days.pairs);
const bpInTargetPct = last90Sufficient
? (current.last90Days?.pct ?? null)
: (current.allTime?.pct ?? null);
+2 -4
View File
@@ -267,8 +267,7 @@ function bareWeightTrendScore(
// 25-point span keeps the unfavourable floor honest (a steep sustained
// gain reads "concerning", not "failing") while the favourable ceiling
// reaches the full 100.
const score =
slope < 0 ? 75 + 25 * normalised : 75 - 50 * normalised;
const score = slope < 0 ? 75 + 25 * normalised : 75 - 50 * normalised;
return Math.round(Math.max(0, Math.min(100, score)));
}
@@ -502,8 +501,7 @@ function redistribute(
const source = resolveSourceLabel(value !== null, attr.sources);
// When a component is `none`, fall back to the window-end anchor so
// the UI can still render "as of <today>" without inventing a date.
const asOf =
source === "none" ? windowEndAt : (attr.asOf ?? windowEndAt);
const asOf = source === "none" ? windowEndAt : (attr.asOf ?? windowEndAt);
return {
value,
weight: weightFor(key),
+5 -3
View File
@@ -67,7 +67,9 @@ export interface MergedDashboardAnalytics {
glucoseByContext: Record<string, unknown> | undefined;
}
function hasContent(record: Record<string, unknown> | null | undefined): boolean {
function hasContent(
record: Record<string, unknown> | null | undefined,
): boolean {
return record != null && Object.keys(record).length > 0;
}
@@ -91,8 +93,8 @@ export function mergeSlimAndThickAnalytics(
const summaries: Record<string, DataSummary> = slimSummariesHaveContent
? (slim!.summaries as Record<string, DataSummary>)
: ((thick?.summaries as Record<string, DataSummary> | null | undefined) ??
(slim?.summaries as Record<string, DataSummary> | null | undefined) ??
{});
(slim?.summaries as Record<string, DataSummary> | null | undefined) ??
{});
const slimFreshnessHasContent = hasContent(slim?.lastSeenByType ?? null);
const lastSeenByType:
+10 -2
View File
@@ -121,7 +121,11 @@ export async function computeRangeDelta(
const windowDays = rangeWindowDays(range);
// Read the full 2N span in one go so the current and previous halves share
// a granularity; the router picks the coarsest tier that resolves 2N.
const resolved = await readBestGranularityRollups(userId, type, windowDays * 2);
const resolved = await readBestGranularityRollups(
userId,
type,
windowDays * 2,
);
if (!resolved) {
const empty: WindowAggregate = {
count: 0,
@@ -140,7 +144,11 @@ export async function computeRangeDelta(
deltaPct: null,
};
}
const { current, previous } = sliceWindowDelta(resolved.rows, windowDays, now);
const { current, previous } = sliceWindowDelta(
resolved.rows,
windowDays,
now,
);
const { delta, deltaPct } = composeDelta(current, previous);
return {
range,
+1 -4
View File
@@ -130,10 +130,7 @@ export function resolveRestingPulseSeries(input: {
);
return { series, which: "resting" };
}
const proxy = deriveRestingProxyFromPulse(
input.pulseSamples,
input.dayKeyOf,
);
const proxy = deriveRestingProxyFromPulse(input.pulseSamples, input.dayKeyOf);
if (proxy.length > 0) return { series: proxy, which: "proxy" };
return { series: [], which: "none" };
}
+3 -1
View File
@@ -137,7 +137,9 @@ export function pickCanonicalSourceRows<T extends SourcePickerRow>(
// rather than once per row. Resolution itself lives in
// `getDeviceTypeLadder` — the cache is the only extra layer here.
const ladderCache = new Map<string, readonly DeviceType[]>();
function resolveLadder(rowType: MeasurementType | null | undefined): readonly DeviceType[] {
function resolveLadder(
rowType: MeasurementType | null | undefined,
): readonly DeviceType[] {
const cacheKey = rowType ?? "__default__";
let ladder = ladderCache.get(cacheKey);
if (!ladder) {
+10 -2
View File
@@ -362,7 +362,11 @@ async function computeFromRollups(userId: string): Promise<SummariesSlice> {
// the unqualified rollup + inner-subquery columns, `m."…"` for the latest
// read's outer alias.
const priorityJson = await loadUserSourcePriority(userId);
const rankUnqualified = buildSourceRankCase(priorityJson, '"type"', '"source"');
const rankUnqualified = buildSourceRankCase(
priorityJson,
'"type"',
'"source"',
);
const rankM = buildSourceRankCase(priorityJson, 'm."type"', 'm."source"');
const [narrows, latests, dayBuckets] = await Promise.all([
@@ -628,7 +632,11 @@ async function computeFromLiveAggregate(
// parity). `canonicalMeasurementsFrom` swaps `FROM measurements m` for a
// canonical-source-filtered subquery; the latest read uses the ladder rank.
const priorityJson = await loadUserSourcePriority(userId);
const rankUnqualified = buildSourceRankCase(priorityJson, '"type"', '"source"');
const rankUnqualified = buildSourceRankCase(
priorityJson,
'"type"',
'"source"',
);
const rankM = buildSourceRankCase(priorityJson, 'm."type"', 'm."source"');
const [allTime, windowed, latests] = await Promise.all([
+3 -8
View File
@@ -66,9 +66,7 @@ function isTolerableRequestProbeError(err: unknown): boolean {
// Covers both modern `Cannot read properties of undefined
// (reading 'X')` and the older `Cannot read property 'X' of
// undefined` wordings.
/Cannot read propert(?:y|ies)\b.*\bof (?:undefined|null)\b/.test(
msg,
)
/Cannot read propert(?:y|ies)\b.*\bof (?:undefined|null)\b/.test(msg)
);
}
@@ -144,11 +142,8 @@ export function apiHandler<T extends (...args: any[]) => Promise<Response>>(
route: url.pathname,
status: 200,
user_agent:
safeRequestProp(
request,
(r) => r.headers.get("user-agent"),
null,
) ?? undefined,
safeRequestProp(request, (r) => r.headers.get("user-agent"), null) ??
undefined,
ip:
safeRequestProp(
request,
+2 -9
View File
@@ -188,11 +188,7 @@ export function returnAllZodIssues(
* key is *not* echoed into the JSON body — it is consumed by the
* NextResponse constructor and stripped from the meta envelope.
*/
export function apiError(
message: string,
status = 400,
meta?: ErrorMeta,
) {
export function apiError(message: string, status = 400, meta?: ErrorMeta) {
return buildJsonErrorResponse({ data: null, error: message }, status, meta);
}
@@ -229,10 +225,7 @@ export async function safeJson<T = unknown>(
}
if (raw.length > opts.maxBytes) {
return {
error: apiError(
`Request body exceeds ${opts.maxBytes} bytes`,
413,
),
error: apiError(`Request body exceeds ${opts.maxBytes} bytes`, 413),
};
}
try {
+3 -1
View File
@@ -315,7 +315,9 @@ describe("verb helpers", () => {
await apiPost(
"/api/foo",
{ a: 1 },
{ headers: new Headers({ "Content-Type": "application/json-patch+json" }) },
{
headers: new Headers({ "Content-Type": "application/json-patch+json" }),
},
);
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
+5 -1
View File
@@ -2,7 +2,11 @@ import { describe, expect, it } from "vitest";
import { readError } from "../read-error";
function makeResponse(body: unknown, status: number, opts?: { invalidJson?: boolean }): Response {
function makeResponse(
body: unknown,
status: number,
opts?: { invalidJson?: boolean },
): Response {
if (opts?.invalidJson) {
return new Response("<<not json>>", {
status,
+1 -5
View File
@@ -58,11 +58,7 @@ export class ApiError extends Error {
/** `meta` object of the error envelope, when the body carried one. */
readonly meta: Record<string, unknown> | undefined;
constructor(
message: string,
status: number,
meta?: Record<string, unknown>,
) {
constructor(message: string, status: number, meta?: Record<string, unknown>) {
super(message);
this.name = "ApiError";
this.status = status;
+3 -1
View File
@@ -118,7 +118,9 @@ describe("recordInviteConsumer", () => {
it("swallows failures — the use is already counted", async () => {
transaction.mockRejectedValueOnce(new Error("boom"));
await expect(recordInviteConsumer("inv1", "user1")).resolves.toBeUndefined();
await expect(
recordInviteConsumer("inv1", "user1"),
).resolves.toBeUndefined();
});
});
+4 -1
View File
@@ -127,7 +127,10 @@ describe("isCookielessNativeCaller (M-3 refresh-token issuance gate)", () => {
it("true for a native caller whose cookie header lacks the session cookie", () => {
expect(
isCookielessNativeCaller(
h({ "x-client-type": "native", cookie: "other=1; hl_onboarding=pending" }),
h({
"x-client-type": "native",
cookie: "other=1; hl_onboarding=pending",
}),
),
).toBe(true);
});
-1
View File
@@ -185,4 +185,3 @@ export async function destroyAllSessions(userId: string): Promise<void> {
}),
]);
}
+3 -9
View File
@@ -35,11 +35,7 @@ export function detectAvatarMimeType(buffer: Buffer): AvatarMime | null {
if (buffer.length < 12) return null;
// JPEG — FF D8 FF
if (
buffer[0] === 0xff &&
buffer[1] === 0xd8 &&
buffer[2] === 0xff
) {
if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
return "image/jpeg";
}
@@ -195,10 +191,8 @@ function readWebpDimensions(buffer: Buffer): AvatarDimensions | null {
// width-1 + 3-byte canvas height-1 (all little-endian, padded
// to 24 bits).
if (buffer.length < 30) return null;
const widthMinusOne =
buffer[24] | (buffer[25] << 8) | (buffer[26] << 16);
const heightMinusOne =
buffer[27] | (buffer[28] << 8) | (buffer[29] << 16);
const widthMinusOne = buffer[24] | (buffer[25] << 8) | (buffer[26] << 16);
const heightMinusOne = buffer[27] | (buffer[28] << 8) | (buffer[29] << 16);
const width = widthMinusOne + 1;
const height = heightMinusOne + 1;
if (width <= 0 || height <= 0) return null;
+1 -3
View File
@@ -69,9 +69,7 @@ export function computeWindowStats(
const sorted = [...finite].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
const median =
sorted.length % 2 === 0
? (sorted[mid - 1] + sorted[mid]) / 2
: sorted[mid];
sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
return { count: finite.length, min, max, median, mean };
}
@@ -58,7 +58,9 @@ describe("resolveShareGateState", () => {
});
it("returns null for an expired link", async () => {
findUnique.mockResolvedValue(row({ expiresAt: new Date(Date.now() - 1000) }));
findUnique.mockResolvedValue(
row({ expiresAt: new Date(Date.now() - 1000) }),
);
expect(await resolveShareGateState(VALID_TOKEN)).toBeNull();
});
Loaded 100 of 1000 files, more files were not shown because too many files have changed in this diff. Show more