---
name: scholion-reviewer-suggest
description: >
  Recommend peer reviewers for a manuscript, abstract, one-line research idea, or PDF using
  the Scholion.ai catalogue (60+ business journals, 220k+ papers). Builds its own evidence
  from five retrieval lanes — keyword search, semantic search, the manuscript's reference
  list, authors of papers CITING the closest seed works, and bibliographically coupled
  papers — then filters conflicts of interest (co-authors, target-journal editors,
  institutions) and ranks candidates by topical fit, current-frontier activity, and inferred
  career stage, preferring active junior/mid-career scholars over overloaded senior names.
  Use when asked who should review a paper, for a suggested-reviewers list for a submission
  or cover letter, or to check reviewer conflicts of interest ("recommend reviewers /
  审稿人推荐 / find reviewers for this manuscript").
license: MIT
compatibility: >
  Needs Python 3.8+ and either (a) the Scholion MCP server connected to the agent, or
  (b) outbound HTTPS to scholion.ai with a SCHOLION_API_KEY environment variable.
  PDF input extraction additionally wants pypdf (auto-installed on first use if pip is available).
metadata:
  author: Scholion.ai
  version: "2.0"
  api-docs: https://scholion.ai/api-docs
  corpus: "60+ business journals (FT50 + UTD24 + statistics), ~220,000 papers"
---

# Scholion Reviewer Suggestion

Evidence-based reviewer shortlists. You (the agent) do the retrieval and the judgement; the
bundled scripts do the arithmetic so the reasoning stays auditable and reproducible.

Throughout this file, `<skill-dir>` means the directory containing this SKILL.md.

## Interpretation and privacy limits

The bundled career-stage bands are publication-history heuristics, not verified
career stage, age, workload or willingness to review. Author order, citing activity
and a recent corpus footprint do not establish junior status. Treat stage-based
preferences below as scoring assumptions, not facts about a person.
Conflict checks cover available catalog evidence and supplied constraints only;
missing or ambiguous identities, affiliations and co-author links require manual
verification against the journal's policies before invitations.
Read README.md's security and privacy guidance before running scripts: manuscript
queries leave the host agent, outputs are written locally, PDF extraction may
invoke pip, and configured endpoint overrides receive credentials.

## Non-negotiable rules

1. **Never call the platform's `recommend_reviewers` endpoint** (REST `POST /recommend/reviewers`
   or its MCP tool). This skill deliberately replaces that black box with an inspectable
   evidence chain. Every other Scholion endpoint is fair game.
2. **Never invent evidence.** Every recommended name must be traceable to ≥1 real paper
   returned by Scholion. No names from memory, no web search alone.
3. **Never skip the conflict-of-interest pass.** Recommending the author's co-author or a
   current editor of the target journal is the worst possible failure here.
4. **Every restriction has a reason.** Every exclusion, penalty and bonus must appear in the
   final report as human-readable text.
5. **Prefer the career-stage sweet spot.** Reviewers who actually accept and write timely
   reviews are typically 2–10 years post-PhD. Very senior names are usually editors already,
   or decline. The citation lanes (D/E) naturally surface exactly this cohort.
6. **Respect the credit budget.** The free pool is 100 credits/day. Tell the user the
   approximate cost before a long run if they did not set a budget.

## Transport: MCP or REST, whichever the host agent has

Two equivalent channels — same JSON, same credit costs:

| Channel | How | When |
|---------|-----|------|
| **MCP (preferred)** | Host agent has the Scholion MCP server connected (Streamable HTTP, `https://scholion.ai/mcp`, header `Authorization: Bearer <key>`). Tools are usually named like `search_keyword`, `get_author_profile`, ... possibly prefixed per client. | Agent has an MCP client. Cheapest in tokens; call tools directly. |
| **REST (fallback)** | `export SCHOLION_API_KEY=...` then run the bundled stdlib-only client: `python3 <skill-dir>/scripts/scholion_client.py <command> ...` | Any agent without MCP — this is what makes the skill portable. |

The client mirrors every endpoint used here (`search-keyword`, `search-semantic`, `paper-by-doi`,
`paper-citations`, `paper-related`, `paper-references`, `author-profile`, `author-search`,
`coauthors`, `journal-editors`, `journal-lookup`, `review-cycle`, `stats`, plus `budget` /
`reset-budget` for spend tracking). `--dry-run` prints the URL and spends nothing — use it when
unsure about a parameter name, because a 400 still charges credits. Get a key at
`https://scholion.ai/account/api`.

When this document says "call X", use your client's MCP tool for X if available, otherwise the
matching `scholion_client.py` subcommand.

## Quick start

```bash
S=<skill-dir>/scripts

# Stage 0 — normalize the input (PDF / docx / txt / md / raw text / stdin)
python3 $S/extract_input.py manuscript.pdf --self-authors "A. Author" --out /tmp/ms.json

# Author-name parsing sanity check (17 assertions, synthetic fixtures)
python3 $S/author_norm.py --selftest

# Stage 7 — score & rank
python3 $S/score_reviewers.py --input /tmp/candidates.json --out /tmp/scored.json --career-stage junior

# Stage 8 — deliverables
python3 $S/render_report.py --input /tmp/scored.json --md reviewers.md --html reviewers.html
```

`python3 $S/score_reviewers.py --print-template` shows the exact input contract
(`references/json_contract.md`).

---

## Workflow

### Stage 0 — Input normalization

Run `extract_input.py` once, whatever the input shape. It produces `manuscript.json`
(title, abstract, declared keywords, **reference list**, warnings).

| Input | Handling |
|-------|----------|
| One sentence / idea | Weakest input — say so, and expand it yourself into constructs/theory/method before searching |
| Abstract | Use directly; no reference list available |
| Full text (paste, `.txt`, `.md`, `.tex`) | Extract structure + references |
| PDF | `pdfplumber` → `pypdf`/`PyPDF2`/`pypdfium2` → `pdftotext` → auto `pip install pypdf`. If every backend fails (scanned PDF), ask for abstract + reference list as text |
| DOCX | `python-docx` → fallback raw `word/document.xml` parse (no dependency) |

**The reference list is the single most valuable part.** If `reference_section_detected=false`,
say so and offer to paste it. Supported styles: `[1] Author...` / `(2015) Author...` /
`Surname AB, Surname CD (2024) Title. Journal 35(2):1-20.` If a new style appears, add a
splitter in `extract_input.py`, not an ad hoc one-off.

### Stage 1 — Constraints intake

Ask at most ONE bundled round of questions, then proceed with documented defaults:

| Constraint | Effect | Default |
|------------|--------|---------|
| Target journal | Prefer candidates who published there; pull its editorial board for COI | none (journal_fit weight redistributes) |
| Manuscript author(s) | Excluded + co-author closure excluded | ask; empty = no COI filtering possible |
| Author institutions | **Hard exclusion** — affiliation data is now available via `get_author_profile` | none |
| Career stage | `junior` / `mixed` / `senior` | `junior` |
| Volume wanted | Request 2× what is needed (editors want alternates) | 12 shortlisted, top 5 highlighted |
| Explicit exclusions | "do not suggest X" | none |

### Stage 2 — Query construction

Do this yourself, writing `/tmp/ms_profile.json`. Do NOT hand the raw manuscript to the
search tools — that is how you get generic results.

- **Semantic query**: 3–5 sentence distillation of title + abstract, ≤1500 chars. Name the
  theory, constructs, mechanism and setting explicitly.
- **Keyword queries**: build **6–9** short queries, 2–4 words each, always quoting
  multi-word phrases. Mix: quoted construct pairs (`"algorithmic control" "worker wellbeing"`),
  synonym ORs (`"gig work|gig economy|online labor platform"`), theory+outcome, setting+construct,
  and one target-journal-restricted query (`journals="MISQ,ISR"`). Never search a single generic
  word (`AI`, `platform`) — OR-relaxation floods the results. Set `year_from = current_year - 12`
  on most queries to surface *currently active* scholars; leave one unrestricted so classic
  papers (mid-career authors) still appear.

### Stage 3 — Retrieval: five lanes

Always pass `include="authors"` on keyword/semantic search — it adds ORCID, OpenAlex links,
identity evidence and affiliations **at no extra credit**.

| Lane | Tool | Cost | What it gives you |
|------|------|-----:|-------------------|
| A. Keyword | `search_keyword` | 1/call | `limit=30–50`. Structured authors. Relaxed/OR matching — relevance must be screened by you |
| B. Semantic | `search_semantic` | 2/call | `limit=30–50`. Conceptual match; authors come as a flat mixed-style string → parse with `author_norm.py` |
| C. References | resolve the manuscript's reference list: `get_paper_by_doi` if the entry has a DOI, else `search_keyword` on the quoted title | 1/entry | Cited authors are topical experts by construction. Cap at 15–20 entries, the most central ones |
| D. Citing | `get_paper_citations(doi, limit=25)` on 3–5 seed papers (best lane-A/B hits, relevance ≥0.8, DOI present) | 1/seed + 1/resolve | **Who is working on this RIGHT NOW.** Results carry NO authors — title-only. You pre-screen titles (free), then resolve the 8–12 most on-topic via `get_paper_by_doi(doi, include="authors")`. Authors become candidates with `lane: "citing"` |
| E. Coupled | `get_paper_related(doi, limit=10)` on 2–3 seeds | 1/seed + 1/resolve | Bibliographic coupling, not semantics. Keep `shared_reference_count ≥ 6`; same resolve-author step; `lane: "coupled"` |

Lane C mechanics: quote **one** distinctive phrase from the reference title. Terms are ANDed on
top of an exact phrase — do not append extra words to a quoted title. If a quoted title returns
nothing, retry with the unquoted core phrase.

Lane D/E mechanics: `get_paper_citations.total` counts only *locally indexed* citing papers
(e.g. 49) while `crossref_citation_count` (e.g. 479) is the real-world total — say which you
mean. `issue_year` in citation results can reflect online-first dates; don't treat it as gospel.

**Screening is mandatory.** Assign a relevance to every retained paper yourself, before it
becomes evidence: `0.9` same constructs/theory/setting · `0.7` closely adjacent · `0.5`
tangential · `<0.4` **drop**. Lane A's OR-relaxation means a query about "algorithmic
management gig workers" also returns compliance papers — without screening you will recommend
the wrong people confidently.

**Who becomes a candidate?** All authors of retained papers, with a deliberate bias toward
junior-looking co-authors: a second author on a closely matching 2026 paper is often the ideal
reviewer (did the work, still building reputation). Lane D is the cheapest way to find them —
citing papers skew young and active by construction.

### Stage 4 — Candidate assembly & canonicalization

Expect the same person under several spellings.

```bash
python3 $S/author_norm.py --string "Ng, Wai Ming, Ferrari, Anna Maria, Duarte, Joao Pedro"
# -> Wai Ming Ng | Anna Maria Ferrari | Joao Pedro Duarte
```

Use `First Last` canonical form downstream; if unsure which spelling is canonical,
`author-search --name <surname>` returns the catalogue's own form. Duplicates merge
only when `score_reviewers.py` finds the same valid ORCID. Distinct ORCIDs and
unresolved names remain separate, even when names or initials match. Preserve
confirmed aliases in `aliases`; all aliases are screened against exclusions.

### Stage 5 — Conflict-of-interest pass

Build the exclusion set **before** scoring:

1. **Manuscript authors** → excluded by definition.
2. **Co-author closure**: `get_author_profile(name, include="coauthors", coauthors_limit=50)`
   for each manuscript author (1 credit each — and it returns the profile too). Every returned
   co-author name is excluded. ⚠️ Co-authors are *name-grouped*, not resolved people —
   "Campbell, Rory Stuart" and "Rory Campbell" can appear as separate rows; merge on
   identity evidence rather than merging people by surname+initial. Preserve the response's
   `has_more` as `coauthors_has_more` on each manuscript-author exclusion record.
   If true, retrieve additional results where supported, or explicitly report incomplete
   screening. If omitted/unknown, the scorer also flags incomplete COI coverage in every
   retained candidate's report. Set false only when the retrieved response confirms no
   additional co-authors; this still does not imply coverage outside the catalog.
3. **Target-journal editorial board**: `journal-editors --journal-name <journal>`.
   Exclude current editors (including generic `Editor` and any `is_chief: true` record),
   not all board members. Editorial Review Board (ERB), review-board members and
   equivalent reviewer roles remain eligible, subject to all other conflict checks.
   ISR ERB membership is relevant reviewer experience, not an automatic conflict.
   Preserve `role`, `is_chief` and `is_current` in `editor_role`
   (or `editor_roles` for multiple target-board records). Only attach records from the
   resolved target journal's board. Historical records with `is_current: false` are not
   automatically excluded. Missing current status is treated conservatively as current
   for editor roles. Verify unclear board labels manually; do not infer editor status
   from board membership alone.
   Report excluded editors separately if useful ("who might handle this paper").
4. **Institutions** (now a hard filter): collect each candidate's affiliations and match against
   the user's excluded institutions. Conservative string matching lives in
   `score_reviewers.py` (`affiliation_matches`) — two institutions that merely share a city
   or an acronym are deliberately NOT merged.
5. **Explicit exclusions** from the user.
6. **Optional second-order check for Tier A only**: `get_author_profile(candidate)` and see
   whether any manuscript author appears among the co-authors.

Record every exclusion with its reason — they go into the report's "Excluded" section.

### Stage 6 — Profile verification

Use `get_author_profile(name, include="papers,coauthors", limit=20, coauthors_limit=20)` —
**1 credit returns what used to need 3 calls**: paper count, first/last year, ORCID,
identity evidence, affiliation (with provenance), papers and co-author network.

Verified traps (details in `references/api_gotchas.md`):

- **Ambiguous names**: the call returns `status: "disambiguation_required"` + `candidates[]`
  instead of a profile. Pick the candidate whose papers match your evidence (or whose ORCID
  matches one seen with `include=authors`), then re-query with that ORCID. Never merge them.
- **Top-level `affiliation` can be null** even when the person clearly has one (verified on a
  live call; name withheld). Mine affiliations from `papers.results[].authors[].affiliations[].organization`
  before concluding "no affiliation known".
- **Profile ↔ paper-record contradictions are real** (verified on live calls; names
  withheld: one author has `first_year: null` with a 2025 target-journal paper; another's
  `last_year: 2018` predates their 2025 paper). `score_reviewers.py` backfills missing years from your evidence and refuses to
  punish imputed values — make sure your evidence carries `year`.
- **Target-journal check**: `query_papers(author_name=..., journal_name=..., limit=50)` is
  OR-token *substring* matching — asking for "Anna Maria Ferrari" also returns
  "Ferraro, Giulia".
  Verify every hit with `author_norm.paper_has_author()` before crediting a track record.

```bash
python3 - <<'PY'
import sys; sys.path.insert(0, "<skill-dir>/scripts")
from author_norm import paper_has_author
print(paper_has_author("Park, Jisoo, Liu, Yuyang (Anthony), Barros, Elena M.", "Anna Maria Ferrari"))  # False
PY
```

### Stage 7 — Scoring

Write `/tmp/candidates.json` per `references/json_contract.md`, then:

```bash
python3 $S/score_reviewers.py --input /tmp/candidates.json --out /tmp/scored.json --career-stage junior
```

| Component | Weight | What it measures |
|-----------|-------:|------------------|
| `relevance` | 0.34 | best matching paper + count + multi-lane corroboration |
| `stage_fit` | 0.20 | career age & volume band; penalties for ≥60 papers (too senior) and ≥5 silent years |
| `frontier` | 0.14 | citation-graph evidence (lanes D/E): are they on this question *now*? |
| `journal_fit` | 0.12 | verified publications in the target journal |
| `activity` | 0.12 | recency of latest corpus publication |
| `evidence_depth` | 0.08 | how much this person exists in the corpus at all |

Weights renormalize automatically when there is no target journal, and the `frontier`
component drops out (with renormalization) when no citation lanes were run — the report's
`weights_used` always shows what actually drove the ranking.

Tiers: **A** ≥ 0.68 · **B** ≥ 0.55 · **C** below. Knobs: `--career-stage`, `--target-journal`,
`constraints.min_paper_count` (default 2; drop to 1 for ML/CS-flavoured topics — the corpus is
business journals only), `constraints.strong_evidence_relief` (default 0.80 — a thin-footprint
candidate with evidence that strong is kept and flagged, not deleted).

### Stage 8 — Report

```bash
python3 $S/render_report.py --input /tmp/scored.json --md reviewers.md --html reviewers.html
```

Deliver the HTML (single file, inline CSS, prints cleanly) plus the Markdown for pasting into
a submission system. Write 2–3 sentences per Tier A candidate explaining the fit — the score
alone doesn't survive contact with an editor.

**Before delivering, verify:**

- [ ] Every Tier A name has ≥1 real matching paper cited in the report
- [ ] No current editor of the target journal appears in the recommended list
- [ ] No co-author of any manuscript author appears in the recommended list
- [ ] Manuscript authors themselves are in the excluded list, not merely absent
- [ ] Institution exclusions were actually checked (or flagged as unverifiable per candidate)

---

## Credit budget (typical full run)

| Step | Calls | Credits |
|------|-------|--------:|
| Keyword queries (`include=authors`) | 6–9 | 6–9 |
| Semantic search | 1 | 2 |
| Reference resolution (lane C) | ≤15 | ≤15 |
| Citation lanes: `paper-citations`/`paper-related` on 3–5 seeds | 3–5 | 3–5 |
| Resolving citing/coupled papers to authors | 8–14 | 8–14 |
| Co-author closure + profiles (`author-profile`) | 20–30 | 20–30 |
| Target-journal verification | 5–10 | 5–10 |
| Editorial board | 1 | 1 |
| **Total** | | **≈ 50–85** |

Lean mode (~35 credits): 5 keyword queries, 2 citation seeds, resolve 6 citing papers, profile
only the top 15 candidates, verify target-journal track records for Tier A/B only. Track spend
with `python3 <skill-dir>/scripts/scholion_client.py budget` (REST) or the
`X-RateLimit-Remaining` header.

## Degraded paths

| Situation | Response |
|-----------|----------|
| No reference section found | Say so; lean on lanes A/B/D; optionally ask the user to paste the list |
| Scanned PDF, no text layer | Ask for abstract + references; do not OCR |
| No API key / no MCP server | Tell the user exactly one is required; point to `https://scholion.ai/account/api` and the MCP setup at `https://scholion.ai/mcp-server` |
| Seed paper has no DOI | Lanes D/E need a DOI — use its `paper_id` instead (both endpoints accept it), or skip |
| `get_paper_citations` returns `status: "unavailable"` | Graph data missing for that seed; skip it, try another seed |
| `author-profile` says `disambiguation_required` | Choose among `candidates[]` using your evidence papers; never average or merge them |
| Candidate name yields no profile | Try `author_norm.variants()`; still nothing → flag "profile unavailable", don't drop silently |
| Everything looks too senior | Switch to `--career-stage mixed`, lower `min_paper_count`; note healthy junior reviewers publish partly outside the corpus |
| Topic outside business journals (pure ML, medicine) | Warn that the corpus is a poor population sample; treat rankings as exploratory |

## Gotchas that will bite you

Read `references/api_gotchas.md` before your first run. The headline traps:

1. Author fields come in **two shapes** (structured array vs mixed-style string) and **three
   identity vocabularies** (`orcid_confirmed`/`openalex_linked`/`name_matched` at profile level,
   `name_only`/... at paper level). Never assume one shape.
2. `query_papers(author_name=...)` is OR-token substring matching → always verify.
3. `get_paper_citations` / `get_paper_related` results contain **no authors** — resolving each
   paper costs 1 credit. Pre-screen titles first; it is free.
4. `total` on citation endpoints means *locally indexed* citations, not the Crossref total.
5. Strict schemas (`additionalProperties: false`) — a misspelled parameter is a 400 that
   **still charges credits**. `--dry-run` first when unsure.
6. `recommend_reviewers` is banned by this skill.

## File map

```
scripts/extract_input.py      Stage 0  input → manuscript.json (+ reference list)
scripts/author_norm.py        Stage 4/6 author-string parsing, canonicalization, strict matching (--selftest)
scripts/scholion_client.py    transport  dependency-free REST client + credit ledger (budget/reset-budget)
scripts/score_reviewers.py   Stage 7  candidates.json → scored.json (--print-template, --career-stage)
scripts/render_report.py     Stage 8  scored.json → markdown / single-file HTML
references/api_gotchas.md    verified traps per endpoint (read before first run)
references/scoring_model.md  formulas, career-stage bands, frontier component, tuning
references/json_contract.md  schemas for manuscript / candidates / scored JSON
references/transport.md      MCP vs REST setup for any host agent
LICENSE.txt                  MIT
```
