mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
fix(insights): strip lowercase chart tokens from AI prose
Marc reported a raw "metric: blood_pressure_sweet" string at the bottom of /insights — looked like a debug code or unrendered template. Root cause: the v1.4.16 strip regex `/metric:[A-Z_]+/g` only matches uppercase letters and underscores, but the model occasionally emits lowercase / snake_case forms like `metric:blood_pressure` or `metric:weight_trend`. Those slipped through `stripChartTokens()` unchanged and the literal token text landed in the DOM. Split the regex in two: - `STRIP_TOKEN_REGEX` (`[A-Za-z0-9_]+`) — permissive; cleaves any case from prose so unrenderable / hallucinated tokens never surface as text. - `PARSE_TOKEN_REGEX` (`[A-Z_]+`) — strict; the render path still only mounts charts for the uppercase allowlisted vocabulary. Allowlist semantics are unchanged — `parseChartTokens()` still returns only entries from `ALLOWED_CHART_TOKENS`. The change is purely about defending the visible-prose surface from the wider class of token shapes the LLM occasionally emits. XSS-style trailing junk (`metric:WEIGHT' onclick='alert(1)'`) still cleaves cleanly: the apostrophe terminates either class. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
335f288d69
commit
fa91a73aef
1 file changed
+16
-7
@@ -39,11 +39,20 @@ export type ChartToken = (typeof ALLOWED_CHART_TOKENS)[number];
|
|||||||
|
|
||||||
const ALLOWED_SET = new Set<string>(ALLOWED_CHART_TOKENS);
|
const ALLOWED_SET = new Set<string>(ALLOWED_CHART_TOKENS);
|
||||||
|
|
||||||
// Greedy `[A-Z_]+` match. Uppercase letters and underscores only — anything
|
// v1.4.19 A3 — strip vs parse split. The strip-side character class
|
||||||
// else (apostrophes, spaces, lowercase, digits) terminates the token, so
|
// is permissive (`[A-Za-z0-9_]+`) so model-emitted lowercase or
|
||||||
// trailing junk like `metric:WEIGHT' onclick='alert(1)'` cleaves cleanly
|
// snake_case tokens like `metric:blood_pressure_sweet_spot` (verbatim
|
||||||
// into the safe metric and an inert text remainder.
|
// Marc 2026-05-10) get cleaved out of prose instead of leaking to the
|
||||||
const TOKEN_REGEX = /metric:[A-Z_]+/g;
|
// DOM. The parse-side class stays uppercase-only so unrenderable /
|
||||||
|
// hallucinated tokens are dropped from the *render* path entirely;
|
||||||
|
// only the *strip* is permissive.
|
||||||
|
//
|
||||||
|
// Trailing junk like `metric:WEIGHT' onclick='alert(1)'` still
|
||||||
|
// cleaves cleanly: the apostrophe terminates either character class,
|
||||||
|
// and the surviving uppercase token is the only one filtered through
|
||||||
|
// the allowlist for rendering.
|
||||||
|
const STRIP_TOKEN_REGEX = /metric:[A-Za-z0-9_]+/g;
|
||||||
|
const PARSE_TOKEN_REGEX = /metric:[A-Z_]+/g;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find every well-formed chart token in `text` and return only those that
|
* Find every well-formed chart token in `text` and return only those that
|
||||||
@@ -62,7 +71,7 @@ export function parseChartTokens(
|
|||||||
text: string | null | undefined,
|
text: string | null | undefined,
|
||||||
): ChartToken[] {
|
): ChartToken[] {
|
||||||
if (typeof text !== "string") return [];
|
if (typeof text !== "string") return [];
|
||||||
const matches: string[] = text.match(TOKEN_REGEX) ?? [];
|
const matches: string[] = text.match(PARSE_TOKEN_REGEX) ?? [];
|
||||||
return matches.filter((m): m is ChartToken => ALLOWED_SET.has(m));
|
return matches.filter((m): m is ChartToken => ALLOWED_SET.has(m));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +87,7 @@ export function parseChartTokens(
|
|||||||
export function stripChartTokens(text: string | null | undefined): string {
|
export function stripChartTokens(text: string | null | undefined): string {
|
||||||
if (typeof text !== "string") return "";
|
if (typeof text !== "string") return "";
|
||||||
return text
|
return text
|
||||||
.replace(TOKEN_REGEX, "")
|
.replace(STRIP_TOKEN_REGEX, "")
|
||||||
.replace(/\s{2,}/g, " ")
|
.replace(/\s{2,}/g, " ")
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user