fix(timeline): exclude undated events from the cluster lookup

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 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-15 22:08:39 +02:00
committed by marcel
parent 4b965e655e
commit 5870d244fc
2 changed files with 10 additions and 4 deletions

View File

@@ -13,7 +13,7 @@ const makeEvent = (overrides: Partial<TimelineEntryDTO> = {}): TimelineEntryDTO
makeEntry({ kind: 'EVENT', type: 'PERSONAL', documentId: undefined, ...overrides }); makeEntry({ kind: 'EVENT', type: 'PERSONAL', documentId: undefined, ...overrides });
describe('eventClustering — buildEventLookup', () => { 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 = { const timeline: TimelineDTO = {
years: [ years: [
{ {
@@ -25,8 +25,11 @@ describe('eventClustering — buildEventLookup', () => {
}; };
const lookup = buildEventLookup(timeline); const lookup = buildEventLookup(timeline);
expect(lookup.get(EV_A)).toBe('Ein gewaltiger Stadtbrand'); expect(lookup.get(EV_A)).toBe('Ein gewaltiger Stadtbrand');
expect(lookup.get(EV_B)).toBe('Briefe von der Front'); // An undated event renders as a plain pill in the undated bucket — out of clustering
expect(lookup.size).toBe(2); // 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', () => { it('ignores derived events (no eventId) and letters', () => {

View File

@@ -29,6 +29,10 @@ export interface SplitLetters {
* life-events and letters do not, so they never enter the lookup. HISTORICAL events are excluded * 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 * too: a world event always keeps its full-width WorldBand and never clusters, even with linked
* letters (REQ-014) — those letters stay loose. * 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<string, string> { export function buildEventLookup(timeline: TimelineDTO): Map<string, string> {
const lookup = new Map<string, string>(); const lookup = new Map<string, string>();
@@ -40,7 +44,6 @@ export function buildEventLookup(timeline: TimelineDTO): Map<string, string> {
} }
}; };
for (const band of timeline.years) collect(band.entries); for (const band of timeline.years) collect(band.entries);
collect(timeline.undated);
return lookup; return lookup;
} }