ref-manager

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.

Example data. Four fictional papers (9910000199100004) 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

GATHER · READ · CORRECT 1 project projects/ thesis-ch3/ project.yaml 2 add papers/<pmid>/ meta.json citekey · abstract tier 3 screen·queue papers.yaml screening.jsonl your decision 4 attach·fetch raw/<sha256>/ versions/<id>/ source.md 5 extract claims.json claim_registry ref-extractor × N 6 verify corrections.json accept · edit · reject GROUP · COMPARE · WRITE · CHECK 7 study studies/ studies.jsonl evidence required 8 compare tables/ch3-matrix/ table.json frozen membership 9 ask · brief briefs/ ch3-thickness/ ref-synthesizer × 1 10 check-cit. checks/ check-<id>/ input.md kept verbatim 11 export exports/ch3-bib/ references.bib + csl.json · manifest
your decision, recorded with a reason saved artifact under projects/thesis-ch3/ paper or library record
Every artifact in the second row records the set of papers it was built from. Re-running with the same label reuses that saved set, and only --refresh changes it. That way a conclusion never quietly shifts because the papers behind it changed.
  1. 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 q1 is fine here even if another project also has a q1.

    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"
  2. 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-pubmed saves the query and runs it. The systematic review tutorial covers that route.

  3. 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 --screened selector 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"
    }
  4. Bring in full text: attach your PDFs, fetch the rest

    Most thesis PDFs come from your university's access, so /ref:attach takes 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 no source.md was 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
  5. Extract what each paper claims

    /ref:extract runs one ref-extractor subagent 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.

  6. 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.json as 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:ask and /ref:brief no 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_review so 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.
  7. 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)
  8. Inspect disagreements with a comparison matrix

    /ref:compare builds 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_reported means a claim exists but the paper doesn't give that field. not_extracted means 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-value overrides a single cell. If the evidence behind that cell changes later, a --refresh flags the override as stale.
    Current behaviour. compare.py builds cells from every claim whose status is active. That's why the rejected claim c-5a2acb6872de still appears under 99100001 above, and why the edit from step 6 doesn't change the population cell. Retrieval for /ref:ask and /ref:brief does skip rejected claims. For now, check /ref:verify show alongside the table.
  9. Ask the question, then save the answer as a brief

    /ref:ask retrieves 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-synthesizer subagent writes an answer that cites only those candidates. validate_citations.py then 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, --refresh saves a new snapshot and reports added_support_claim_ids, new_pmids, withdrawn_evidence, and retraction_status_changes. Your own --edit text is kept through a refresh, and marked "stale": true if 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.)

  10. 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, or unavailable. 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.py refuses a finding whose assertion_text isn'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/ as input.md, findings.json, manifest.json, and, because of --export-bib, references.bib and references.csl.json for the PMIDs it cites.
  11. 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 @moreno2021increased and so on (/ref:cite prints the key for any PMID), and point your writing tool at exports/ch3-bib/references.bib. The report shows that one included paper has retraction status unknown. Before submitting, /ref:audit can refresh that status. The verification-state count still reads not_yet_tracked in this version, even after the reviews in step 6.

Acceptance check

The test asks you to…Where it happened
take papers for one questionProject thesis-ch3, question q1, and screening recorded with reasons in screening.jsonl.
identify what they establishClaims extracted with locators and quotes, then accepted, edited, or rejected in corrections.json. The follow-up paper was grouped as one study.
inspect disagreementsThe 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 verifiedThe 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.