Tutorial · PhD acceptance test
From 20 papers to a paragraph you can defend
PLAN.md §8 defines the PhD acceptance test as: take 20 papers for one thesis question, identify what they establish, inspect disagreements, and produce a paragraph whose citations can be verified. This tutorial walks through that whole test using project membership, local attachment, claim correction, evidence comparison, and bibliography export.
99100001–99100004) stand in for your 20. The same slugs are used throughout: project thesis-ch3, question q1, study moreno-mri-cohort, table ch3-matrix, brief ch3-thickness, bibliography ch3-bib. Blocks marked output from a scratch library are real script output. Where a step depends on a subagent or your own judgment, the JSON it would produce was written by hand in the documented shape and then passed to the real script. Long JSON is trimmed at ….Journey map
projects/thesis-ch3/
paper or library record
--refresh changes it. That way a conclusion never quietly shifts because the papers behind it changed.-
Frame the chapter: project and question
A project keeps its question separate from the papers. Question IDs only need to be unique within their project, so
q1is fine here even if another project also has aq1.output from a scratch library
› /ref:project create thesis-ch3 --scope "Cortical thickness in autistic adults" { "slug": "thesis-ch3", "scope": "Cortical thickness in autistic adults", "questions": [] } › /ref:project add-question thesis-ch3 --id q1 --text "Is cortical thickness altered in autistic adults?" # prints the updated project record with q1 in "questions"
-
Add the papers
Paste all 20 PMIDs into a single
/ref:add. Metadata comes from the PubMed connector in one call, and you get one result line per PMID. Then add each paper to the project.output from a scratch library
› /ref:add 99100001 99100002 99100003 99100004 99100001: added (citekey=moreno2021increased) 99100002: added (citekey=haddad2022no) 99100003: added (citekey=moreno2023cortical) 99100004: added (citekey=okafor2020age) › /ref:project add-paper thesis-ch3 99100001 --reading-status to_screen # … repeated for 99100002–99100004
If you're starting from a search rather than a PMID list,
/ref:search-pubmedsaves the query and runs it. The systematic review tutorial covers that route. -
Screen, then queue what you'll read
A screening decision belongs to this project only. Excluding a paper here doesn't remove it from the library or from other projects. Every decision needs a reason and is appended to
screening.jsonl, which is never rewritten. The latest decision is also copied onto the paper's project entry, so the--screenedselector can find it.output from a scratch library
› /ref:screen --project thesis-ch3 --pmid 99100001 --decision included --reason "Adult sample, cortical thickness outcome" { "pmid": "99100001", "decision": "included", "reason": "Adult sample, cortical thickness outcome", "timestamp": "2026-09-15T10:17:14.530791+00:00" } # 99100002 and 99100003 included the same way › /ref:screen --project thesis-ch3 --pmid 99100004 --decision excluded --reason "Mixed-age sample; adults not reported separately" { "pmid": "99100004", "decision": "excluded", "reason": "Mixed-age sample; adults not reported separately", "timestamp": "2026-09-15T10:17:14.762938+00:00" } › /ref:queue set --project thesis-ch3 --pmid 99100001 --status to_read --priority 1 --why "Anchor positive finding" { "added_at": "2026-09-15T10:17:14.059080+00:00", "pmid": "99100001", "priority": 1, "reading_status": "to_read", "relevance": null, "screening": { "decision": "included", "reason": "Adult sample, cortical thickness outcome", "timestamp": "2026-09-15T10:17:14.530791+00:00" }, "why_saved": "Anchor positive finding" }
-
Bring in full text: attach your PDFs, fetch the rest
Most thesis PDFs come from your university's access, so
/ref:attachtakes pairs of PMID and file path. Before storing anything, it looks for the paper's DOI or a run of title words in the PDF's first two pages. In this example the second PDF belongs to a different paper, and it's refused:output from a scratch library
› /ref:attach 99100001 ~/Downloads/moreno2021.pdf 99100002 ~/Downloads/wrong.pdf 99100001: attached diagnostic: PDF conversion unavailable: anydoc not installed (D16 supersedes pdf2md; no substitute converter is used) 99100002: refused -- neither DOI nor a run of the title text was found in the PDF's first 2 page(s) -- refusing silent attachment (§6) › /ref:attach 99100001 ~/Downloads/moreno2021.pdf 99100001: duplicate_noop
The PDF is now stored under
raw/<sha256>/source.pdf, and attaching the same file again does nothing. This machine has no anydoc, so nosource.mdwas produced and the paper stays at the abstract tier. The diagnostic tells you that instead of hiding it. For open-access papers, use/ref:fetch:output from a scratch library
› /ref:fetch 99100002 99100002: acquired (source=plain_text) diagnostic: source has no markup (plain extracted text); section/heading structure and figures are not recoverable
-
Extract what each paper claims
/ref:extractruns oneref-extractorsubagent per paper, in parallel. A failure on one paper doesn't block the others. Papers with full text get full-tier claims and the rest get abstract-tier claims, and each claim records which tier it came from.output from a scratch library · subagent JSON written by hand
› /ref:extract 99100001 99100002 99100003 99100001: extracted (study_type=imaging, claims=2) 99100002: extracted (study_type=imaging, claims=1) 99100003: extracted (study_type=cohort, claims=1)
99100001 produced two claims from its abstract. One is correct (
c-c30763f4756d, frontal thickness, d = 0.41). The other (c-5a2acb6872de) says the effect is whole-brain, which the paper doesn't say. That's exactly the kind of error you correct in the next step. -
Verify claims: accept, edit, reject
This step is what "identify what they establish" means in practice: you review the extracted claims. Each decision goes into
papers/<pmid>/corrections.jsonas a separate record. The raw extraction itself is never overwritten.output from a scratch library
› /ref:verify claim 99100001 c-c30763f4756d accept --rationale "Matches abstract" --reviewer ana { "correction_id": "cor-a0c755ad1e36", "target_type": "claim", "target_id": "c-c30763f4756d", "decision": "accept", … "status": "active" } › /ref:verify claim 99100001 c-5a2acb6872de reject --rationale "Paper reports frontal regions only, not whole-brain" --reviewer ana { "correction_id": "cor-4537f675779b", "target_type": "claim", "target_id": "c-5a2acb6872de", "decision": "reject", "original_value": { "population": "autistic adults", "intervention": "autism diagnosis", "comparator": "non-autistic controls", "outcome": "whole-brain cortical thickness" }, "replacement_value": null, "rationale": "Paper reports frontal regions only, not whole-brain", "reviewer": "ana", "timestamp": "2026-09-15T10:17:16.039966+00:00", "evidence_locator": "abstract", "status": "active" } › /ref:verify claim 99100002 c-1dd89ae7ddbe edit --replacement repl.json --rationale "Population restricted in Methods" # repl.json: {"population": "autistic adults (full-scale IQ > 70)"} { "correction_id": "cor-b90989775dcf", "decision": "edit", … "replacement_value": { "population": "autistic adults (full-scale IQ > 70)" }, … }
- A rejected claim is marked
excluded_from_synthesis, so/ref:askand/ref:briefno longer retrieve it. It stays on file for audit. - Your decisions survive a re-extraction as long as the evidence hasn't changed. If a re-extraction supersedes the claim, the correction switches to
pending_reviewso you look at it again, rather than being applied or dropped without telling you./ref:verify show <pmid>re-checks the corrections first, then lists them.
- A rejected claim is marked
-
Group papers that report the same study
99100003 is the five-year follow-up of the cohort in 99100001. Counting them as two independent findings would make the positive result look stronger than it is. A study grouping needs a stated reason, and shared authors or a shared dataset aren't enough on their own:
output from a scratch library
› /ref:study create-study moreno-mri-cohort --pmid 99100001 99100003 --confidence confirmed --evidence "99100003 states it is the five-year follow-up of the cohort reported in 99100001" { "study_id": "moreno-mri-cohort", "pmids": ["99100001", "99100003"], "confidence": "confirmed", "evidence": "99100003 states it is the five-year follow-up of the cohort reported in 99100001", "review_state": "unreviewed", … } › /ref:study create-study bad-grouping --pmid 99100001 99100002 --confidence likely error: evidence is required: why these PMIDs are the same investigation (§3b)
-
Inspect disagreements with a comparison matrix
/ref:comparebuilds a table over any selection of papers. Papers in the same study share a row. Every cell either points to the claims behind it or says why it's empty.output from a scratch library · trimmed
› /ref:compare --project thesis-ch3 --screened included --batch ch3-matrix { "status": "created", "batch": "ch3-matrix", "manifest": { "selector_expression": "--project thesis-ch3 --screened included", "pmids": ["99100001", "99100002", "99100003"], "report": { "count": 3, "by_extraction_tier": { "abstract": 2, "full": 1, … } }, … }, "rows": [ { "pmids": ["99100001", "99100003"], "study": { "study_id": "moreno-mri-cohort", "confidence": "confirmed", … }, "cells": { "99100001": { "results": { "value": ["increase", "increase / 0.41 / Cohen's d"], "claim_ids": ["c-5a2acb6872de", "c-c30763f4756d"] }, "uncertainty": { "value": "not_reported" }, "sample_size": { "value": "not_extracted" }, … }, "99100003": { "design": { "value": ["longitudinal cohort"], "claim_ids": ["c-9c38d0dd8322"] }, "results": { "value": ["increase"], "claim_ids": ["c-9c38d0dd8322"] }, … } } }, { "pmids": ["99100002"], "study": null, "cells": { "99100002": { "design": { "value": ["consortium mega-analysis"], "claim_ids": ["c-1dd89ae7ddbe"] }, "results": { "value": ["no significant difference / 0.03 / Cohen's d"], "claim_ids": ["c-1dd89ae7ddbe"] }, "uncertainty": { "value": ["95% CI -0.07 to 0.13"], "claim_ids": ["c-1dd89ae7ddbe"] }, … } } } ] } › /ref:compare --project thesis-ch3 --screened included --batch ch3-matrix { "status": "reused_frozen_table", …
The disagreement is now easy to see. One cohort, reported twice, finds increased frontal thickness. A larger consortium sample finds no difference in mean thickness. Those outcomes aren't the same measurement, which is why the plan says a mismatched outcome doesn't count as a contradiction.
not_reportedmeans a claim exists but the paper doesn't give that field.not_extractedmeans no claim covers it: sample size, limitations, and relevance are never extracted in this phase. Treat these as two different gaps.- The table is saved in
projects/thesis-ch3/tables/ch3-matrix/.--edit-pmid/--edit-column/--edit-valueoverrides a single cell. If the evidence behind that cell changes later, a--refreshflags the override asstale.
Current behaviour.compare.pybuilds cells from every claim whosestatusisactive. That's why the rejected claimc-5a2acb6872destill appears under 99100001 above, and why the edit from step 6 doesn't change the population cell. Retrieval for/ref:askand/ref:briefdoes skip rejected claims. For now, check/ref:verify showalongside the table. -
Ask the question, then save the answer as a brief
/ref:askretrieves only from the selected papers. The rejected claim is gone: 99100001 now contributes only its accepted claim.output from a scratch library · retrieval step, trimmed
› /ref:ask "Is cortical thickness altered in autistic adults?" --project thesis-ch3 --screened included # ask_retrieve.py … --expand "cortical thinning" "autism spectrum disorder" candidates (pmid · claim_id · tier · retraction): 99100003 c-9c38d0dd8322 abstract unknown 99100001 c-c30763f4756d abstract none 99100002 c-1dd89ae7ddbe full none "report": { "total_matches_before_limits": 3, "returned": 3, "insufficient_coverage": false, "selector_constrained": true, … }
The
ref-synthesizersubagent writes an answer that cites only those candidates.validate_citations.pythen checks that every[^pmid]in the answer points to one of them:output from a scratch library · answer text written by hand
Evidence is mixed. One multi-site cohort reports increased frontal cortical thickness in autistic adults [^99100001], persisting at five-year follow-up [^99100003], but a larger consortium sample found no difference in mean thickness [^99100002]. { "cited_pmids": ["99100001", "99100002", "99100003"], "resolved": ["99100001", "99100002", "99100003"], "unresolved": [], "all_resolved": true }
To keep the answer for your chapter, run the same flow through
/ref:brief. It saves the answer, its evidence, and the questions still open:output from a scratch library
› /ref:brief "Is cortical thickness altered in autistic adults?" --key ch3-thickness --project thesis-ch3 { "status": "created", "snapshot_id": "brief-17ae6fa70deb", "manifest": { "snapshot_id": "brief-17ae6fa70deb", "key": "ch3-thickness", "project": "thesis-ch3", "question": "Is cortical thickness altered in autistic adults?", "selector_expression": "<all>", "pmids_at_resolution": null, "unresolved_questions": [ "Do regional and mean-thickness findings conflict, or measure different things?" ], "evidence_hash": "c890aeb2592d6b6b3753e9841eb8a5f53ef488fbe5a5b13a457fde0623315b7b", … }, … }
The files land in
projects/thesis-ch3/briefs/ch3-thickness/. After you add papers or make corrections,--refreshsaves a new snapshot and reportsadded_support_claim_ids,new_pmids,withdrawn_evidence, andretraction_status_changes. Your own--edittext is kept through a refresh, and marked"stale": trueif the evidence under it changed. (This brief was saved without a retrieval selector, so the manifest reads<all>. When a selector resolution is passed in, the manifest records the selector and the resolved PMIDs.) -
Check the draft paragraph's citations
Now write your own paragraph and give it to
/ref:check-citations. Claude splits it into assertions and gives each one a verdict:supported,overstated,conflicting,insufficient, orunavailable. It also checks that each existing[^pmid]actually supports the sentence it's attached to. Your paragraph is stored exactly as written and is never rewritten.Autistic adults have thicker frontal cortex than non-autistic adults [^99100001]. This difference is present across the whole cortex [^99100002]. It persists into later adulthood.
output from a scratch library · findings written by hand
› /ref:check-citations --file draft.md --project thesis-ch3 --export-bib { "check_id": "check-14fa5e5cc350", "manifest": { "referenced_pmids": ["99100001", "99100002", "99100003"], "verdict_counts": { "supported": 1, "overstated": 1, "conflicting": 1, "insufficient": 0, "unavailable": 0 }, "bibliography_exported": true, … }, "findings": [ { "assertion_text": "Autistic adults have thicker frontal cortex than non-autistic adults", "verdict": "supported", "evidence": [{ "pmid": "99100001", "claim_id": "c-c30763f4756d" }] }, { "assertion_text": "This difference is present across the whole cortex", "verdict": "conflicting", "evidence": [{ "pmid": "99100002", "claim_id": "c-1dd89ae7ddbe" }], "citation_mismatch": true, "existing_citation_pmid": "99100002" }, { "assertion_text": "It persists into later adulthood", "verdict": "overstated", "evidence": [{ "pmid": "99100003", "claim_id": "c-9c38d0dd8322" }] } ], "caveat": "Available library coverage does not establish a comprehensive literature check (§5a) -- an 'unavailable' or 'insufficient' verdict means this library doesn't have evidence either way, not that none exists." }
- The second sentence cites a paper that contradicts it, which is flagged as
citation_mismatch. The third sentence says more than a single five-year follow-up can support. check_citations.pyrefuses a finding whoseassertion_textisn't an exact substring of your paragraph. It also refuses evidence references that weren't among the retrieved candidates.- The report goes to
projects/thesis-ch3/checks/check-14fa5e5cc350/asinput.md,findings.json,manifest.json, and, because of--export-bib,references.bibandreferences.csl.jsonfor the PMIDs it cites.
- The second sentence cites a paper that contradicts it, which is flagged as
-
Export the chapter bibliography
Export the included papers under a named batch. The PMID set is saved at this point, so the chapter's reference list only changes when you pass
--refresh.output from a scratch library
› /ref:export --project thesis-ch3 --screened included --batch ch3-bib { "status": "created", "batch": "ch3-bib", "manifest": { "batch": "ch3-bib", "selector_expression": "--project thesis-ch3 --screened included", "pmids": ["99100001", "99100002", "99100003"], "citekeys": ["moreno2021increased", "haddad2022no", "moreno2023cortical"], "resolved_at": "2026-09-15T10:17:17.791951+00:00", "report": { "count": 3, "by_extraction_tier": { "abstract": 2, "full": 1, "unavailable": 0, "missing_record": 0 }, "by_human_verification_state": { "not_yet_tracked": 3 }, "by_retraction_errata_status": { "none": 2, "unknown": 1 } } } }
Cite the papers in your manuscript as
@moreno2021increasedand so on (/ref:citeprints the key for any PMID), and point your writing tool atexports/ch3-bib/references.bib. The report shows that one included paper has retraction statusunknown. Before submitting,/ref:auditcan refresh that status. The verification-state count still readsnot_yet_trackedin this version, even after the reviews in step 6.
Acceptance check
| The test asks you to… | Where it happened |
|---|---|
| take papers for one question | Project thesis-ch3, question q1, and screening recorded with reasons in screening.jsonl. |
| identify what they establish | Claims extracted with locators and quotes, then accepted, edited, or rejected in corrections.json. The follow-up paper was grouped as one study. |
| inspect disagreements | The ch3-matrix table puts regional increases next to a null result on mean thickness, each linked to its claim IDs. |
| a paragraph whose citations can be verified | The ch3-thickness brief and a citation check where every verdict points to a pmid and claim_id, plus the bibliography frozen in ch3-bib. |
To go further, /ref:weave turns the claims into concept and relation edges, and marks opposite directions in comparable settings as potential_conflict. The graph discovery tutorial picks up from there.