mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
fix(ai): stop the outbound risk guard from blocking this app's own scores
The named-risk-engine pattern in the outbound safety screen used score2? (the 2 optional), so it matched bare "score" too — any mention of one of this app's own computed, documented scores (Sleep Score, Readiness Score, Health Score, Recovery Score, Stress Score, Strain Score) tripped the bank meant only to catch a fabricated named clinical-risk calculator (SCORE2, Framingham, ASCVD, QRISK). Require the literal "SCORE2" instead. The other risk patterns — percentage-shaped risk claims and the other three named engines — are untouched, so a genuinely fabricated risk figure or a named risk-calculator claim still blocks. Closes #587.
This commit is contained in:
1 parent
35a3842bba
commit
c256d945b3
3 files changed
+84
-2
No files matched your search
@@ -67,6 +67,34 @@ describe("screenCoachReply — risk score", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Issue #587 — a grounded Sleep Score explanation was replaced with the
|
||||
// generic clinical-risk refusal because the named-risk-engine pattern
|
||||
// matched bare "score", not just the SCORE2 risk calculator it was meant to
|
||||
// name.
|
||||
describe("screenCoachReply — issue #587 ordinary wellness-score wording", () => {
|
||||
const allowed = [
|
||||
"Your Sleep Score is based on sleep duration and stage balance — it's currently in a healthy range for you.",
|
||||
"Your Readiness Score is lower than your recent baseline, likely from the shorter sleep window.",
|
||||
"This Health Score is based on the available tracked pillars: BP, weight, mood, and compliance.",
|
||||
];
|
||||
for (const reply of allowed) {
|
||||
it(`allows: ${reply.slice(0, 40)}`, () => {
|
||||
const d = screenCoachReply(reply, "en");
|
||||
expect(d.block).toBe(false);
|
||||
expect(d.reason).toBeNull();
|
||||
});
|
||||
}
|
||||
|
||||
it("still blocks a reply naming the SCORE2 risk engine", () => {
|
||||
const d = screenCoachReply(
|
||||
"Based on SCORE2, your cardiovascular risk profile looks elevated.",
|
||||
"en",
|
||||
);
|
||||
expect(d.block).toBe(true);
|
||||
expect(d.reason).toBe("risk_score");
|
||||
});
|
||||
});
|
||||
|
||||
describe("screenCoachReply — clean replies", () => {
|
||||
it("passes a grounded, non-prescriptive reply", () => {
|
||||
const reply =
|
||||
|
||||
@@ -107,6 +107,54 @@ describe("screenModelOutput — risk-score, all six locales", () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ── issue #587 — bare "score" is not a fabricated clinical risk score ─────
|
||||
|
||||
/**
|
||||
* The named-risk-engine pattern used `score2?` (the `2` optional), so it
|
||||
* matched bare "score" too — any mention of one of THIS APP's own computed,
|
||||
* documented scores (Sleep Score, Readiness Score, Health Score, …) tripped
|
||||
* the fabricated-risk-engine bank meant for SCORE2/Framingham/ASCVD/QRISK.
|
||||
*/
|
||||
const WELLNESS_SCORE_CLEAN: readonly string[] = [
|
||||
"Your Sleep Score is based on sleep duration and stage balance.",
|
||||
"Your Readiness Score is lower than your recent baseline.",
|
||||
"This Health Score is based on the available tracked pillars.",
|
||||
"Your Recovery Score and Stress Score both moved this week.",
|
||||
"The Strain Score reflects yesterday's training load.",
|
||||
];
|
||||
|
||||
describe("screenModelOutput — issue #587 ordinary wellness-score wording", () => {
|
||||
for (const text of WELLNESS_SCORE_CLEAN) {
|
||||
it(`passes: ${text}`, () => {
|
||||
const d = screenModelOutput(text, "en", CONVERSATIONAL_CONTRACTS);
|
||||
expect(d.block).toBe(false);
|
||||
expect(d.reason).toBeNull();
|
||||
});
|
||||
}
|
||||
|
||||
it("still blocks the literal named risk engine SCORE2", () => {
|
||||
const d = screenModelOutput(
|
||||
"Based on SCORE2, your cardiovascular risk profile looks elevated.",
|
||||
"en",
|
||||
CONVERSATIONAL_CONTRACTS,
|
||||
);
|
||||
expect(d.block).toBe(true);
|
||||
expect(d.reason).toBe("risk_score");
|
||||
});
|
||||
|
||||
it("still blocks Framingham, ASCVD, and QRISK by name", () => {
|
||||
for (const engine of [
|
||||
"your Framingham result was concerning",
|
||||
"the ASCVD estimate points to elevated risk",
|
||||
"your QRISK figure came back high",
|
||||
]) {
|
||||
const d = screenModelOutput(engine, "en", CONVERSATIONAL_CONTRACTS);
|
||||
expect(d.block).toBe(true);
|
||||
expect(d.reason).toBe("risk_score");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── causal claims (GROUND RULE 12) ───────────────────────────────────────
|
||||
|
||||
const CAUSAL_VIOLATIONS: Record<Locale, string> = {
|
||||
|
||||
@@ -215,8 +215,14 @@ const RISK_PATTERNS: Record<Locale, readonly RegExp[]> = {
|
||||
/\b\d{1,3}\s*%\s+(?:risk|chance|probability|likelihood)\b/i,
|
||||
/\b(?:risk|chance|probability|likelihood)\s+(?:of|is|at)\s+(?:about\s+|roughly\s+|~)?\d{1,3}\s*%/i,
|
||||
/\b(?:10[- ]year|ten[- ]year|lifetime)\s+(?:cardiovascular|cardiac|heart|stroke|mortality|cvd|ascvd)\s+risk\b/i,
|
||||
// Named risk engines are fabrications on any surface — the server runs none.
|
||||
/\b(?:framingham|ascvd|score2?|qrisk)\b/i,
|
||||
// Named risk engines are fabrications on any surface — the server runs
|
||||
// none. `score2?` (the `2` optional) was the #587 bug: it matched bare
|
||||
// "score" too, so every mention of this app's OWN computed scores
|
||||
// (Sleep Score, Readiness Score, Health Score, …) tripped the named-
|
||||
// engine bank. SCORE2 is a specific clinical risk calculator (like
|
||||
// Framingham/ASCVD/QRISK) — only the literal name is a fabrication
|
||||
// signal; the bare word "score" carries none on its own.
|
||||
/\b(?:framingham|ascvd|score2|qrisk)\b/i,
|
||||
],
|
||||
de: [
|
||||
/\brisiko\s+(?:von|bei|liegt\s+bei)\s+(?:etwa\s+|ungefähr\s+|~)?\d{1,3}\s*%/i,
|
||||
|
||||
Reference in new issue
Block a user