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;
}
|