Metadata-Version: 2.4
Name: xrad
Version: 0.2.0
Summary: Your agent cannot assert without quoting the source. A claim store with verbatim-evidence enforcement, for LLM extraction pipelines.
Project-URL: Homepage, https://github.com/narimannemo/xrad
Project-URL: Issues, https://github.com/narimannemo/xrad/issues
License: Apache-2.0
License-File: LICENSE
Keywords: agents,citation,claims,evidence,extraction,hallucination,knowledge-graph,llm,mcp,provenance
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3.11
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Text Processing :: Linguistic
Requires-Python: >=3.11
Requires-Dist: pydantic>=2.9
Requires-Dist: pyyaml>=6.0
Requires-Dist: rich>=13.7
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: pytest>=8.3; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# xrad

[![ci](https://github.com/narimannemo/xrad/actions/workflows/ci.yml/badge.svg)](https://github.com/narimannemo/xrad/actions/workflows/ci.yml)
[![pypi](https://img.shields.io/pypi/v/xrad)](https://pypi.org/project/xrad/)
[![python](https://img.shields.io/pypi/pyversions/xrad)](https://pypi.org/project/xrad/)
[![licence](https://img.shields.io/badge/licence-Apache--2.0-blue)](LICENSE)

**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.

```bash
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

```python
>>> 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

```yaml
# contracts.yaml
entity_types: [Party, Obligation, Instrument]
relations:
  ACQUIRED:    {subject: [Party], object: [Party]}
  GOVERNED_BY: {subject: [Obligation], object: [Instrument]}
```

```json
{"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:

```bash
$ xrad disagreements
  subject  relation  object  says                    against
  Acme     ACQUIRED  Beta    the filing (assertion)  the auditor (rejected)
```

And what makes this question askable at all:

```bash
$ xrad trace Acme Gamma
  1. Acme --ACQUIRED--> Beta          the filing
  2. Beta --OWNS--> Gamma  (reported) Reuters
  2 hops · 1 asserted · 1 reported or hedged

$ xrad trace Acme Gamma --modality assertion
  no chain — the connection may exist when reported claims are allowed
```

`trace`, not `path`, because what comes back is not a route. It is **the
argument a source built**, with who committed to each hop and how strongly. The
second form strips everything the source merely reports and asks whether the
argument survives on its own authority — here it does not. 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:

```bash
$ 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 trace "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. `Osiris` the deity and `Osirite` the 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)` against
  `Acme Ltd`, `Belus minor` against `Belus priscus` — the source telling you
  these differ is the most reliable evidence available.
* **An unknown extra word** stops a merge. `Acme` against `Acme Products` is 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*?

```bash
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

```bash
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

```bash
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:

```python
>>> 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.
