Benchmarks¶
Deterministic measurements of three claims the library makes, run against committed fixtures with one command. No network, no API keys, no LLM calls.
pip install -e ".[dev]"
python -m benchmarks # regenerate results/results.json and results/summary.md
python -m benchmarks --check # fail if the committed results differ from a fresh run
Results: results/summary.md. Every number there is produced by the command above; CI runs --check so the committed results cannot drift from the code. This page describes method and limits only and deliberately repeats no results.
| Suite | Claim under test | Data |
|---|---|---|
| chunking.py | Clause-aware chunking keeps clauses in one chunk more often than structure-blind splitting | CUAD test split |
| provenance.py | ProvenanceTracker resolves an extracted value to where it came from | CUAD test split |
| privacy.py | The regex PII fallback catches the formats it claims; secret documents never reach a provider; restricted documents reach only a LocalProvider | Synthetic fixture |
Not measured yet: extraction accuracy downstream of chunking, and accuracy per LLM provider (cloud versus local). Both need model calls and are postponed. Presidio-backed PII detection is not measured either: the privacy suite runs the regex fallback only.
Data¶
CUAD v1 test split — fixtures/cuad_test.json.gz: all 102 contracts of the official test split and their 2,643 gold answer spans, reduced to contract text plus (category, start, end) per span. Rebuild it from the official release with python -m benchmarks.build_cuad_fixture path/to/data.zip; the builder checks the zip's SHA-256 and the output is byte-for-byte reproducible.
Synthetic PII fixture — fixtures/privacy_cases.jsonl: 128 contract-style sentences, each with one planted value and several non-PII distractors. Values are fictitious (.test domains, 555-01xx phone numbers, published test card and IBAN numbers). Rebuild with python -m benchmarks.privacy build.
Method¶
Chunk integrity¶
Each contract is chunked by ClauseAwareChunker, SemanticChunker (paragraph mode) and fixed_window, a structure-blind baseline defined in chunking.py that cuts consecutive windows at the last whitespace before the size limit. All run at 256, 512, 1024 and 2048 tokens with overlap 0. A gold span is intact when it lies wholly inside one chunk. Chunks are mapped back to source offsets ignoring whitespace differences, because SemanticChunker re-joins paragraphs. Spans longer than a chunk can hold are excluded from that size's score and counted separately.
The summary also breaks results down by span length and CUAD category, lists every category where the clause-aware chunker does not beat both baselines, and classifies why it splits the spans it does split.
Provenance¶
Each gold span stands in for a value an extractor returned. Chunks come from ClauseAwareChunker (512 tokens, overlap 50) and are registered with ProvenanceTracker in two modes: with source_text (offsets located in the document) and without it (the tracker's older behaviour, which assumes chunks tile the document). Each value is looked up verbatim, with whitespace collapsed, and lower-cased — approximations of how an LLM returns text. A resolution is correct when the returned span overlaps the gold span, and exact when the source text at the returned offsets equals the value.
Privacy¶
Detection: PIIDetector(use_presidio=False) runs on every case. A planted value is caught when every letter and digit of it is inside a detected span, so nothing identifying survives redaction. Cases are grouped by variant: plain, obfuscated (zero-width characters, Unicode dashes, fullwidth digits, Cyrillic homoglyphs), and unsupported — formats the fallback is not designed for (names, addresses, international phone numbers, spaced IBANs, dates of birth in words), included so the limits appear in the results. A distractor is a false positive when any detected span overlaps it. EvalHarness.run_privacy() also reports entity-type recall and precision.
Routing: every sensitivity level × {cloud provider, LocalProvider} × entry point (PrivacyAwareLLMRouter, TaskPipeline, the comparison task's second document, LegalRAGPipeline). Providers are stubs that record every prompt. A cell passes when the outcome matches the policy — blocked, rejected, excluded from context, sent redacted, or sent unchanged — and the stub never received text it must not see. EvalHarness.run_privacy() reports blocking accuracy on the same policy.
Limitations¶
- CUAD is one corpus. 102 English-language commercial contracts drawn from SEC EDGAR filings, annotated for CUAD's categories (40 of its 41 categories have answers in the test split). Other document types, jurisdictions, languages and scanned documents are not covered.
- Tokens are estimated as characters ÷ 4, the library's own estimate, not a model tokenizer.
- Integrity is a proxy. It shows whether an extraction prompt could see a whole clause, not whether the extraction is right. Clause-aware chunking produces more, smaller chunks than fixed windows at the same limit, which means more model calls; the summary reports chunk counts so this cost is visible.
SemanticChunkerexceeds its size limit on some chunks, which favours it on integrity. - Only overlap 0 is measured for chunking.
- Gold spans are not LLM output. Real extractions paraphrase more than the three variants tested, so provenance on real output will be lower. When a value occurs more than once in a contract, the tracker returns the first occurrence.
- The privacy fixture is small, synthetic and written for this suite. Its results show which formats the fallback handles; they are not an estimate of recall on real documents. Only the regex fallback is measured.
- The routing matrix uses stub providers. It verifies the library's enforcement logic, not network behaviour, and it trusts any
LocalProviderregardless of the host it points at, as the router does.
Licensing and attribution¶
The CUAD fixture is derived from the Contract Understanding Atticus Dataset (CUAD) v1 by The Atticus Project, licensed under CC BY 4.0 (licence as declared on the dataset card). Changes: only the test split is kept, reduced to contract text and answer-span offsets. Dataset: https://www.atticusprojectai.org/cuad. Paper: Hendrycks, Burns, Chen and Ball, CUAD: An Expert-Annotated NLP Dataset for Legal Contract Review, arXiv:2103.06268.
The synthetic privacy fixture and all benchmark code are part of this repository and licensed Apache-2.0.
Results¶
Generated by python -m benchmarks; do not edit by hand. Methodology and limitations: benchmarks/README.md.
CUAD v1 test split: 102 contracts, 2643 gold answer spans, 4,778,515 characters.
Chunk integrity¶
Share of gold clause spans that fall wholly inside one chunk (overlap 0).
| Chunker | Size (tokens) | Spans intact | Integrity | Chunks | Mean chunk tokens | Chunks over size |
|---|---|---|---|---|---|---|
| clause_aware | 256 | 2534 / 2586 | 98.0% | 6343 | 187.2 | 0 |
| clause_aware | 512 | 2612 / 2640 | 98.9% | 3094 | 384.8 | 0 |
| clause_aware | 1024 | 2631 / 2643 | 99.5% | 1491 | 799.8 | 0 |
| clause_aware | 2048 | 2638 / 2643 | 99.8% | 741 | 1610.7 | 0 |
| semantic | 256 | 2305 / 2586 | 89.1% | 6415 | 184.1 | 167 |
| semantic | 512 | 2522 / 2640 | 95.5% | 3314 | 356.9 | 123 |
| semantic | 1024 | 2626 / 2643 | 99.4% | 1447 | 818.3 | 102 |
| semantic | 2048 | 2638 / 2643 | 99.8% | 681 | 1739.6 | 93 |
| fixed_window | 256 | 2027 / 2586 | 78.4% | 4739 | 251.3 | 0 |
| fixed_window | 512 | 2340 / 2640 | 88.6% | 2391 | 498.9 | 0 |
| fixed_window | 1024 | 2488 / 2643 | 94.1% | 1220 | 978.5 | 0 |
| fixed_window | 2048 | 2563 / 2643 | 97.0% | 639 | 1868.8 | 0 |
Spans longer than the chunk size are excluded from that size's score: 57 at 256, 3 at 512, 0 at 1024, 0 at 2048.
By span length at 512 tokens¶
| Span length (chars) | clause_aware | semantic | fixed_window |
|---|---|---|---|
| 0-200 | 99.8% (n=1362) | 99.0% (n=1362) | 97.0% (n=1362) |
| 200-1000 | 98.8% (n=1218) | 93.1% (n=1218) | 80.7% (n=1218) |
| 1000-4000 | 83.3% (n=60) | 65.0% (n=60) | 60.0% (n=60) |
| 4000+ | n/a (n=0) | n/a (n=0) | n/a (n=0) |
Categories where clause_aware does not beat every baseline at 512 tokens¶
| Category | Spans | clause_aware | semantic | fixed_window |
|---|---|---|---|---|
| Agreement Date | 93 | 100.0% | 100.0% | 100.0% |
| Document Name | 102 | 100.0% | 100.0% | 100.0% |
| Effective Date | 84 | 100.0% | 100.0% | 96.4% |
| Most Favored Nation | 3 | 100.0% | 100.0% | 33.3% |
| Notice Period To Terminate Renewal | 18 | 100.0% | 100.0% | 88.9% |
| Parties | 543 | 99.8% | 100.0% | 99.4% |
| Termination For Convenience | 41 | 100.0% | 100.0% | 87.8% |
| Unlimited/All-You-Can-Eat-License | 6 | 83.3% | 83.3% | 83.3% |
| Volume Restriction | 35 | 97.1% | 100.0% | 94.3% |
Why clause_aware splits a span at 512 tokens¶
| Reason | Spans |
|---|---|
| size limit split a section inside the span | 11 |
| span contains a heading; split there by design | 14 |
| span longer than half the chunk budget | 2 |
Provenance¶
ProvenanceTracker over ClauseAwareChunker chunks (512 tokens, overlap 50). Correct: the SourceSpan overlaps the gold span. Exact: source[span] == value. 45 values also occur earlier in their contract, so a first-occurrence match points elsewhere.
| Mode | Value as returned | Resolved | Correct | Exact |
|---|---|---|---|---|
| source_text | verbatim | 98.9% | 97.2% | 98.8% |
| source_text | whitespace | 83.0% | 81.3% | 82.7% |
| source_text | lowercase | 5.6% | 3.0% | 4.2% |
| legacy | verbatim | 98.9% | 56.5% | 27.9% |
| legacy | whitespace | 83.0% | 51.0% | 26.9% |
| legacy | lowercase | 5.6% | 1.4% | 1.3% |
Privacy¶
PII detection (regex fallback, use_presidio=False)¶
Planted values whose every letter and digit was redacted.
| Entity | fullwidth | homoglyph | plain | unicode_dash | unsupported | zero_width |
|---|---|---|---|---|---|---|
| CREDIT_CARD | 8 / 8 | 0 / 4 | 4 / 4 | |||
| DATE_OF_BIRTH | 8 / 8 | 0 / 4 | ||||
| EMAIL_ADDRESS | 4 / 4 | 8 / 8 | 4 / 4 | |||
| IBAN_CODE | 8 / 8 | 0 / 4 | ||||
| LOCATION | 0 / 4 | |||||
| PASSPORT_NUMBER | 8 / 8 | |||||
| PERSON | 0 / 8 | |||||
| PHONE_NUMBER | 4 / 4 | 12 / 12 | 8 / 8 | 0 / 8 | 4 / 4 | |
| US_SSN | 4 / 4 | 8 / 8 | 4 / 4 |
False positives on non-PII distractors: 0 of 224. EvalHarness.run_privacy() on the supported cases: entity-type recall 95.8%, precision 95.8%.
Routing matrix¶
32 of 32 cells match the policy. EvalHarness blocking accuracy: 100%.
| Entry point | Sensitivity | Cloud provider | LocalProvider |
|---|---|---|---|
| router | public | sent_raw | sent_raw |
| router | confidential | sent_redacted | sent_redacted |
| router | restricted | routing_error | sent_redacted |
| router | secret | blocked | blocked |
| pipeline | public | sent_raw | sent_raw |
| pipeline | confidential | sent_redacted | sent_redacted |
| pipeline | restricted | routing_error | sent_redacted |
| pipeline | secret | blocked | blocked |
| comparison_doc_b | public | sent_raw | sent_raw |
| comparison_doc_b | confidential | sent_redacted | sent_redacted |
| comparison_doc_b | restricted | routing_error | sent_redacted |
| comparison_doc_b | secret | blocked | blocked |
| rag | public | sent_raw | sent_raw |
| rag | confidential | sent_redacted | sent_redacted |
| rag | restricted | excluded | sent_redacted |
| rag | secret | excluded | excluded |