[High] Conflict-zone events are plotted at fabricated coordinates derived from a hardcoded zone anchor, not the article's location
Severity: High (fabricated intelligence data) Confidence: Confirmed by direct inspection of the actual, unmodified route logic (deterministic — no execution needed to demonstrate the transformation). Audited commit: bd4057567de8ee18d49a8b2744c9746bcd67f3e0 (master, 2026-09-13 audit).
/api/conflicts plots real news headlines as precise-looking map coordinates that have no relationship to the article's actual location — the "event" pin is a hardcoded zone anchor plus a small deterministic jitter, not anything derived from the story. The route also reports a top-level activeWarzones count that is a static constant from a hardcoded array, not a measurement of anything live, alongside fields that read as real-time intelligence.
Evidence and mechanism
- src/app/api/conflicts/route.ts:42-118:
KNOWN_CONFLICTSis a hardcoded list of 15 zones, each with a fixed anchorlat/lngand a staticseveritythat never changes based on any fetched data. - Lines 228-263: every item from three world-news RSS feeds (BBC, Al Jazeera, NYT) is matched against a zone by crude keyword substring logic —
terms.every(term => searchText.includes(term))or simplysearchText.includes(zone.region)— so, for example, any BBC World headline containing the bare word "ukraine" anywhere in its title or description matches the Ukraine zone, regardless of what the story is actually about. - Lines 243-245, 251-259: the matched item is then given a synthetic coordinate —
zone.lat + offsetLat, zone.lng + offsetLng, where the offset is a small deterministic function of an internal counter, not of anything in the article. The real news title/URL is kept (so it looks sourced), but the location is invented. - Line 306:
activeWarzones: zones.filter(z => z.severity === 'war').length—severityis the static value fromKNOWN_CONFLICTSabove; this count is always the same 6 regardless of any live signal, but sits next tototalLiveEventsin a response that presents itself as live intelligence.
Reproducible example
A BBC World story titled "UN marks anniversary of Ukraine war with London vigil" (an event in London, about Ukraine) contains both "ukraine" and "war" in its text. It matches the Ukraine zone's queries: ['ukraine war', ...] term check and is plotted at 48.5 + offsetLat, 31.2 + offsetLng — a fabricated point inside Ukraine — even though the actual event described is in London. Nothing in the matching logic distinguishes a story that reports an event happening in a conflict zone from a story that merely mentions the zone's name.
Impact
An analyst viewing this feed sees what looks like precisely geolocated, sourced conflict events. In reality every pin's coordinate is invented from a coarse keyword match plus a jitter formula; the map can show "activity" clustered around a zone anchor for stories that have nothing to do with events at that location, and a static severity/warzone count that never reflects live conditions is presented alongside genuinely live-looking fields.
Smallest correction and acceptance criteria
- Do not plot a news item's location as a derived offset from the matched zone's anchor. Either extract a genuine coordinate from the source (e.g. GDELT's own geocoded events, which this route's own doc comment says it's supposed to use) or do not attach coordinates to keyword-matched RSS items at all — surface them as a text list tied to the zone, not as map pins with invented precision.
- Tighten or remove the bare
searchText.includes(zone.region)fallback, which matches on the zone name alone with no other context. - Either compute
activeWarzones/severityfrom a live signal or clearly label them as static reference classifications rather than positioning them as intelligence metrics in the same payload as live counts. - Acceptance: an RSS item that merely mentions a zone's name/region without describing an event located there must not produce a plotted "conflict event" coordinate purporting to be at that location.
Prior-issue check: #135 (closed) addressed the frontend README's static-marker claim for the map layer; #144 (closed) addressed the older GDELT route randomizing coordinates. Neither covers this current /api/conflicts route's RSS-to-fabricated-coordinate pipeline, which is a distinct, still-present mechanism.
Source: simplifaisoul/osiris