ContractRadar test set

A test set for AI extraction of council contracts

Synthetic UK local-authority procurement documents with exact gold labels (generated from the same spec as each PDF), the tricky layouts that break OCR-and-regex pipelines, the compliance flags a Procurement Act 2023 rule engine should raise, and a scorer. Built for the ContractRadar pilot; free for anyone to use.

Download the test set (zip) Code on GitHub Field schema & rules
12synthetic cases (PDF + gold)
39labelled fields per case
36layout & content hazards
24expected compliance flags
2extractors scored

How it works

Synthetic PDF in, expected CSV out, plus the wrong extraction. Each case folder has document.pdf, gold.json / expected.csv (what an extractor should return), notes.md (why the case is hard) and the expected rule-engine flags. A deliberately naive pdftotext + regex baseline is scored on every case so you can see exactly where a text-dump approach breaks.

Every figure, authority and supplier is fictional and each page carries a visible "synthetic test document" marking. Real council notices can be pulled from Find a Tender and Contracts Finder (Open Government Licence) with crtest.collect, which records source and licence per file.

git clone https://github.com/Marxist-Leninist/contractradar-testset
cd contractradar-testset
pip install -r requirements.txt

# score your own extractor: write predictions in the
# results/ format, then
python -m crtest.score --pred results/my_extractor.json

# or run the built-in LLM extractor against any
# OpenAI-compatible endpoint (no keys in the repo)
export LLM_BASE_URL=https://api.deepseek.com/v1 LLM_API_KEY=... LLM_MODEL=deepseek-chat
python -m crtest.extract_llm --out results/deepseek.json --vision --ocr
python -m crtest.score --pred results/deepseek.json

Leaderboard

Field accuracy counts every field that is non-null in the gold labels (plus any value an extractor invents where the gold is null). "Critical-field pass" means authority, supplier, title, value and dates (and the modification fields for change notices) were all correct. High-severity flag precision/recall is only shown for extractors that output flags.

ExtractorField accuracyCritical-field passHigh-severity flagsFiles
llm:nvidia/nemotron-3-ultra-550b-a55b:free:text
nvidia/nemotron-3-ultra-550b-a55b:free via https://openrouter.ai/api/v1 (text+ocr)
94.5%7/12P 0.88 / R 0.88scored json · predictions
2026-10-31
naive_text
pdftotext -layout + label regexes. Deliberately simple baseline.
51.9%2/12no flags outputscored json · predictions
2026-10-31

Cases

First page of CR-001
CR-001 · contract award notice · 1 page

Contract award notice, clean layout

baseline
PA23-threshold-in-scope
naive text baseline: 100% fields · critical pass: yes
First page of CR-002
CR-002 · contract details notice · 2 pages

Contract details notice with a priced schedule crossing a page

table_crosses_pagerepeated_headerfooter_interleavesannual_vs_total_valuetotals_row
PA23-threshold-in-scope
naive text baseline: 96% fields · critical pass: no
First page of CR-003
CR-003 · contract details notice · 1 page

Contract details notice with borderless, grouped particulars

borderless_tablegrouped_rowslabel_synonymsannual_vs_total_valuedate_abbr
PA23-threshold-in-scope
naive text baseline: 25% fields · critical pass: no
First page of CR-004
CR-004 · contract change notice · 1 page

Contract change notice relying on the wrong ground

wrong_legal_groundpercent_not_printeddate_uk_format
PA23-threshold-in-scopePA23-s74-exceeds-below-thresholdPA23-s74-substantial-term
naive text baseline: 78% fields · critical pass: yes
First page of CR-005
CR-005 · contract document · 1 page

Scanned contract particulars (image-only PDF)

scanned_no_text_layertrading_as_namekpi_shortfall
PA23-threshold-in-scopePA23-s52-kpi-min3
naive text baseline: 0% fields · critical pass: no
First page of CR-006
CR-006 · contract details notice · 1 page

Contract details notice in a two-column stacked layout

two_column_layoutstacked_label_valuemoney_short_formattrading_as_namelate_contract_details_noticepayment_terms_over_30
PA23-threshold-in-scopePA23-s68-payment-over-30dPA23-s53-cdn-late
naive text baseline: 17% fields · critical pass: no
First page of CR-007
CR-007 · contract award notice · 2 pages

Multi-lot award notice with an unawarded lot and a short standstill

multi_lotunawarded_lottotals_rowevaluation_scores_tabletable_crosses_pagestandstill_too_short
PA23-threshold-in-scopePA23-s51-standstill-short
naive text baseline: 79% fields · critical pass: no
First page of CR-008
CR-008 · contract change notice · 1 page

Contract change notice correctly below threshold (negative control)

negative_controlmoney_spaced_formatdate_uk_formatpercent_not_printed
PA23-threshold-in-scope
naive text baseline: 75% fields · critical pass: no
First page of CR-009
CR-009 · contract document · 2 pages

Deed of variation extending an expired contract with no change notice

retrospective_extensionno_ccn_publishedwrong_legal_groundvalues_in_prosedate_ordinal
PA23-threshold-in-scopePA23-s74-substantial-termPA23-s75-ccn-required-not-publishedAUDIT-retrospective-extension
naive text baseline: 3% fields · critical pass: no
First page of CR-010
CR-010 · contract details notice · 1 page

Works contract details notice with wrapped supplier name and value in words

supplier_name_wrappedmoney_words_and_figuresdate_ordinal
PA23-threshold-in-scope
naive text baseline: 71% fields · critical pass: no
First page of CR-011
CR-011 · contract change notice · 1 page

Works change notice exceeding the 50% unforeseeable-circumstances cap

percent_not_printedmoney_gbp_suffixdate_iso_format
PA23-threshold-in-scopePA23-sch8-para3-cap-50pcPA23-s77-publish-modified-contract
naive text baseline: 56% fields · critical pass: no
First page of CR-012
CR-012 · contract award notice · 1 page

Direct award notice with values embedded in prose

direct_awardvalues_in_prosedate_iso_format
PA23-threshold-in-scopeAUDIT-direct-award-above-threshold
naive text baseline: 14% fields · critical pass: no

Hazards covered

HazardWhat it isCases
baselinePlain single-column notice with bordered key/value table. Should be easy.1
table_crosses_pageA table runs over a page boundary; the header row repeats on the next page.2
repeated_headerRepeated table header rows must not be read as data rows.1
footer_interleavesPage footer text (authority, page x of y) falls between table rows in a text dump.1
annual_vs_total_valueAn annual price and a total contract value both appear; only the total is the contract value.2
borderless_tableKey/value table has no rules; column boundaries must be inferred from alignment.1
grouped_rowsA section label is printed once and spans several rows (blank cells below it).1
label_synonymsFields use non-standard labels (e.g. 'Initial expiry' for the end date).1
two_column_layoutPage is laid out in two columns; a line-based text dump interleaves them.1
stacked_label_valueEach label is printed on one line and its value on the next.1
money_short_formatMoney printed as £2.31m rather than £2,310,000.1
money_spaced_formatMoney printed with spaces as thousands separators (£4 200 000).1
money_words_and_figuresMoney printed in words followed by figures.1
money_gbp_suffixMoney printed as 1,234.00 GBP.1
date_uk_formatDates printed as DD/MM/YYYY.2
date_iso_formatDates printed as YYYY-MM-DD.2
date_ordinalDates printed as 3rd March 2026.2
date_abbrDates printed as 03-Mar-2026.1
scanned_no_text_layerImage-only PDF (simulated photocopy): no text layer, slight skew and noise. Needs OCR or vision.1
trading_as_nameSupplier printed with a trading name in brackets; the legal name is the answer.2
supplier_name_wrappedSupplier legal name wraps across two lines mid-name.1
multi_lotSeveral lots awarded to different suppliers.1
unawarded_lotOne lot was not awarded; it must appear with awarded=false and no supplier.1
totals_rowA totals row must not be read as a lot or a line item.2
percent_not_printedThe modification percentage is not printed and must be computed from the values.3
values_in_proseKey values are embedded in running text rather than in a table.2
negative_controlNothing is wrong with this document; a rule engine should raise no high-severity flag.1
wrong_legal_groundThe authority cites a modification ground that does not cover the change made.2
retrospective_extensionThe variation is dated after the contract had already expired.1
no_ccn_publishedThe document states that no contract change notice was published before the modification.1
standstill_too_shortThe stated standstill end date is fewer than 8 working days after publication.1
direct_awardDirect award under section 41 with a stated justification.1
kpi_shortfallContract over £5m with fewer than three KPIs.1
late_contract_details_noticeContract details notice published more than 30 days after signature.1
payment_terms_over_30Stated payment terms exceed 30 days.1
evaluation_scores_tableTenderer scoring table that could be mistaken for lot values.1

Contribute

Add a case. Add a spec to crtest/gen.py (gold fields + layout style + hazards) and run make cases. The PDF and gold labels are generated from the same spec, so they cannot disagree. Good targets from the pilot discussion: tables without set boundaries, grouped ID rows such as dates, and page boundaries - we have one of each, more variants welcome.

Score an extractor. Write predictions as {"extractor": "name", "cases": {"CR-001": {"fields": {...}, "flags": [...]}}} and run the scorer. Commit the predictions and the .scored.json to results/ and the leaderboard picks them up.

Label real documents. Use crtest.collect to fetch council notices, copy the OCDS values into a gold.json with the same field names, and attach the published document. Keep the _provenance block: the pilot's data rules require source and licence for everything uploaded.

Rules. crtest/pa2023.py holds the reference checks (thresholds from 1 Jan 2026, 10%/15% modification limits, 8-working-day standstill, 30-day notices, 3 KPIs over £5m). If you think a check is wrong, open an issue with the section of the Act.

Discussion: pilot thread on Discord. Generated 31 October 2026.