mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
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:
1 parent
430cbb2ceb
commit
29673c1155
5 files changed
+65
-7
No files matched your search
+1
-1
@@ -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
@@ -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/*`
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user