From bc605e6a42b85adbc1d282d18ef1b70a7fa8e153 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Sun, 14 Jun 2026 08:15:31 -0400 Subject: [PATCH] fix(11-05): WR-01 positive-duration TRIGGER classifies as custom (no Math.abs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit classifyValarms: check sign of dur.toSeconds() before preset lookup. Positive value = alarm fires after event (RFC 5545 TRIGGER:+PT15M or TRIGGER;RELATED=END:PTNm) → return {kind:'custom'} for preserve path. Compute leadMinutes as -seconds/60 (was Math.abs) for negative triggers. Prevents alarm direction inversion: +PT15M was being stored as 15-min-before lead and re-fired at dtstartUtc-15min — the opposite of the original intent. --- apps/api/src/broker/vevent.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/api/src/broker/vevent.ts b/apps/api/src/broker/vevent.ts index 5d45a4f..77de856 100644 --- a/apps/api/src/broker/vevent.ts +++ b/apps/api/src/broker/vevent.ts @@ -181,11 +181,18 @@ export function classifyValarms(rawVevent: string): AlarmClassification { const firstValue = triggerProp.getFirstValue() as unknown; if (firstValue instanceof ICAL.Time) return { kind: 'custom' }; - // Relative DURATION trigger — extract lead minutes + // Relative DURATION trigger — extract lead minutes. const dur = firstValue as ICAL.Duration; if (!dur || typeof dur.toSeconds !== 'function') return { kind: 'custom' }; - const leadMinutes = Math.round(Math.abs(dur.toSeconds()) / 60); + const seconds = dur.toSeconds(); + // WR-01 (Phase 11 Plan 05): positive seconds = alarm fires AFTER the event start + // (RFC 5545 TRIGGER:+PT15M or TRIGGER;RELATED=END:PT15M). This is a post-event alarm + // and cannot be expressed as a before-event lead. Classify as custom so the preserve + // path keeps the original VALARM rather than inverting the alarm direction. + if (seconds > 0) return { kind: 'custom' }; + + const leadMinutes = Math.round(-seconds / 60); return PRESET_MINUTES.has(leadMinutes) ? { kind: 'preset', leadMinutes } : { kind: 'offlist', leadMinutes };