fix(coach): read free weekdays for a prompt only with the work section released

This commit is contained in:
Marc-André Bombeck committed 2026-10-16 05:42:15 +02:00
1 parent 120b0966b1
commit 8ae60e2cb3
3 files changed
+67 -2

No files matched your search

+2 -2
View File
@@ -15,7 +15,7 @@
* values with weekday labels and aggregates the older window into
* weekly buckets so the prompt budget stays tight.
*/
import { loadFreeWeekdaySpans } from "@/lib/profile/life-circumstances";
import { loadFreeWeekdaySpansForPrompt } from "@/lib/profile/life-circumstances";
import { prisma } from "@/lib/db";
import { extractFeatures } from "@/lib/insights/features";
import type { CoachDataCluster } from "@/lib/validations/coach-prefs";
@@ -807,7 +807,7 @@ async function buildCoachSnapshotImpl(
sourcePriorityJson: prefsRow?.sourcePriorityJson ?? null,
userTz,
ageYears: derivedProfile.ageYears,
freeWeekdaySpans: await loadFreeWeekdaySpans(userId),
freeWeekdaySpans: await loadFreeWeekdaySpansForPrompt(userId),
snapshot,
metrics,
registerBlock,
+37
View File
@@ -47,6 +47,7 @@ import {
computePackYears,
DEFAULT_FREE_WEEKDAYS,
isReviewDue,
LIFE_KIND_AI_SECTION,
screeningHints,
type FreeWeekdaysSpan,
type PackYears,
@@ -214,6 +215,42 @@ export async function loadFreeWeekdaySpans(
}
}
/**
* The free-weekday answers for a prompt: only when the section they belong
* to (`WORK`) is released to the AI. Otherwise the answers are never read or
* decrypted for a prompt, and the night-by-night day type falls back to the
* weekend default, exactly as for a record that never answered. The data
* surfaces (the sleep page, the dashboard) read `loadFreeWeekdaySpans`
* directly: they show the person their own answer, no model sees it.
*/
export async function loadFreeWeekdaySpansForPrompt(
userId: string,
): Promise<FreeWeekdaysSpan[]> {
// Fail-soft and fail-closed, like the read below: a failed switch read
// gives the weekend default and never the answers.
try {
const profile = await prisma.userHealthProfile.findUnique({
where: { userId },
select: { aiIncludedSections: true },
});
if (
!storedSections(profile?.aiIncludedSections).has(
LIFE_KIND_AI_SECTION.FREE_WEEKDAYS,
)
) {
return [];
}
} catch (error) {
getEvent()?.addWarning(
`free weekdays switch read failed: ${
error instanceof Error ? error.message : String(error)
}`,
);
return [];
}
return loadFreeWeekdaySpans(userId);
}
/** Assemble the whole read model for one record. */
export async function buildLifeCircumstances(
userId: string,
@@ -297,6 +297,34 @@ describe("life-circumstance facts", () => {
});
});
describe("free weekdays in a prompt", () => {
it("are read for a prompt only while the work section is released", async () => {
const user = await makeUser();
await signIn(user.id);
await data(
await postFact({ kind: "FREE_WEEKDAYS", value: VALUES.FREE_WEEKDAYS }),
201,
);
const { loadFreeWeekdaySpans, loadFreeWeekdaySpansForPrompt } =
await import("@/lib/profile/life-circumstances");
await getPrismaClient().userHealthProfile.upsert({
where: { userId: user.id },
create: { userId: user.id, aiIncludedSections: ["WORK"] },
update: { aiIncludedSections: ["WORK"] },
});
expect(await loadFreeWeekdaySpansForPrompt(user.id)).toHaveLength(1);
await getPrismaClient().userHealthProfile.update({
where: { userId: user.id },
data: { aiIncludedSections: ["ABOUT_ME"] },
});
expect(await loadFreeWeekdaySpansForPrompt(user.id)).toEqual([]);
// The data surfaces still show the person their own answer.
expect(await loadFreeWeekdaySpans(user.id)).toHaveLength(1);
});
});
describe("AUDIT-C", () => {
async function postAuditC(body: unknown): Promise<Response> {
const { POST } = await import("@/app/api/anamnesis/audit-c/route");