# ex-citation-check — is every cited claim actually in the chunk it cites?

A RAG answer that cites `[2]` is only as trustworthy as the check that `[2]` really says what the
claim says. No model here: a judge model comes later (`ex-ragas-mock`), and it is only worth
trusting once you have a deterministic check to calibrate it against. This one is token overlap
plus a number check, both readable in the red row that failed.

## What to implement

`src/citation_check/core.py`:

- `support(claim, chunk) -> float` — a score in `[0, 1]`: the fraction of the claim's content
  words (stopwords excluded) that also appear in the chunk, halved-and-then-some when the claim
  states a number the chunk does not contain.
- `check(answer_with_cites, chunks) -> Report` — split `answer_with_cites` into lines, each
  ending in a `[n]` marker (1-indexed, matching how the marker reads on the page — `[1]` is
  `chunks[0]`), and label each claim `"supported"`, `"unsupported"` or `"wrong-number"` against
  the chunk its marker names.

## Run it

```
cd exercises/ex-citation-check
python3 -m venv /tmp/lbv-cc && /tmp/lbv-cc/bin/pip install -q pytest
/tmp/lbv-cc/bin/python -m pytest -q
```

(`uv sync && uv run pytest -q` works too.)

## If you get stuck

- **`support`** — build the content-word overlap first (lowercase, split on non-alphanumerics,
  drop stopwords) and get that returning sane numbers before touching the number check at all.
- **`check`** — a marker's `[n]` is written 1-indexed the way a citation reads on a page; the
  list you index into is 0-indexed. Write the off-by-one down before you write the slice.
- **`wrong-number` vs `unsupported`** — they're both "not `supported`", but they mean different
  things to the person reading the report: a claim can share every content word with its chunk
  and still cite a number the chunk never says.
