Skip to main content

spancheck

© 2026 Trevor J. Romack — MIT-licensed (LICENSE) · pip install spancheck · tjromack@gmail.com

Score a grounded-answer system on citation accuracy, abstention correctness, hallucination rate, cost and latency — and get an audit log a compliance reviewer can read.

spancheck is an installable, corpus-agnostic evaluation library for RAG and retrieval-then-answer pipelines. Point it at any system through a thin adapter, give it a test set, and it produces a scorecard on the four metrics that decide whether the system's answers can be trusted — plus a versioned audit log that reconstructs why each answer passed or failed.

Library first, CLI second, GitHub Action third: the Python API is the product; the command line and the Action are thin wrappers over it.


The four metrics

Metric Question it answers How it's scored
Citation accuracy Does the cited span actually support the claim? Deterministic span check (the capability built first and tested deepest)
Abstention correctness Does it decline when the answer isn't in the corpus, and answer when it is? Deterministic
Hallucination rate What share of answers assert something the retrieved context doesn't support? Deterministic groundedness + calibrated LLM-judge for the qualitative residue
Cost & latency Tokens / dollars / wall-clock per answer, against a budget Deterministic, from a cached run — no network

Status

Complete and dogfooded. The measurement core (Case / evaluate / Run / Scorecard / diff / gate), the thin adapter contract, the deterministic graders, citation-span verification — the capability this build exists to add — the cost/latency + versioned audit log layer, the calibrated LLM-judge, and the CLI + GitHub Action are implemented, dependency-free (the judge reaches a model only through a caller-supplied callable), and tested (68 tests, green on a clean pip install and in CI). Citation verification splits into provenance (is the cited span really in the retrieved context, verbatim?) and support (does the span cover the claim?); both must hold, so a fabricated quote or a right-answer-wrong-citation cannot pass. A captured run re-scores offline (Run.rescore), and run.audit_log() emits a versioned, self-describing record with per-citation verdicts (schema in docs/AUDIT-LOG.md). The LLM-judge is opt-in — the default grader set stays deterministic and offline — and is calibrated before it is trusted.

Results (measured)

spancheck was run end-to-end against a real product pipeline as a black box (a grounded answer-a-document tool on claude-sonnet-5, over its own sample contract), and the judge was calibrated against a human-labelled gold set. Full write-up in docs/CASE-STUDY.md; the audit log is committed at dogfood/suver_audit.json.

Metric Result
Citation accuracy (real pipeline) 1.00 — every cited span real and supporting
Hallucination (groundedness) 0 hallucinations (pass-rate 1.00)
Abstention correctness 11/12 — spancheck caught one false-abstention on the real system
Overall (12 cases) 0.967
Judge vs human gold set — stub / claude-sonnet-5 0.75 / 1.00
Tests 68, clean pip install + CI (3.11 & 3.12)

The one miss is the point: the eval found a real recall gap in a shipping system — a measured behaviour, not a spancheck bug. Reproduce with python dogfood/run_dogfood.py and python -m spancheck.calibrate --real.

Command line & CI

The CLI is a thin wrapper over the library. It works out of the box against a shipped demo target — no key, no user code:

pip install spancheck          # or `pip install -e .` from a clone
spancheck run examples/cases.jsonl --target spancheck.demo:system --out audit.json
spancheck score audit.json                                    # recompute metrics from the cache, offline
spancheck gate  audit.json citation_accuracy=1.0 groundedness=0.9   # exit 1 on failure (for CI)

Point --target at your own system with module:callable (any input -> answer function; the answer may be a string or a dict with contexts/citations/usage).

The deterministic path needs no key. The opt-in LLM-judge (--real calibration, or judge_support(provider=...)) reads ANTHROPIC_API_KEY — set it in the environment, or drop it in a gitignored .env at the repo root (ANTHROPIC_API_KEY=sk-ant-...), which the CLI and spancheck.calibrate load automatically.

As a GitHub Action (fails a PR on a regression):

- uses: tjromack/spancheck@main
  with:
    cases: eval/cases.jsonl
    target: myapp.rag:answer
    thresholds: "citation_accuracy=1.0 groundedness=0.9"
    baseline: baseline-audit.json   # optional: also fail on a pass-rate drop

Lineage

spancheck is the spinoff of llm-eval-guardrails-harness — the version that proved the method against a single real target (a regulatory RAG copilot), with LLM-judge agreement 1.00 against a human gold set. This package is the same method, extracted from an in-repo lab, generalised to any corpus, and packaged so a stranger can install it and point it at their own system. What carried over unchanged, what was rewritten and why, and what the harness got wrong that this fixes are recorded in LINEAGE.md.

Quickstart

The Phase-1 core below runs today (pip install -e .). Lines marked (Phase 3) / (Phase 2) are the intended shape of what those phases add; see Status and TODO.md.

from spancheck import Case, evaluate, adapter, citation_accuracy

# 1. Wrap your system in a thin adapter: input -> {answer, contexts, citations, usage, latency_ms}
#    (optional — evaluate() also accepts a plain input->answer callable and times it for you)
my_system = adapter(lambda q: my_rag_pipeline(q))

# 2. Describe your test set — answerable, unanswerable, and adversarial cases
cases = [
    Case(id="q1", input="What is the notice period?", category="answerable",
         expected="30 days", meta={"answerable": True}),
    Case(id="q2", input="What is the company's revenue?", category="unanswerable",
         meta={"answerable": False}),  # should abstain
]

# 3. Score it — a run is captured once, then the metrics compute offline
run = evaluate(cases, my_system)
print(run.scorecard().by_grader())   # citation accuracy, abstention correctness, groundedness, PII-leak
run.save("run.json")                 # persist a run for baselines / regression gating
run.audit_log("audit.json")          # the versioned, compliance-readable record (per-citation verdicts + cost/latency)
run.rescore([citation_accuracy(support_threshold=0.8)])  # re-score the SAME run offline — no system call
# CLI (thin wrapper over the same API) — command surface exists; wired to the library in Phase 5
spancheck run cases.jsonl --target my_module:my_system --out audit.json
spancheck score audit.json        # recompute metrics from a cached run, no network

Limits — what a passing score does not claim

  • Deterministic support is a lexical proxy, not entailment. By default, citation support is scored by how much of the claim's wording the cited span covers — fast, offline, and unable to see a span that shares the claim's words but contradicts it ("the notice period is not 30 days"). The opt-in LLM-judge upgrades this to true entailment (citation_accuracy(support_fn=judge_support(provider=...))), and is calibrated against a human gold set before it is trusted (python -m spancheck.calibrate). The deterministic core flags the proxy's boundary rather than hiding it (a test pins the false-positive; the judge catches it).
  • Provenance requires a verbatim quote. A paraphrased citation fails provenance by design — a deliberate incentive for a system to quote its sources exactly. spancheck does not (yet) match a citation by meaning.
  • A citation isn't scoped to its named source. A cited span found in any retrieved context passes; tying a citation to the specific source_id it names is a planned refinement.
  • Groundedness is a word-overlap proxy too, and the metrics are only as good as the test set you bring. spancheck measures a system against cases you author; it does not generate them.

Design pins

  • Library-first, CLI-second — the API is the product.
  • No provider lock-in — a thin adapter from day one; stub by default, any model behind a callable.
  • Every metric computable without network given a cached run — capture once, re-score offline.
  • The audit-log schema is versioned — a breaking change is a major version bump; it's a contract with a reviewer.
  • Judge prompts live in version-controlled files — a rubric change is a reviewable, version-stamped diff.
  • No dependency on any other project's internals — standalone; dogfooded as a black box, never coupled.

License

MIT — see LICENSE. Free to use, modify, and distribute; the library is a tool meant to be run against your own systems.

Metadata

Release files for spancheck 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for spancheck 0.1.1
File Size Uploaded
spancheck-0.1.1.tar.gz 38.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for spancheck 0.1.1
File Interpreter ABI Platform
spancheck-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 70.2 kB

Release files / spancheck-0.1.1.tar.gz

Download URL spancheck-0.1.1.tar.gz
Size 38.7 kB
Tags Source
SHA-256 checksum
How to use checksums
5f0261130afcee8f666b922b6b206323c8a467cb879b6f9ab6c8929cc16a54ef
BLAKE2b-256 checksum
How to use checksums
1ed95e72a8b96e7652d82bee05a8eab7da24978d4a638cb77ecd1e61728a04c4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Release files / spancheck-0.1.1-py3-none-any.whl

Download URL spancheck-0.1.1-py3-none-any.whl
Size 31.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2649e5b57908ea59b1dbe9e41ceed28f4e3ccfa91ce98df1e540c8411c212640
BLAKE2b-256 checksum
How to use checksums
af85fa9d008e0c93ed4d5674d68619809058fa94438cd51c9fe163333b530d33
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Release history Release notifications | RSS feed

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page