fix(cycle): second-audit safety, privacy, and write-path hardening

Address the second-round audit (server cluster):

- AVOID_PREGNANCY surfaces the fertile window, so it must carry the
  stronger "not a contraceptive method" disclaimer — wire the existing
  strong copy via a shared cycleDisclaimerKey(goal), server and client.
- Re-anchor the FOLLOWING neighbour cycle on a back-filled period start so
  an inserted-between-two-cycles start no longer leaves a stale open cycle.
- Resolve the sensitive-category encryption flag once per bulk/import
  batch instead of re-reading the profile per day; hoist the bulk cycle
  attribution in-memory (no per-entry query).
- Drop the raw driver message from the bulk wide-event meta (annotate meta
  is not run through the central redactor); keep the stable reason + code.
- Hard-purge now clears the cycle audit trail and the cycle reminder rows
  in the push-attempts ledger, so no dated reproductive trace survives.
- Discreet mode collapses the Web-Push coalescing tag and drops cycle
  metadata (phase) from APNs, matching the existing APNs event collapse.
- Add the reproductive intent field names to the central redactor denylist.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.8 committed 2026-06-06 13:15:22 +02:00
1 parent 31a14ce7bf
commit 3375bbabac
11 files changed
+184 -21

No files matched your search

+12 -1
View File
@@ -90,7 +90,18 @@ export function CycleView() {
const loading = calendar.isLoading && !calendar.data;
const calendarError = calendar.isError && !calendar.data;
const tileHue = wheel.phase ? PHASE_HUE[wheel.phase] : undefined;
const disclaimerText = t("cycle.prediction.disclaimer");
// AVOID_PREGNANCY surfaces the fertile window, so it must show the stronger
// "not a contraceptive method" caveat. Prefer the server-resolved
// prediction.disclaimer (already goal-correct); fall back to the goal-derived
// key for the no-prediction calendar tab (QA H-1).
const goal = calendar.data?.profile.goal;
const disclaimerText =
calendar.data?.prediction?.disclaimer ??
t(
goal === "AVOID_PREGNANCY"
? "cycle.disclaimer"
: "cycle.prediction.disclaimer",
);
return (
<div className="space-y-6">