mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
perf(insights): compress status snapshots via graded series (raw->week->month->year)
The seven per-card generators embedded the full ~360-day daily array per metric plus correlation pair arrays, and never touched the WEEK/MONTH/YEAR rollup tier. A dense daily weigher ran the weight snapshot ~80-110 KB; a many-type account could push general-status past 100 K tokens. Embed the shared graded series instead of the full daily array, cap the embedded correlation/paired arrays to their most recent entries, skip measurement types with no data in general-status, and apply a generic 24 000-char snapshot budget that sheds yearly -> weekly -> array-tails -> recent until it fits. The applyPayloadBudget daily buckets stay in-process to drive the latest-day focus, in-target gates, and correlation pairing; only the compact shape reaches the prompt. Measured on a 2-year daily account: weight-status ~15 KB, general-status ~12 KB across six dense types — both well under the cap.
This commit is contained in:
1 parent
1b78df345d
commit
11a06196f3
15 files changed
+448
-158
No files matched your search
@@ -54,8 +54,8 @@ beforeEach(() => {
|
||||
vi.mocked(getMedicationCategories).mockResolvedValue({});
|
||||
});
|
||||
|
||||
describe("generateBloodPressureStatusForUser — v1.4.6 bucketed payload", () => {
|
||||
it("emits {daily, monthly} per metric series with correct field shape", async () => {
|
||||
describe("generateBloodPressureStatusForUser — graded payload", () => {
|
||||
it("emits a graded {recent, weekly, monthly} per-metric series, not the full daily array", async () => {
|
||||
const now = new Date();
|
||||
|
||||
const records: Array<{
|
||||
@@ -99,20 +99,22 @@ describe("generateBloodPressureStatusForUser — v1.4.6 bucketed payload", () =>
|
||||
const snapshot = JSON.parse(match![0]);
|
||||
|
||||
const sys = snapshot.bloodPressure.systolic.series;
|
||||
expect(sys).toHaveProperty("daily");
|
||||
expect(sys).toHaveProperty("recent");
|
||||
expect(sys).toHaveProperty("weekly");
|
||||
expect(sys).toHaveProperty("monthly");
|
||||
expect(sys.daily.length).toBeGreaterThan(0);
|
||||
expect(sys.monthly.length).toBeGreaterThan(0);
|
||||
expect(sys.daily[0]).toHaveProperty("dayOffset");
|
||||
expect(sys.daily[0]).toHaveProperty("value");
|
||||
expect(sys.daily[0]).toHaveProperty("n");
|
||||
expect(sys.monthly[0]).toHaveProperty("monthOffset");
|
||||
expect(sys.monthly[0]).toHaveProperty("value");
|
||||
expect(sys.monthly[0]).toHaveProperty("n");
|
||||
expect(sys).toHaveProperty("yearly");
|
||||
expect(sys.recent.length).toBeLessThanOrEqual(21);
|
||||
expect(sys.recent[0]).toHaveProperty("date");
|
||||
expect(sys.recent[0]).toHaveProperty("mean");
|
||||
expect(sys.monthly[0]).toHaveProperty("month");
|
||||
|
||||
const dia = snapshot.bloodPressure.diastolic.series;
|
||||
expect(dia.daily.length).toBeGreaterThan(0);
|
||||
expect(dia.monthly.length).toBeGreaterThan(0);
|
||||
expect(dia).toHaveProperty("recent");
|
||||
expect(dia).toHaveProperty("monthly");
|
||||
expect(dia.recent.length).toBeLessThanOrEqual(21);
|
||||
|
||||
// Embedded correlation pair arrays are capped, not full-length.
|
||||
expect(snapshot.weightVsSystolic.pairs.length).toBeLessThanOrEqual(30);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -46,8 +46,8 @@ beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("generateBmiStatusForUser — v1.4.6 bucketed payload", () => {
|
||||
it("emits {daily, monthly} BMI series derived from bucketed weight", async () => {
|
||||
describe("generateBmiStatusForUser — graded payload", () => {
|
||||
it("emits a graded {recent, weekly, monthly} BMI series, not the full daily array", async () => {
|
||||
const now = new Date();
|
||||
const records: Array<{ value: number; measuredAt: Date }> = [];
|
||||
for (let day = 0; day < 1000; day++) {
|
||||
@@ -75,16 +75,20 @@ describe("generateBmiStatusForUser — v1.4.6 bucketed payload", () => {
|
||||
const snapshot = JSON.parse(match![0]);
|
||||
|
||||
const bmi = snapshot.bmi.series;
|
||||
expect(bmi).toHaveProperty("daily");
|
||||
expect(bmi).toHaveProperty("recent");
|
||||
expect(bmi).toHaveProperty("weekly");
|
||||
expect(bmi).toHaveProperty("monthly");
|
||||
expect(bmi.daily.length).toBeGreaterThan(0);
|
||||
expect(bmi.monthly.length).toBeGreaterThan(0);
|
||||
expect(bmi.daily[0]).toHaveProperty("dayOffset");
|
||||
expect(bmi.daily[0]).toHaveProperty("value");
|
||||
expect(bmi.daily[0]).toHaveProperty("n");
|
||||
expect(bmi.monthly[0]).toHaveProperty("monthOffset");
|
||||
expect(bmi.monthly[0]).toHaveProperty("value");
|
||||
expect(bmi.monthly[0]).toHaveProperty("n");
|
||||
expect(bmi).toHaveProperty("yearly");
|
||||
expect(bmi.recent.length).toBeLessThanOrEqual(21);
|
||||
expect(bmi.recent[0]).toHaveProperty("date");
|
||||
expect(bmi.recent[0]).toHaveProperty("mean");
|
||||
expect(bmi.monthly[0]).toHaveProperty("month");
|
||||
const total =
|
||||
bmi.recent.length +
|
||||
bmi.weekly.length +
|
||||
bmi.monthly.length +
|
||||
bmi.yearly.length;
|
||||
expect(total).toBeLessThanOrEqual(50);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -47,8 +47,8 @@ beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("generateGeneralStatusForUser — v1.4.6 bucketed payload", () => {
|
||||
it("emits {daily, monthly} per metric series with dayOffset/monthOffset/value/n", async () => {
|
||||
describe("generateGeneralStatusForUser — graded payload", () => {
|
||||
it("emits a graded {recent, weekly, monthly} per-metric series, not the full daily array", async () => {
|
||||
const now = new Date();
|
||||
|
||||
const weightRecords: Array<{
|
||||
@@ -90,18 +90,47 @@ describe("generateGeneralStatusForUser — v1.4.6 bucketed payload", () => {
|
||||
const snapshot = JSON.parse(match![0]);
|
||||
|
||||
const weight = snapshot.measurementSeries.WEIGHT.series;
|
||||
expect(weight).toHaveProperty("daily");
|
||||
expect(weight).toHaveProperty("recent");
|
||||
expect(weight).toHaveProperty("weekly");
|
||||
expect(weight).toHaveProperty("monthly");
|
||||
expect(Array.isArray(weight.daily)).toBe(true);
|
||||
expect(Array.isArray(weight.monthly)).toBe(true);
|
||||
expect(weight.daily.length).toBeGreaterThan(0);
|
||||
expect(weight.monthly.length).toBeGreaterThan(0);
|
||||
expect(weight.daily[0]).toHaveProperty("dayOffset");
|
||||
expect(weight.daily[0]).toHaveProperty("value");
|
||||
expect(weight.daily[0]).toHaveProperty("n");
|
||||
expect(weight.monthly[0]).toHaveProperty("monthOffset");
|
||||
expect(weight.monthly[0]).toHaveProperty("value");
|
||||
expect(weight.monthly[0]).toHaveProperty("n");
|
||||
expect(weight).toHaveProperty("yearly");
|
||||
expect(weight.recent.length).toBeLessThanOrEqual(21);
|
||||
expect(weight.recent[0]).toHaveProperty("date");
|
||||
expect(weight.recent[0]).toHaveProperty("mean");
|
||||
expect(weight.monthly[0]).toHaveProperty("month");
|
||||
const total =
|
||||
weight.recent.length +
|
||||
weight.weekly.length +
|
||||
weight.monthly.length +
|
||||
weight.yearly.length;
|
||||
expect(total).toBeLessThanOrEqual(50);
|
||||
});
|
||||
|
||||
it("omits measurement types with no data", async () => {
|
||||
const now = new Date();
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||
dateOfBirth: null,
|
||||
} as never);
|
||||
vi.mocked(prisma.auditLog.findFirst).mockResolvedValue(null);
|
||||
vi.mocked(prisma.measurement.findMany).mockResolvedValue([
|
||||
{ type: "WEIGHT", value: 80, measuredAt: now },
|
||||
] as never);
|
||||
vi.mocked(prisma.medicationIntakeEvent.findMany).mockResolvedValue(
|
||||
[] as never,
|
||||
);
|
||||
vi.mocked(prisma.moodEntry.findMany).mockResolvedValue([] as never);
|
||||
vi.mocked(prisma.auditLog.create).mockResolvedValue({
|
||||
createdAt: now,
|
||||
} as never);
|
||||
|
||||
const captured: { userPrompt: string | null } = { userPrompt: null };
|
||||
stubCompletion('{"summary":"OK"}', captured);
|
||||
|
||||
await generateGeneralStatusForUser("user-1", { locale: "en" });
|
||||
const snapshot = JSON.parse(captured.userPrompt!.match(/\{[\s\S]*\}/)![0]);
|
||||
|
||||
// Only WEIGHT has data — no empty PULSE/BP/etc. series objects.
|
||||
expect(Object.keys(snapshot.measurementSeries)).toEqual(["WEIGHT"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -64,8 +64,8 @@ beforeEach(() => {
|
||||
vi.mocked(getMedicationCategories).mockResolvedValue({});
|
||||
});
|
||||
|
||||
describe("generateMedicationComplianceStatusForUser — v1.4.6 bucketed payload", () => {
|
||||
it("emits {daily, monthly} compliance series per medication spanning 3 years", async () => {
|
||||
describe("generateMedicationComplianceStatusForUser — graded payload", () => {
|
||||
it("emits a graded {recent, weekly, monthly} compliance series per medication", async () => {
|
||||
const now = new Date();
|
||||
const medication = {
|
||||
id: "med-1",
|
||||
@@ -118,16 +118,14 @@ describe("generateMedicationComplianceStatusForUser — v1.4.6 bucketed payload"
|
||||
expect(snapshot.medications).toBeInstanceOf(Array);
|
||||
expect(snapshot.medications.length).toBe(1);
|
||||
const dailySeries = snapshot.medications[0].dailySeries;
|
||||
expect(dailySeries).toHaveProperty("daily");
|
||||
expect(dailySeries).toHaveProperty("recent");
|
||||
expect(dailySeries).toHaveProperty("weekly");
|
||||
expect(dailySeries).toHaveProperty("monthly");
|
||||
expect(dailySeries.daily.length).toBeGreaterThan(0);
|
||||
expect(dailySeries.monthly.length).toBeGreaterThan(0);
|
||||
expect(dailySeries.daily[0]).toHaveProperty("dayOffset");
|
||||
expect(dailySeries.daily[0]).toHaveProperty("value");
|
||||
expect(dailySeries.daily[0]).toHaveProperty("n");
|
||||
expect(dailySeries.monthly[0]).toHaveProperty("monthOffset");
|
||||
expect(dailySeries.monthly[0]).toHaveProperty("value");
|
||||
expect(dailySeries.monthly[0]).toHaveProperty("n");
|
||||
expect(dailySeries).toHaveProperty("yearly");
|
||||
expect(dailySeries.recent.length).toBeLessThanOrEqual(21);
|
||||
expect(dailySeries.recent[0]).toHaveProperty("date");
|
||||
expect(dailySeries.recent[0]).toHaveProperty("mean");
|
||||
expect(dailySeries.monthly[0]).toHaveProperty("month");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -45,8 +45,8 @@ beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("generateMoodStatusForUser — v1.4.6 bucketed payload", () => {
|
||||
it("emits {daily, monthly} mood series spanning 3 years", async () => {
|
||||
describe("generateMoodStatusForUser — graded payload", () => {
|
||||
it("emits a graded {recent, weekly, monthly} mood series, not the full daily array", async () => {
|
||||
const now = new Date();
|
||||
const entries: Array<{
|
||||
date: string;
|
||||
@@ -80,16 +80,20 @@ describe("generateMoodStatusForUser — v1.4.6 bucketed payload", () => {
|
||||
const snapshot = JSON.parse(match![0]);
|
||||
|
||||
const mood = snapshot.mood.series;
|
||||
expect(mood).toHaveProperty("daily");
|
||||
expect(mood).toHaveProperty("recent");
|
||||
expect(mood).toHaveProperty("weekly");
|
||||
expect(mood).toHaveProperty("monthly");
|
||||
expect(mood.daily.length).toBeGreaterThan(0);
|
||||
expect(mood.monthly.length).toBeGreaterThan(0);
|
||||
expect(mood.daily[0]).toHaveProperty("dayOffset");
|
||||
expect(mood.daily[0]).toHaveProperty("value");
|
||||
expect(mood.daily[0]).toHaveProperty("n");
|
||||
expect(mood.monthly[0]).toHaveProperty("monthOffset");
|
||||
expect(mood.monthly[0]).toHaveProperty("value");
|
||||
expect(mood.monthly[0]).toHaveProperty("n");
|
||||
expect(mood).toHaveProperty("yearly");
|
||||
expect(mood.recent.length).toBeLessThanOrEqual(21);
|
||||
expect(mood.recent[0]).toHaveProperty("date");
|
||||
expect(mood.recent[0]).toHaveProperty("mean");
|
||||
expect(mood.monthly[0]).toHaveProperty("month");
|
||||
const total =
|
||||
mood.recent.length +
|
||||
mood.weekly.length +
|
||||
mood.monthly.length +
|
||||
mood.yearly.length;
|
||||
expect(total).toBeLessThanOrEqual(50);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -46,8 +46,8 @@ beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("generatePulseStatusForUser — v1.4.6 bucketed payload", () => {
|
||||
it("emits {daily, monthly} pulse series with dayOffset/monthOffset/value/n", async () => {
|
||||
describe("generatePulseStatusForUser — graded payload", () => {
|
||||
it("emits a graded {recent, weekly, monthly} pulse series, not the full daily array", async () => {
|
||||
const now = new Date();
|
||||
const records: Array<{ value: number; measuredAt: Date }> = [];
|
||||
for (let day = 0; day < 1000; day++) {
|
||||
@@ -77,16 +77,20 @@ describe("generatePulseStatusForUser — v1.4.6 bucketed payload", () => {
|
||||
const snapshot = JSON.parse(match![0]);
|
||||
|
||||
const pulse = snapshot.pulse.series;
|
||||
expect(pulse).toHaveProperty("daily");
|
||||
expect(pulse).toHaveProperty("recent");
|
||||
expect(pulse).toHaveProperty("weekly");
|
||||
expect(pulse).toHaveProperty("monthly");
|
||||
expect(pulse.daily.length).toBeGreaterThan(0);
|
||||
expect(pulse.monthly.length).toBeGreaterThan(0);
|
||||
expect(pulse.daily[0]).toHaveProperty("dayOffset");
|
||||
expect(pulse.daily[0]).toHaveProperty("value");
|
||||
expect(pulse.daily[0]).toHaveProperty("n");
|
||||
expect(pulse.monthly[0]).toHaveProperty("monthOffset");
|
||||
expect(pulse.monthly[0]).toHaveProperty("value");
|
||||
expect(pulse.monthly[0]).toHaveProperty("n");
|
||||
expect(pulse).toHaveProperty("yearly");
|
||||
expect(pulse.recent.length).toBeLessThanOrEqual(21);
|
||||
expect(pulse.recent[0]).toHaveProperty("date");
|
||||
expect(pulse.recent[0]).toHaveProperty("mean");
|
||||
expect(pulse.monthly[0]).toHaveProperty("month");
|
||||
const total =
|
||||
pulse.recent.length +
|
||||
pulse.weekly.length +
|
||||
pulse.monthly.length +
|
||||
pulse.yearly.length;
|
||||
expect(total).toBeLessThanOrEqual(50);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -45,8 +45,8 @@ beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("generateWeightStatusForUser — v1.4.6 bucketed payload", () => {
|
||||
it("emits {daily, monthly} weight series with bucket fields", async () => {
|
||||
describe("generateWeightStatusForUser — graded payload", () => {
|
||||
it("emits a graded {recent, weekly, monthly} weight series, not the full daily array", async () => {
|
||||
const now = new Date();
|
||||
const records: Array<{
|
||||
type: string;
|
||||
@@ -77,16 +77,26 @@ describe("generateWeightStatusForUser — v1.4.6 bucketed payload", () => {
|
||||
const snapshot = JSON.parse(match![0]);
|
||||
|
||||
const weight = snapshot.weight.series;
|
||||
expect(weight).toHaveProperty("daily");
|
||||
expect(weight).toHaveProperty("recent");
|
||||
expect(weight).toHaveProperty("weekly");
|
||||
expect(weight).toHaveProperty("monthly");
|
||||
expect(weight.daily.length).toBeGreaterThan(0);
|
||||
expect(weight.monthly.length).toBeGreaterThan(0);
|
||||
expect(weight.daily[0]).toHaveProperty("dayOffset");
|
||||
expect(weight.daily[0]).toHaveProperty("value");
|
||||
expect(weight.daily[0]).toHaveProperty("n");
|
||||
expect(weight.monthly[0]).toHaveProperty("monthOffset");
|
||||
expect(weight.monthly[0]).toHaveProperty("value");
|
||||
expect(weight.monthly[0]).toHaveProperty("n");
|
||||
expect(weight).toHaveProperty("yearly");
|
||||
// No raw daily array beyond the bounded recent window.
|
||||
expect(weight.recent.length).toBeLessThanOrEqual(21);
|
||||
expect(weight.recent[0]).toHaveProperty("date");
|
||||
expect(weight.recent[0]).toHaveProperty("mean");
|
||||
expect(weight.recent[0]).toHaveProperty("min");
|
||||
expect(weight.recent[0]).toHaveProperty("max");
|
||||
expect(weight.monthly[0]).toHaveProperty("month");
|
||||
expect(weight.monthly[0]).toHaveProperty("mean");
|
||||
// The whole graded series collapses 1000 daily readings to a tiny
|
||||
// bucket count.
|
||||
const total =
|
||||
weight.recent.length +
|
||||
weight.weekly.length +
|
||||
weight.monthly.length +
|
||||
weight.yearly.length;
|
||||
expect(total).toBeLessThanOrEqual(50);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -27,6 +27,10 @@ import {
|
||||
dayOffsetToBerlinDayKey,
|
||||
type DailyBucket,
|
||||
} from "@/lib/insights/bucket-series";
|
||||
import {
|
||||
buildGradedSeriesFromPoints,
|
||||
degradeStatusSnapshotToBudget,
|
||||
} from "@/lib/insights/graded-series";
|
||||
import { stripChartTokens } from "@/lib/insights/chart-tokens";
|
||||
import { runStatusCompletion } from "@/lib/insights/status-provider";
|
||||
import { readFreshStatusText } from "@/lib/insights/status-cache";
|
||||
@@ -36,6 +40,13 @@ import { toBerlinDayKey } from "@/lib/tz/resolver";
|
||||
|
||||
type SupportedLocale = "de" | "en";
|
||||
|
||||
/**
|
||||
* Cap on the embedded correlation / paired-daily arrays. The Pearson
|
||||
* coefficients stay computed over the full overlap; only the row-by-row
|
||||
* lists the prompt carries are trimmed to their most recent entries.
|
||||
*/
|
||||
const CORRELATION_PAIR_CAP = 30;
|
||||
|
||||
function round(value: number, digits = 1): number {
|
||||
const factor = 10 ** digits;
|
||||
return Math.round(value * factor) / factor;
|
||||
@@ -370,6 +381,21 @@ export async function generateBloodPressureStatusForUser(
|
||||
);
|
||||
const moodMean = moodSummary?.mean ?? null;
|
||||
|
||||
// Compact graded series for the prompt. The `*Series.daily` buckets
|
||||
// above stay for the correlation pairing + in-target gate; only the
|
||||
// graded shape is embedded so the full daily arrays never ship.
|
||||
const gradedFromDaily = (buckets: DailyBucket[]) =>
|
||||
buildGradedSeriesFromPoints(
|
||||
buckets.map((b) => ({
|
||||
measuredAt: new Date(dayOffsetToBerlinDayKey(now, b.dayOffset)),
|
||||
value: b.value,
|
||||
})),
|
||||
now,
|
||||
);
|
||||
const sysGraded = gradedFromDaily(sysSeries.daily);
|
||||
const diaGraded = gradedFromDaily(diaSeries.daily);
|
||||
const moodGraded = gradedFromDaily(moodSeries.daily);
|
||||
|
||||
const continuityVsSystolicPairs: PairedPoint[] = continuityVsSystolicSeries
|
||||
.map((entry) => {
|
||||
if (entry.continuityPct == null) return null;
|
||||
@@ -419,13 +445,13 @@ export async function generateBloodPressureStatusForUser(
|
||||
summary: summarizeSeries(
|
||||
sysSeries.daily.map((bucket) => ({ value: bucket.value })),
|
||||
),
|
||||
series: sysSeries,
|
||||
series: sysGraded,
|
||||
},
|
||||
diastolic: {
|
||||
summary: summarizeSeries(
|
||||
diaSeries.daily.map((bucket) => ({ value: bucket.value })),
|
||||
),
|
||||
series: diaSeries,
|
||||
series: diaGraded,
|
||||
},
|
||||
paired: {
|
||||
summary: summarizeSeries(
|
||||
@@ -433,7 +459,7 @@ export async function generateBloodPressureStatusForUser(
|
||||
value: (entry.sys + entry.dia) / 2,
|
||||
})),
|
||||
),
|
||||
series: pairedBloodPressure,
|
||||
series: pairedBloodPressure.slice(-CORRELATION_PAIR_CAP),
|
||||
},
|
||||
targets: bpTargets
|
||||
? {
|
||||
@@ -445,7 +471,7 @@ export async function generateBloodPressureStatusForUser(
|
||||
},
|
||||
weightVsSystolic: {
|
||||
correlation: weightVsSystolicCorrelation,
|
||||
pairs: weightVsSystolicPairs.map((entry) => ({
|
||||
pairs: weightVsSystolicPairs.slice(-CORRELATION_PAIR_CAP).map((entry) => ({
|
||||
day: entry.dayKey,
|
||||
weight: round(entry.a, 2),
|
||||
systolic: round(entry.b, 2),
|
||||
@@ -455,7 +481,7 @@ export async function generateBloodPressureStatusForUser(
|
||||
expectedIntakesPerDay: expectedBpIntakesPerDay,
|
||||
medicationCount: bpMedications.length,
|
||||
correlation: continuityVsSystolicCorrelation,
|
||||
series: continuityVsSystolicSeries,
|
||||
series: continuityVsSystolicSeries.slice(-CORRELATION_PAIR_CAP),
|
||||
},
|
||||
bpMedications: medicationCompliance,
|
||||
moodContext:
|
||||
@@ -464,7 +490,7 @@ export async function generateBloodPressureStatusForUser(
|
||||
points: moodSeries.daily.length,
|
||||
mean: moodMean,
|
||||
latest: moodSeries.daily[0]?.value ?? null,
|
||||
series: moodSeries,
|
||||
series: moodGraded,
|
||||
moodVsSystolicCorrelation: (() => {
|
||||
const moodVsSysPairs = pairDailyBuckets(
|
||||
moodSeries.daily,
|
||||
@@ -477,11 +503,17 @@ export async function generateBloodPressureStatusForUser(
|
||||
: null,
|
||||
};
|
||||
|
||||
const shed = degradeStatusSnapshotToBudget(
|
||||
snapshot as unknown as Record<string, unknown>,
|
||||
);
|
||||
const snapshotJson = JSON.stringify(snapshot, null, 2);
|
||||
|
||||
annotate({
|
||||
action: { name: cacheAction },
|
||||
meta: { payload_size_bytes: snapshotJson.length },
|
||||
meta: {
|
||||
payload_size_bytes: snapshotJson.length,
|
||||
...(shed.length > 0 ? { snapshot_shed: shed } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
const previousContext = await getPreviousInsightContext(
|
||||
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
getPreviousInsightContext,
|
||||
} from "@/lib/insights/memory";
|
||||
import { applyPayloadBudget } from "@/lib/insights/bucket-series";
|
||||
import {
|
||||
buildGradedSeriesFromPoints,
|
||||
degradeStatusSnapshotToBudget,
|
||||
} from "@/lib/insights/graded-series";
|
||||
import { stripChartTokens } from "@/lib/insights/chart-tokens";
|
||||
import { runStatusCompletion } from "@/lib/insights/status-provider";
|
||||
import { readFreshStatusText } from "@/lib/insights/status-cache";
|
||||
@@ -130,26 +134,19 @@ export async function generateBmiStatusForUser(
|
||||
.then((rows) => rows.reverse());
|
||||
|
||||
const now = new Date();
|
||||
const weightSeries = applyPayloadBudget(
|
||||
measurements.map((measurement) => ({
|
||||
measuredAt: measurement.measuredAt,
|
||||
value: measurement.value,
|
||||
})),
|
||||
{ now },
|
||||
);
|
||||
|
||||
const heightFactor = (user.heightCm / 100) ** 2;
|
||||
|
||||
const bmiPoints = measurements.map((measurement) => ({
|
||||
measuredAt: measurement.measuredAt,
|
||||
value: round(measurement.value / heightFactor, 2),
|
||||
}));
|
||||
// `applyPayloadBudget` daily buckets drive the latest/previous focus;
|
||||
// the compact graded series is what reaches the prompt.
|
||||
const weightSeries = applyPayloadBudget(bmiPoints, { now });
|
||||
const bmiGraded = buildGradedSeriesFromPoints(bmiPoints, now);
|
||||
const bmiSeries = {
|
||||
daily: weightSeries.daily.map((bucket) => ({
|
||||
dayOffset: bucket.dayOffset,
|
||||
value: round(bucket.value / heightFactor, 2),
|
||||
n: bucket.n,
|
||||
})),
|
||||
monthly: weightSeries.monthly.map((bucket) => ({
|
||||
monthOffset: bucket.monthOffset,
|
||||
value: round(bucket.value / heightFactor, 2),
|
||||
n: bucket.n,
|
||||
})),
|
||||
daily: weightSeries.daily,
|
||||
monthly: weightSeries.monthly,
|
||||
};
|
||||
|
||||
// daily[0] = newest bucket (lowest dayOffset).
|
||||
@@ -189,7 +186,7 @@ export async function generateBmiStatusForUser(
|
||||
summary: summarizeSeries(
|
||||
bmiSeries.daily.map((bucket) => ({ value: bucket.value })),
|
||||
),
|
||||
series: bmiSeries,
|
||||
series: bmiGraded,
|
||||
latestDayFocus: latestBmi
|
||||
? {
|
||||
dayOffset: latestBmi.dayOffset,
|
||||
@@ -208,11 +205,17 @@ export async function generateBmiStatusForUser(
|
||||
},
|
||||
};
|
||||
|
||||
const shed = degradeStatusSnapshotToBudget(
|
||||
snapshot as unknown as Record<string, unknown>,
|
||||
);
|
||||
const snapshotJson = JSON.stringify(snapshot, null, 2);
|
||||
|
||||
annotate({
|
||||
action: { name: cacheAction },
|
||||
meta: { payload_size_bytes: snapshotJson.length },
|
||||
meta: {
|
||||
payload_size_bytes: snapshotJson.length,
|
||||
...(shed.length > 0 ? { snapshot_shed: shed } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
const previousContext = await getPreviousInsightContext(
|
||||
|
||||
@@ -12,6 +12,10 @@ import {
|
||||
getPreviousInsightContext,
|
||||
} from "@/lib/insights/memory";
|
||||
import { applyPayloadBudget } from "@/lib/insights/bucket-series";
|
||||
import {
|
||||
buildGradedSeriesFromPoints,
|
||||
degradeStatusSnapshotToBudget,
|
||||
} from "@/lib/insights/graded-series";
|
||||
import { stripChartTokens } from "@/lib/insights/chart-tokens";
|
||||
import { runStatusCompletion } from "@/lib/insights/status-provider";
|
||||
import { readFreshStatusText } from "@/lib/insights/status-cache";
|
||||
@@ -130,25 +134,39 @@ export async function generateGeneralStatusForUser(
|
||||
|
||||
const now = new Date();
|
||||
|
||||
// `dailyByType` keeps the `applyPayloadBudget` daily buckets for the
|
||||
// downstream BP in-target pairing; the prompt embeds the compact
|
||||
// graded series instead of the full daily array. Skip types with no
|
||||
// data so a many-metric account can't balloon the snapshot with empty
|
||||
// series objects.
|
||||
const dailyByType = new Map<
|
||||
(typeof MEASUREMENT_TYPES)[number],
|
||||
ReturnType<typeof applyPayloadBudget>
|
||||
>();
|
||||
const measurementSeries = Object.fromEntries(
|
||||
MEASUREMENT_TYPES.map((type) => {
|
||||
MEASUREMENT_TYPES.flatMap((type) => {
|
||||
const records = measurements
|
||||
.filter((measurement) => measurement.type === type)
|
||||
.map((measurement) => ({
|
||||
measuredAt: measurement.measuredAt,
|
||||
value: measurement.value,
|
||||
}));
|
||||
if (records.length === 0) return [];
|
||||
|
||||
const series = applyPayloadBudget(records, { now });
|
||||
dailyByType.set(type, series);
|
||||
const graded = buildGradedSeriesFromPoints(records, now);
|
||||
|
||||
return [
|
||||
type,
|
||||
{
|
||||
summary: summarizeSeries(
|
||||
series.daily.map((bucket) => ({ value: bucket.value })),
|
||||
),
|
||||
series,
|
||||
},
|
||||
[
|
||||
type,
|
||||
{
|
||||
summary: summarizeSeries(
|
||||
series.daily.map((bucket) => ({ value: bucket.value })),
|
||||
),
|
||||
series: graded,
|
||||
},
|
||||
] as const,
|
||||
];
|
||||
}),
|
||||
);
|
||||
@@ -195,6 +213,7 @@ export async function generateGeneralStatusForUser(
|
||||
value: value.total > 0 ? round((value.taken / value.total) * 100, 1) : 0,
|
||||
}));
|
||||
const adherenceSeries = applyPayloadBudget(adherenceRecords, { now });
|
||||
const adherenceGraded = buildGradedSeriesFromPoints(adherenceRecords, now);
|
||||
|
||||
// Fetch mood context (optional — for enrichment only). v1.4.28
|
||||
// FB-D2 — cap at 90 entries.
|
||||
@@ -212,6 +231,7 @@ export async function generateGeneralStatusForUser(
|
||||
value: entry.score,
|
||||
}));
|
||||
const moodSeries = applyPayloadBudget(moodRecords, { now });
|
||||
const moodGraded = buildGradedSeriesFromPoints(moodRecords, now);
|
||||
const moodSummary = summarizeSeries(
|
||||
moodSeries.daily.map((bucket) => ({ value: bucket.value })),
|
||||
);
|
||||
@@ -220,9 +240,9 @@ export async function generateGeneralStatusForUser(
|
||||
const bpTargets = getBpTargets(user?.dateOfBirth ?? null);
|
||||
let bpInTargetLast30Days: number | null = null;
|
||||
if (bpTargets) {
|
||||
const sysDaily = measurementSeries.BLOOD_PRESSURE_SYS?.series.daily ?? [];
|
||||
const sysDaily = dailyByType.get("BLOOD_PRESSURE_SYS")?.daily ?? [];
|
||||
const diaMap = new Map(
|
||||
(measurementSeries.BLOOD_PRESSURE_DIA?.series.daily ?? []).map(
|
||||
(dailyByType.get("BLOOD_PRESSURE_DIA")?.daily ?? []).map(
|
||||
(bucket) => [bucket.dayOffset, bucket.value] as const,
|
||||
),
|
||||
);
|
||||
@@ -285,7 +305,7 @@ export async function generateGeneralStatusForUser(
|
||||
summary: summarizeSeries(
|
||||
adherenceSeries.daily.map((bucket) => ({ value: bucket.value })),
|
||||
),
|
||||
series: adherenceSeries,
|
||||
series: adherenceGraded,
|
||||
},
|
||||
bloodPressureTargets: bpTargets
|
||||
? {
|
||||
@@ -300,16 +320,22 @@ export async function generateGeneralStatusForUser(
|
||||
points: moodSeries.daily.length,
|
||||
mean: moodMean,
|
||||
latest: moodSeries.daily[0]?.value ?? null,
|
||||
series: moodSeries,
|
||||
series: moodGraded,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
|
||||
const shed = degradeStatusSnapshotToBudget(
|
||||
snapshot as unknown as Record<string, unknown>,
|
||||
);
|
||||
const snapshotJson = JSON.stringify(snapshot, null, 2);
|
||||
|
||||
annotate({
|
||||
action: { name: cacheAction },
|
||||
meta: { payload_size_bytes: snapshotJson.length },
|
||||
meta: {
|
||||
payload_size_bytes: snapshotJson.length,
|
||||
...(shed.length > 0 ? { snapshot_shed: shed } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
// v1.4: pull the previous cached general-status into the prompt so
|
||||
|
||||
@@ -365,6 +365,97 @@ export async function buildGradedSeriesWithRollups(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Assembled-snapshot soft char cap, mirroring the Coach's
|
||||
* `MAX_SNAPSHOT_CHARS` (~24 000 chars ≈ ~6 000 tokens against the
|
||||
* pretty-printed form). The status snapshots had no cap at all; a
|
||||
* many-metric `general-status` could balloon past 100 K tokens.
|
||||
*/
|
||||
export const MAX_SNAPSHOT_CHARS = 24_000;
|
||||
|
||||
/**
|
||||
* Walk an arbitrary snapshot object and, while it exceeds
|
||||
* `MAX_SNAPSHOT_CHARS` (measured against the pretty-printed form the
|
||||
* prompt ships), shed the lowest-signal slices in order:
|
||||
* 1. drop every graded `yearly` array,
|
||||
* 2. drop every graded `weekly` array,
|
||||
* 3. truncate any other array to its tail `arrayTailCap` entries
|
||||
* (correlation pair arrays, paired-daily rows, …),
|
||||
* 4. drop every graded `recent` array (last resort — the coarse
|
||||
* monthly summary survives).
|
||||
*
|
||||
* Mutates `snapshot` in place and returns the slices it shed so the
|
||||
* caller can annotate. Generic by design: it keys off the graded field
|
||||
* names + array length, never off a specific generator's shape.
|
||||
*/
|
||||
export function degradeStatusSnapshotToBudget(
|
||||
snapshot: Record<string, unknown>,
|
||||
arrayTailCap = 30,
|
||||
): string[] {
|
||||
const shed: string[] = [];
|
||||
const size = () => JSON.stringify(snapshot, null, 2).length;
|
||||
if (size() <= MAX_SNAPSHOT_CHARS) return shed;
|
||||
|
||||
const walk = (
|
||||
node: unknown,
|
||||
fn: (parent: Record<string, unknown>, key: string, value: unknown) => void,
|
||||
): void => {
|
||||
if (Array.isArray(node)) {
|
||||
for (const item of node) walk(item, fn);
|
||||
return;
|
||||
}
|
||||
if (node && typeof node === "object") {
|
||||
const obj = node as Record<string, unknown>;
|
||||
for (const key of Object.keys(obj)) {
|
||||
fn(obj, key, obj[key]);
|
||||
walk(obj[key], fn);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const dropField = (field: string) => {
|
||||
walk(snapshot, (parent, key) => {
|
||||
if (key === field && Array.isArray(parent[key])) {
|
||||
delete parent[key];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
dropField("yearly");
|
||||
if (size() <= MAX_SNAPSHOT_CHARS) {
|
||||
shed.push("yearly");
|
||||
return shed;
|
||||
}
|
||||
shed.push("yearly");
|
||||
|
||||
dropField("weekly");
|
||||
if (size() <= MAX_SNAPSHOT_CHARS) {
|
||||
shed.push("weekly");
|
||||
return shed;
|
||||
}
|
||||
shed.push("weekly");
|
||||
|
||||
// Truncate every remaining non-graded array to its tail.
|
||||
walk(snapshot, (parent, key, value) => {
|
||||
if (
|
||||
Array.isArray(value) &&
|
||||
key !== "recent" &&
|
||||
value.length > arrayTailCap
|
||||
) {
|
||||
parent[key] = value.slice(-arrayTailCap);
|
||||
}
|
||||
});
|
||||
if (size() <= MAX_SNAPSHOT_CHARS) {
|
||||
shed.push("array-tails");
|
||||
return shed;
|
||||
}
|
||||
shed.push("array-tails");
|
||||
|
||||
dropField("recent");
|
||||
shed.push("recent");
|
||||
return shed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Headline summary across the whole graded series — min/max/mean/n —
|
||||
* for the snapshot's summary line. Composes the per-bucket stats; uses
|
||||
|
||||
@@ -17,6 +17,10 @@ import {
|
||||
getPreviousInsightContext,
|
||||
} from "@/lib/insights/memory";
|
||||
import { applyPayloadBudget } from "@/lib/insights/bucket-series";
|
||||
import {
|
||||
buildGradedSeriesFromPoints,
|
||||
degradeStatusSnapshotToBudget,
|
||||
} from "@/lib/insights/graded-series";
|
||||
import { stripChartTokens } from "@/lib/insights/chart-tokens";
|
||||
import { runStatusCompletion } from "@/lib/insights/status-provider";
|
||||
import { isTimeoutStub } from "@/lib/insights/status-cache";
|
||||
@@ -218,8 +222,11 @@ export async function generateMedicationComplianceStatusForUser(
|
||||
value: round(Math.min(100, (stats.taken / expectedPerDay) * 100), 1),
|
||||
}));
|
||||
|
||||
const dailySeries = applyPayloadBudget(perDayRecords, { now });
|
||||
const latestDay = dailySeries.daily[0] ?? null;
|
||||
// `applyPayloadBudget` gives the latest-day focus; the compact
|
||||
// graded series is what reaches the prompt.
|
||||
const dailyBudgeted = applyPayloadBudget(perDayRecords, { now });
|
||||
const dailySeries = buildGradedSeriesFromPoints(perDayRecords, now);
|
||||
const latestDay = dailyBudgeted.daily[0] ?? null;
|
||||
|
||||
return {
|
||||
medicationId: medication.id,
|
||||
@@ -289,11 +296,17 @@ export async function generateMedicationComplianceStatusForUser(
|
||||
medications: medicationSnapshots,
|
||||
};
|
||||
|
||||
const shed = degradeStatusSnapshotToBudget(
|
||||
snapshot as unknown as Record<string, unknown>,
|
||||
);
|
||||
const snapshotJson = JSON.stringify(snapshot, null, 2);
|
||||
|
||||
annotate({
|
||||
action: { name: cacheAction },
|
||||
meta: { payload_size_bytes: snapshotJson.length },
|
||||
meta: {
|
||||
payload_size_bytes: snapshotJson.length,
|
||||
...(shed.length > 0 ? { snapshot_shed: shed } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
const previousContext = await getPreviousInsightContext(
|
||||
|
||||
@@ -14,6 +14,10 @@ import {
|
||||
dayOffsetToBerlinDayKey,
|
||||
type DailyBucket,
|
||||
} from "@/lib/insights/bucket-series";
|
||||
import {
|
||||
buildGradedSeriesFromPoints,
|
||||
degradeStatusSnapshotToBudget,
|
||||
} from "@/lib/insights/graded-series";
|
||||
import { stripChartTokens } from "@/lib/insights/chart-tokens";
|
||||
import { runStatusCompletion } from "@/lib/insights/status-provider";
|
||||
import { readFreshStatusText } from "@/lib/insights/status-cache";
|
||||
@@ -152,13 +156,14 @@ export async function generateMoodStatusForUser(
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const moodSeries = applyPayloadBudget(
|
||||
entries.map((entry) => ({
|
||||
measuredAt: entry.moodLoggedAt,
|
||||
value: entry.score,
|
||||
})),
|
||||
{ now },
|
||||
);
|
||||
const moodPoints = entries.map((entry) => ({
|
||||
measuredAt: entry.moodLoggedAt,
|
||||
value: entry.score,
|
||||
}));
|
||||
// `applyPayloadBudget` daily buckets drive the in-target %, latest,
|
||||
// and correlations; the compact graded series reaches the prompt.
|
||||
const moodSeries = applyPayloadBudget(moodPoints, { now });
|
||||
const moodGraded = buildGradedSeriesFromPoints(moodPoints, now);
|
||||
const moodSummary = summarizeSeries(
|
||||
moodSeries.daily.map((bucket) => ({ value: bucket.value })),
|
||||
);
|
||||
@@ -302,7 +307,7 @@ export async function generateMoodStatusForUser(
|
||||
},
|
||||
mood: {
|
||||
summary: moodSummary,
|
||||
series: moodSeries,
|
||||
series: moodGraded,
|
||||
latestDayFocus: latestMood
|
||||
? {
|
||||
dayOffset: latestMood.dayOffset,
|
||||
@@ -364,11 +369,17 @@ export async function generateMoodStatusForUser(
|
||||
: null,
|
||||
};
|
||||
|
||||
const shed = degradeStatusSnapshotToBudget(
|
||||
snapshot as unknown as Record<string, unknown>,
|
||||
);
|
||||
const snapshotJson = JSON.stringify(snapshot, null, 2);
|
||||
|
||||
annotate({
|
||||
action: { name: cacheAction },
|
||||
meta: { payload_size_bytes: snapshotJson.length },
|
||||
meta: {
|
||||
payload_size_bytes: snapshotJson.length,
|
||||
...(shed.length > 0 ? { snapshot_shed: shed } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
const previousContext = await getPreviousInsightContext(
|
||||
|
||||
@@ -13,6 +13,10 @@ import {
|
||||
} from "@/lib/analytics/pulse-targets";
|
||||
import { getNoKeyPulseStatusText } from "@/lib/insights/no-key-fallbacks";
|
||||
import { applyPayloadBudget } from "@/lib/insights/bucket-series";
|
||||
import {
|
||||
buildGradedSeriesFromPoints,
|
||||
degradeStatusSnapshotToBudget,
|
||||
} from "@/lib/insights/graded-series";
|
||||
import { stripChartTokens } from "@/lib/insights/chart-tokens";
|
||||
import { runStatusCompletion } from "@/lib/insights/status-provider";
|
||||
import { readFreshStatusText } from "@/lib/insights/status-cache";
|
||||
@@ -125,13 +129,15 @@ export async function generatePulseStatusForUser(
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const pulseSeries = applyPayloadBudget(
|
||||
measurements.map((measurement) => ({
|
||||
measuredAt: measurement.measuredAt,
|
||||
value: measurement.value,
|
||||
})),
|
||||
{ now },
|
||||
);
|
||||
const pulsePoints = measurements.map((measurement) => ({
|
||||
measuredAt: measurement.measuredAt,
|
||||
value: measurement.value,
|
||||
}));
|
||||
// `applyPayloadBudget` daily buckets still drive the derived stats
|
||||
// below (latest, in-target %, delta). They are NOT embedded in the
|
||||
// prompt — the compact graded series replaces the full daily array.
|
||||
const pulseSeries = applyPayloadBudget(pulsePoints, { now });
|
||||
const pulseGraded = buildGradedSeriesFromPoints(pulsePoints, now);
|
||||
const pulseSummary = summarizeSeries(
|
||||
pulseSeries.daily.map((bucket) => ({ value: bucket.value })),
|
||||
);
|
||||
@@ -148,13 +154,12 @@ export async function generatePulseStatusForUser(
|
||||
})
|
||||
.then((rows) => rows.reverse());
|
||||
|
||||
const moodSeries = applyPayloadBudget(
|
||||
moodEntries.map((entry) => ({
|
||||
measuredAt: entry.moodLoggedAt,
|
||||
value: entry.score,
|
||||
})),
|
||||
{ now },
|
||||
);
|
||||
const moodPoints = moodEntries.map((entry) => ({
|
||||
measuredAt: entry.moodLoggedAt,
|
||||
value: entry.score,
|
||||
}));
|
||||
const moodSeries = applyPayloadBudget(moodPoints, { now });
|
||||
const moodGraded = buildGradedSeriesFromPoints(moodPoints, now);
|
||||
const moodSummary = summarizeSeries(
|
||||
moodSeries.daily.map((bucket) => ({ value: bucket.value })),
|
||||
);
|
||||
@@ -218,7 +223,7 @@ export async function generatePulseStatusForUser(
|
||||
},
|
||||
pulse: {
|
||||
summary: pulseSummary,
|
||||
series: pulseSeries,
|
||||
series: pulseGraded,
|
||||
latestDayFocus: latestPulse
|
||||
? {
|
||||
dayOffset: latestPulse.dayOffset,
|
||||
@@ -243,16 +248,22 @@ export async function generatePulseStatusForUser(
|
||||
points: moodSeries.daily.length,
|
||||
mean: moodMean,
|
||||
latest: moodSeries.daily[0]?.value ?? null,
|
||||
series: moodSeries,
|
||||
series: moodGraded,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
|
||||
const shed = degradeStatusSnapshotToBudget(
|
||||
snapshot as unknown as Record<string, unknown>,
|
||||
);
|
||||
const snapshotJson = JSON.stringify(snapshot, null, 2);
|
||||
|
||||
annotate({
|
||||
action: { name: cacheAction },
|
||||
meta: { payload_size_bytes: snapshotJson.length },
|
||||
meta: {
|
||||
payload_size_bytes: snapshotJson.length,
|
||||
...(shed.length > 0 ? { snapshot_shed: shed } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
const previousContext = await getPreviousInsightContext(
|
||||
|
||||
@@ -17,6 +17,10 @@ import {
|
||||
dayOffsetToBerlinDayKey,
|
||||
type DailyBucket,
|
||||
} from "@/lib/insights/bucket-series";
|
||||
import {
|
||||
buildGradedSeriesFromPoints,
|
||||
degradeStatusSnapshotToBudget,
|
||||
} from "@/lib/insights/graded-series";
|
||||
import { stripChartTokens } from "@/lib/insights/chart-tokens";
|
||||
import { runStatusCompletion } from "@/lib/insights/status-provider";
|
||||
import { readFreshStatusText } from "@/lib/insights/status-cache";
|
||||
@@ -26,6 +30,13 @@ import { toBerlinDayKey } from "@/lib/tz/resolver";
|
||||
|
||||
type SupportedLocale = "de" | "en";
|
||||
|
||||
/**
|
||||
* Cap on the embedded correlation pair arrays. The Pearson coefficient
|
||||
* is still computed over the full overlap; only the row-by-row pair
|
||||
* list the prompt carries is trimmed to its most recent entries.
|
||||
*/
|
||||
const CORRELATION_PAIR_CAP = 30;
|
||||
|
||||
function round(value: number, digits = 1): number {
|
||||
const factor = 10 ** digits;
|
||||
return Math.round(value * factor) / factor;
|
||||
@@ -212,6 +223,38 @@ export async function generateWeightStatusForUser(
|
||||
);
|
||||
const moodMean = moodSummary?.mean ?? null;
|
||||
|
||||
// Compact graded series for the prompt. The `*Series.daily` buckets
|
||||
// above stay for the correlation pairing + latest-day focus; only the
|
||||
// graded shape is embedded so the full daily arrays never ship.
|
||||
const weightGraded = buildGradedSeriesFromPoints(
|
||||
weightSeries.daily.map((b) => ({
|
||||
measuredAt: new Date(dayOffsetToBerlinDayKey(now, b.dayOffset)),
|
||||
value: b.value,
|
||||
})),
|
||||
now,
|
||||
);
|
||||
const sysGraded = buildGradedSeriesFromPoints(
|
||||
sysSeries.daily.map((b) => ({
|
||||
measuredAt: new Date(dayOffsetToBerlinDayKey(now, b.dayOffset)),
|
||||
value: b.value,
|
||||
})),
|
||||
now,
|
||||
);
|
||||
const diaGraded = buildGradedSeriesFromPoints(
|
||||
diaSeries.daily.map((b) => ({
|
||||
measuredAt: new Date(dayOffsetToBerlinDayKey(now, b.dayOffset)),
|
||||
value: b.value,
|
||||
})),
|
||||
now,
|
||||
);
|
||||
const moodGraded = buildGradedSeriesFromPoints(
|
||||
moodSeries.daily.map((b) => ({
|
||||
measuredAt: new Date(dayOffsetToBerlinDayKey(now, b.dayOffset)),
|
||||
value: b.value,
|
||||
})),
|
||||
now,
|
||||
);
|
||||
|
||||
const weightVsSystolicPairs = pairDailyBuckets(
|
||||
weightSeries.daily,
|
||||
sysSeries.daily,
|
||||
@@ -296,7 +339,7 @@ export async function generateWeightStatusForUser(
|
||||
summary: summarizeSeries(
|
||||
weightSeries.daily.map((bucket) => ({ value: bucket.value })),
|
||||
),
|
||||
series: weightSeries,
|
||||
series: weightGraded,
|
||||
latestDayFocus: latestWeight
|
||||
? {
|
||||
day: latestWeightDay,
|
||||
@@ -315,19 +358,22 @@ export async function generateWeightStatusForUser(
|
||||
summary: summarizeSeries(
|
||||
sysSeries.daily.map((bucket) => ({ value: bucket.value })),
|
||||
),
|
||||
series: sysSeries,
|
||||
series: sysGraded,
|
||||
},
|
||||
diastolic: {
|
||||
summary: summarizeSeries(
|
||||
diaSeries.daily.map((bucket) => ({ value: bucket.value })),
|
||||
),
|
||||
series: diaSeries,
|
||||
series: diaGraded,
|
||||
},
|
||||
pairedDaily: pairedSystolicDiastolic,
|
||||
// Keep the correlation coefficients (computed over the full
|
||||
// overlap) but cap the embedded pair arrays to the most recent
|
||||
// paired days — the raw pair lists were a major payload driver.
|
||||
pairedDaily: pairedSystolicDiastolic.slice(-CORRELATION_PAIR_CAP),
|
||||
},
|
||||
weightVsSystolic: {
|
||||
correlation: weightVsSystolicCorrelation,
|
||||
pairs: weightVsSystolicPairs.map((entry) => ({
|
||||
pairs: weightVsSystolicPairs.slice(-CORRELATION_PAIR_CAP).map((entry) => ({
|
||||
day: entry.dayKey,
|
||||
weight: round(entry.a, 2),
|
||||
systolic: round(entry.b, 2),
|
||||
@@ -335,7 +381,7 @@ export async function generateWeightStatusForUser(
|
||||
},
|
||||
weightVsMeanBloodPressure: {
|
||||
correlation: weightVsMeanBpCorrelation,
|
||||
pairs: weightVsMeanBpPairs.map((entry) => ({
|
||||
pairs: weightVsMeanBpPairs.slice(-CORRELATION_PAIR_CAP).map((entry) => ({
|
||||
day: entry.dayKey,
|
||||
weight: round(entry.a, 2),
|
||||
meanBloodPressure: round(entry.b, 2),
|
||||
@@ -347,7 +393,7 @@ export async function generateWeightStatusForUser(
|
||||
points: moodSeries.daily.length,
|
||||
mean: moodMean,
|
||||
latest: moodSeries.daily[0]?.value ?? null,
|
||||
series: moodSeries,
|
||||
series: moodGraded,
|
||||
moodVsWeightCorrelation: (() => {
|
||||
const moodVsWeightPairs = pairDailyBuckets(
|
||||
moodSeries.daily,
|
||||
@@ -360,11 +406,17 @@ export async function generateWeightStatusForUser(
|
||||
: null,
|
||||
};
|
||||
|
||||
const shed = degradeStatusSnapshotToBudget(
|
||||
snapshot as unknown as Record<string, unknown>,
|
||||
);
|
||||
const snapshotJson = JSON.stringify(snapshot, null, 2);
|
||||
|
||||
annotate({
|
||||
action: { name: cacheAction },
|
||||
meta: { payload_size_bytes: snapshotJson.length },
|
||||
meta: {
|
||||
payload_size_bytes: snapshotJson.length,
|
||||
...(shed.length > 0 ? { snapshot_shed: shed } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
const previousContext = await getPreviousInsightContext(
|
||||
|
||||
Reference in new issue
Block a user