# Verification

Every ranked paper is checked against the live record before it ships. What is checked, what the mismatch codes mean, and why a failure is flagged rather than repaired.

`verify` is the last stage before `emit`. It checks every ranked paper against the
live record and records what did not match.

It runs late on purpose. Verification is cheap relative to screening, and running
it on the papers that will actually be delivered means the check covers exactly
the set that matters.

## What gets checked

For each ranked paper: the DOI resolves, and the title, year and first author on
the live record agree with what the pipeline holds.

Verification is done against **Crossref**, with **OpenAlex** as the fallback if
Crossref is unreachable. Crossref is never a retrieval source — it appears in the
pipeline only here.

## The verification record

Every `EvidencePacket` carries one, and `emit` refuses to run without it.

| Field | What it holds |
|---|---|
| `verified` | Whether the record checked out. |
| `verified_by` | Which sources confirmed it: `crossref`, `openalex`, `arxiv`, `s2`. |
| `verified_on` | When the check ran. |
| `title_match_ratio` | 0–100. Fuzzy match between the held title and the live one. |
| `mismatches` | What disagreed, if anything. |

## Mismatch codes

| Code | Meaning |
|---|---|
| `doi_unresolved` | The DOI did not resolve. |
| `no_record` | No record was found to compare against. |
| `title` | The title did not match closely enough. |
| `year` | The publication year disagreed. |
| `author` | The first author disagreed. |
| `retracted` | The live record marks the paper as retracted. |

## The matching threshold

Titles are compared fuzzily, because punctuation, subtitles and capitalisation
differ legitimately between sources. The default threshold is a 90 match ratio.

```bash
research-scan verify --strict
```

`--strict` requires 95 instead of 90. Use it when a false positive is more
expensive than a manual check.

## Flagged, not repaired

This is the rule that matters most, and it is worth being exact about.

A paper whose DOI did not resolve, or whose title, year or first author disagreed
with the live record, **ships anyway** — with its `mismatches[]` populated and an
`[UNVERIFIED — check manually]` marker in the rendered Markdown.

It is not silently corrected. It is not silently dropped.

Correcting it would mean writing metadata that came from somewhere other than a
source of record, which is precisely the failure this pipeline exists to prevent.
Dropping it would hide the fact that something in the chain produced a record that
does not check out — information you want, especially if it recurs.

**The one exception is retraction.** A paper the live record marks as retracted is
removed. Everything else is flagged and kept, and the reader decides.

## Reading it

In `evidence.json`, filter on the verification record directly:

```bash
jq '.[] | select(.verification.verified | not)
        | {rank, title, mismatches: .verification.mismatches}' evidence.json
```

In `evidence.md`, unverified papers carry their marker inline, so a human reading
the deliverable sees the same caveat the JSON records.

Per-call detail is in `verify.log.jsonl`.

## What verification is not

It confirms that a record exists and that the bibliographic details match. It says
nothing about whether the paper is any good, whether its claims replicate, or
whether the abstract accurately describes the work — no PDF is fetched, parsed or
read anywhere in the pipeline.

Verification is a check against fabrication and drift, not a quality judgement.
The quality judgement is the rerank, and it is
[the agent's](/architecture/agent-layer/).
