From 5870d244fcee129ba8ba16633dd2ed250c992365 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 15 Jun 2026 22:08:39 +0200 Subject: [PATCH] fix(timeline): exclude undated events from the cluster lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildEventLookup also collected `timeline.undated`, so an undated curated event — which renders as a plain EventPill in the undated bucket, out of clustering scope — still seeded clusters: its dated linked letters scattered into year bands and each collapsed into a ✉ cross-year card with no edit link and no spatial tie to the pill, showing the event title twice with no relationship. Only year-band events are collected now. Fixes review finding #7. Refs #850 Co-Authored-By: Claude Opus 4.8 --- frontend/src/lib/timeline/eventClustering.spec.ts | 9 ++++++--- frontend/src/lib/timeline/eventClustering.ts | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/timeline/eventClustering.spec.ts b/frontend/src/lib/timeline/eventClustering.spec.ts index c0419c02..f469f1c9 100644 --- a/frontend/src/lib/timeline/eventClustering.spec.ts +++ b/frontend/src/lib/timeline/eventClustering.spec.ts @@ -13,7 +13,7 @@ const makeEvent = (overrides: Partial = {}): TimelineEntryDTO makeEntry({ kind: 'EVENT', type: 'PERSONAL', documentId: undefined, ...overrides }); describe('eventClustering — buildEventLookup', () => { - it('maps each curated event (kind EVENT + eventId) to its title across years + undated', () => { + it('maps a curated year-band event to its title, excluding undated-bucket events (#7)', () => { const timeline: TimelineDTO = { years: [ { @@ -25,8 +25,11 @@ describe('eventClustering — buildEventLookup', () => { }; const lookup = buildEventLookup(timeline); expect(lookup.get(EV_A)).toBe('Ein gewaltiger Stadtbrand'); - expect(lookup.get(EV_B)).toBe('Briefe von der Front'); - expect(lookup.size).toBe(2); + // An undated event renders as a plain pill in the undated bucket — out of clustering + // scope. Including it here would scatter its dated letters into orphaned ✉ cross-year + // cards detached from the pill (#7), so it must NOT enter the lookup. + expect(lookup.has(EV_B)).toBe(false); + expect(lookup.size).toBe(1); }); it('ignores derived events (no eventId) and letters', () => { diff --git a/frontend/src/lib/timeline/eventClustering.ts b/frontend/src/lib/timeline/eventClustering.ts index 3b2e7e62..22e3ee78 100644 --- a/frontend/src/lib/timeline/eventClustering.ts +++ b/frontend/src/lib/timeline/eventClustering.ts @@ -29,6 +29,10 @@ export interface SplitLetters { * life-events and letters do not, so they never enter the lookup. HISTORICAL events are excluded * too: a world event always keeps its full-width WorldBand and never clusters, even with linked * letters (REQ-014) — those letters stay loose. + * + * Only year-band events are collected: an undated event renders as a plain pill in the undated + * bucket (out of clustering scope), so including it would scatter its dated letters into orphaned + * cross-year cards detached from that pill (#7). */ export function buildEventLookup(timeline: TimelineDTO): Map { const lookup = new Map(); @@ -40,7 +44,6 @@ export function buildEventLookup(timeline: TimelineDTO): Map { } }; for (const band of timeline.years) collect(band.entries); - collect(timeline.undated); return lookup; }