aperivue/sync-submission
Audit SSOT-to-submission drift and create journal submission manifests from canonical manuscript artifacts.
npx skills add https://github.com/Aperivue/medsci-skills --skill sync-submission
You help keep the canonical manuscript and journal-specific submission packages
from drifting apart. The skill treats submission/{journal}/ as derived output
and records whether it is current, stale, or frozen.
/orchestrate --e2e marks a project as submission-ready.project.yaml, or a direct canonical manuscript path.chest, ryai, academic_radiology.audit: compare existing submission against canonical source.build: copy canonical source into submission/{journal}/manuscript/ and write metadata.freeze: mark a package as submitted/frozen.python "${CLAUDE_SKILL_DIR}/scripts/sync_submission.py" audit --project-root . --journal chest
python "${CLAUDE_SKILL_DIR}/scripts/sync_submission.py" build --project-root . --journal chest
python "${CLAUDE_SKILL_DIR}/scripts/sync_submission.py" freeze --project-root . --journal chest --status submitted
For double-blind journals, sweep author identifiers across all upload artifacts:
python "${CLAUDE_SKILL_DIR}/scripts/blind_sweep.py" \
--registry _shared/authors/author_registry.yaml \
--files submission/{journal}/supplementary/*.md submission/{journal}/cover_letter.md \
--backup-dir .cache/blind_sweep_backup
The registry is a project-local YAML mapping author identifiers (full names, native scripts, initials with/without periods, email, ORCID) to role labels (e.g., "Reviewer 1"). See scripts/author_registry_example.yaml for schema. Never commit a populated registry to a public repository — keep it next to the manuscript.
| Artifact | Path | Purpose |
|---|---|---|
| Submission metadata | submission/{journal}/.journal_meta.json | Source hash, status, canonical path |
| Sync audit | qc/submission_sync_{journal}.json | Drift result consumed by orchestrator |
| Manifest update | artifact_manifest.json | Submission package registry |
| Pre-flight gate | qc/preflight_gate_report.json | Aggregated halt-on-failure manifest (see "Pre-flight gate" below) |
| Supplement structure | qc/supplement_structure.json | Gate 14: index↔file 1:1, sub-section gaps, callout coverage |
Run this once, right before freeze/submission. It orchestrates the existing
deterministic checks and the /verify-refs audit into one halt-on-failure gate,
writes a single aggregated manifest (qc/preflight_gate_report.json), and exits
non-zero so a build wrapper or CI step can stop the freeze. It shells out to
the per-check scripts and reimplements none of them — the halt decision is driven
by each sub-check's normalized exit code.
python "${CLAUDE_SKILL_DIR}/scripts/preflight_gate.py" --project-root . --journal chest
# add --strict to also halt on the heuristic/conditional (P1) checks
# add --online to make fabricated / author-mismatched references halt (PubMed/CrossRef)
# add --double-blind to make the asset-anonymization scan halt
By default the gate halts only on the unambiguous, deterministic errors (P0):
leftover placeholder/markers (check_placeholders.py), undefined [@key]
citations (check_citation_keys.py), duplicate references (verify_refs.py,
offline-deterministic), a canonical-vs-submission hash mismatch
(sync_submission.py audit), and an internal-audit dump leaked into a
reviewer-facing file (check_checklist_dump_leak.py — see below). The heuristic or conditional checks — check_xref,
detect_copy_divergence, scope_drift_check, cover_letter_drift_check,
cross_document_n_check, check_cross_artifact_stale — **run and report as P1
warn but do not halt** unless promoted with --strict or --require ID;
check_asset_anonymization is P1 unless --double-blind. A check whose inputs are
absent (no rendered docx, no cover letter, no copies, no journal) is recorded
skipped, never a blocker. Exit codes: 0 clean, 1 halt (≥1 blocker), 2 gate
config error (e.g. a --require'd check could not run).
The gate's offline references pass is the deterministic subset (duplicates +
pagination placeholders); an online /verify-refs --strict against PubMed/CrossRef
remains the authoritative fabrication and author-name check before submission.
Audit-dump leak check (P0). A /check-reporting or /self-review report is an *internal working audit* — it carries auto-fix annotations, a raw JSON block (compliance_pct, fixable_by_ai, check_reporting_version), pipeline-log paths, and "Action Items". It is NOT the official reporting checklist a journal expects, and must never reach a reviewer. A near-miss: a prior project's STROBE_checklist_v4.pdf was actually this dump, reused by filename into a later submission and compiled into the reviewer-visible proof. scripts/check_checklist_dump_leak.py --dir submission/ scans every .md/.docx/.pdf in the package for these tokens; any hit is a P0 leak. Run it (the pre-flight gate already does, over the journal asset directory) before freeze and confirm submission_safe: true. Writes qc/checklist_dump_leak.json.
Disclosure & availability check (standalone). Top medical-AI journals require, before review, an AI-use disclosure carrying four tokens (version + access channel + date/date-range + responsible party — the tool name only *triggers* the check) and Data/Code Availability statements. Run python3 ${CLAUDE_SKILL_DIR}/scripts/check_disclosure_availability.py --manuscript <file> --journal <stem> [--ai-study] [--require data_availability ...] [--strict] (reads references/journal_availability_policy.json). It blocks on a missing required statement or an AI disclosure that is present but missing a token / carrying a placeholder; "available on reasonable request" where the journal expects a repository is a P1 warning. Writes qc/disclosure_availability_report.json.
project.yaml or explicit input.audit reports DRIFT, do not retarget or freeze until the user eitherpatches the canonical manuscript or records the difference as journal-only.
build succeeds, run /verify-refs before final submission.scripts/preflight_gate.py --project-root . --journal {journal} to aggregate the deterministic checks below into one halt-on-failure manifest (qc/preflight_gate_report.json). Non-zero exit blocks the freeze. See "Pre-flight gate" above for the P0/P1 tiering and flags. This orchestrates Gates 1–3, 5b, 8, 9, 11 plus the placeholder and citation-key checks; the individual gates remain runnable on their own./verify-refs audit before marking a package submission-safe.document.paragraphs scan is insufficient..txt fields (abstract.txt, keywords.txt, …) are cut from the markdown but never stripped of it, so a trailing ---, a bold, or a cm^2^ superscript pastes into — and publishes in — the field literally. The pre-flight gate runs scripts/check_portal_field_residue.py --dir portal_fields/ (P1, --strict-promotable) over portal_fields/; only .txt is scanned (a .md is meant to carry markdown), and the emphasis/super/sub patterns require paired markers so significance stars and approximation tildes do not fire. It also carries a Minor char_expansion advisory: ≥/≤ in a paste-verbatim field are verbose-expanded by ScholarOne to "{greater than or equal to}" (five words), inflating the word count — pre-substitute >=/<= (only ≥/≤; × and the en-dash paste cleanly)..tiff/.jpeg/.eps, rejecting .png). The pre-flight gate runs scripts/figure_portal_readiness_check.py --figures-dir <dir> (P1) over submission/<journal>/figures (or ./figures), emitting FIGURE_OVERSIZE and — when the portal's formats are supplied via --figure-accept tiff jpeg eps — FIGURE_FORMAT_REJECTED. Fix by regenerating with /make-figures export_portal_tiff.py (LZW + RGBA→RGB flatten). The check is skipped (never an error) when there is no figures directory.pandoc --reference-doc=manuscript.docx for response/cover/supplementary text-only docx — the reference docx ships its embedded media (figure files) into the new docx, bloating size 50–100×. Use plain pandoc input.md -o output.docx for text-only artifacts.scripts/cover_letter_drift_check.py to verify the cover letter's word-count / reference-count / table-figure-count claims still match the manuscript. Cover letters routinely go stale across v_N → v_(N+1) branching and are not covered by any docx-level audit. See "Phase 4 — Cover-letter free-text drift" below.scripts/cross_document_n_check.py over the manuscript bundle (abstract, body, PROSPERO record, cover letter, supplementary, INDEX, PRISMA flow caption). Any N category with >1 distinct integer value is a P0 drift. When a FINAL_POOL_LOCK.yaml is present, supply --pool-lock to make the locked counts the authoritative baseline. See "Phase 5 — Cross-document N consistency" below.scripts/scope_drift_check.py against the manuscript (and optionally the PROSPERO record). Numeric anchors (AUC, OR/HR/RR, sensitivity/specificity) appearing in Limitations / Discussion but absent from Methods + Results are P0 SCOPE_DRIFT. PROSPERO ↔ Methods synthesis-method disagreement is a P0 PROSPERO_DRIFT.scripts/verify_package_integrity.py --assert-vN-docx-changed --vN-docx <prev>.docx --new-docx <next>.docx. Identical MD5 = unmodified seed copy = block submission. Defense-in-depth — required even when the upstream pipeline appears to have regenerated the docx.scripts/detect_copy_divergence.py --ssot <ssot>.md --copy <copy>.md ... before freeze or circulation. Any STALE_COPY (an SSOT numeric claim or heading that did not propagate to a copy) is a P0 drift. See "Phase 8 — Multi-copy manuscript divergence" below. python3 "${CLAUDE_SKILL_DIR}/scripts/check_cross_artifact_stale.py" \
--manuscript manuscript.md --aux supplement/ --aux figures/legends.md --aux revision/response_to_reviewers.md \
--retired-term "location-stratified benchmark" --old-value 1.72
A retired_framing_survivor / stale_old_value finding is a P1 stale claim-site; this automates the claim-site grep of manuscript-versioning.md §6.1 across all artifacts rather than a sample. (Numeric survivors are digit-bounded, so 1.72 never matches 11.723.)
build / retarget, cross-check the target the manuscript is written *for* against the target the project is being submitted *to*. Compare project.yaml target (and any in-manuscript header/footer "for submission to X" string) against the journal the package is built for, and check the structural metadata the target dictates — abstract heading structure (4- vs 5-heading), body word limit, citation style (Vancouver / AMA), required elements (Highlights / Central Illustration / Key Points). A mismatch (e.g., a header still reading the previous journal after a cascade retarget, or a 4-heading abstract for a 5-heading target) is a target-restructure trigger — branch to v_(N+1) per manuscript-versioning.md §2 and sync every sidecar (cover letter, title page, ICMJE COI list) — not a silent build. # header target vs project.yaml target
TGT=$(python3 -c "import yaml;print(yaml.safe_load(open('project.yaml')).get('target',''))" 2>/dev/null)
grep -niE 'for submission to|submitted to|prepared for' manuscript/manuscript.md # compare against "$TGT"
/revise pass), run scripts/check_wordcount_cap.py against the target journal profile's body cap. WORDCOUNT_OVER_CAP is a P0 (relocate methods/sensitivity detail to the Supplement); WORDCOUNT_NEAR_CAP (>0.95×) warns that the next pass will breach. The binding number is the rendered count (citeproc expands [@key] → "(Author Year)"), so prefer the built DOCX count with --rendered-words N; otherwise the script estimates it from the markdown body + inline-citation expansion. python3 "${CLAUDE_SKILL_DIR}/scripts/check_wordcount_cap.py" \
--manuscript manuscript/manuscript.md \
--journal-profile "${MEDSCI_SKILLS_ROOT:-$HOME/workspace/medsci-skills}/skills/find-journal/references/journal_profiles/<Journal>.md" \
--article-type "Original Article" --out qc/wordcount_cap.json --strict
# or, deterministic: --limit 4000 (and --rendered-words N from the built DOCX when available)
S{N}_*.md sections plus an index, hand-concatenated into _combined.md. Across revision rounds that set desynchronizes silently: an index row with no file, a file the index never lists, two files claiming the same S{N}, or a sub-section gap after an insert (S6.3 then S6.5). A reviewer opening "Supplementary Table S9" and finding the wrong content is the failure mode. Before freeze, run scripts/assemble_supplement.py to validate index↔file 1:1, rebuild _combined.md in index order (so the assembly is reproducible rather than hand-maintained), and — with --manuscript — report callout coverage: body callouts with no section file (CALLOUT_WITHOUT_SECTION) and section files the body never cites (SECTION_UNCITED). The four structural kinds are P0 under --strict; coverage findings are advisory. python3 "${CLAUDE_SKILL_DIR}/scripts/assemble_supplement.py" \
--dir submission/{journal}/supplementary --index 00_index.md \
--manuscript manuscript/manuscript.md \
--out submission/{journal}/supplementary/_combined.md \
--json qc/supplement_structure.json --strict
Some portals publish the box, not the paper. SNAPP prints it on the form itself, at Author
Contributions, Competing Interests, Data Availability and Acknowledgements:
> "This replaces any statement written within the manuscript and is the one that we will publish."
So the manuscript file is the copy reviewers read and the portal box is the copy the world
gets. A declaration that lives only in the manuscript is not a harmless duplicate — it will
not exist in the published record, and nothing warns you, because neither document is wrong
on its own. Two sentences that came one click from vanishing this way:
† footnote on the title page. There is **no equal-contributioncheckbox** on the author page — unless "X and Y contributed equally to this work" is typed
into the Author Contributions box, the published paper has no co-first authors.
The structured *Research funding* field takes a funder and a grant ID and has nowhere to put
a role disclaimer, so pasting only an AI-use note into the Acknowledgements box drops it.
Do not hand-compose the boxes. Generate them from the manuscript, then check:
SS="${CLAUDE_SKILL_DIR}/scripts"
# scaffold every replacing field straight from the manuscript (lifts the equal-contribution
# sentence in from the title page, which is the one place --emit cannot copy it from)
python3 "$SS/check_portal_mirror.py" --manuscript manuscript/manuscript.md \
--profile "<...>/journal_profiles/npj_Digital_Medicine.md" --emit portal_fields/
# then verify nothing was lost on the way to the box
python3 "$SS/check_portal_mirror.py" --manuscript manuscript/manuscript.md \
--portal-dir portal_fields/ --profile "<...>/npj_Digital_Medicine.md" \
--out qc/portal_mirror.json
| Verdict | Fires when |
|---|---|
| PORTAL_FIELD_NOT_MIRRORED | A sentence in a replacing manuscript section has no home in that field's paste artifact. |
| PORTAL_FIELD_MISSING | The manuscript has the section, the journal replaces it, and no artifact exists — the field publishes empty or as the portal's auto-extraction guessed it. |
| EQUAL_CONTRIBUTION_NOT_IN_PORTAL | The manuscript asserts equal / co-first contribution and the Author Contributions text does not. |
All three are major and exit 1; the pre-flight runs this as P1 (--strict-promotable).
Which fields replace is a journal fact, not a guess. It is read from the journal profile's
## Portal Mechanics block (Fields that REPLACE the manuscript: …). A journal whose portal
contract has never been recorded makes this check exit 2 and assert nothing — record the block
at first submission rather than letting the gate invent a contract. Matching is graded through
_quote_match.py, so re-flowing a sentence while pasting is not reported as a loss.
This is the complement of Gate 5c, not a duplicate: 5c asks whether what you paste is *clean*,
this asks whether what you did *not* paste is quietly gone.
A contribution taxonomy is a factual claim, published with the paper, and every co-author
reads it. Nothing ties a term to anything. During one byline negotiation three terms were
requested in sequence — Visualization, Methodology, Formal analysis — each unsupported by the
project record; a fourth, Conceptualization, was entirely legitimate and had no repository
artifact at all, because it lived in email and in a critique that drove a restructure.
That asymmetry is the design. The taxonomy is checkable; the work behind it often is not.
python3 "${CLAUDE_SKILL_DIR}/scripts/check_credit_integrity.py" \
--manuscript manuscript/manuscript.md --out qc/credit_integrity.json
| Verdict | Severity | Fires when |
|---|---|---|
| CREDIT_TERM_INVALID | major | A term outside the official fourteen in a section that says CRediT — "Statistical analysis", "Manuscript writing", "Study design" all read as CRediT and are not. The message names the term that was meant. |
| CREDIT_INITIALS_UNRESOLVED | major | Initials matching no author, or two. This is the residue a byline edit leaves: the removed author's initials keep reading as valid. |
| CREDIT_AUTHOR_UNLISTED | major | A byline author with no contribution attributed. Under ICMJE that is either an authorship question or a dropped clause. |
| CREDIT_UNCORROBORATED | prompt | A term whose footprint is absent — Visualization on a paper with no figures, Software with no Code Availability statement, or (only if the project keeps one, passed with --contribution-record) a contributor absent from the record. |
Author order and equal-contribution designation are never gated. They are negotiated, and
negotiation is legitimate; conflating them with the taxonomy is why they get edited as one
block. Corroboration is a prompt and can be answered with an attestation — a gate that failed
the build on an off-repo contribution would be wrong, and would teach its user to disable it.
Two things it declines to guess: with fewer than two resolvable byline names the
author/initials cross-check is skipped and says so (a wrong byline would accuse every
author at once), and with no contributions section it exits 2 and asserts nothing.
Cover letters live outside the submission docx files but are read by the
editor side-by-side with the manuscript. Their ## Article details
block — body word count, abstract word count, reference count,
table/figure count — is a sidecar SSOT that routinely goes stale when a
manuscript branches v_N → v_(N+1) (word limit retarget, abstract
restructure, late reference batch).
scripts/cover_letter_drift_check.py measures the manuscript truth and
compares it to the cover letter's numeric claims:
python "${CLAUDE_SKILL_DIR}/scripts/cover_letter_drift_check.py" \
--manuscript manuscript.md \
--cover-letter cover_letter.md \
--refs refs.bib \
--out qc/cover_letter_drift.json
Body words are matched with a 5% tolerance ("approximately N words"
phrasing). Abstract words tolerate ±5. Reference / table / figure counts
require exact match.
Output qc/cover_letter_drift.json:
{
"submission_safe": false,
"truth": {"body_words": 3036, "abstract_words": 319, "references": 12,
"tables": 3, "figures": 4},
"claims": {"body_words": 3790, "abstract_words": 250, "references": 12},
"drifts": [
{"field": "body_words", "truth": 3036, "cover_letter_claim": 3790,
"severity": "MAJOR",
"note": "|claim - truth| = 754 > tolerance 151"}
]
}
Drift resolution: regenerate the cover letter from the manuscript at
v_(N+1) build time. The script never edits the cover letter — that is
left to the manuscript build pipeline so the cover letter stays a
deliberate authored artifact.
Multi-document cohort-size drift is a high-frequency desk-reject pattern.
Manuscript abstracts, body prose, PROSPERO records, supplementary extraction
sheets, and PRISMA flow captions all repeat the same k included / k excluded
/ N patients totals — and any disagreement between them is read by reviewers
as either a data-integrity failure or a late-edit failure. Either reading
ends the round.
scripts/cross_document_n_check.py scans the submission package, extracts
every "N <noun>" claim by category (patients, cases, included, excluded,
nodules, tumors, studies_total), and groups them by category. A category with
more than one distinct integer value is a P0 drift.
python "${CLAUDE_SKILL_DIR}/scripts/cross_document_n_check.py" \
--root . \
--out qc/cross_document_n.json
When the project has frozen a 2_Data/FINAL_POOL_LOCK.yaml from /meta-analysis
Phase 3f.5, pass it as the authoritative anchor:
python "${CLAUDE_SKILL_DIR}/scripts/cross_document_n_check.py" \
--root . \
--pool-lock 2_Data/FINAL_POOL_LOCK.yaml \
--out qc/cross_document_n.json
Output qc/cross_document_n.json:
{
"submission_safe": false,
"drift_count": 1,
"drifts": [
{
"category": "included",
"values": [63, 64],
"locations": [
{"file": "abstract.md", "line": 4, "value": 63, "context": "..."},
{"file": "supplementary/s1.md", "line": 12, "value": 64, "context": "..."}
],
"severity": "MAJOR"
}
],
"lock_violations": []
}
Treat submission_safe: false as a halt. Resolve drift by tracing each
location to its data artifact (extraction sheet, PRISMA cascade TSVs) and
correcting the document(s) that disagree with the locked count.
Late-revision sensitivity analyses sometimes get introduced in the
Discussion or Limitations subsection without ever propagating back to
Methods + Results. The manuscript then makes claims (with explicit AUC,
OR, sensitivity numbers) whose primary report never exists. Reviewers
read this as a fabrication-grade red flag, and editors desk-reject.
A second variant of the same anti-pattern: the PROSPERO record commits to
a synthesis method (Freeman-Tukey, random-effects DerSimonian-Laird,
bivariate, HSROC, Bayesian, etc.) but the Methods section uses a
different one — or the PROSPERO record was updated and Methods stayed
behind. When accompanied by a Methods line saying "no amendment lodged",
this becomes a documented silent protocol deviation.
scripts/scope_drift_check.py detects both patterns:
python "${CLAUDE_SKILL_DIR}/scripts/scope_drift_check.py" \
--manuscript manuscript.md \
--prospero prospero/prospero_v2.md \
--out qc/scope_drift.json
Output:
{
"submission_safe": false,
"limitations_only_anchors": [
{
"anchor": "0.869",
"kind": "AUC",
"found_in": ["Limitations:31"],
"missing_from": ["Methods", "Results"]
}
],
"synthesis_method_drift": [
{"method": "Freeman-Tukey", "prospero": true, "methods": false}
]
}
Resolution: either (a) propagate the anchor into Methods + Results as a
primary report or (b) remove it from Limitations / Discussion. For
synthesis-method drift, file a PROSPERO amendment and update Methods to
match — both must agree before submission.
When a v_N submission package was frozen and a v_(N+1) is being built
(after a markdown body edit, reviewer round, or cascade-rejection
re-target), the v_(N+1) docx MUST differ from the v_N docx. The most
common silent-revert pattern is a `cp v_N/manuscript.docx
v_(N+1)/manuscript.docx` step that skips the pandoc / Zotero CWYW
regeneration entirely. The markdown body is then edited, but the docx
the portal receives is the frozen v_N — the change silently reverts at
peer review.
Run the byte-identity assertion at the top of the v_(N+1) submission
gate:
python3 /path/to/medsci-skills/scripts/verify_package_integrity.py \
--assert-vN-docx-changed \
--vN-docx SUBMISSION/<journal>/v<N>/manuscript.docx \
--new-docx SUBMISSION/<journal>/v<N+1>/manuscript.docx
Identical MD5 → exit 1 with explanatory error. Block submission until
the regeneration step is fixed.
When a project keeps several hand-maintained manuscript copies — manuscript.md
(the working SSOT), manuscript_circulation.md (co-author feedback), and
submission/<journal>/manuscript.md (portal) — a batch of edits applied to the
SSOT routinely lands in only some of the copies. The portal then receives a copy
missing a subset of the edits, and the divergence surfaces (if at all) only when a
reviewer notices the inconsistency.
Before freezing a package or sending a circulation round, run the directional
detector (SSOT → each copy):
python3 ${CLAUDE_SKILL_DIR}/scripts/detect_copy_divergence.py \
--ssot manuscript.md \
--copy manuscript_circulation.md \
--copy submission/<journal>/manuscript.md \
--out qc/copy_divergence.json --strict
It reports, per copy, the SSOT *claims* (numeric assertions — n = N, percentages,
p, OR/HR/RR, 95% CI — and section headings) that did not propagate. A STALE_COPY
(DIVERGENT overall) is a P0 blocker: re-propagate the unpropagated claims, or —
better — stop hand-maintaining parallel copies and **generate the circulation /
submission variants from the single SSOT via a build step** (pandoc transform), so
there is only one editable source. Claims are matched as normalized strings, so
wording differences do not register — only a changed or absent number/heading does;
legitimately copy-specific content (a circulation cover note) shows up as copy_only
and can be ignored.
Some Springer Editorial Manager journals offer only Manuscript / Figure / Table / Supplementary / LaTeX upload item types — no separate Title Page or Cover Letter slot, and sometimes no Graphical Abstract slot. Common for observational / cohort submissions.
<w:br w:type="page"/>; a bare \newpage is silently dropped in docx output) + the manuscript body with its byline / affiliations / corresponding-author footnote removed so the title page is not duplicated.for s in Funding "Competing Interests" "Ethics Approval" "Consent to Participate" "Consent for Publication" "Author Contributions" "Data Availability"; do
unzip -p manuscript.docx word/document.xml | sed 's/<[^>]*>//g' | grep -q "$s" && echo "OK $s" || echo "MISSING $s"; done
Every revision round asks for a marked manuscript: the revised paper with tracked changes against the version the reviewers saw. Two rules, both load-bearing.
The baseline is R0, not the previous round. The base of the diff is always the *originally reviewed* submission; only the target advances each round. An editor wants every change made since the version under review, so do not diff v7 against v8.
Word's Compare is the only safe producer — but it is scriptable. pandiff and LibreOffice --compare corrupt OOXML on real manuscripts (tables collapse, affiliation superscripts are lost); do not use them. Word for Mac exposes compare through AppleScript with author name, so the build needs no GUI pass and no post-hoc rewriting of w:author:
python3 "${CLAUDE_SKILL_DIR}/scripts/build_marked_manuscript.py" \
--original submission/{journal}/R0/manuscript.docx \
--revised submission/{journal}/R1/manuscript_clean.docx \
--out submission/{journal}/R1/manuscript_marked.docx \
--author "Submitting Author" --line-numbers
(macOS + Word only. On any other platform, produce the marked file in Word by hand — then still run the gate below.)
Confirming that "the marked file contains sentence X" passes even when Compare has dropped a paragraph, duplicated one, or split the revisions between two authors. Verify it the only way that is correct by construction — accepting every revision must reproduce the revised manuscript exactly, and rejecting every revision must reproduce the original:
python3 "${CLAUDE_SKILL_DIR}/scripts/check_marked_manuscript.py" \
--marked submission/{journal}/R1/manuscript_marked.docx \
--original submission/{journal}/R0/manuscript.docx \
--revised submission/{journal}/R1/manuscript_clean.docx \
--author "Submitting Author" --strict
Verdicts: MARKED_ACCEPT_MISMATCH, MARKED_REJECT_MISMATCH (content dropped, duplicated, or invented), MARKED_NO_REVISIONS (Compare produced a clean copy), MARKED_AUTHOR_MIXED, MARKED_TABLE_LOSS, MARKED_BASE_TRACKED (a baseline still carrying live tracked changes, which makes the comparison ill-defined — accept or reject them first).
A move is not an insert plus a delete. Word encodes relocated content as w:moveFrom / w:moveTo, and a verifier that knows only w:ins / w:del reconstructs the original with the moved paragraph in it *twice* — reporting a perfectly good file as corrupt. The gate resolves revised = unchanged + w:ins + w:moveTo and original = unchanged + w:delText + w:moveFrom. Any docx probe written here must walk exact w:t / w:delText elements: the regex <w:t[^>]*> also matches <w:tbl>, <w:tc> and <w:tr>, silently swallowing table markup as prose.
The marked file carries the baseline's embedded images as deleted content, so it can exceed a portal's size cap even when the clean file is small. Before re-encoding, rule out the ordinary causes: the file is still open in Word (a ~$…docx lock), the portal session expired, or the upload is transient — retry. If it is genuinely too large, downsample only word/media/* and repackage; tracked changes live in word/document.xml and are untouched. Re-run the gate afterwards and keep the full-resolution original as *.full.docx.
Post-submission learnings (npj Digital Medicine R1, 2026-05): a clean docx-level audit still missed several stale artifacts that surfaced only at the portal review stage. Apply these whenever auditing a submission package.
python-docx paragraph.runs does not expose runs inside <w:hyperlink>; document.paragraphs skips table cells; document.tables does not recurse into nested tables. Figures, captions, and reporting checklists are routinely wrapped in 1×1 or nested tables, so flat scans silently miss them.
paragraphs + tables + nested-table cells recursively for every stale-string scan..runs — a missing inline element can be misread as an empty () artifact and "fixed" into a real defect.Cover letter, Data Availability, Acknowledgements, Abstract, and Author Contributions are often typed directly into the journal portal, outside any docx this skill audits. A clean docx audit does not imply a clean portal.
A clean manuscript-level blind sweep does not imply a clean portal-level blind. Author identifiers commonly leak through:
.md/.docx files, especially methodology logs, agreement metrics, amendment logs)Blind sweep regex coverage must include both period and no-period initial forms (e.g., Y.N. and YN), full names in roman + native scripts, institution names, ORCID IDs, and submission email domains. The first blind PDF export from the portal is the authoritative drift detector — always export and grep before final submit.
PROSPERO's "Print/PDF" export from the public record renders only the current amendment narrative. Previous versions are accessible only by selecting older versions in the public-record version-history dropdown. When citing PROSPERO version state, never rely on a single PDF export to verify cross-version consistency — record each published version's PDF independently and clarify in cover/supplementary which version anchors the methodology vs. which version reflects documentation-only erratum.
For documentation-only PROSPERO errata (correcting a narrative fact without changing methods/eligibility/synthesis), prefer a single Revision-Note append over a new structured amendment entry. Preserves historical audit trail and minimizes portal edit surface.
If response_to_reviewers.docx / cover_letter.docx / supplementary text-only docx grow to >100 KB after a rebuild, suspect --reference-doc pulling manuscript figure media. Verify with unzip -l output.docx | grep word/media/ — should be empty for text-only artifacts.
A tone, wording, or number change applied to one file (e.g. the abstract) must propagate to every file that repeats it — discussion, response-to-reviewers quotes, reporting checklists, supplementary captions, title page.
expertise-dependent patterns vs expertise-dependent evaluation patterns) — an exact-match grep on the short form passes while the long form remains stale./write-paper; it packages already canonical content..journal_meta.json.Take aperivue/sync-submission from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.