Builds the released relevance judgments for the projected benchmark from a reviewer workbook, then expands them over the corpus duplicate graph.
The released qrels are per question: a set of relevant documents for each question. The per-hop assignments this pipeline works with internally are used to build and audit that set, and are not part of the release.
Everything is CLI-driven; no step has a path baked in. Set the three inputs once and the rest is defaults:
export QRELS_WORKBOOK=/path/review.xlsm # reviewer workbook
export QRELS_QUESTIONS=/path/verified.jsonl # verified questions
export QRELS_DOC_CMD="python3 cm.py doc" # retrieval CLI, `doc` half
export QRELS_NEAR_DUP_DIR=/path/near_dup/doc_duplicates_t70
export QRELS_EXACT_DUPS=/path/duplicate_groups.jsonl
export QRELS_WORK_DIR=work # everything is written herecm.py is the same two-command retrieval CLI the rest of this pipeline uses
(see ../README.md); only doc <docid> is needed here.
| step | script | what happens |
|---|---|---|
| 0 | step0_build_candidates.py |
workbook × verified questions → one candidate pool per question |
| 1 | step1_fetch_docs.py |
cache the full text of every candidate document |
| 2 | step2_render_worksheet.py |
print what still needs a judgment, full text, one question at a time |
| 3 | step3_record_verdicts.py |
rejection list → verdicts file |
| 4 | step4_build_qrels.py |
candidates + verdicts → qrels JSONL |
| 5 | step5_expand_duplicates.py |
expand over exact and near duplicates, to closure |
| 6 | analysis/qrels_stats.py, analysis/qrels_near_dup.py |
distributions and paper figures |
python3 step0_build_candidates.py
python3 step1_fetch_docs.py --all
python3 step2_render_worksheet.py --qid 78 # read; decide
python3 step3_record_verdicts.py --all
python3 step4_build_qrels.py --out work/qrels.jsonlSteps 2 and 3 are the manual/agentic core, and the reason the pipeline is resumable per question. For each question:
- Write the standard for each hop before reading — what a document must state to support it, and what near miss it must reject. Deciding the bar while looking at a candidate is how a pool drifts.
- Read every pending document in full from the step-1 cache. Never judge from the workbook excerpt or from a truncated render: the deciding sentence is often past the first screen, and phrasing varies between copies of the same page.
- Record only the failures, in
work/rejections/<qid>.json, each with the sentence that decides it and a contrast case. Everything unrecorded is accepted, which keeps the file small and the reasoning auditable.
Two rules the reviewer set, both encoded in step 0:
- A hop is skipped only when judged "No" and the note says it is not
needed. A bare "No" is judged normally; if it ends with no support, step 4
reports it in
needs_reviewrather than dropping it. - "Supports" / "Partial support" votes are guaranteed to reach the output. "Does not support" binds unless an explicit, conservative override is recorded.
A judged document usually has twins in the corpus, and a retriever that returns an unjudged copy of a relevant document should not be scored as wrong.
- Exact duplicates inherit the parent's hops with no reading.
- Near duplicates are not inherited. A copy at Jaccard 0.7 can drop the very sentence that grounded a hop, so each is read and judged on its own — and against all live hops of the query, not only the parent's, since a fuller copy can support more than its parent did.
Run to closure: accepting a document puts its own neighbours on the frontier.
python3 step5_expand_duplicates.py scan --qrels work/qrels.jsonl
python3 step1_fetch_docs.py --ids-file work/expand/fetch_ids.json
python3 step5_expand_duplicates.py render --qid 78 --qrels work/qrels.jsonl
python3 step5_expand_duplicates.py apply --qid 78 --decisions /tmp/d78.json
python3 step5_expand_duplicates.py scan --qrels work/qrels.jsonl # until empty
python3 step5_expand_duplicates.py export --qrels work/qrels.jsonl --out work/qrels_expanded.jsonlscan reports nothing to scan; closure reached when no included document has
an unchecked neighbour left.
python3 analysis/qrels_stats.py work/qrels_expanded.jsonl --outdir out --png
python3 analysis/qrels_near_dup.py work/qrels_expanded.jsonl --outdir out --pngFigures are authored at the width they will occupy on the page (--figwidth,
inches) and saved on a fixed canvas rather than a tight bounding box, so
\includegraphics never rescales them and the point sizes on the page are the
ones set here. See analysis/figstyle.py.
--png writes a raster copy beside each PDF. Following the corpus_analysis
convention, the PNG is what gets committed and the PDF is treated as a build
product; the CSVs and the per-question JSONL stay out of the repository.
Document ids have two spellings. The corpus analysis artifacts zero-pad the
sequence (shard_00045_06622); the retrieval API and the qrels do not
(shard_00045_6622). Joining without normalising finds nothing for every id
whose sequence is under five digits — and it does not error, it just silently
returns fewer duplicates than exist. step5_expand_duplicates._ana / ._api
normalise on every join; keep them.
Do not to_pylist() a whole duplicate column. The near-duplicate parquet has
one list-of-struct row per corpus document; materialising the column costs
minutes per shard and gigabytes. Filter to the rows you want with Arrow .take()
first.
Percentage histograms need half-open buckets. Inclusive integer ends
(0–9, 10–19) silently drop every value between 9 and 10. The bucketing
asserts it accounted for every input.
A truncated render is a wrong judgment. Both render steps print a TRUNCATED
marker and the flag to raise. Re-render rather than judge what you can see.