ref-manager

Tutorial

A PI publication report you can defend

This tutorial walks the PI acceptance path from PLAN.md §8. You'll build a researcher profile, confirm which author entries are really that person, link publications to a grant with evidence, list roles and coauthors, record dated citation observations, and export a reproducible report. Commands used: /ref:person, /ref:discover, /ref:verify, /ref:grant, /ref:weave, /ref:publications, /ref:audit --citations, and /ref:report.

The journey

1 PERSON create · ORCID 2 DISCOVER PubMed → candidates 3 CONFIRM verify identity 4 GRANT aliases · links 5 ROLES role · coauthors 6 CITATIONS audit --citations 7 REPORT csv · md people/lindqvist-maja.json variants · candidates · runs confirmed_… corrections.json grants/ <slug>.json read-only no file written papers/<pmid>/ citations.json reports/ <label>/ No model call is needed to generate the report: it only queries records you have reviewed.
deterministic script needs PubMed or PMC data your evidence-backed decision
Name similarity never establishes identity. Every publication in the report traces back to a confirmed (pmid, author_index) pair that you accepted, and each acceptance is logged in that paper's corrections.json.

The researcher is lindqvist-maja, one coauthor is rivera-ana, and the grant is nimh-wm-r01. The PMIDs 3800010138000103 and all titles are placeholders, already added with /ref:add. The ORCID is ORCID's public documentation example. Outputs labelled scratch library are real script output. In those runs, hand-written JSON stood in for the PubMed and PMC lookups.

Steps

  1. Create the researcher profile

    Person IDs are slugs you choose, and they're library-wide. The name you give becomes the first name variant.

    output from a scratch library

     /ref:person create lindqvist-maja --name "Lindqvist, Maja" --orcid 0000-0002-1825-0097
    {
      "slug": "lindqvist-maja",
      "name_variants": ["Lindqvist, Maja"],
      "orcid": "0000-0002-1825-0097",
      "affiliations": [],
      "confirmed_publications": [],
      "candidate_publications": [],
      "rejected_publications": []
    }
     /ref:person create lindqvist-maja --name "Lindqvist, M"
    error: person 'lindqvist-maja' already exists at people/lindqvist-maja.json
    Name variants: current limit. /ref:person implements only create, show and list. There's no subcommand for adding variants or affiliations after creation, so name_variants holds only the name you gave at create time. Duplicate slugs are refused rather than suffixed. If a researcher publishes under several forms, tell Claude which ones to use when it builds the discovery query in step 2. That query is saved exactly as run.
  2. Discover candidate publications

    Claude builds a PubMed author query from the profile, runs it, and saves the exact query, the retrieval time and the candidates onto the person record. Candidates don't count as publications yet.

    output from a scratch library (trimmed)

     /ref:discover --person lindqvist-maja
    {
      "candidate_publications": [
        { "pmid": "38000101", "title": "Transcranial stimulation and working memory in adults with ADHD: a randomized trial" },
        { "pmid": "38000102", "title": "Working memory training outcomes: a cohort study" },
        { "pmid": "38000103", "title": "Stimulation effects on attention in children" },
        { "pmid": "37999001", "title": "Lindqvist M. Soil microbiome diversity in boreal forests" }
      ],
      "confirmed_publications": [],
      …
      "discovery_runs": [
        { "query": "\"Lindqvist M\"[Author] OR 0000-0002-1825-0097[auid]",
          "retrieved_at": "2026-09-15T10:16:49.875997+00:00", "candidate_count": 4 }
      ]
    }
    --grant discovery isn't implemented. Grant records have no candidate fields yet, so the script refuses --grant and asks for --person.
  3. Confirm, or reject, each match

    A match links the person to one position in the author list. author_index is zero-based and refers to the order preserved at ingest. Accepting a match moves the candidate to confirmed_publications. Rejecting it keeps the PMID in rejected_publications, so later discovery runs don't suggest it again.

    output from a scratch library (trimmed)

     /ref:verify person-identity 38000101 lindqvist-maja 2 accept --rationale "affiliation and ORCID match" --reviewer maja
    {
      "correction_id": "cor-0ccfa93fd6c0",
      "target_type": "person_identity",
      "target_id": "lindqvist-maja:38000101:2",
      "decision": "accept",
      …
      "evidence_locator": "authorship.json#author[2]",
      "status": "active"
    }
     /ref:verify person-identity 38000102 lindqvist-maja 0 accept --reviewer maja
     /ref:verify person-identity 38000103 lindqvist-maja 1 accept --reviewer maja
     /ref:verify person-identity 37999001 lindqvist-maja 0 reject --rationale "different researcher (soil science)" --reviewer maja
    # the coauthor, so coauthor rows can use a confirmed identity
     /ref:verify person-identity 38000101 rivera-ana 0 accept --reviewer maja

    Contribution flags are separate, and position alone never sets them:

    output from a scratch library

     /ref:verify author-contribution 38000101 2 corresponding
    error: author_contribution flag 'corresponding' refused: an explicit evidence statement is required, never inferred from author position (§3c)
     /ref:verify author-contribution 38000101 2 corresponding --evidence "Correspondence: Maja Lindqvist" --reviewer maja
    {
      "author_index": 2,
      "flag": "corresponding",
      "evidence_statement": "Correspondence: Maja Lindqvist",
      "reviewer": "maja",
      "timestamp": "2026-09-15T10:16:51.083767+00:00"
    }
  4. Record the grant, its aliases, and evidenced links

    PubMed often reports one award in several spellings. Keep each spelling as an approved alias. Linking a publication is a separate, reviewed decision that records where the evidence came from.

    output from a scratch library (trimmed)

     /ref:grant create nimh-wm-r01 --funder "NIMH" --award "R01 MH123456" --title "Neuromodulation of working memory" --pi lindqvist-maja
    {
      "slug": "nimh-wm-r01",
      "funder": "NIMH",
      "award_number": "R01 MH123456",
      "approved_aliases": ["R01 MH123456"],
      "title": "Neuromodulation of working memory",
      "pi": "lindqvist-maja",
      "aims": null,
      "publication_links": []
    }
     /ref:grant add-alias nimh-wm-r01 --alias "R01MH123456"
    { … "approved_aliases": ["R01 MH123456", "R01MH123456"], … }
     /ref:verify grant-link 38000101 nimh-wm-r01 accept --evidence-locator "PubMed GrantList: R01 MH123456/MH/NIMH NIH HHS" --award-number "R01 MH123456" --reviewer maja
    {
      "pmid": "38000101",
      "award_number": "R01 MH123456",
      "match_method": "manual_review",
      "evidence_locator": "PubMed GrantList: R01 MH123456/MH/NIMH NIH HHS",
      "decision": "accept",
      "reviewer": "maja",
      "rationale": "",
      "timestamp": "2026-09-15T10:16:51.730855+00:00"
    }
     /ref:verify grant-link 38000102 nimh-wm-r01 accept --evidence-locator "PubMed GrantList: R01MH123456/MH/NIMH NIH HHS" --award-number "R01MH123456" --reviewer maja
    Why links never add aliases. Folding a new award string into a grant automatically could merge two genuinely different awards from the same funder. grant-link records the link and nothing else. Adding an alias is always a separate, deliberate add-alias.

    To see these decisions as graph edges, regenerate the derived views:

    output from a scratch library (people graph part only)

     /ref:weave --regenerate-only
    # … okf_emit output omitted …
    {
      "researcher_authored_publication": 4,
      "publication_acknowledges_grant": 2,
      "publication_supports_aim": 0,
      "researcher_lab_membership": 0,
      "publication_supports_aim_note": "unpopulated -- no mechanism in any phase links a publication to a specific grant aim (§5a)",
      "researcher_lab_membership_note": "unpopulated -- no /ref:lab command exists in PLAN.md; labs/<slug>.json has no writer yet"
    }

    publication_acknowledges_grant edges come from exact matches between a paper's funding.json award strings and the grant's award number or approved aliases. Here, the second paper matches only because of the alias.

  5. List authorship roles

    Roles are computed from the confirmed index and the preserved author list. Filter with --role sole|first|last|middle|unresolved.

    output from a scratch library

     /ref:publications --person lindqvist-maja
    [
      { "pmid": "38000101", "author_index": 2, "role": "last",   "author_count": 3 },
      { "pmid": "38000102", "author_index": 0, "role": "sole",   "author_count": 1 },
      { "pmid": "38000103", "author_index": 1, "role": "middle", "author_count": 4 }
    ]
     /ref:publications --person lindqvist-maja --role last
    [
      { "pmid": "38000101", "author_index": 2, "role": "last", "author_count": 3 }
    ]
    CONFIRMED AUTHOR POSITIONS 38000101 Rivera Okafor Lindqvist → last [0][1][2] · corresponding flag 38000102 Lindqvist → sole [0] · counted once, not also first + last 38000103 Chen Lindqvist Rivera Consortium → middle classify_role(index, total, complete) list incomplete, or index missing? total authors = 1? index = 0? index = total − 1? unresolved sole first last middle yesyesyesyes no INDEPENDENT FLAGS: NOT A ROLE, NOT FROM POSITION shared_first shared_senior corresponding set only by /ref:verify author-contribution with an explicit statement; refused without one. Stored in authorship.json → contribution_flags, beside the literal position. Last position never implies senior or corresponding.
    mutually exclusive reporting role no confident role evidence-only flag
    The four roles partition a verified, complete author list, and unresolved catches everything else. Flags answer a different question and can coexist with any role.
  6. Derive a coauthor declaration

    Use this for NSF Collaborators and Other Affiliations or NIH conflict forms. You get one row per distinct coauthor across the window, each with its identity method and the PMIDs that establish it.

    output from a scratch library

     /ref:publications --person lindqvist-maja --coauthors --since 2023 --to 2024
    {
      "person": "lindqvist-maja",
      "since": "2023",
      "until": "2024",
      "coauthors": [
        { "name": "ADHD Imaging Consortium", "identity_method": "exact_name", "affiliation": null, "most_recent_pub": "2024", "pmids": ["38000103"] },
        { "name": "Chen W",    "identity_method": "exact_name",         "affiliation": null, "most_recent_pub": "2024", "pmids": ["38000103"] },
        { "name": "Okafor C",  "identity_method": "exact_name",         "affiliation": null, "most_recent_pub": "2023", "pmids": ["38000101"] },
        { "name": "Rivera A",  "identity_method": "confirmed_identity", "affiliation": null, "most_recent_pub": "2023", "pmids": ["38000101"] },
        { "name": "Rivera A",  "identity_method": "exact_name",         "affiliation": null, "most_recent_pub": "2024", "pmids": ["38000103"] }
      ],
      "unconfirmed_candidates": [],
      "gaps": [],
      "scope_note": "derived from library holdings, not a complete publication record (§3c)"
    }
    Why "Rivera A" appears twice. On 38000101 that author position is a confirmed identity, rivera-ana. On 38000103 it only matches by published name. The two cases are kept apart rather than merged. Confirm the second position, and the rows collapse into one confirmed identity.
    Gaps and group authors depend on flags in authorship.json. A paper lands in gaps when its authorship.json has "complete": false. A group is reported as a group when its author entry has "is_group": true. /ref:add writes neither flag, and no current command sets them. In this scratch library the consortium is therefore listed by exact name, and gaps is empty. Check truncated or consortium author lists yourself before you submit a declaration.
  7. Record dated citation observations

    /ref:audit --citations appends one dated observation per paper to citations.json. It never overwrites an earlier one. A paper without a PMCID gets a no_pmcid marker. A failed lookup gets check_failed, never a zero.

    No cited-by source is wired in yet. None of the PubMed MCP tools in this environment return a PMC cited-by count. find_related_articles measures similarity, not citations. The persistence machinery is complete and tested. Until a real PMC ELink source is available, Claude should tell you there's nothing to record rather than invent counts. The output below comes from observations we supplied by hand.

    output from a scratch library (hand-supplied observations)

     /ref:audit 38000101 38000102 38000103 --citations
    [
      { "pmid": "38000101",
        "entry": { "source": "pmc_elink", "query": "elink.fcgi?dbfrom=pmc&linkname=pmc_pmc_citedby&id=PMC0000101", "count": 14,
                   "coverage": "citing articles indexed in PMC; not a total citation count",
                   "retrieved_at": "2026-09-15T10:16:52.063180+00:00" },
        "total_observations": 1 },
      { "pmid": "38000102",
        "entry": { "retrieved_at": "2026-09-15T10:16:52.065402+00:00", "status": "no_pmcid", "reason": "paper has no PMCID" },
        "total_observations": 1 },
      { "pmid": "38000103",
        "entry": { "retrieved_at": "2026-09-15T10:16:52.066812+00:00", "status": "check_failed", "reason": "ELink request timed out" },
        "total_observations": 1 }
    ]
     /ref:audit show-citations 38000101 38000102 38000103
    [
      { "pmid": "38000101", "latest_observation": { "count": 14, … "source": "pmc_elink" }, "stale": false },
      { "pmid": "38000102", "latest_observation": null, "stale": null },
      { "pmid": "38000103", "latest_observation": null, "stale": null }
    ]

    Each result record must carry exactly one of observation, no_pmcid or error. Anything else is rejected as malformed:

    output from a scratch library

    error: citations result for pmid '38000101' has none of 'observation'/'no_pmcid'/'error' -- malformed input, not a real failed lookup
  8. Generate the report

    The report covers confirmed publications only, within a year window. It writes CSV and Markdown plus a manifest of the policies used. Rerun it with unchanged data, and the CSV and Markdown come out identical. Only generated_at in the manifest changes.

    output from a scratch library

     /ref:report --person lindqvist-maja --from 2023 --to 2024 --citations
    {
      "person": "lindqvist-maja",
      "from": "2023",
      "to": "2024",
      "date_basis": "publication_year",
      "missing_date_handling": "excluded",
      "identity_review_policy": "confirmed_publications_only",
      "dedup_policy": "unique_by_pmid",
      "publication_count": 3,
      "generated_at": "2026-09-15T10:16:52.380729+00:00",
      "includes_citations": true,
      "citation_stale_days": 180,
      "citation_coverage_note": "citing articles indexed in PMC; not a total citation count (D25)"
    }

    reports/lindqvist-maja-2023-2024/report.md:

    # Publication report — lindqvist-maja
    
    Scope: confirmed publications in this library, 2023–2024 (`publication_year`). PubMed-only coverage; portfolio completeness not reviewed (§3c).
    
    Citation counts below are **PMC-indexed citing-article counts (D25)** — not total citations, and not comparable to Scopus/Web of Science. Each count names its source and retrieval date; `unknown` means no observation has been recorded, never zero.
    
    | PMID | Year | Role | Title | Citing articles (PMC) | Source | Retrieved | Stale |
    |---|---|---|---|---|---|---|---|
    | 38000101 | 2023 | last | Transcranial stimulation and working memory in adults with ADHD: a randomized trial | 14 | pmc_elink | 2026-09-15 | no |
    | 38000102 | 2024 | sole | Working memory training outcomes: a cohort study | unknown | — | — | — |
    | 38000103 | 2024 | middle | Stimulation effects on attention in children | unknown | — | — | — |
    
    Total unique publications: 3
    REVIEWED RECORDS (INPUT) people/lindqvist-maja.json confirmed_publications only papers/<pmid>/meta.json year · journal · title · citekey · DOI papers/<pmid>/authorship.json ordered list → author_role papers/<pmid>/citations.json latest real count · --citations only /ref:report year window · missing year excluded confirmed identities only unique by PMID · no model call reports/lindqvist-maja-2023-2024/ publications.csv pmid · citekey · doi · year · role + citation_count/source/retrieved/stale report.md scope statement · coverage caveat no observation → "unknown", never 0 manifest.json date basis · policies · count generated_at (only field that changes)
    The report can only state what reviewed records establish. Its scope line says "confirmed publications in this library", and every citation figure carries its source and retrieval date.
    Not yet the full acceptance test. PLAN.md §8 asks for an annual report grouped by grant and authorship role. The implemented /ref:report is the basic slice: one person and one year window, with the role as a column. It has no --role filter and no grant or lab grouping. For per-grant views, use the publication_links on grants/nimh-wm-r01.json and the publication_acknowledges_grant edges from step 4.

What you now have

<LIBRARY>/
  people/
    lindqvist-maja.json          # variants, ORCID, confirmed / rejected, discovery_runs
    rivera-ana.json
  grants/
    nimh-wm-r01.json             # award, approved_aliases, publication_links with evidence locators
  papers/38000101/ … 38000103/
    authorship.json              # preserved order + contribution_flags
    corrections.json             # person_identity, grant_link, author_contribution decisions
    citations.json               # append-only dated observations and markers
  papers/37999001/corrections.json   # the rejected match, kept for audit
  graph/
    people_relations.jsonl       # researcher_authored_publication, publication_acknowledges_grant
  reports/lindqvist-maja-2023-2024/
    publications.csv · report.md · manifest.json

Command reference: /ref:person, /ref:publications, /ref:report, /ref:verify, /ref:audit.