Your agent cannot assert without quoting the source. A claim store with verbatim-evidence enforcement, for LLM extraction pipelines.
Project description
xrad
Your agent cannot assert here without quoting the source.
Extraction pipelines produce knowledge graphs that assert. Acme ACQUIRED Beta, as a fact. That is fine until the source only implies it, attributes it
to someone, or hedges it — and then the graph has silently turned a rumour into
a fact, and nothing downstream can tell.
xrad stores claims: who said it, with what commitment, and the verbatim span that proves it. Three mechanisms enforce that, and all three are refusals.
pip install xrad
# see it refuse things in 60 seconds
python examples/demo.py
xrad serve --db graph.db --source contract.txt --vocab contracts.yaml
1. A quote that is not in the source is refused
>>> record_claim(subject="Acme", relation="ACQUIRED", object="Beta",
... quote="Acme purchased Beta", modality="assertion",
... asserter="the contract")
{"ok": false, "error": "quote_not_found",
"message": "quote not found in source: 'Acme purchased Beta'.
Quote verbatim from the text, including original spelling..."}
The document says "Acme Ltd hereby acquires Beta Inc". The paraphrase is plausible, would have passed any review, and is rejected — because models are asked for the exact words and finding them is arithmetic, done here.
There is no fuzzy matching. A near-miss means the model paraphrased, and a paraphrase pointing at real offsets is exactly the silent corruption this exists to prevent.
2. A relation outside the vocabulary is refused
# contracts.yaml
entity_types: [Party, Obligation, Instrument]
relations:
ACQUIRED: {subject: [Party], object: [Party]}
GOVERNED_BY: {subject: [Obligation], object: [Instrument]}
{"ok": false, "problems": ["unknown relation 'SORT_OF_RELATED_TO' —
use one of: ACQUIRED, GOVERNED_BY"]}
Closed by design. An open vocabulary lets a model invent four synonyms for one
relation, and the graph stops being queryable. vocab.prompt_fragment()
generates the prompt text from the same file the code enforces, so the two
cannot drift.
3. Modality is fixed, and the graph keeps disagreements
A source asserts, reports, hedges, denies, declines to decide, or asks. Six, and they do not vary by domain. Everything else is yours.
That is what lets two sources contradict each other without one being deleted:
$ xrad disagreements
subject relation object says against
Acme ACQUIRED Beta the filing (assertion) the auditor (rejected)
And what makes this question askable at all:
$ xrad path Acme Gamma
1. Acme --ACQUIRED--> Beta the filing
2. Beta --OWNS--> Gamma (reported) Reuters
2 hops · 1 asserted · 1 reported or hedged
$ xrad path Acme Gamma --modality assertion
no chain — the connection may exist when reported claims are allowed
The chain does not survive on the filing's own authority. In a graph of edges that question has no meaning.
Building the graph
Recording claims gives you a claim log. resolve makes it a graph:
$ xrad resolve
9 mentions -> 3 entities
Acme Limited <- Acme Ltd, Acme Limited, ACME
Beta Inc <- Beta, Beta Inc
2 merges refused, ranked by what the answer moves:
5 Acme into 'Acme Limited'
type conflict: Party vs Product
3 Acme Products into 'Acme Limited'
'acme products' extends 'acme' with a word that is not a known empty suffix
Before it, Acme Ltd, Acme Limited and ACME are three nodes and
xrad path "Acme Ltd" Gamma finds nothing though the document plainly connects
them. After it they are one node with three surface forms, and a query finds it
by any of them.
The design is in what it refuses. Clustering on string similarity is easy and wrong in a specific, expensive way: the pairs that look most alike are often the ones the source is deliberately distinguishing.
- A type boundary stops a merge.
Osiristhe deity andOsiritethe stone differ by two characters; merging them moved 322 claims about a mineral onto a god. - An explicit discriminator stops a merge.
Acme Ltd (UK)againstAcme Ltd,Belus minoragainstBelus priscus— the source telling you these differ is the most reliable evidence available. - An unknown extra word stops a merge.
AcmeagainstAcme Productsis as often a parent and a subsidiary as it is two spellings. Known empty suffixes (Ltd,GmbH,Holdings) are a documented equivalence and are stripped; anything else is a judgement and is queued.
Refusals are ranked by how many claims the answer moves, because a wrong merge puts every claim about one thing onto another.
Auditing what was recorded
locate guarantees the quote is real. The audit asks the harder question:
does it establish the claim?
xrad audit --emit batches.json # hand these to a model or an agent fleet
xrad audit --ingest verdicts.json
# 14 supported · 4 refuted
# 4/18 refuted = 22.2%
A genuine quotation can fail to support what was hung on it, and nothing else in a pipeline catches it:
claim: Ferdinand III IDENTIFIED_WITH Mercurius Trismegistus
span: "FERDINANDVS III Caesar, / Vivat Mercurius Trismegistus redivivus,"
verdict: refuted — two separate acclamation lines. The second carries its own
verb and need not predicate of Ferdinand. Identification is adjacency,
not statement.
Both names are in the quotation. The extractor saw two names near each other in a dedication and inferred an equation.
The auditor is shown the span and nothing else — not the document, not the neighbouring claims, not how it was extracted. With the context around it, a verifier reconstructs plausibility from the surroundings and confirms nearly everything; that measures agreement, not evidence. It is also told to try to refute and to default to unsupported, because an agreeable verifier finds nothing.
A refuted claim is not deleted. The source still said it, and the refutation is a fact about the citation rather than about the source.
Exporting
xrad export jsonld -o graph.jsonld # rdf:Statement reification
xrad export graphml -o graph.graphml # Gephi, yEd, Cytoscape
xrad export cypher -o graph.cypher # Neo4j
xrad export csv -o graph # nodes + edges, for pandas
Every format carries modality, asserter and the evidence quote on each
claim. JSON-LD uses rdf:Statement, and Cypher reifies —
(a)<-[:SUBJECT]-(:Claim)-[:OBJECT]->(b) — rather than writing
(a)-[:ACQUIRED]->(b) and losing who said it. An export that flattens a claim
into an edge produces a file asserting things no source asserted.
Searching
xrad search "sacrificium"
FTS5 has no stemmer for most languages, and inflected corpora are nothing but
stems. Measured on the source project: sacrificium returned 0 hits where 9
rows contained sacrific-, and Aegyptius returned 4 against 110. A reader
would conclude the source never mentions it.
So each term is searched as written and as a prefix on its stem. This over-matches by design — BM25 sorts it out, and a false positive you can see beats a silent absence you cannot. Results carry modality and asserter, because a quotation shown alone invites reading it as a fact.
Widening a citation that stopped too early
The commonest way a correct claim fails an audit is not fabrication. It is a citation that ends a clause too soon:
>>> widened_quote(text, "We hold it to be the same instrument", ["Acme Ltd"])
"Acme Ltd entered the agreement on 1 March. ... We hold it to be the same
instrument"
We hold it to be the same cites a real identification whose subject is it.
Measured: 49% of claims were refused and 76% of those refusals were
referential rather than substantive. Widening turns a right claim that fails
an audit into a right claim that passes one — and the budget stops it making a
wrong claim right by swallowing the document.
Why it exists
Extracted from a pipeline that put 5,646 claims from a 1652 folio into a graph where every one resolves to a rectangle on a scanned page. The audit is the reason to trust any of it: a second model, shown only the cited span, tries to refute each claim. Given the surrounding page it would reconstruct plausibility and confirm everything — starved of it, the question becomes answerable.
23% of claims did not survive. The largest single failure was an attribution the quote itself did not contain: the source names Strabo once and writes for three paragraphs, and the extractor quotes paragraph three while still crediting him. One prompt rule fixed it — the attribution must be inside the cited span — and refutation fell to 11.5%, replicated on a second volume (z = 5.05).
That is why record_claim warns when a reported claim names an asserter its
own quote does not.
What it is not
Not a vector index — no embeddings, no similarity search. A claim store you traverse. Not an extraction model: bring your own agent, xrad decides what it is allowed to record. Not a fact database. Sources are wrong; a source being wrong is recorded, with the modality showing how it held the statement.
Apache-2.0.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file xrad-0.1.0.tar.gz.
File metadata
- Download URL: xrad-0.1.0.tar.gz
- Upload date:
- Size: 55.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
08415271fc9ef35214ee62aa9f6dab0a3aa41a0cb68792dc41fd38f4acf84051
|
|
| MD5 |
d73e856e53f6550aa220c9dc3e3c817e
|
|
| BLAKE2b-256 |
c1011bfc565ff3cb9cc249a61885db4f28fb1babfe9a08d8cfb1f9724f7bf886
|
Provenance
The following attestation bundles were made for xrad-0.1.0.tar.gz:
Publisher:
release.yml on narimannemo/xrad
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
xrad-0.1.0.tar.gz -
Subject digest:
08415271fc9ef35214ee62aa9f6dab0a3aa41a0cb68792dc41fd38f4acf84051 - Sigstore transparency entry: 2337271122
- Sigstore integration time:
-
Permalink:
narimannemo/xrad@1a756144b2c4ea53415be0c5c8c89739758b8603 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/narimannemo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1a756144b2c4ea53415be0c5c8c89739758b8603 -
Trigger Event:
push
-
Statement type:
File details
Details for the file xrad-0.1.0-py3-none-any.whl.
File metadata
- Download URL: xrad-0.1.0-py3-none-any.whl
- Upload date:
- Size: 52.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2d18a0d6bf244fd2d4a13b1102dc76ecd63244ddf485c00c55aa1358475ed943
|
|
| MD5 |
1ff4d85c673cba26ec8ef01a55a52725
|
|
| BLAKE2b-256 |
3343fd3c72931715cb3c80d3b8e8eece523e9d2914d04a68dfeac59863098e7f
|
Provenance
The following attestation bundles were made for xrad-0.1.0-py3-none-any.whl:
Publisher:
release.yml on narimannemo/xrad
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
xrad-0.1.0-py3-none-any.whl -
Subject digest:
2d18a0d6bf244fd2d4a13b1102dc76ecd63244ddf485c00c55aa1358475ed943 - Sigstore transparency entry: 2337271141
- Sigstore integration time:
-
Permalink:
narimannemo/xrad@1a756144b2c4ea53415be0c5c8c89739758b8603 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/narimannemo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1a756144b2c4ea53415be0c5c8c89739758b8603 -
Trigger Event:
push
-
Statement type: