ref-manager

Commands · people · phases 1–2, 11

Researchers, grants, and reports

These commands answer PI questions: which papers are hers, in what role, under which grant, and who she worked with. The rule behind all of them is that a name match never proves identity. A publication counts only once someone confirms a specific author position, and anything unresolved stays visible instead of being quietly dropped.

Records and evidence

people/<slug>.json name_variants · orcid affiliations confirmed_publications {pmid, author_index} candidate_publications rejected_publications discovery_runs {query, retrieved_at, candidate_count} papers/<pmid>/authorship.json authors[] in raw PubMed order · complete? ROLE (EXACTLY ONE) sole first last middle unresolved incomplete list CONTRIBUTION_FLAGS (INDEPENDENT) shared_first shared_senior corresp. /ref:verify overlays review-author-contribution: a flag needs an evidence statement; never inferred from last position review-person-identity: confirm / reject a position review-grant-link: records a manual link on the grant; never folds aliases automatically papers/<pmid>/funding.json · observations[].kind indexed_funding_association PubMed GrantList at /ref:add explicit_acknowledgement_verified JATS funding-group / funding-statement, at /ref:fetch possible_match acknowledgement prose only · state not_checked / unknown if none grants/<slug>.json funder · award_number · title · pi · aims approved_aliases ["R01 HL123456", "HL123456"] publication_links exact match graph_people.py → publication_acknowledges_grant · researcher_authored_publication
user-minted record needs explicit evidence weaker or reviewed-later evidence
Role and contribution are separate on purpose. Being listed last doesn't make someone senior or corresponding, and a footnote saying "these authors contributed equally" doesn't change anyone's position. So publications.py derives role from position alone, and shared or corresponding authorship is a separate reviewed flag.

From discovery to a report

/ref:person create slug, name, optional ORCID /ref:discover PubMed author query → candidates + discovery_run you decide confirm-publication --author-index N reject-publication /ref:publications roles · --coauthors /ref:report one person, year window reports/<label>/ manifest.json publications.csv report.md rejected PMIDs are kept so they are never re-suggested Only confirmed_publications count. Candidates and rejected matches never reach roles, coauthor lists, or reports (identity_review_policy: confirmed_publications_only).
Discovery never changes what counts on its own. It only records the exact query and what came back, so you can see later why a paper was suggested.

Reference

/ref:person

phase 1

Create and inspect researcher identity records: name variants, ORCID, and confirmed, candidate and rejected publication matches.

/ref:person create <slug> --name "<Last, First>" [--orcid <id>] /ref:person show <slug> /ref:person list

Arguments

create
Mints people/<slug>.json with one name variant. The slug is yours and library-global. A duplicate is refused and the conflicting record is named. It is never suffixed or overwritten.
show
The full record, including discovery_runs once /ref:discover has run.
list
Slug and name variants for every person.

The script also implements confirm-publication --slug --pmid --author-index and reject-publication --slug --pmid, which /ref:discover points you to. The /ref:person command file lists only create/show/list; /ref:verify review-person-identity reaches the same functions and also logs a correction record.

What happens

  1. Resolves the library root.
  2. Runs person.py with the matching action and prints its JSON verbatim.

Reads & writes

Writes people/<slug>.json (create). Fields: slug, name_variants, orcid, affiliations, confirmed_publications, candidate_publications, rejected_publications.

Example

output from a scratch library

 /ref:person create moreau-c --name "Moreau, Claire" --orcid 0000-0002-1825-0097
{
  "slug": "moreau-c",
  "name_variants": ["Moreau, Claire"],
  "orcid": "0000-0002-1825-0097",
  "affiliations": [],
  "confirmed_publications": [],
  "candidate_publications": [],
  "rejected_publications": []
}
 /ref:person create moreau-c --name "Moreau, C"
error: person 'moreau-c' already exists at people/moreau-c.json
 /ref:person list
[
  {"slug": "moreau-c", "name_variants": ["Moreau, Claire"]},
  {"slug": "tanaka-h", "name_variants": ["Tanaka H"]}
]

Under the hood

person.py {create|show|list|confirm-publication|reject-publication} --repo <root> --slug … [--name …] [--orcid …] [--pmid …] [--author-index N]

See also

/ref:discover · /ref:publications · /ref:verify · PI report tutorial

/ref:publications

phase 2read-only

List a person's confirmed publications with their authorship role, or derive a coauthor declaration for a date window.

/ref:publications --person <id> [--role sole|first|last|middle|unresolved] /ref:publications --person <id> --coauthors --since <date> [--to <date>]

Arguments

--person
Required person slug.
--role
Filter the role list. Roles are mutually exclusive: one author is sole, index 0 is first, the final index is last, anything else is middle, and an incomplete author list gives unresolved.
--coauthors --since [--to]
A collaborator declaration (e.g. for NSF/NIH), compared at year granularity.

What happens

  1. Reads confirmed_publications only; candidates never count.
  2. Roles: classifies each confirmed author_index against authorship.json.
  3. Coauthors: an author at a position another person has confirmed is grouped by that identity (confirmed_identity). Group authors stay one named group. A name matching another person's variant with no confirmed link goes to unconfirmed_candidates, and everyone else is keyed by exact_name.
  4. Every paper in the window with an incomplete author list appears in gaps. None of these lists may be dropped when summarizing.

Reads & writes

Reads people/*.json, papers/<pmid>/meta.json, authorship.json. Writes nothing. contribution_flags (shared_first / shared_senior / corresponding) are not part of this output.

Example

output from a scratch library

 /ref:publications --person moreau-c
[
  {"pmid": "38000001", "author_index": 2, "role": "last",  "author_count": 3},
  {"pmid": "38000002", "author_index": 0, "role": "first", "author_count": 2},
  {"pmid": "38000003", "author_index": 0, "role": "sole",  "author_count": 1}
]
 /ref:publications --person moreau-c --coauthors --since 2023 --to 2024
{
  "person": "moreau-c",
  "since": "2023",
  "until": "2024",
  "coauthors": [
    {"name": "Lindqvist E", "identity_method": "exact_name", "affiliation": null,
     "most_recent_pub": "2023", "pmids": ["38000001"]},
    {"name": "Okafor A", "identity_method": "exact_name", "affiliation": null,
     "most_recent_pub": "2023", "pmids": ["38000001"]}
  ],
  "unconfirmed_candidates": [
    {"name": "Tanaka H", "candidate_person": "tanaka-h", "pmids": ["38000002"]}
  ],
  "gaps": [],
  "scope_note": "derived from library holdings, not a complete publication record (§3c)"
}

Under the hood

publications.py --repo <root> --person <id> [--role …] [--coauthors --since … [--to …]]

See also

/ref:report · /ref:person · /ref:verify

/ref:grant

phase 1

Create and inspect grant records: funder, award number, approved award aliases, title, PI and aims.

/ref:grant create <slug> --funder "<name>" --award "<award number>" [--title "…"] [--pi <person-slug>] [--aims "…"] /ref:grant add-alias <slug> --alias "<award string>" /ref:grant show <slug> /ref:grant list

Arguments

create
Mints grants/<slug>.json. The award number seeds approved_aliases. A duplicate slug is refused and the conflicting record is named.
add-alias
Keeps another string the funder uses for the same award, e.g. HL123456 next to R01 HL123456. Existing aliases are never overwritten, and re-adding one is a no-op.
show / list
Full record, or slug/funder/award for all grants.

Why explicit aliases? Funders report one award under several normalized or legacy strings, but two similar strings can also be two different awards. Matching is exact against approved aliases, so merging them is always your decision.

What happens

  1. Resolves the library root.
  2. Runs grant.py and prints its JSON verbatim.
  3. On the next /ref:weave, graph_people.py links each funding.json observation whose award string exactly matches an alias as publication_acknowledges_grant, carrying the observation's evidence_kind.

Reads & writes

Writes grants/<slug>.json: slug, funder, award_number, approved_aliases, title, pi, aims, publication_links (filled by /ref:verify review-grant-link).

Example

output from a scratch library

 /ref:grant create hl123456 --funder "NHLBI" --award "R01 HL123456" --title "Blood pressure and brain health" --pi moreau-c
 /ref:grant add-alias hl123456 --alias "HL123456"
{
  "aims": null,
  "approved_aliases": ["R01 HL123456", "HL123456"],
  "award_number": "R01 HL123456",
  "funder": "NHLBI",
  "pi": "moreau-c",
  "publication_links": [],
  "slug": "hl123456",
  "title": "Blood pressure and brain health"
}
 /ref:weave --regenerate-only
# graph_people.py half; both GrantList strings now match one grant
{
  "researcher_authored_publication": 3,
  "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"
}

Under the hood

grant.py {create|add-alias|show|list} --repo <root> --slug … [--funder …] [--award …] [--alias …] [--title …] [--pi …] [--aims …]

See also

/ref:verify · /ref:weave · /ref:fetch (JATS funding extraction)

/ref:discover

phase 2PubMed

Run a manually started PubMed author search for one researcher and store the results as candidates awaiting your confirmation.

/ref:discover --person <id>

Arguments

--person
Required person slug.
--grant
Not implemented: grant records have no candidate-publication fields yet. The script refuses with an error.

What happens

  1. Reads the person record and builds an author query from name variants (and ORCID, if present).
  2. Calls PubMed search with that exact query, then writes the candidates (at least pmid, title) to a temp file.
  3. discover.py appends a discovery_runs entry and adds new candidates. PMIDs already confirmed or rejected are skipped.
  4. You confirm each real match at its author position, or reject it. The agent never treats a candidate as confirmed.

Reads & writes

Updates people/<slug>.json: candidate_publications, discovery_runs. Does not add papers to the library; ingest confirmed papers with /ref:add.

Example

output from a scratch library (candidate list supplied by hand)

 /ref:discover --person moreau-c
# PubMed query: Moreau C[Author] → 4 hits
{
  "candidate_publications": [
    {"pmid": "38000001", "title": "Drug X lowers systolic blood pressure in adults with hypertension"},
    {"pmid": "38000002", "title": "Drug X raises systolic blood pressure at 12 weeks"},
    {"pmid": "38000003", "title": "Systolic blood pressure and cognitive decline"},
    {"pmid": "37999999", "title": "An unrelated Moreau paper"}
  ],
  "confirmed_publications": [],
  …
  "discovery_runs": [
    {"query": "Moreau C[Author]", "retrieved_at": "2026-09-15T10:15:26.979997+00:00", "candidate_count": 4}
  ]
}
# confirm Moreau's real positions, reject the stranger (person.py actions)
person.py confirm-publication --slug moreau-c --pmid 38000001 --author-index 2
person.py reject-publication  --slug moreau-c --pmid 37999999
 /ref:discover --grant hl123456
error: --grant discovery is not implemented in phase 2 (grants have no candidate-publication fields yet); use --person

Under the hood

person.py show, then discover.py --repo <root> --person <id> --query-text "<exact query>" --candidates-file <json>. The PubMed call is made by the command, never by the script.

See also

/ref:person · /ref:verify · /ref:search-pubmed

/ref:report

phase 2--citations: phase 11

Generate a reproducible publication report for one researcher and year window, as a manifest, a CSV and Markdown.

/ref:report --person <id> --from <date> --to <date> [--label <slug>] [--citations [--stale-days N]]

Arguments

--person
Required person slug.
--from / --to
Required. Compared at year granularity (publication_year).
--label
Output folder name. Default <person>-<from>-<to>.
--citations
Add PMC-indexed citing-article counts from /ref:audit --citations observations.
--stale-days N
Flag a count older than N days as stale (default 180).

What happens

  1. Takes confirmed publications only. Papers with no meta.json or no year are excluded, never guessed.
  2. Classifies role as /ref:publications does and sorts by year, then PMID.
  3. Writes the CSV and Markdown, then a manifest recording the policies used. Rerunning with unchanged data reproduces identical CSV/Markdown; only generated_at differs.
  4. Each exported row now includes provenance fields (extraction_tier, abstract_available, full_text, and checked_at) so freshness and source state travel with the report.
  5. With --citations, each count carries its source and retrieval date. No observation reads unknown, never 0, and the number is never called "citations".

Not implemented yet: grouping by grant or lab. This is the basic single-person report.

Reads & writes

Reads people/<slug>.json, meta.json, authorship.json, citation observations. Writes reports/<label>/manifest.json, publications.csv (pmid, citekey, doi, year, journal, title, author_role, plus extraction_tier, abstract_available, full_text, checked_at, citation_count, citation_source, citation_retrieved_at, citation_coverage, citation_stale with --citations), report.md.

Example

output from a scratch library

 /ref:report --person moreau-c --from 2023 --to 2024
{
  "person": "moreau-c",
  "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:15:27.854144+00:00",
  "includes_citations": false
}
# reports/moreau-c-2023-2024/publications.csv
pmid,citekey,doi,year,journal,title,author_role
38000001,okafor2023drug,10.1000/a1,2023,Hypertension,Drug X lowers systolic blood pressure in adults with hypertension,last
38000002,moreau2024drug,10.1000/a2,2024,J Hypertens,Drug X raises systolic blood pressure at 12 weeks,first
38000003,moreau2024systolic,10.1000/a3,2024,Neurology,Systolic blood pressure and cognitive decline,sole
 /ref:report --person moreau-c --from 2023 --to 2024 --label moreau-2023-2024-cit --citations
# report.md, no /ref:audit --citations run yet
| PMID | Year | Role | Title | Citing articles (PMC) | Source | Retrieved | Stale |
|---|---|---|---|---|---|---|---|
| 38000001 | 2023 | last | Drug X lowers systolic blood pressure in adults with hypertension | unknown | — | — | — |
| 38000002 | 2024 | first | Drug X raises systolic blood pressure at 12 weeks | unknown | — | — | — |
| 38000003 | 2024 | sole | Systolic blood pressure and cognitive decline | unknown | — | — | — |

Total unique publications: 3

Under the hood

report.py --repo <root> --person <id> --from … --to … [--label …] [--citations] [--stale-days N]; role logic from publications.classify_role, observations from audit.latest_real_observation.

See also

/ref:publications · /ref:audit · PI report tutorial