Building WikiWar
July 2026
Current State
WikiWar began as a way for me to try and detect edit wars live on English Wikipedia. In doing this, it accidentally became an evidence-backed system for classifying and cataloguing historical Wikipedia disputes.
The current product has four main evidence views:
- Live — a global edit map of current developing disputes.
- Curated — a hand-picked selection of complete edit-war case files.
- Leaderboard — configurable rankings of eligible conflicts.
- Review — blind, fingerprinted annotations for a supervised model.
Defining Controversy
At first, I wanted just to look at the current edit wars on Wikipedia (materials like this one were a fun read, but not quite live). This is actually harder than you might think, and the obvious approaches have obvious flaws:
Working Locally
The conclusion I came to is that “most controversial” is not one heuristic. It’s just not possible for one scalar to simultaneously represent mutual reverting, broad page activity, discussion volume, formal intervention, and a readable dispute story, all controlled and filtered to remove graffiti. This meant a lot more post-processing than I initially envisioned.
Building this at runtime was obviously impossible, and collecting a multi-year page history across thousands of pages from an API crawl would be slow, rate-limit-prone, incomplete, and difficult to reproduce (compared to prototyping these things, where I could work off of the MediaWiki Action API and EventStreams for a live source of truth).
Local mirroring was the only option, but this meant processing ~127 GiB of
compressed Wikimedia dumps from the May 2026 English
mediawiki_history snapshot. Naturally, processing this snapshot
turns the data into monthly snapshots—additional work had to be done to make it
continuous, just at minimum. Still, this was much better than GPT 5.5’s
“solution” (xHigh btw) of hitting the API for this data in what would have been
effectively the world’s least effective and wasteful DoS attack on MediaWiki,
which it silently reverted to not once, but three times. Sol 5.6 was smarter,
here.
The project also built:
- a
historical_evidence_cache; - a
wikiwar.evidencecommand-line pipeline; - local parsing of
pages-meta-historyrevision-content dumps; - candidate-first article and talk-page targeting;
- target-to-shard indexes, page-ID caches, checkpoints, early stopping, and stale-state recovery.
What’s Our Unit, Again?
As I was building this out conceptually, I realized I was caught between simultaneously trying to look at two types of units of analysis: articles and wars.
Articles are useful for answering “which pages keep attracting conflict?” but they are a terrible container for saying when one war starts or ends. The same page, especially frequently controversial ones, can have unrelated fights years apart. So the actual behavioral unit became the war: a bounded bout of source-linked, mutual reverting around a particular piece of text. Focusing on a distinct selection of text here helped for visual comprehension and the product perspective, as well.
In terms of classifying intensity and features of actual conflict episodes, we score each conflict independently. Defining the boundary was its own shitty problem, because this naturally raises the pair of hard questions of “how long is an edit war?” and “what level of intensity defines an edit war, especially a prolonged one?” The workable rule is a 30-day quiet gap: if the evidence goes silent for longer than that, the next flare-up is a new bout. Every phrase, revision, talk quote, editor count, score, and protection event shown for a battle is scoped to that same pile.
Cleaning Graffiti
Graffiti is not a corner case here; it is the adversarial baseline. On the byte level, a sincere disagreement over whether a territory was “occupied” or “annexed” can look functionally identical to someone adding a slur and another editor removing it. Raw revert counts cannot tell the difference, and neither can a semantic-distance trick without a lot of contextual help (recall the vector-map issues from before).
The filtering stack therefore combines behavior and text: bot filtering, cleanup and anti-vandal edit-summary markers, one-sided reverter concentration, generated undo boilerplate, repeated junk tokens, obvious graffiti, personal attacks, media-caption churn, and leaked wikitext or template fragments. It is deliberately contextual rather than title-based. If a case still cannot prove mutual conflict around source-linked article text after that cleaning, it does not get ranked.
The current deterministic-v2 logic:
- requires at least three source-linked article revisions and one contested segment;
- classifies cleanup when cleanup-marked evidence reaches 70%;
- classifies one-sided activity when the top reverter reaches 75%;
- requires at least one effective mutual-revert pair for a verified historical conflict;
- treats live behavioral matches without complete phrase evidence as provisional;
- calls narrow repeated wording changes localized;
- calls large spans, many segments, or large total changes broad;
- separates formal, discussed, and low escalation;
- maps verified localized conflict to the petty lane and verified broad conflict to substantive, leaving cases that cannot be defended either way ambiguous.
The classifier explicitly excludes article title, topic keywords, language, editor identity, human labels, and trained-model output. Every result retains normalized facts, rule IDs, reasons, completeness, version, and sensitivity flags. This replaced earlier candidate boosts for known contentious titles and removed the risk that a handwritten topic list would quietly define “controversy.”
What Are “Substantive” and “Petty,” Really?
There’s a scene in Who Framed Roger Rabbit where Eddie Valiant is sawing the handcuffs off of himself and Roger Rabbit under considerable effort. Midway through, Roger Rabbit easily slides out as a bit, prompting Eddie to angrily ask, “Do you mean to tell me you could’ve taken your hand out of that cuff at any time?” Roger answers: “No, only when it was funny.”
One obvious problem with this project is that one man’s petty dispute is another man’s very substantive and consequential academic disagreement. But even if I did have a magical button to divine the difference objectively and toss the former, I wouldn’t really want to use it, because showing Wikipedia editor pettiness and pedanticness is exactly what I was trying to smuggle into the ranking. The Roger Rabbit rule basically applies: a dispute should be included when it is funny, especially when it is petty, even if the figures might suggest it isn’t that “controversial.” This is a good editorial instinct, but a dogshit production classifier.
So instead of pretending “petty” and “substantive” are objective truths, the system classifies observable geometry: how many words changed, how many segments moved, whether the same swap recurred, and whether Wikipedia formally intervened. This determines a sort of magnitude, and then a human layer determines the classification of “petty” versus “substantive.” The “human layer” in question is, of course, me, but this isn’t sustainable. The idea was to then translate my subjective judgment into a basic model that could automatically classify between Petty, Substantive, and hopefully even Graffiti at scale.
Data annotation platforms are notoriously really, definitely… pieces of software, but thankfully for something as simple as classification I could just rebuild a Tinder-style UI and swipe left, right, or down.
Classify ten of the clearest current results. The system’s answer stays hidden until your final scorecard.
A first candidate linear model trained on 48 usable labels produced 30% held-out accuracy and macro-F1 of 0.361. It performed worse than a trivial majority baseline on important classes and failed the promotion gates. This is terrible and will be improved; for now we are letting GPT 5.6 classify things.
The Tech Stack
Recent changes arrive continuously; the bounded MediaWiki API adds page identity, revision SHA-1, content metadata, catch-up, and spot checks.
PostgreSQL commits the event and outbox together. Dedicated ingest, enrichment, and scoring workers use idempotent keys, retries, stale reclaim, and a durable dead-letter stream.
306 mediawiki_history partitions join revision-content
XML/7z and exact pages-logging protection actions.
Candidate-first targeting, shard indexes, page-ID caches, early stopping, fingerprints, fail-closed audits, and atomic promotion make rebuilds resumable and reproducible.
Undo and rollback markers, content-identity restores, mutual-revert graphs, revision diffs, A ↔ B wording swaps, talk evidence, and protection sources become source-linked conflict packages.
Live events, enrichment, scores, episodes, and the transactional outbox.
An atomically promoted, read-optimized corpus for evidence and rankings.
Annotations and product interactions survive every corpus replacement.
One service exposes the dashboard, JSON APIs, health and corpus status, and server-sent events. It runs against local SQLite or as the read/API tier of the full container stack.
One data model, bounded repository queries, transactional writes, and 15 forward migrations across SQLite and PostgreSQL.
Pytest, Ruff, mypy, JavaScript checks, Docker/Compose integration, and a three-hour forced-worker-restart soak ask whether it survives reality.
The Final Scoring System
Pre-scoring evidence gate (pass/fail). The score only runs after the evidence-gate and graffiti-removal layer. An edit war first has to prove at least three source-linked article revisions, a documented contested passage, and effective mutual reverting, while staying below the 70% cleanup-dominated and 75% top-reverter cutoffs. Base evidence is not a bonus; it is the price of admission, which avoids giving a well-documented fight extra points for being well documented.
Once admitted, the bout gets a bounded 0–100 score. Multiple times I tried an unbounded conflict index, as it felt more mathematically honest. I valued this for about ten minutes, until “2003 in music” became the most controversial article on Wikipedia. Removing the ceilings let routine volume sneak back in. The bounded score is built from three positive factors—battle strength, participation, and Wikipedia involvement—with a separate cleanup deduction. The default split is 70/10/20, with presets that lean toward pure battle strength, mass participation, or governance.
Main Scoring Parameters
Select Battle Strength, Participation, or Wikipedia Involvement to reveal how that share of the score is calculated.
This combines phrase-level battle dynamics with mutual conflict. The three strongest contested segments are weighted 1.0 / 0.55 / 0.25. Each segment rewards repeated back-and-forth changes and reversals, short A ↔ B swaps, multiple wording variants, and a substantive first-shot edit summary, while very large changed spans are discounted.
Phrase points = min(48, 7.2 × ln(1 + phrase score)) Mutual points = min(22, 3 × √effective reverts + 1.5 × mutual-revert pairs)Together those 70 internal points fill whatever share the viewer assigned to Battle Strength.
This looks only at the number of unique editors inside the selected bout. The logarithm is doing real work here: going from two combatants to ten matters a lot; going from 200 to 208 does not deserve to move an article eight places by itself. And yes, the goal here is lowkey to look for dogpiling and graffiti.
Participation points = min(10, 2.1 × ln(1 + unique editors))Formal intervention is typed rather than treated as one giant admin-action count. Protection actions start at weight 2, current restrictions at 3, RfC signals at 5, restriction-discussion signals at 6, and arbitration signals at 10. Each type uses the formula below; the combined raw involvement score is capped at 20 and scaled to the selected allocation.
Signal points = type weight × log₂(1 + count)This lets arbitration matter more than another routine protection log without allowing governance to create a conflict where no text battle exists.
Additional Scoring Parameters
-
Cleanup Penalty 0 to −12 points
If enabled, the first 10% of cleanup-like evidence is tolerated, then the penalty grows until the case reaches the hard 70% cleanup exclusion. Switching the penalty off changes the ranking among otherwise eligible wars; it cannot make a cleanup-dominated or one-sided case eligible.
Deduction = min(12, 20 × max(0, cleanup share − 0.10)) -
Talk-Page Size 0 points
Talk-edit counts and stored talk-text bytes remain available as evidence and for the separate “most discussed” historical snapshot, but they never add points to the configurable controversy score. Discussion volume is too easy to inflate with archiving, boilerplate, and trolling; a talk-page argument is valuable here because it explains a source-linked wording fight, not because it is large.
-
Battle vs. Article Ranking up to 15 recurrence points
The component score above belongs to one bout. “Battle” ranks the strongest qualifying bout on each page. “Article” starts with that peak and adds 5% of each of the next four qualifying bout scores, capped at 15 recurrence points and 100 overall. Bouts are split after a 30-day quiet gap, and raw elapsed time, routine edit volume, and unverified flare-ups contribute nothing.
Yes, this is less emotionally satisfying than one magic controversy number, but it is vastly harder for a list page or vandalism cleanup to make its way up to number one.
Storytelling
Once the scoring system mostly worked, I realized the score was still the least interesting part. “Armenia: some number” is trivial, and “Israel: Rank 1” is not novel.
But watching editors repeatedly switch “Russia” and “Europe” in Armenia’s page of allies, and then reading the talk-page argument about whether Armenia belongs inside Europe’s borders, is a story. Seeing “occupation” go to “annexation”; that is a real controversy!
That changed the product architecture. Now, we prioritize this sort of word or phrase swapping and aim to produce narratives of controversies as opposed to nebulous numbers. This is the Curated tab, where we only admit complete packages: a verified conflict, an exact wording swap, and a readable talk-page argument tied to that same phrase. The Leaderboard answers the separate question of comparative controversy.