File size: 2,961 Bytes
9d2d895
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import type { UcdpViolenceEvent as ProtoUcdpEvent } from '@/generated/client/worldmonitor/conflict/v1/service_client';

export type ConflictIntensity = 'none' | 'minor' | 'war';

export interface UcdpConflictStatus {
  location: string;
  intensity: ConflictIntensity;
  year: number;
  sideA?: string;
  sideB?: string;
}

// Leaf module by design: it imports nothing at runtime, so tests can load it
// without Vite's import.meta.env. scripts/_ucdp-dashboard.mjs mirrors
// deriveUcdpClassifications (it cannot import from src/ — Railway builds seeders
// from a scripts-only Nixpacks root, #5268) and
// tests/ucdp-dashboard-projection.test.mts pins the two implementations equal.

function isRecentUcdpClassificationDate(dateStart: unknown, now: number, windowMs: number): boolean {
  const eventMs = Number(dateStart);
  return Number.isFinite(eventMs)
    && Number.isFinite(now)
    && eventMs <= now
    && now - eventMs < windowMs;
}

// Exported for tests/ucdp-dashboard-projection.test.mts: scripts/_ucdp-dashboard.mjs
// carries a mirror of this function (it cannot import from src/ — Railway builds
// seeders from a scripts-only root, #5268), and the parity test pins them equal.
export function deriveUcdpClassifications(events: ProtoUcdpEvent[], now = Date.now()): Map<string, UcdpConflictStatus> {
  const byCountry = new Map<string, ProtoUcdpEvent[]>();
  for (const e of events) {
    const country = e.country;
    if (!byCountry.has(country)) byCountry.set(country, []);
    byCountry.get(country)!.push(e);
  }

  const twoYearsMs = 2 * 365 * 24 * 60 * 60 * 1000;
  const result = new Map<string, UcdpConflictStatus>();

  for (const [country, countryEvents] of byCountry) {
    // Filter to trailing 2-year window
    const recentEvents = countryEvents.filter(e => isRecentUcdpClassificationDate(e.dateStart, now, twoYearsMs));
    const totalDeaths = recentEvents.reduce((sum, e) => sum + e.deathsBest, 0);
    const eventCount = recentEvents.length;

    let intensity: ConflictIntensity;
    if (totalDeaths > 1000 || eventCount > 100) {
      intensity = 'war';
    } else if (eventCount > 10) {
      intensity = 'minor';
    } else {
      intensity = 'none';
    }

    // Find the highest-death event for sideA/sideB
    let maxDeathEvent: ProtoUcdpEvent | undefined;
    for (const e of recentEvents) {
      if (!maxDeathEvent || e.deathsBest > maxDeathEvent.deathsBest) {
        maxDeathEvent = e;
      }
    }

    // Most recent event year
    const mostRecentEvent = recentEvents.reduce<ProtoUcdpEvent | undefined>(
      (latest, e) => (!latest || e.dateStart > latest.dateStart) ? e : latest,
      undefined,
    );
    const year = mostRecentEvent ? new Date(mostRecentEvent.dateStart).getFullYear() : new Date(now).getFullYear();

    result.set(country, {
      location: country,
      intensity,
      year,
      sideA: maxDeathEvent?.sideA,
      sideB: maxDeathEvent?.sideB,
    });
  }

  return result;
}