An Evidence Graph Built From Extracted Claims Keeps Contradictions Attached to Their Sources
Feed forty articles about a rumored plant restart to a summarizer and you get one smooth paragraph: several outlets report that production will resume in March, though the ministry has pushed back. It reads well. It also throws away what you’d need to act on it. You can’t tell which outlets reported it, whether each had its own source or all copied one wire story, or what the ministry actually said, a denial or a polite refusal to comment.
A summary compresses by dropping structure, and the structure it drops is the part an analyst works with: who said it, how firmly, when, in which words.
The tool worth building stores claims instead. A claim is a small record with a speaker, a predicate and an object, a modality (asserts, denies, expects, confirms, alleges), a time, and the verbatim span of text it came from. Repeats of one claim merge into a single node that remembers every document that made it. Contradictions don’t get resolved or averaged. They stay as separate nodes about the same subject and predicate, each attached to its own sources, and the reader decides. The result is a queryable evidence graph, closer to an intelligence tool than another summarizer.
None of the pieces is new. ClaimBuster scores sentences by how check-worthy they are, a useful first filter that says nothing about who said the sentence. The schema.org ClaimReview markup suits publishing a finished fact-check and does nothing to find claims in raw text. GDELT pulls events out of world news at a scale no side project will match, but an event record says that something happened, where you need a record of somebody saying it did. Open information extraction gives you subject, relation and object triples, and plenty of pipelines stop there, leaving the speaker and the hedge in the sentence where no query can reach them.
The method this serves is older than all of it. Richards Heuer’s Analysis of Competing Hypotheses has an analyst list the hypotheses, list the evidence, and mark how each item bears on each hypothesis, hunting for the evidence that tells them apart instead of the pile that merely agrees. People build that matrix by hand in a spreadsheet. Its rows are claims.
A Claim Is a Speaker, a Stance and a Quote
The pipeline is a chain of dull stages, and the order matters. Ingest a document and keep the raw bytes as a hashed snapshot, the same trick as polling a source and hashing every version. Split it into sentences. Ask a language model to extract claims from each passage into a fixed JSON schema. Check that the quote appears verbatim in the source. Resolve mentions to canonical entities, cluster paraphrases, then compare claims that share a subject and predicate.
The verbatim check is the best step in the system and the cheapest. A fabricated claim arrives with a fabricated quote, and a string search against the source text throws it out. What’s left is a smaller problem: real quotes, extracted wrongly. Each survivor becomes a record like this one (the field names are a sketch):
{
"id": "clm_0417",
"speaker": "ent:energy_ministry",
"modality": "expects",
"subject": "ent:field_alpha",
"predicate": "production_increase",
"value": "10 percent",
"said_on": "2026-09-14",
"refers_to": "2026-Q3",
"quote": "output at Field Alpha should rise by about ten percent by the third quarter",
"doc": "doc_5521",
"span": [1840, 1915],
"relayed_from": "doc_5100",
"cluster": "cl_88"
}
Two details matter more than they look. said_on and refers_to are different dates: a September statement about the third quarter has to be filed under both, or “what did they say in September” and “what do we know about Q3” each get half an answer. The doc, span and quote fields point into an archived snapshot, the same footing as any value in a provenance-first store. The field that turns a pile of claims into an analysis tool, though, is relayed_from.
Fifty Articles Can Be One Source
Fifty articles repeating one wire story are one source, not fifty. Keyword search can’t see it, because fifty articles look like fifty confirmations. A claim graph can see it if it tracks attribution chains. A story saying officials told a wire service the plant will restart gets stored with its relay: this document took the claim from that one, which took it from an anonymous official. Walk the relayed_from edges to the end (a recursive CTE does it in SQLite) and every cluster has an origin set, the roots of the chains. The query that matters is short:
SELECT cluster,
count(*) AS documents,
count(DISTINCT origin_doc) AS origins
FROM claim_origin
GROUP BY cluster
HAVING count(*) >= 5 AND count(DISTINCT origin_doc) = 1;
That lists every claim with five or more documents behind it and a single origin: corroboration that collapses on inspection. The opposite query finds the claims worth leaning on.
Contradictions get their own query: claims about the same subject and predicate with different values or opposite polarity, listed with the sources on each side. Nothing is averaged and nothing gets a truth score. The graph says who claimed what and how many independent origins stand behind each side, and belief stays with the analyst. “No outlet has denied this” is a claim about what a corpus search didn’t find, worth only as much as the record of that search, which is the problem of verified absence.
The Hard Part Is Entity Resolution
“The ministry,” “the minister” and “officials in the capital” might be one speaker or four. Entity resolution is the hard problem in every knowledge graph, and a claim graph inherits it in an unforgiving form. Merge too eagerly and two people’s statements fuse, so a contradiction vanishes. Merge too timidly and one speaker contradicts themselves across two nodes. Keep every mention as its own node, join them with same_as edges that carry a confidence, and let each query pick a strict or loose reading. A wrong merge costs more than a missed one, so the default is cautious.
Attribution nests. “Officials said the minister told them the restart was on track” holds three layers: the outlet’s claim, the officials’ claim, the minister’s. Extract the whole stack or the first-appearance query will credit the wrong party. Anonymous sources add doubt, since “a senior official” in two articles may be one person or two. Report a floor and a ceiling for independent origins, say two to five, because a single count would claim a certainty the text doesn’t give.
Modality is a trap of its own. “Could,” “reportedly,” “denied” and “declined to comment” mean different things, and a model will file the last as a denial if you let it. Use a closed list of modalities with an explicit “unclear,” and run a dumb deterministic check on the dangerous ones: a claim marked denies needs a denial cue in its quote, such as denied, rejected or dismissed. A real quote with flipped polarity passes the verbatim check and does the most damage, so it needs its own guard.
Time and cost round it out. A ministry expects a restart in March, then moves the date, so nothing gets overwritten: a later claim by the same speaker on the same predicate supersedes the earlier one without erasing it, and how long a claim deserves belief is a question for facts that expire. On cost, most sentences contain no claim. A cheap filter on speech verbs and modals skips them, and a cache keyed on normalized sentence text means a wire story republished by fifty outlets gets extracted once.
The corpus is hostile. Pages can carry text addressed to the extraction model, planted articles can imitate a wire story, and a coordinated push can fake fifty independent origins. Treat page text as data, lock the output to the schema, and never present an origin count as proof. Origins describe the reporting. They say nothing about the world.
Scope for a First Version
Version 0.1 is a command-line tool over one SQLite file. Point it at a folder of saved articles, run one extractor, and get tables for documents, mentions, claims and relay edges. Four queries ship: who claims X, first appearance of a claim, claims with one origin, and claims with a contradiction. Output is a terminal table with the quote beside each claim.
It refuses a lot. No credibility scores for outlets, because ranking sources is an editorial opinion. No truth labels. No automatic merging of people; suggested same_as links wait for a human. No live crawler, so it ingests what you’ve saved.
The buyer is whoever keeps this in a spreadsheet today, a newsroom research desk say, or an analyst doing open-source intelligence work. The extraction is a commodity. The single-origin report is the product.
Count the origins before you count the articles.