← Method & Data

Internal design document, rendered for transparency. Operational sections (deployment, job recipes, credentials) have been removed. Dated 2026-10-15; may be amended by later work recorded in the changes log.

Incident event study — design

1. Purpose

Pick conflict incidents by a fixed rule, isolate the material about each one by topic, wherever it was published, trace how far, how widely and how fast it travelled, then compare the incidents to see whether a pattern repeats.

Two principles from the user shape everything below:

The reader is the user first, then the PI. Success is: sixteen incident profiles whose search quality is measured and stated, a comparison that separates repeated patterns from single-case observations by a rule fixed in advance, and a cannot-claim list as plain as the findings.

Framing (who is blamed, alignment, function) is out of scope for this pass; it is a second pass on the material this study isolates (§14).

1.1 Facts that shaped the design (prod, 2026-10-15)

Quantity Value
UCDP events (MLI/BFA/NER) 5,445, 2021-01-02 .. 2026-08-31
UCDP date precision 1 / 2 / 3+ 4,248 / 951 / 246
Events with "Government of Russia" coded as a party 233, all from 2024 on, all one-sided (vs civilians)
Events with Russian forces named in UCDP source headlines or coded 336 (2022: 11 · 2023: 34 · 2024: 148 · 2025: 100 · 2026: 43)
Moura (Mar 2022) in UCDP "Government of Mali vs JNIM", state-based, 321 dead of whom 243 civilians, dated 26–31 Mar (precision 2); headlines do not name Wagner
Telegram channels posting per quarter 15 (2021 Q2) · 19 (2022 Q1) · 26 (2022 Q3) · 48 (2023 Q3) · ~73 (2025–26)
Telegram posts per quarter ~12k (2021) → ~70k (2025–26)
message.views / forwards filled (posts since 2022) 99.4 %
View/forward counts refreshed after first storage never (INSERT … ON CONFLICT DO NOTHING; the weekly differ updates only edits and deletions)
Post-level forward origin message.fwd_from_channel, message.fwd_from_msg
Channel strata attributed 23 · discovered 54 · institutional 1
Embeddings BGE-M3 dense, 1024-d, CLS pooling + L2 norm (multilingual: a French query finds Russian/Arabic posts)

2. Incident selection

2.1 Types (four, stratified)

Type Rule
coup Closed list of successful coups in the corpus period: Mali 2021-05-24, Burkina Faso 2022-01-24, Burkina Faso 2022-09-30, Niger 2023-07-26. Not from UCDP.
russian Any UCDP event where side_a contains "Government of Russia" or the source headlines (source_meta->>'source_headline') match `wagner
civilian type_of_violence = 3 (one-sided), not russian. Ranked by civilian deaths. Perpetrator may be a jihadist group or the state.
army type_of_violence = 1, side_a = Government of Mali / Burkina Faso / Niger, side_b ∈ {JNIM, IS}, not russian. Ranked by best deaths. FLA, MPLJ and JAS are out.

Types follow UCDP's coding, not media framing. Moura is therefore an army incident; the gap between how UCDP and the media classify it is something the results may show, not something the selection corrects.

2.2 Mechanics

  1. Incident = merged events. Events with the same type, same side_a/side_b, same country and same region (gid_1, else adm_1), each starting ≤ 3 days after the cluster's first event, form one incident. Deaths are summed; the incident date (day 0) is the earliest date_start.
  2. Date precision. The first event must have date_prec ≤ 2 (exact day or a range of a few days). Speed is measured from the first possible day; the incident's precision is shown beside its speed figures.
  3. One per type per year, 2022–2025, deadliest first across types. Within a year, all candidates of the three violent types are queued by deaths (civilian deaths for civilian), and each type takes the first valid candidate in that queue. Coups are placed before everything.
  4. No overlap within a region. A candidate whose window (§4.1, day −14..+45) overlaps an already-chosen incident in the same region is skipped. A coup blocks its whole country. Incidents in different regions of the same country may overlap; they are then a close pair (§5.4).
  5. Empty year stays empty. No borrowing from other years.
  6. Frozen output. incidents.yaml: the chosen incidents and the full ranked candidate log (every candidate considered, with the reason it was picked or skipped). The user reviews it; it is fingerprinted before stage 2.

2.3 Probe result (indicative; the frozen file is authoritative)

A throwaway probe of these rules on prod ucdp_event (2026-10-15) gave:

Year russian civilian army
2022 Ségou, 2022-03-05, 33 Ménaka, 2022-03-28, 236 Moura (Mopti), 2022-03-26, 321
2023 Niono (Ségou), 2023-11-22, 40 Karma (BF Nord), 2023-04-20, 154 Djibo (BF Sahel), 2023-11-26, 244
2024 Tinzaouaten (Kidal), 2024-07-25, 55 Barsalogho (BF Centre-Nord), 2024-08-24, 394 BF Est, 2024-08-08, 226
2025 Kidal, 2025-02-17, 28 BF Est, 2025-03-19, 254 Soum (BF), 2025-10-09, 104

Skips logged: Hombori (russian 2022, 50) overlaps Moura in Mopti; Koulikoro (russian 2025, 65) has date precision 4.

2.4 Known consequences (accepted)

3. Search inputs (stage 2)

For each incident, drafted by Claude from the UCDP record(s) and their source headlines, then corrected by the user, then frozen:

Keyword match rule: (≥ 1 place term and ≥ 1 event term) or ≥ 1 standalone term; case- and diacritic-insensitive, word-boundary aware.

The descriptions are encoded with the same BGE-M3 weights, pooling and normalisation as doc_embedding (the repo's bge_m3_encoder; model revision recorded). Text and vectors are frozen together in search_inputs.yaml + search_vectors.parquet, fingerprinted.

4. Search and the incident material (stage 3a)

4.1 Window and corpus

4.2 Candidates and inclusion

4.3 Coverage report

Per incident: posts in the window, share with an embedding (the meaning route cannot see the rest), keyword hits, meaning candidates, overlap of the two routes, and whether the top-2,000 cap binds (the 2,000th similarity ≥ τ once τ is fixed); a binding cap is flagged and the pool is widened for that incident before stage 4.

Output: material.parquet (one row per candidate or copy: doc key, incident, route, similarity, keyword terms hit, copy_of, channel, stratum, language, ts) + _fingerprint.json + run manifest.

5. Validation by a coder (stage 3b)

5.1 Sample (per incident, ~100 posts)

5.2 The coder sheet

Amendment 2026-10-15: the judgements are now given by the two AI judges plus human adjudication (§5.6); the coder text below is kept as the original design, and the same sheet layout and answer options apply.

Each row: the post text (original language, plus machine translation where available) and a neutral description of the incident. Answer: yes / no / unsure ("is this post about this incident?"). The coder does not see the route, the similarity or other incidents' rows. Close-pair rows show both incident descriptions and ask: this one / the other / both / neither.

5.3 What the returns fix

5.4 Close pairs

A post that is a candidate for both incidents goes to the incident whose place or standalone terms it matches exclusively. If it matches both or neither, it is excluded from both and counted as ambiguous. The close-pair sample (answered by the AI judges plus adjudication, §5.6; originally the coder) measures how often this rule assigns correctly; the rate is reported for each pair.

5.5 Noise floor

For violent incidents (unforeseeable), included material in days −14..−1 measures what the search lets in by accident: noise floor = mean included posts per day over days −14..−1. For coups, pre-window material may be genuine early chatter; it is reported as such and not used as noise. A coup's floor is borrowed: for each violent incident take floor ÷ (all corpus posts per day in its days −14..−1); the coup's floor = the median of those twelve ratios × all corpus posts per day in the coup's days −14..−1.

5.6 AI judges (amendment 2026-10-15)

The coder is replaced by two AI agents, Codex and Gemini, which the user runs on the Mac, each in its own self-contained folder (each agent is instructed to read only its own input/ and write only its own output/, with no network, and is sandboxed where the tool allows (Codex's workspace-write sandbox limits writes and network; reads are restricted by instruction only)). The blind sample of §5.1 is unchanged, and so is what the judges see: post text and neutral incident description(s), never the route, the similarity, the stratum or the incident id.

6. Re-fetch of view and forward counts (stage 4)

Risks the plan must settle before building this stage:

  1. Session safety. Telethon sessions sit on single-node RWO volumes, one per account; using one session from two places can get it logged out. The re-fetch runs either during a short pause of that account's ingestor or with a spare account. The plan determines which is possible.
  2. Forward view counts. Whether a forwarded post's views is its own or its origin's. If the origin's, reach counts originals only and forwards contribute spread, not views.

7. Measures (stage 5)

Day 0 = the incident's first possible day. Days are UTC calendar days. All measures use the included material (originals + copies) in days 0..+45 unless stated.

7.1 Reach (Telegram only)

7.2 Spread

7.3 Speed

7.4 Fairness across eras

Every count is shown raw and relative to the ecosystem active in that window (carrier shares, posts as a share of all window posts, views multiples). Comparisons between incidents use the relative form.

8. Comparison and the reading rule

8.1 Feature catalogue (fixed now; questions, not expected answers)

Each feature is computed mechanically per incident:

# Feature Values
F1 Which stratum posts first attributed / discovered / same day / only one stratum carries
F2 Which language posts first language code / tie
F3 Attributed channels' share of views vs their share of carriers higher / lower / equal (±5 pts)
F4 ≥ 1 forward crosses from attributed to discovered yes / no
F5 ≥ 1 forward or reused-text copy crosses a language boundary yes / no
F6 Median views multiple > 1 yes / no
F7 Half-life ≤ 3 days yes / no
F8 Tail beyond day +14 yes / no
F9 Peak on day 0 or 1 yes / no
F10 Coups only: pre-window material above the borrowed noise floor (§5.5). For violent incidents the pre-window defines the floor, so F10 is not computed yes / no / n.a.

8.2 What counts as a pattern

8.3 Severity view

For the twelve violent incidents: deaths against total views, carrier share and half-life, shown as a scatter with Spearman's rho and its bootstrap interval, labelled descriptive (n = 12).

9. Outputs

13. Cannot-claim list (verbatim in the report and the run manifest)

  1. No cause and effect: the study describes how material about an incident travelled, not what it caused or what caused it.
  2. The material comes from the 78 Telegram channels and the web sites we collect. It is not "Sahel media" or "the Russian information space" as a whole.
  3. Views are not persuasion, and Telegram view counts can be inflated; reach means "displayed", not "believed".
  4. Coverage is estimated only against the candidates the two search routes produced; material neither route surfaced is unmeasured.
  5. Incident types follow UCDP's coding (e.g. Moura is an army–JNIM battle in UCDP), not how the media framed the events.
  6. Patterns describe these sixteen rule-chosen incidents; they are not estimates for incidents in general.
  7. Russian involvement before 2024 is identified through UCDP's source headlines, so incidents whose sources did not name Russian forces are missed.
  8. Speed is measured in days from the first possible day of the incident, not in hours; incidents with a date range carry that uncertainty.
  9. Relevance was judged by two AI agents (Codex and Gemini) with human adjudication of their disagreements; the search being validated also relies on AI embeddings, so the check is not fully independent. Agreement between the agents is reported.
  10. Views multiples compare incident posts with a baseline sample from the 90 days before the window; because the baseline is older, channel growth alone can push a multiple above 1, so a multiple above 1 is not evidence of extra attention.

14. Out of scope

Rendered from 2026-10-15-incident-study-design.md · changes log