fix(insights): register the arrival reaction line with the tone harness

The harness landed in v1.30.35, after the slices that built this
release were already cut — so the reaction line's prompt module was
never registered, and its own docblock's warning against the
two-locale collapse was untested. Registering it surfaced a real gap:
the meaning-first instruction lived only in the system prompt's own
words, never in the user prompt in the family's shared phrasing, which
is where the rest of the surfaces carry it and where the tone contract
checks for it. The German body carried no such instruction at all.

Threads locale through the user-prompt builder so the fixed
instruction line matches the reader's own reply language, and wires
the per-user per-day opener-archetype rotation the milestone spec
called for and the first implementation had not connected.
This commit is contained in:
Marc-André Bombeck committed 2026-07-19 15:48:57 +02:00
1 parent 430cbb2ceb
commit 29673c1155
5 files changed
+65 -7

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "healthlog",
"version": "1.30.35",
"version": "1.31.0",
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
"license": "PolyForm-Noncommercial-1.0.0",
"homepage": "https://healthlog.dev",
+1 -1
View File
@@ -36,7 +36,7 @@ try {
// v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild.
const CACHE_VERSION =
(typeof self !== "undefined" && self.__APP_VERSION__) ||
/* @sw-version-fallback */ "v1.30.35";
/* @sw-version-fallback */ "v1.31.0";
const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`;
const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`;
// v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*`
+34 -1
View File
@@ -85,12 +85,45 @@ export interface ArrivalReactionPromptInput {
* only to say what the block already establishes.
*/
evidence: string;
/**
* Rotating opener-archetype hint (`openerArchetypeHint`, seeded
* `${userId}:reaction:${kind}:${localDate}` at the call site) — the same
* per-user, per-day rotation every other assessment surface uses so this
* line does not open on the same shape every time. This sits ALONGSIDE the
* fixed meaning-first instruction below, not in place of it — the rotation
* varies the angle, the instruction is the invariant every surface owes.
*/
openerHint?: string;
}
/**
* The fixed, always-present meaning-first instruction, phrased exactly the
* way every other assessment surface's user prompt phrases it. The system
* prompt's `REACTION_CONTRACT` states the same rule in its own words for the
* model to follow; this line exists so the INSTRUCTION SHAPE itself matches
* the family's shared contract, in the same prompt the harness inspects for
* it. Two locale bodies only — fr/es/it/pl ride the English one, exactly
* like the system prompt's own `instructionLocale` split.
*/
const OPENING_INSTRUCTION_EN =
"Open with what just landed MEANS in plain words — the overall read, not the number — then bring the figure in right after as support.";
const OPENING_INSTRUCTION_DE =
"Beginne mit dem, was gerade eingetroffen ist, in klaren Worten — nicht der Zahl — und bring sie danach als Beleg.";
export function getArrivalReactionUserPrompt(
input: ArrivalReactionPromptInput,
locale: Locale,
): string {
return `WHAT JUST LANDED: ${KIND_SUBJECT[input.kind]}.
const openingInstruction =
instructionLocale(locale) === "en"
? OPENING_INSTRUCTION_EN
: OPENING_INSTRUCTION_DE;
const openerLine =
input.openerHint && input.openerHint.trim().length > 0
? `\nOPENER HINT: ${input.openerHint}`
: "";
return `WHAT JUST LANDED: ${KIND_SUBJECT[input.kind]}. ${openingInstruction}${openerLine}
EVIDENCE BLOCK — everything below is already computed from this person's own record. Use only what is here; if it does not support a verdict, say so.
${input.evidence}
+17
View File
@@ -15,6 +15,10 @@
*/
import type { Locale } from "@/lib/i18n/config";
import {
getArrivalReactionSystemPrompt,
getArrivalReactionUserPrompt,
} from "@/lib/ai/prompts/arrival-reaction";
import {
getBiomarkerSystemPrompt,
getBiomarkerUserPrompt,
@@ -182,4 +186,17 @@ export const ASSESSMENT_SURFACES: readonly AssessmentSurface[] = [
system: getWorkoutInsightSystemPrompt,
user: (l) => getWorkoutInsightUserPrompt(SNAPSHOT, TODAY, l, OPENER_HINT),
},
{
// The one-sentence Today-hero reaction to a salient arrival. Smallest
// surface in the registry, and the one whose own module docblock warns
// most explicitly against the two-locale collapse — grading it here is
// what makes that warning enforced rather than just written down.
name: "arrival-reaction",
system: getArrivalReactionSystemPrompt,
user: (l) =>
getArrivalReactionUserPrompt(
{ kind: "sleep_night", evidence: SNAPSHOT, openerHint: OPENER_HINT },
l,
),
},
];
+12 -4
View File
@@ -49,6 +49,7 @@ import { singleUserTurn } from "@/lib/ai/types";
import { screenCoachReply } from "@/lib/ai/coach/outbound-guard";
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
import { defaultLocale, locales, type Locale } from "@/lib/i18n/config";
import { openerArchetypeHint } from "@/lib/ai/prompts/opener-archetype";
import { loadDailyDigest } from "@/lib/daily/load-digest";
import {
getArrivalReactionSystemPrompt,
@@ -231,10 +232,17 @@ export async function runReactionLine(
const result = await chain[0].instance.generateCompletion(
singleUserTurn({
system: getArrivalReactionSystemPrompt(locale),
user: getArrivalReactionUserPrompt({
kind: job.kind,
evidence: buildEvidence(digest),
}),
user: getArrivalReactionUserPrompt(
{
kind: job.kind,
evidence: buildEvidence(digest),
openerHint: openerArchetypeHint(
`${job.userId}:reaction:${job.kind}:${job.localDate}`,
locale,
),
},
locale,
),
temperature: budget.temperature,
maxTokens,
timeoutMs: REACTION_LINE_TIMEOUT_MS,