--- name: manage-refs description: Use when references must be written, rendered or converted. Checks [@key] citation keys, renders the reference list with a journal CSL via pandoc, converts [N] markers, injects Zotero Word field codes and runs manuscript-DOCX cross-reference QC. Read-only auditing is /verify-refs. metadata: triggers: "manage-refs, references, citation, citation keys, pandoc citeproc, journal CSL, CSL swap, cascade rejection re-render, cross-reference QC, [@bibkey], Zotero CWYW, ADDIN ZOTERO_ITEM, marker conversion, [N] to [@key], reference manager, render manuscript, check_citation_keys, check_xref" --- # Manage-Refs Skill Pick the tool from the Decision Tree; do not invent a parallel pipeline. References are never hand-typed: the list is always rendered by pandoc citeproc + journal CSL or by the Zotero Word plugin (CWYW). This skill writes (renders, injects, converts); bibliographic correctness against PubMed/CrossRef stays in `/verify-refs` — one read-only audit, one writer. ## Decision Tree | Situation | Tool | Why | |---|---|---| | Validate `[@bibkey]` ↔ `refs.bib` (UNDEFINED / UNUSED keys) | `scripts/check_citation_keys.py` | Hard build gate, runs in seconds | | Single-author submission lockdown, frozen output | `scripts/render_pandoc.sh -j ` | Reproducible, CI-friendly | | Cascade rejection (e.g., ER → JVIR → CVIR) | `render_pandoc.sh` with new `-j` | CSL swap reformats references in seconds | | Verify a journal CSL renders the in-text format / DOI / journal-name style the author guide actually requires | `scripts/check_csl_render.py --csl .csl --bib refs.bib --journal ` | A stub/"dependent" CSL inherits its parent's format, which may differ from the guide (parenthetical vs superscript, DOI kept, full journal names). Run BEFORE submission, not after the proof PDF | | Reference list prints FULL journal names but the journal wants NLM abbreviations | `scripts/fill_journal_abbrev.py` | Resolves each entry DOI → PMID → PubMed NLM `shortjournal` into the `.bib` so CSL `form="short"` renders abbreviations; never invents abbreviations | | Reviewer revision: add 1–2 refs to a Word doc with co-authors live | Zotero Word plugin (user GUI) | Minimal disruption to track-changes flow | | Reviewer revision: bulk reference change | Edit markdown SSOT, re-run `render_pandoc.sh` | Consistency, no cherry-pick risk | | Migrate `[N]` numeric markers → `[@key]` for pandoc | `scripts/md_marker_convert.py --to-keys` | Mapping-driven, partial conversion safe | | Convert `[@key]` → `[N]` for round-trip / debug | `scripts/md_marker_convert.py --to-numbers` | Same map, opposite direction | | Wire native Zotero CWYW field codes into a .docx (live Refresh in Word) | `scripts/inject_zotero_cwyw.py` | Co-author Word workflow, post-circulation editability | | Manuscript ↔ rendered DOCX cross-reference QC | `scripts/check_xref.py --strict` | Submission gate (P0 blocker on mismatch) | | Figures/tables submitted as separate attachments (radiology, most medical journals) | `check_xref.py --strict --allow-separate-attachments` | Downgrades `MISSING_DOCX` to WARN; `MISSING_BODY`/`MISMATCH` remain P0 | | **v_(N+1) docx build-time regeneration check** | `check_xref.py --vN-docx-md5 .docx [--vN-md .md]` | Identity = unmodified seed copy; missing diff lines = body not regenerated | | **Duplicate bibliography in the built artifact** | `scripts/check_reference_duplication.py --docx .docx` (or `--text .md`) | `DUP_REF_HEADING` / `REF_NUMBER_RESTART` / `REF_SIGNATURE_DUP` (Major). Catches a hand-typed `## References` list plus the pandoc `--citeproc` auto-bibliography, which renders **two** lists (the second often after the legends). Run after any citeproc build | | **Publisher markup in a `.bib` title** (renders as garbage) | `scripts/check_bib_title_markup.py --bib refs.bib --strict` | CrossRef titles carry `WHO` / `IDH`; BBT escapes them (`{$<$}scp{$>$}`) or strips them without restoring the space (`andTERTPromoter`). `verify_refs` proves the reference is *true*; this proves it will *print*. `TITLE_MARKUP` / `TITLE_FUSION` (Major) | | **Master pre-submission gate** (recommended before any submission) | `scripts/pre_submission_gate.sh` | Chains `check_citation_keys` → `check_bib_title_markup` → `verify_refs --strict` → `render_pandoc` (optional) → `check_xref --strict`; single artifact `qc/pre_submission_gate.json` | | Direct render with a built-in reference audit | `scripts/render_pandoc.sh` (audits the `.bib` via `/verify-refs` first; blocks on FABRICATED/MISMATCH/duplicates; reports UNVERIFIED rows as not clean, without blocking) | Best-effort (skips with a warning if `/verify-refs` is not alongside); opt out with `-S`. The master gate passes `-S` because it runs `verify_refs` in its stage 3 | | Bibliographic audit against PubMed / CrossRef | **delegate** to `/verify-refs` | Audit-only — keep writer/auditor separation | ## Workflows ### A. Pandoc citeproc (default for solo authors and final submissions) User provides `manuscript.md` with `[@bibkey]` citations + `refs.bib`. 1. **Gate**: `python3 "${CLAUDE_SKILL_DIR}/scripts/check_citation_keys.py" manuscript.md refs.bib` — exits non-zero on UNDEFINED keys. Fix and re-run. A `[@NEW:topic_slug]` drafting placeholder from `/write-paper` is reported as UNDEFINED too: resolve each by adding the citation to Zotero (`/lit-sync` then refreshes `refs.bib`) and replacing the placeholder with the real `[@bibkey]`. Never let a `[@NEW:...]` reach a rendered DOCX. 2. **Render**: ```bash "${CLAUDE_SKILL_DIR}/scripts/render_pandoc.sh" \ -j european-radiology \ -i manuscript.md \ -b refs.bib \ -o manuscript_final.docx ``` For the current inventory and what each style renders, read `citation_styles/README.md` — that table is the registry. `render_pandoc.sh` also lists what is on disk when `-j` names a style it cannot find, so ask the script rather than trusting a list written here. Two standing fallbacks: use `radiology` for RYAI and `vancouver` for JVIR (neither has a dedicated CSL). A cited key that is not in the `.bib` makes the render exit 5 and name the key: pandoc prints it as `(key?)` in the output and does not fail on its own. 3. **QC**: ```bash python3 "${CLAUDE_SKILL_DIR}/scripts/check_xref.py" \ --md manuscript.md --docx manuscript_final.docx \ --out qc/xref_audit.json --strict ``` Treat `submission_safe: false` as a halt. Route fixes by symptom — see the table in `references/check_xref_symptoms.md`. 4. **Audit hand-off**: invoke `/verify-refs` for the PubMed/CrossRef audit before sign-off. ### B. Zotero CWYW (co-author Word workflow) User has a markdown SSOT and wants reviewers to edit citations directly in Word. Each reference must already exist as a Zotero item; the user supplies a `[N] → ZoteroKey` mapping. 1. **Convert markers**: ```bash python3 "${CLAUDE_SKILL_DIR}/scripts/md_marker_convert.py" \ --input manuscript.md --output manuscript_keys.md \ --map ref_map.json --to-keys ``` The conversion never guesses a Zotero key for a number: unmapped markers stay as `[N]`, are reported on stderr, and make the script exit 1 (the partial output is still written; pass `--allow-partial` to accept it). Ranges such as `[1-3]` are expanded. Any bracketed number counts as a marker, so a year in brackets (`[2019-2021]`) is reported UNMAPPED too. Optionally stage with `--active-ns 1,2,3,4,19` for a sample build first (a 5-reference sample limits the Word Refresh blast radius while debugging). 2. **Render to .docx** with pandoc (workflow A) so the body has plain text `[@key]` markers, OR pre-build a .docx some other way that still contains plain `[@key]` text. 3. **Inject CWYW**: ```bash python3 "${CLAUDE_SKILL_DIR}/scripts/inject_zotero_cwyw.py" \ --input manuscript_keys.docx --output manuscript_cwyw.docx \ --user-id --keys-from keys.txt ``` The script fetches item metadata live from the local Zotero connector (port 23119), so Zotero must be running locally; there is no web-API fallback. Any HTTP failure aborts with a non-zero exit, so a partial bibliography never reaches the user. Never invent Zotero metadata. 4. **First-build instruction** (REQUIRED): the script writes an empty `ADDIN ZOTERO_BIBL` stub, which Word's Zotero Refresh treats as user-customized and refuses to populate. Open the output in Word → Zotero tab → **Add/Edit Bibliography** once. After that, **Refresh** keeps citations and bibliography in sync as authors edit. 5. **Surgical patches are unsafe**: for ref additions in later rounds, edit the markdown SSOT and rebuild the whole .docx instead of regex-patching the post-CWYW file. Zotero's rendered `[N]` superscripts can collide with plain `[N]` markers and corrupt the field codes. ### C. Cascade rejection re-render (find-journal hand-off) User got rejected from journal A and `/find-journal` recommended journal B. 1. Confirm the new CSL exists in `citation_styles/` (or fetch from https://citationstyles.org/styles and drop in). 2. Re-run `render_pandoc.sh -j ` against the same `manuscript.md` + `refs.bib`. 3. Re-run `check_xref.py --strict`. 4. Re-run `/verify-refs` if any new references were added during the inter-journal revision. ### D. Cross-reference QC only User shipped a manuscript and a reviewer flagged a Table/Figure mismatch. 1. Run `check_xref.py --strict` on the current `manuscript.md` + `.docx`. 2. Inspect `qc/xref_audit.json`. Body caption is the SSOT — fix `manuscript.md` and rebuild, never patch the .docx by hand. 3. See `references/check_xref_symptoms.md` for the `MISSING_BODY` / `MISSING_DOCX` / `MISMATCH` triage table. 4. For journals that accept figures and tables as **separate attachment files** (European Radiology, Radiology, AJR, JVIR, KJR, and most medical journals), pass `--allow-separate-attachments`. It downgrades two rows, reported apart because their evidence differs: - `MISSING_DOCX` — a `--docx` was supplied and **proved** the float is not in the rendered main document. That is what a separate attachment looks like. - `MISSING_BODY` with **no `--docx` supplied** — nothing was checked. Excused on your word, printed as `EXCUSED WITHOUT EVIDENCE`, and counted in `summary.downgraded_unchecked`. `MISMATCH` stays P0. So does `MISSING_BODY` when the float **is** in the rendered DOCX — that is SSOT drift, which no attachment policy excuses. **Run once with `--docx` before submitting.** The flag is a declaration, not a verification; supplying the DOCX is what converts an excuse into evidence. ### D'. v_(N+1) docx regeneration check (build-time companion) When building v_(N+1) from a frozen v_N, the v_(N+1) docx MUST differ from v_N docx by content — a byte-identical copy is a silent seed-copy that will revert markdown edits at peer review. `check_xref.py` carries two flags for the build-time companion to the submission-time gate in `/sync-submission`'s `scripts/verify_package_integrity.py --assert-vN-docx-changed`: ```bash python3 "${CLAUDE_SKILL_DIR}/scripts/check_xref.py" \ --md manuscript_v2.md \ --docx manuscript_v2.docx \ --vN-docx-md5 manuscript_v1.docx \ --vN-md manuscript_v1.md \ --strict ``` - `--vN-docx-md5` alone: MD5 identity check. Identical bytes = FAIL. - `--vN-docx-md5 + --vN-md`: additionally extracts the markdown-only diff between v_N and v_(N+1) and verifies each ≥40-char diff line appears verbatim (whitespace-normalized, case-insensitive) in the new docx body XML. Missing diff lines = body did not pick up the markdown edits. Output records the result under `vN_docx_check` in `qc/xref_audit.json`. Either failure mode causes a non-zero exit even without `--strict`. ### E. Master pre-submission gate (recommended end-to-end chain) The single entry point that combines workflows A and D plus `/verify-refs` into one aborting chain. Use this immediately before submission or before circulating a v_N package to senior co-authors. ```bash bash "${CLAUDE_SKILL_DIR}/scripts/pre_submission_gate.sh" \ --md manuscript/manuscript.md \ --bib manuscript/_src/refs.bib \ --docx submission//manuscript.docx \ --allow-separate-attachments # omit if the journal accepts inline figures/tables ``` Stage order (first failure aborts): 1. `check_citation_keys.py manuscript.md refs.bib` — UNDEFINED / UNUSED keys 2. `check_bib_title_markup.py --strict` — publisher markup / tag-strip fusion in `.bib` titles 3. `verify_refs.py refs.bib --strict` — PubMed / CrossRef per-entry verification 4. `render_pandoc.sh -S -j -i ... -b ... -o ...` — invoked only when `--docx` is omitted (`--journal `, default `vancouver`) 5. `check_xref.py --md ... --docx ... --strict [--allow-separate-attachments]` On success the chain writes `qc/pre_submission_gate.json` (plus the per-stage artifacts `qc/bib_title_markup.json`, `qc/reference_audit.json` and `qc/xref_audit.json`) with `submission_safe: true`. On any failure the JSON records the failing stage and exit code, and the script exits non-zero — do not submit until the failing stage passes. The gate does **not** reimplement any check; it calls the existing scripts as subprocesses. A new check belongs in the underlying script, which the gate then picks up. ### F. BibTeX author-format corruption (rendered-name check) Entries written as `author = {Surname AB and Surname2 CD}` (family + initials, **no comma**) make BibTeX treat the last token as the family name, rendering "AB S, CD S2". Always store `author = {Family, Full Given}`. Concatenated initials even with a comma (`Family, AB`) still collapse to a single initial under CSL `initialize-with`, so use the full forename from PubMed `efetch`. `/verify-refs` compares bib content against PubMed but does not see the rendered output; grep the rendered docx and the bib separately: ```bash unzip -p out.docx word/document.xml | sed 's/<[^>]*>//g' | grep -oE "[A-Z]{2} [A-Z], [A-Z]{2} [A-Z]" # corruption signature in output grep -nE 'author\s*=\s*\{[A-Z][a-z]+ [A-Z]{1,3}( |\})' refs.bib # no-comma source entries ``` ## Quality Gates This skill defines **three submission gates** and **one user approval gate**: - **Gate 1 (citekey integrity)**: `check_citation_keys.py` exits non-zero on UNDEFINED keys. The pipeline halts; the user reviews and fixes. - **Gate 2 (cross-reference integrity)**: `check_xref.py --strict` exits 1 on any `MISSING_DOCX` / `MISSING_BODY` / `MISMATCH` row (a float the markdown defines but the DOCX lacks is `MISSING_DOCX` even when no in-text citation of it was recognised; a legend inside an HTML comment or fenced code is not rendered, so it stays `UNCITED`). With `--docx` and no `python-docx` it exits 2: the DOCX audit did not run. The user reviews `qc/xref_audit.json` and resolves before proceeding. Under `--allow-separate-attachments`, check `summary.downgraded_unchecked` as well as `submission_safe`: a non-zero count means rows passed without being checked. - **Gate 3 (audit hand-off)**: before sign-off, the user must run `/verify-refs` and confirm `submission_safe: true` in `qc/reference_audit.json`. This skill never marks the bibliography audited on its own. - **User approval gate (CWYW first build)**: the user must perform Word → Zotero → Add/Edit Bibliography manually after the first `inject_zotero_cwyw.py` build. The skill cannot automate this and warns on stderr that it is required. ## Provenance `scripts/_vendor_citation_writer.py` is vendored from `alisoroushmd/zotero-mcp` @ `ed5dfb71`, MIT licensed. See [`NOTICE.md`](./NOTICE.md) and [`LICENSE.zotero-mcp`](./LICENSE.zotero-mcp). ## Known Limitations - **Webpage / non-journal item types**: handled by the patched `zotero_to_csl_json` that fetches Zotero's native CSL-JSON; do not bypass this patch. - **"Table 1 and 2" (singular kind word, number list)**: `check_xref.py` reads only `Table 1` from it; parsing a bare number after "and" in prose would read "Table 1 and 2 patients" as a citation. A float defined in the markdown but absent from the DOCX is still blocked as `MISSING_DOCX`; one that is neither defined nor rendered is not seen. Write "Tables 1 and 2" or "Table 1 and Table 2".