Claude Skill

revise

Parse peer reviewer comments and generate a structured Response to Reviewers document with tracked manuscript changes. Classifies comments as MAJOR/MINOR/REBUTTAL, coordinates new analyses with /analyze-stats and /make-figures, and produces cover letter for editor.

LLM Mart · 0 points · 3 views 1 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download Aperivue-medsci-skills-skills_revise-55a3f75.zip · 37 KB
Part of aperivue/medsci-skills — 47 skills

Install

skills CLI npx skills add https://github.com/Aperivue/medsci-skills/tree/main/skills/revise
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install aperivue-medsci-skills@llmmart
Git git clone https://github.com/Aperivue/medsci-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole aperivue/medsci-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Revision Skill -- Response to Peer Reviewers

Purpose

Parse reviewer decision letters, classify each comment by type, generate a formal Response to Reviewers document, track required manuscript changes, and coordinate with /analyze-stats or /make-figures when new analyses or visuals are needed.


Activation

When the user provides reviewer comments (pasted text, PDF, or file path), or requests revision of a manuscript, this skill activates. Before proceeding, confirm:

  1. The reviewer decision letter (pasted text or file path)
  2. The current manuscript file (paper/main.tex or paper/main.qmd)
  3. The revision round number (default: R1)
  4. The journal name (affects cover letter format)

Reference Files

  • Response-letter voice gallery: ${CLAUDE_SKILL_DIR}/references/r2r_voice.md -- before/after examples, three response skeletons (accept / partial-accept / polite-rebuttal), and a meta-phrase-to-natural conversion table. Read it before drafting the Response to Reviewers document.

Step 1: Parse and Number All Comments

Read the full decision letter. Extract every discrete comment from every reviewer and the editor.

Numbering Convention

E-1, E-2, ...       <- Editor comments
R1-1, R1-2, ...     <- Reviewer 1 comments
R2-1, R2-2, ...     <- Reviewer 2 comments
R3-1, R3-2, ...     <- Reviewer 3 (if present)

If a reviewer groups multiple requests in one paragraph, split them into sub-items: R1-3a, R1-3b, R1-3c

Classification

Type Symbol Definition
MAJOR [MAJ] Requires new experiment, re-analysis, new figure/table, or substantial structural rewrite
MINOR [MIN] Requires text revision, clarification, formatting change, or additional citation
REBUTTAL [REB] Reviewer is factually incorrect, misunderstood the study, or requests something scientifically unjustified

Output a classified comment list before generating responses:

E-1   [MIN]  Request to shorten abstract
R1-1  [MAJ]  Requires subgroup analysis by scanner type
R1-2  [MIN]  Clarify exclusion criteria rationale
R1-3  [REB]  Claims our sample size is underpowered (we disagree)
R2-1  [MAJ]  Requires additional figure showing calibration curve
R2-2  [MIN]  Add reference to [Author Year]

Gate: Present the classified comment list to the user. Confirm classifications (especially REBUTTAL vs MAJOR) before generating responses. A misclassified REBUTTAL generates a response that argues with a valid reviewer point.


Step 2: Triage -- Flag External Actions Needed

Before writing responses, identify which comments require external action:

Comments requiring /analyze-stats: Flag any MAJOR comment that requires new statistical analysis, re-run of existing analysis, additional metric (calibration, NRI, ICC), or sample size recalculation. When the source is a /self-review finding, any issue carrying requires_reanalysis: true (power/MDE re-simulation under the full model, first-visit / one-record-per-subject dedup, an extended- or reduced-adjustment over-adjustment sensitivity, optimism correction of calibration) is automatically a /analyze-stats routing item — it cannot be answered by a prose edit, so it must produce a committed script + CSV whose numbers are then fed back here.

Comments requiring /make-figures: Flag any MAJOR comment that requires a new figure or revised figure (calibration plot, subgroup forest plot, Bland-Altman, new panel).

Output: "The following comments require statistical analysis before responses can be finalized: R1-1, R2-3. Run /analyze-stats with these tasks, then return to /revise."

If /analyze-stats or /make-figures is not installed in this environment, do not invent numbers or figures. Emit the same routing list as an explicit checklist for the author to run manually (the named analysis or figure per comment) and hold those responses as BLOCKED — pending analysis/figure until the committed script + CSV (or figure file) returns. The reviewer-response numbers must always trace to a produced artifact, never to a model estimate.


Step 2.5: Revision Numerical Lineage Check (MANDATORY)

Revision-time is the highest-risk moment for numerical hallucinations. A new analysis script written to satisfy a reviewer — typically a comparative arm, a subgroup, or a sensitivity check — frequently hand-enters values copied by eye from the original paper's tables, bypassing the locked extraction CSV. The resulting numbers then flow into the response letter, the revised manuscript, and regenerated figures, and they can be internally consistent everywhere while still being wrong at the source.

The failure pattern — treat as a lived failure, not hypothetical:

An R1 revision introduced a new comparative-arm analysis script to answer a reviewer request. The Fisher exact matrix was hand-typed from the primary source Table, with an adjacent severity-grade column misread as the event count. The script, the revised manuscript, and an accompanying Table all converged on the same direction-reversed numbers relative to what the primary source actually reported.

Non-negotiable actions when Step 2 flags any /analyze-stats re-run:

  1. Tag every new numerical claim with [VERIFY-CSV] as it is written into the revised manuscript, response letter, or new table. The tag is a tripwire — it only comes off at Step 7 (Final Verification) after explicit CSV + primary-source back-check.

  2. New analysis scripts must read from the locked extraction CSV. Hand-typed matrix(), c(...), or data.frame(...) numerical inputs are PROHIBITED when a CSV row exists. If hand entry is truly unavoidable (e.g., comparative-arm subset not present in the CSV), the line MUST carry a comment citing the CSV coordinate AND the primary-source Table/Figure:

    # source: data_extraction_final.csv row <N> (<first-author> <year>, <arm> only),
    #         verified against <primary source> Table <X>, page <P>
    fisher.test(matrix(c(0, 45, 1, 55), nrow = 2, byrow = FALSE))
    
  3. Comparative / arm-specific values must enter extraction_consensus_log.md as separate rows before the analysis script references them. Do not let a new script invent values that never passed through the dual-extraction consensus layer.

  4. Revision-time numerical audit table — maintain this inside the response document draft and copy into the final change log:

    New claim (response + manuscript location) Source script:line CSV row/col Primary source (Table/Fig, page) Match?
  5. Gate before Step 3 — do not generate response prose for a MAJOR comment whose new numbers have not yet cleared this check. Prose written around un-audited numbers is very hard to unwind cleanly after a mismatch is found.

Why this matters for reviewer politics: a numerical reversal caught by the reviewer in R2 is far more damaging than the same error caught internally in R1 — it implies extraction integrity problems to the editor and licenses deeper scrutiny of the rest of the data. Treat Step 2.5 as a reputation-preservation gate, not just a QC step.


Step 3: Generate Response to Reviewers Document

Output location: revision/R[N]/response_to_reviewers_R[N].md

Document Header

Response to Reviewers

Manuscript ID: [JOURNAL-XXXXX]
Manuscript Title: [Full title]
Authors: [Last name of first author] et al.
Revision Round: [R1 / R2 / R3]
Date: [YYYY-MM-DD]

We thank the Editor and reviewers for their careful reading of our manuscript
and their constructive comments. We have revised the manuscript accordingly
and provide a point-by-point response below. All changes are shown in the
revised manuscript with tracked changes (or highlighted in yellow).

Per-Comment Response Block

---

**Comment R[X]-[Y]** [MAJ/MIN/REB]

*Reviewer's comment:*
> [Exact text of the comment, quoted verbatim]

**Response:**

[Response text -- format by type below]

**Manuscript change:**
- Section: [Methods / Results / Discussion / etc.]
- Page [X], Line [Y] (in the revised manuscript)
- [Quote the new or changed sentence if short]

Step 4: Response Formats by Comment Type

MINOR Comment

Keep concise (3-8 sentences). Acknowledge, explain the change.

We thank the reviewer for this observation. We have [describe change] in
the [section] section. The revised text now reads: "[new sentence]."

MAJOR Comment

Structured response with four parts: acknowledgment -> new analysis -> key result -> location of changes.

We thank the reviewer for this important suggestion. [State the concern.]

To address this, we [describe new analysis/experiment/rewrite].
[Key result: metric = value (95% CI, lower-upper; P = exact value)]
(All new results MUST include 95% CI and exact p-value.)

This finding [supports / strengthens / does not change] our original
conclusion because [brief interpretation].

Note: New text added to the Results section must contain only factual
findings. Interpretation belongs in the response letter text or Discussion.

We have added:
- New [Table X / Figure X / Supplementary Table X] showing [content]
- Methods revised: Page X, Lines Y-Z
- Results revised: Page X, Lines Y-Z

REBUTTAL Comment

Polite but firm. Do not capitulate without scientific justification.

We thank the reviewer for raising this point. We respectfully suggest
that [restate reviewer's claim], while we [state your position].

[Explanation with supporting evidence. Cite literature if available:
"This is consistent with [Author et al., Year; PMID XXXXXX], who
demonstrated that..."]

[If applicable: "We have added the following clarifying sentence to
[section] (Page X, Line Y): '[new sentence].'"]

We believe this issue does not warrant [the specific change requested]
because [reason]. We hope the reviewer finds this explanation satisfactory.

Voice caution: The acknowledgment lines in these templates are schematic placeholders, not literal text to paste under every comment. Repeating the same opener ("We thank the reviewer for this important suggestion.") across a dozen responses is itself an AI-tell that careful reviewers notice. Vary the openers and apply the Response-Letter Voice & AI-Tell Avoidance section below before finalizing any response.


5-Category Triage Strategy

Before writing individual responses, classify every comment into one of five categories. This classification determines the response template and effort level. Process Category 1 (Simple) comments first — they are the most numerous and clearing them early reduces the perceived workload.

Category 1: Simple Question (most common)

Reviewer asks for additional description, clarification, or minor data. Response: Add the requested text and point to the location. Keep the response short. Example: "Please specify the study period" → add dates, reply "Done. See page X, line Y."

Category 2: Misunderstanding

Reviewer misinterpreted the study design, population, or analysis. Response: Never say "you are wrong." Instead: "We apologize for the lack of clarity" → re-explain the intended meaning → revise the manuscript text to prevent future confusion.

Category 3: Further Discussion

Reviewer raises a contextual concern (different healthcare system, different clinical practice). Response: Acknowledge the valid perspective → explain your study context → add a brief note in Discussion if appropriate. The full explanation can stay in the response letter without bloating the manuscript.

Category 4: Additional Results

Reviewer requests new analysis (subgroup, sensitivity, additional metric). Response: Perform the analysis → add results to Supplementary (or main text if important) → describe what was done and what was found. Treat this as a constructive contribution, not an attack. Never ignore these requests — reviewer engagement is a positive signal.

Category 5: Statistical Method Challenge

Reviewer questions or requests changes to statistical methods. Response: Provide a reasoned justification for the method with references. If the reviewer's suggestion is valid, run both analyses and show the results are consistent. If a statistician was in fact consulted, say so; do not write that sentence because it sounds credible — a claim about who reviewed the work is a claim about the world, and this letter goes to an editor.

Mapping to MAJ/MIN/REB

Category Typical Classification
1. Simple Question MIN
2. Misunderstanding MIN or REB
3. Further Discussion MIN (if text change) or REB (if disagree)
4. Additional Results MAJ
5. Statistical Challenge MAJ

Use the 5-category triage to inform the MAJ/MIN/REB classification in Step 1, not replace it.


Handling Low-Quality Reviews

Reviewer quality varies widely. When facing comments that suggest the reviewer did not carefully read the manuscript:

  1. Do not get combative. Respond with the same professionalism regardless of review quality.
  2. Address every point, even trivial or off-topic ones. Skipping a comment signals disrespect to the editor.
  3. For irrelevant comments: Add a clarifying sentence to Discussion or Methods, and reply: "We have added clarification in [section] to address this concern." This shows effort without conceding a scientific point.
  4. For factually incorrect comments: Provide evidence (with references) politely. Frame as "We believe there may be a misunderstanding" rather than "The reviewer is wrong."
  5. Remember the audience: The response letter is read by the editor, not just the reviewer. A measured, thorough response demonstrates manuscript quality even when the review does not.

Response-Letter Voice & AI-Tell Avoidance

A response-to-reviewers letter is a reviewer-facing scientific argument, not an internal change-log. The dominant AI-tell in machine-drafted letters is the editing-mechanism register: prose that narrates how the text was edited ("the revised Methods adds one sentence at line 88", "a grep-and-soften pass replaced six phrases", "no further manuscript change") instead of stating, in plain language, what changed and why.

Three principles when drafting (the AI-tell patterns themselves are defined once in humanize references/ai_patterns.md, patterns 22-24 — this section is the authoring guidance):

  1. Write the change and the science, not the editing mechanism. Describe what changed and why, and quote the new sentence. Never narrate the diff: no version prefixes ("v2 adds..."), no "softened N phrases", no grep/verification language, no internal FIX codes, no bare "No further manuscript change" stubs. Describing a new analysis you ran ("we performed a sensitivity analysis and found X") is the science, not a tell — that is welcome.
  2. No § symbols, no internal draft line numbers. A revised-manuscript page/line ("page 7, lines 177-178", stated once as referring to the revised manuscript) is fine; only internal draft line numbers that will not match the reviewer's view are banned.
  3. Format is free. Free prose, a structured Response: / Changes made: block, an Original → Revised pair, or a left-comment/right-response table are all standard human conventions. Pick any; strip only the mechanism narration.

Reviewer-facing tone

  • Vary openers. "We thank the reviewer for this point." / "We agree." / "This is an important concern." / "We have addressed this as follows." Do not repeat one acknowledgment sentence down the whole letter.
  • Calibrate the stance: full agreement, partial agreement with a bounded clarification, or a polite, evidence-backed rebuttal. Match the register to the substance.
  • Admit error plainly when the reviewer is right ("The reviewer is correct; we have corrected this.") — natural humility reads as human and builds editor trust.
  • Quote the new manuscript text verbatim in quotation marks, then name its section — what experienced authors do, and the single strongest human signal across real letters.

Succinctness & non-defensiveness (especially R2+)

Let the point-by-point prove the work; strip the pre-emptive defence. This matters most on R2+ rounds, where over-explaining reads as anxiety rather than rigor.

  • No pre-emptive hand-holding. Drop "Reviewers 2 and 3 also accepted this," "we left it unchanged because the other reviewers were satisfied," and similar cross-reviewer lobbying. Answer the comment in front of you.
  • A satisfied reviewer gets one sentence. If a reviewer is content or offers only praise, "We thank the reviewer." or a single sentence is the whole response. Do not manufacture paragraphs.
  • Cut defensive meta-comments. Remove "We confirm this statement is unchanged and not softened," "These passages already make the point, so no further text was added." State plainly where the matter is handled and move on.
  • Fold methodology disclosure into the comment it answers. Multiplicity, a SAP deviation, or an analysis caveat goes inside the relevant response — not into a separate "Statistical note" front section. Keep the disclosure (never hide a deviation), but keep it in place.
  • Split, do not bundle. When a reviewer packs several points into one paragraph, answer each as its own comment with that reviewer sentence quoted, not one block reply to the whole paragraph. Succinct means short answers, not fewer comments.

See ${CLAUDE_SKILL_DIR}/references/r2r_voice.md for the before/after gallery, response skeletons, and the meta-phrase conversion table.

Mandatory pre-submission scan

Before circulating or uploading the response letter and cover letter, run /humanize on both documents. The R2R AI-tell patterns (22-24) are defined in humanize references/ai_patterns.md; together with 13 (em dash), 16 (filler), and 19 (§) they form the response-letter scan. Hold the letter to the same classical-style bar as the manuscript: zero § symbols and no (Methods §X) self-references, em-dash use kept low, and the heading style the target journal actually publishes. The enforced item list lives in /write-paper references/section_guides/step7_1_classical_qc.md — these are the marks a senior reviewer reads as machine-drafted, and they are as visible in a letter as in a manuscript.

Response-claim verification gate (MANDATORY, deterministic)

The single source of truth is the revised manuscript, not the response prose. A letter that says "we added the sentence '…'" or "we now cite Tariq et al. [15]" must be verifiable in the body — a claimed edit that was never actually inserted is a reputation-fatal class that both a reviewer round and the authors have missed. Run the gate before sending:

python3 ${CLAUDE_SKILL_DIR}/scripts/check_response_claims.py \
  --response revision/response_to_reviewers.md \
  --manuscript manuscript/manuscript.md --strict

It flags RESPONSE_QUOTE_UNVERIFIED (a quoted added sentence absent from the body) and RESPONSE_CITATION_UNVERIFIED (an added citation whose token is nowhere in the body). It is conservative — vague, paraphrased claims are not flagged — so a firing verdict is a real discrepancy: either insert the promised edit or correct the response wording. This directly enforces the "quote the new manuscript text verbatim" discipline above, and is the same check a reviewer runs against your revision (see /peer-review).

A third verdict, RESPONSE_QUOTE_UNRESOLVED (minor, never drift), exists because the manuscript is often read through an extractor. When the quoted words are all present in order but separated by foreign tokens — a reference column bled into the sentence by a two-column PDF, line numbers from a supplement proof, a footnote marker, a hyphen split across a line — the text is there and only the extraction is dirty. A contiguous substring test cannot tell that from a missing edit and reports the correct quote as absent; that once came one step from having two accurate verbatim quotes deleted. So those cases are reported for a human to eyeball and do not fail --strict; only a genuinely absent quote does.

If a reviewer called the manuscript too long or too dense, prove the body got shorter. Answering a density comment point-by-point is a trap: each point is answered by adding a sentence, so the revision that responds to "shorten this" comes back longer. Revisions do exactly that: the point-by-point answer adds words, the shrink the letter asked for slips another round away, and nothing in the response letter records that it happened. This gate is arithmetic: if the decision letter contains a density/length complaint and the revised body did not shrink, it fires.

python3 ${CLAUDE_SKILL_DIR}/scripts/check_density_complaint.py \
  --comments revision/decision_letter.md \
  --previous manuscript/manuscript_R0.md \
  --revised manuscript/manuscript.md --strict

DENSITY_COMPLAINT_UNADDRESSED fires only when a complaint was raised AND the body word count (Introduction through Discussion, citation markers excluded) did not fall. With no density complaint it stays silent — it is not a "shorter is always better" nag. When it fires, cut or move detail to the supplement; do not defend the length by adding a paragraph that explains it.


Step 5: Cover Letter to Editor

Output location: revision/R[N]/cover_letter_R[N].md

[Date]

Dear Dr. [Editor Name / "Editor-in-Chief"],

Thank you for the opportunity to revise our manuscript, "[Full title]"
(Manuscript ID: XXXX), submitted to [Journal Name]. We have carefully
reviewed the comments from the Editor and reviewers and have revised
the manuscript accordingly.

In brief, the principal changes in this revision are: [1) ..., 2) ...,
3) ...]. A point-by-point response to each comment is provided in the
accompanying Response to Reviewers document. Revised sections are
highlighted in yellow in the manuscript.

We believe the revised manuscript addresses all concerns raised in the
review and is now suitable for publication in [Journal Name].

Sincerely,

[First Author Name], MD/PhD
[Institution]
[Email]
On behalf of all authors

R1 vs R2+ cover-letter protocol

The template above is the R1 convention: a standalone editor cover letter (200-400 words).

On an R2+ round (second revision onward), do not write a separate cover letter. Whatever you would say to the editor — the greeting and the brief change summary — belongs in the head of the response-to-reviewers letter, not in a second document. A standalone cover letter that merely restates the response letter's summary reads as redundant and, on later rounds, as boilerplate. If an earlier round already produced a cover_letter_R1.md, move it to _superseded/, exclude it from the R2+ package, and reuse the response-letter head verbatim in any portal "cover letter" field. (Exception: a journal that explicitly requires a separate cover letter at every round — then keep the head summary and the cover letter from duplicating each other.)

Response-letter head (R2+) — placed at the top of response_to_reviewers_R[N].md, before the point-by-point:

Dear Dr. [Editor Name / "Editor-in-Chief"],

Thank you for the opportunity to revise our manuscript once more. In brief, this
revision [1-2 sentence summary of the principal changes — e.g., "adds the requested
subgroup analysis and tempers the three comparisons the reviewers flagged as
over-stated"].

[If applicable: one sentence on a companion paper, a re-analysis, or a verification
the editor requested.]

All quotations below are from the revised manuscript. A point-by-point response to each
comment follows.

Sincerely,
[First Author Name], on behalf of all authors

Keep the head to a short greeting, a one-paragraph "in brief," an optional companion/verification note, the single line stating quotations are from the revised manuscript, and the signature. Everything else is point-by-point.


Step 6: Change Log

Output location: revision/R[N]/change_log_R[N].md

Comment Type Change Made Section Page Lines
R1-1 MAJ Added subgroup analysis by scanner type Results 4.3, Table 3 12 234-251
R1-2 MIN Clarified exclusion criteria for motion artifact Methods 2.2 6 112-115

Step 7: Final Verification

After all responses are drafted, check:

  • Every reviewer comment has a response (none skipped)
  • Every MAJOR comment has a corresponding manuscript change with location
  • Every REBUTTAL is backed by cited evidence or clear scientific reasoning
  • All new statistics include 95% CI and exact p-values
  • Page/line number references match the revised manuscript (not the original)
  • No internal draft line numbers ("(line 43)"); locations point to section names or revised-manuscript page/line
  • No § symbols and no editing-mechanism narration ("v2 adds one sentence", "grep verification", "No further manuscript change")
  • Acknowledgment openers varied (not one sentence repeated across responses)
  • Response letter AND cover letter ran through /humanize (patterns 22-24 triage hits reviewed; confirmed instances = 0; § = 0 hard)
  • (R2+) No separate cover letter — the editor greeting and "in brief" summary are folded into the response-letter head
  • (R2+) Satisfied reviewers get ≤1-2 sentences; no pre-emptive hand-holding or cross-reviewer lobbying
  • Multi-point reviewer paragraphs are split into discrete comments (reviewer sentence quoted + Response N), not answered as a block
  • Methodology disclosure (multiplicity, SAP deviation) is folded into the relevant response, not a separate front section
  • Cover letter is addressed to the correct editor
  • Response letter is 5000-8000 words
  • The marked manuscript passed the round-trip gate (below) — not merely "tracked changes are on"
  • All new figures/tables are referenced in the response letter

The marked manuscript is gated, not eyeballed

The journal wants the revised paper with tracked changes against the version the reviewers saw (R0 — not the previous round). Produce it with Word's Compare, which /sync-submission drives from the command line, and verify it with a round trip rather than a spot-check: accepting every revision must reproduce the revised manuscript exactly, and rejecting every revision must reproduce the original. Confirming that "sentence X appears as an insertion" passes even when Compare has dropped a paragraph or attributed half the changes to another author.

python3 <medsci-skills>/skills/sync-submission/scripts/check_marked_manuscript.py \
  --marked revision/R1/manuscript_marked.docx \
  --original submission/R0/manuscript.docx \
  --revised revision/R1/manuscript_clean.docx \
  --author "Submitting Author" --strict

See /sync-submission Phase 10 for the build step and for why the check must be move-aware (w:moveFrom / w:moveTo are not w:ins / w:del).


Revision Round File Structure

Round Folder Files
R1 revision/R1/ response_to_reviewers_R1.md, cover_letter_R1.md, change_log_R1.md
R2 revision/R2/ response_to_reviewers_R2.md, cover_letter_R2.md, change_log_R2.md

Revised manuscript: paper/main_revised_R[N].tex (or .qmd)

For R2+, acknowledge whether R1 concerns were fully resolved. If a reviewer raises a new concern at R2, note: "This comment was not raised in the first review round; we address it as follows."


Word Count Guidance

  • Response letter total: 5000-8000 words (including quoted reviewer comments)
  • Cover letter: 200-400 words (R1 only; on R2+ there is no separate cover letter — see Step 5)
  • MINOR response: 50-150 words
  • MAJOR response: 150-400 words
  • REBUTTAL response: 200-500 words
  • R2+ rounds run leaner. Most R1 concerns are already resolved, so the letter is shorter and a satisfied reviewer's response is 1-2 sentences. Do not pad an R2+ reply to reach the R1 range.

Common Mistakes to Avoid

  1. Do not agree with every MAJOR comment without providing the actual new data or analysis.
  2. Do not write vague responses ("We have revised the text accordingly") without specifying what changed and where.
  3. Do not skip any comment, even if trivial or addressed elsewhere.
  4. Do not reference page/line numbers from the original manuscript; use the revised version.
  5. Do not begin a rebuttal aggressively; always open with acknowledgment.
  6. Do not promise changes that were not actually made.
  7. Do not forget to renumber figures and tables if new items were inserted.

Anti-Hallucination

  • Never fabricate references. All citations must be verified via /search-lit with confirmed DOI or PMID. Mark unverified references as [UNVERIFIED - NEEDS MANUAL CHECK].
  • Never invent clinical definitions, diagnostic criteria, or guideline recommendations. If uncertain, flag with [VERIFY] and ask the user.

Gates

Gate Severity Trigger Action on fail
Comment classification (MAJOR / MINOR / REBUTTAL) ENFORCED comment unclassified or classification disputed ask user; do not silently default
Step 2.5 [VERIFY-CSV] tagging on revision-introduced numbers ENFORCED new numerical claim added without [VERIFY-CSV] tag tag automatically; HALT until CSV cross-check completes
Reference re-render after revisions touching citations ENFORCED any new [@bibkey] added in R1+ route to /manage-refs Phase 7.6 re-render before R1 submission
/verify-refs --strict post-revision ENFORCED FABRICATED / HIGH_MISMATCH_FIRST_AUTHOR > 0 HALT R1 submission
New analysis coordination ENFORCED reviewer asks for new analysis route to /analyze-stats (and /make-figures if figure changes); never hand-write new numbers
Body word count vs journal cap (revision-inflation trap) ENFORCED after every revise pass resolving majors pushes the body over the target journal's word limit run /sync-submission scripts/check_wordcount_cap.py (--journal-profile or --limit; prefer the rendered DOCX count); WORDCOUNT_OVER_CAP blocks submission — relocate methods/sensitivity detail to the Supplement, do not silently exceed
Cover letter to editor ENFORCED at R1 submission R1 missing editor cover letter block submission
R2+ cover-letter handling ENFORCED at R2+ submission standalone cover letter present on an R2+ round (not folded into the response-letter head) move it to _superseded/; fold the summary into the head
Response-letter voice / AI-tell ENFORCED before submission editing-mechanism narration, internal draft line refs, §, tooling leak, or repeated openers in response/cover letter run /humanize (patterns 22-24 as triage; § = 0 hard); resolve confirmed tells before submission
Files (medsci-skills)
  • references
    • r2r_voice.md 16.6 KB
      # Response-Letter Voice Reference (R2R / Rebuttal)
      
      A reference for writing response-to-reviewers (R2R) letters and editor cover letters that
      read as human-written scientific argument rather than machine-generated change-logs. Use
      alongside the **Response-Letter Voice & AI-Tell Avoidance** section of the revise SKILL.md.
      
      All examples below are **synthetic** (a fictional deep-learning lung-nodule CT study) and are
      illustrative only. They contain no real study, author, or institution.
      
      Sources:
      - matsuikentaro1/humanizer_academic principles (general AI-writing patterns)
      - Register calibrated from the conventions of published point-by-point response letters
      
      ---
      
      ## The core problem: the editing-mechanism register
      
      Machine-drafted response letters narrate *how the text was edited* instead of the *science*.
      The result reads like a diff log: which line changed, how many phrases were swapped, what a
      verification pass returned. A careful reviewer reads this as auto-generated and as evidence
      the authors are clearing a checklist rather than engaging with the critique.
      
      The fix is one habit: **state what changed and why in plain prose, quote the new sentence,
      name the section — and say nothing about the mechanism by which you found or made the edit.**
      
      This targets the *editing* mechanism only. Narrating a **new analysis you ran** is the science
      the reviewer asked for and is welcome — e.g., "we performed a sensitivity analysis restricted to
      one observation per patient, and the primary findings held." The tell is describing the
      text-editing operation ("we softened six phrases", "v2 adds a sentence"), not describing the
      analysis or its result.
      
      ---
      
      ## Two acceptable formats (the structure is not the problem)
      
      Real published response letters use any of several formats, and all read as human:
      
      - **A. Free prose** — "We agree. We have added to the Methods: '...'."
      - **B. Structured** — a `Response:` paragraph followed by a `Changes made:` (or `Action taken in
        the revised manuscript:`) block, often with `Original: '...'` → `Revised: '...'` before/after
        pairs and the section and revised-manuscript page/line.
      - **C. Two-column table** — reviewer comment on the left, author response on the right.
      
      Other normal conventions seen across real letters: restating the reviewer's comment before
      responding, and numbering multiple changes within one response as (1), (2), (3).
      
      The AI-tell is **not** the structure, the tables, or the before/after pairs — careful authors
      write all of these. The tell is the **editing-mechanism narration** layered on top: version
      prefixes ("v2 adds..."), internal draft line numbers, grep/verification language, internal FIX
      codes, `§` markers, and bare "No further manuscript change" stubs. Keep whichever format you
      like; strip the mechanism.
      
      ---
      
      ## Before / after gallery
      
      ### 1. Version-prefixed change narration + internal line numbers
      
      > Reviewer: The causal language overstates an observational design.
      
      **AI-tell (bad):**
      "v2 Methods §Design (line 43) adds one sentence: 'The user/non-user contrast is reported
      descriptively and is not interpreted as evidence of efficacy.' This is a short visible
      clarification in the manuscript body rather than only in the Limitations."
      
      **Natural (good):**
      "We agree. The design is observational, so we have added a sentence to the Methods (Design
      subsection) making the descriptive intent explicit: 'The user/non-user contrast is reported
      descriptively and is not interpreted as evidence of efficacy.' The corresponding causal
      phrasing in the Results has been removed."
      
      Why: the natural version keeps the quoted new sentence and a section-name pointer but drops
      the version prefix, the `§` marker, the internal line number, and the meta-commentary about
      *where* the clarification sits.
      
      ### 2. Tooling / verification leak
      
      > Reviewer: Several results are described as "numerical trends," which is speculative.
      
      **AI-tell (bad):**
      "A grep-and-soften pass replaced six phrases. Final grep verification across the manuscript
      and the circulated supplementary tables returned zero occurrences of 'numerical trend',
      'consistent direction', or 'possible relationship'."
      
      **Natural (good):**
      "We have rephrased these passages so that non-significant point estimates are no longer
      presented as trends. For example, 'users showed a numerically larger reduction' now reads
      'the point estimate favoured the user arm, but the 95% confidence interval crossed zero and
      is uninformative for inference.'"
      
      Why: the reviewer cares about the corrected wording and the principle behind it, not that the
      authors ran a search to confirm the strings were gone.
      
      ### 3. Redundant "no change" stubs
      
      > Reviewer: Retention was low.
      
      **AI-tell (bad):**
      "This was already reported in v1. No further manuscript change was applied."
      
      **Natural (good):**
      "We have kept the retention figure but moved it to the front of the Conclusions so it leads
      the feasibility assessment rather than appearing as an aside: the final retention of 58%
      fell short of the 80% target and is the binding constraint on the present pilot."
      
      Why: even when little changes, reply with substance. A bare "no change" stub repeated across
      comments is a hallmark of checklist-driven drafting.
      
      ### 4. Uniform openers
      
      **AI-tell (bad):** every response begins "We thank the reviewer for this important suggestion."
      
      **Natural (good):** vary by stance —
      - "We agree, and have revised accordingly."
      - "This is a fair concern about generalisability."
      - "We see how the original wording invited this reading; we have corrected it."
      - "We respectfully maintain the original analysis, for the reasons below."
      
      ### 5. Admitting an error
      
      **AI-tell (bad):**
      "v3 corrects the acquisition-time value at line 211 per the reviewer's observation."
      
      **Natural (good):**
      "The reviewer is correct. During revision we found an error in the reported acquisition time;
      the correct value is 15 s per phase. We have corrected this in the Methods and apologise for
      the oversight."
      
      ### 6. Tempering an over-claim (the most common revision task)
      
      > Reviewer: Several results are described as "significantly outperformed," but the confidence
      > intervals overlap; this overstates the evidence.
      
      **AI-tell (bad):**
      "v2 grep-and-soften pass replaced 'significantly outperformed' at lines 14, 88, and 203. Final
      grep verification across the circulated manuscript returned zero occurrences of
      'significantly', 'consistent direction', or 'numerical superiority' (§Results, §Discussion).
      FIX-2 vocabulary cascade applied."
      
      **Natural (good):**
      "We fully agree. Given the overlapping 95% confidence intervals, 'significantly outperformed'
      overstated the evidence. We have tempered this language in the Abstract, Results, and
      Discussion to describe the difference as a modest, consistent improvement rather than a
      statistically significant one. For example, in the Results:
      *Original:* 'the 2.5D model significantly outperformed the 2D model.'
      *Revised:* 'the 2.5D model achieved a modestly higher C-index (0.71 vs 0.68); the confidence
      intervals overlap, so this is a consistent but not statistically significant difference.'"
      
      Why: the natural version makes the same correction the reviewer asked for, shows the exact
      before/after wording, and names the affected sections — without the grep narration, the
      internal line numbers, the `§` markers, or the internal FIX label.
      
      ---
      
      ## Succinctness & non-defensiveness (R2+)
      
      Items 1-6 strip the *editing-mechanism* tell. These strip the *defensive over-elaboration* tell,
      which dominates later (R2+) rounds: pre-emptive lobbying, manufactured paragraphs for satisfied
      reviewers, and disclosure piled into a front section. The fix is always to say less and let the
      point-by-point carry the work. (Examples remain synthetic — a fictional imaging deep-learning study.)
      
      ### 7. Pre-emptive hand-holding / cross-reviewer lobbying
      
      **AI-tell (bad):**
      "We respectfully note that Reviewers 1 and 3 found this analysis appropriate, and we left the
      primary endpoint unchanged on that basis. We hope Reviewer 2 will agree that the consensus of
      the panel supports our approach."
      
      **Natural (good):**
      "We retained the primary endpoint as pre-registered. Our reasoning is [one-sentence scientific
      justification]; we have added it to the Methods: '[new sentence].'"
      
      Why: answer the reviewer in front of you on the merits. Citing what other reviewers thought is
      lobbying, not science, and reads as defensive.
      
      ### 8. Defensive meta-comment about an unchanged passage
      
      **AI-tell (bad):**
      "We confirm that this statement is unchanged and has not been softened or weakened in any way,
      and we emphasise that it already fully conveys the intended claim."
      
      **Natural (good):**
      "This is already stated in the Discussion: '[existing sentence].'"
      
      Why: point to where the matter is handled and stop. Insisting that nothing was weakened invites
      the suspicion that something was.
      
      ### 9. Over-elaborated response to a satisfied reviewer
      
      > Reviewer: The revised manuscript is much improved and I have no further concerns.
      
      **AI-tell (bad):**
      "We are deeply grateful to the reviewer for this generous assessment. It has been a privilege to
      benefit from such careful and constructive guidance throughout, and we are delighted that the
      substantial revisions across the Methods, Results, and Discussion have fully addressed the
      concerns raised in the previous round."
      
      **Natural (good):**
      "Thank you."
      
      Why: a satisfied reviewer needs one sentence. A paragraph of gratitude is padding and reads as
      machine-generated filler.
      
      ### 10. Methodology disclosure as a separate front section
      
      **AI-tell (bad):**
      A standalone "Statistical Note" at the top of the letter: "Before responding to individual
      comments, we wish to disclose that the subgroup analyses were not adjusted for multiplicity..."
      
      **Natural (good):** inside the response to the comment that raised it —
      "The reviewer is right to ask about multiplicity. These subgroup analyses were exploratory and
      are not adjusted for multiple comparisons; we now state this in the Methods and label them as
      exploratory in Table 3."
      
      Why: keep the disclosure (never hide a deviation) but put it where the reviewer raised it. A
      front-loaded "note" separates the admission from the question and reads as pre-emptive defence.
      
      ### 11. Bundling a multi-point paragraph into one block reply
      
      > Reviewer: The introduction is too long, the cohort definition is unclear, and Figure 2 is
      > hard to read.
      
      **AI-tell (bad):**
      "We thank the reviewer for these helpful comments. We have shortened the introduction, clarified
      the cohort, and improved Figure 2 accordingly."
      
      **Natural (good):** split into three —
      "**Comment 1.** *'The introduction is too long.'* We have cut the introduction by roughly a
      third, removing [what]. …
      **Comment 2.** *'The cohort definition is unclear.'* We have added to the Methods: '[new
      sentence].' …
      **Comment 3.** *'Figure 2 is hard to read.'* We have remade Figure 2 with [change]. …"
      
      Why: each sub-point gets a quoted comment and a specific response. A single block reply hides
      which point you actually addressed and which you skipped. Succinct means short answers, not
      fewer comments.
      
      ---
      
      ## Three response skeletons
      
      Pick the skeleton that matches the comment's substance; do not force every reply into one shape.
      
      ### A. Full agreement (most common)
      
      > [quote the reviewer comment]
      
      "We agree. We have [substantive change] in the [section]. The revised text reads:
      '[new sentence verbatim].'"
      
      Keep it to 2-5 sentences. Quote the new text; name the section; stop.
      
      ### B. Partial agreement with a bounded clarification
      
      > [quote the reviewer comment]
      
      "The reviewer raises a valid point about [aspect]. We have [the part you accept] in the
      [section]. We have not [the part you decline] because [specific scientific reason]; instead,
      we [the bounded alternative], which addresses the underlying concern without [the cost of the
      full request]."
      
      ### C. Polite, evidence-backed rebuttal
      
      > [quote the reviewer comment]
      
      "We understand the concern that [restate the reviewer's point fairly]. We respectfully
      maintain [your position] because [reasoning grounded in the data or design]. [If literature
      supports you: 'This is consistent with (Author, Year), who showed ...'] To make this clearer
      to readers, we have added a sentence to the [section]: '[new sentence].'"
      
      Never open a rebuttal combatively, and never write "the reviewer is wrong." Frame disagreement
      as a shared interest in getting the interpretation right.
      
      ### Graceful defer (when you decline an addition but want to leave the door open)
      
      "We considered [the requested addition], but believe [the change already made] adequately
      addresses the underlying concern without [the cost]. If the reviewer prefers, we are happy to
      add [the requested item] in a subsequent revision."
      
      This is a common, courteous move: it shows you took the request seriously, gives a reason, and
      defers rather than flatly refusing.
      
      ### Response-letter head (R2+)
      
      On R1 the editor gets a separate cover letter; on **R2+ there is no separate cover letter** — its
      content becomes the head of the response letter. Keep the head to a short greeting, a
      one-paragraph "in brief" summary of the principal changes, an optional one-sentence
      companion/verification note, the single line stating that all quotations are from the revised
      manuscript, and the signature. Then go straight to the point-by-point.
      
      "Dear Dr. [Editor], thank you for the opportunity to revise our manuscript once more. In brief,
      this revision [1-2 sentence change summary]. All quotations below are from the revised
      manuscript; a point-by-point response follows. Sincerely, [First Author], on behalf of all
      authors."
      
      Reuse this head verbatim in any portal "cover letter" field rather than writing a second document.
      
      ---
      
      ## Meta-phrase to natural-expression conversion
      
      | Editing-mechanism phrase | Natural reviewer-facing phrasing |
      |---|---|
      | "v2 adds one sentence at line 88" | "we have added to the [section]: '...'" |
      | "softened six over-interpretive phrases" | "we have rephrased the over-interpretive passages so that ..." |
      | "grep verification returned zero occurrences" | (delete entirely — describe the corrected wording instead) |
      | "No further manuscript change" | "the existing text in the [section] already addresses this; the relevant statement is ..." |
      | "the FIX-1 vocabulary cascade" | (delete — name the actual wording change) |
      | "(Methods §X)" / "see §Discussion" | "in the Methods" / "in the Discussion" |
      | "the internal supplementary index (not in the circulated bundle)" | (delete — never reference internal scaffolding) |
      | "demoted the term throughout at all 13 locations" | "we have replaced [old term] with [new term] throughout the manuscript" |
      
      ---
      
      ## Location-pointer rule
      
      - Point to changes by **section name** ("in the Methods, Statistical analysis subsection").
      - A **revised-manuscript page and/or line number is acceptable and common** ("Methods, page 7,
        lines 177-178") *when those numbers refer to the revised manuscript the reviewer is reading*
        (state once at the top: "all page and line numbers refer to the revised manuscript"). This
        is a normal human convention, not an AI-tell.
      - What is banned is the **internal markdown/draft line number** ("line 43" pointing into your
        working file) — it will not match the reviewer's PDF and reads as a diff log. If you cannot
        guarantee the line number matches the reviewer's manuscript, use the section name only.
      - When the change is a sentence or two, **quote the new text** in quotation marks. This is
        concrete, verifiable, and reads as the work of an author who knows their own manuscript.
      - For larger changes (a new paragraph, a restructured subsection), summarise the change in
        one sentence and point to the section; do not transcribe the whole block.
      
      ---
      
      ## Pre-submission checklist (response letter + cover letter)
      
      - [ ] `grep -c "§"` = 0
      - [ ] No internal line-number references ("(line NN)", "at lines N, M, ...")
      - [ ] No version-prefixed change narration ("v2 adds...", "the v3 revision demotes...")
      - [ ] No tooling/verification leak ("grep", "softened N phrases", "circulated bundle", internal FIX labels)
      - [ ] No bare "No further manuscript change" stubs — each reply carries substance
      - [ ] Openers varied across responses
      - [ ] New manuscript text quoted verbatim where a sentence-level change was made
      - [ ] Em dashes below threshold (see humanize Pattern 13)
      - [ ] Ran `/humanize` on both documents; triage hits reviewed, confirmed patterns 22-24 instances = 0 (`§` = 0 hard)
      - [ ] (R2+) No separate cover letter; its summary is folded into the response-letter head
      - [ ] Satisfied reviewers answered in 1-2 sentences; no cross-reviewer lobbying or defensive meta-comments
      - [ ] Multi-point reviewer paragraphs split into discrete, individually quoted comments
      - [ ] Methodology disclosures folded into the relevant response, not a separate front section
      
  • scripts
    • density_complaint_challenge
      • fixture
        • decision_letter.md 418 B
          # Decision: Major Revision
          
          Reviewer 1: The manuscript is very dense and difficult to follow. The extensive
          discussion of statistical methods could be moved to supplementary material.
          
          Reviewer 2: This is a strong methods contribution. My only concern is length —
          the paper is too long and should be shortened. An extensive language editing is needed.
          
          Reviewer 3: The analysis is sound; please add nothing further.
          
        • v20_longer.md 3.1 KB
          # Title
          
          ## Abstract
          Brief. [1]
          
          ## Introduction
          Screening detects disease early. Screening detects disease early. Screening detects disease early. Screening detects disease early. Screening detects disease early. Screening detects disease early. Screening detects disease early. Screening detects disease early. Screening detects disease early. Screening detects disease early. Screening detects disease early. Screening detects disease early. 
          
          ## Methods
          We assembled a cohort and split it at the patient level. We assembled a cohort and split it at the patient level. We assembled a cohort and split it at the patient level. We assembled a cohort and split it at the patient level. We assembled a cohort and split it at the patient level. We assembled a cohort and split it at the patient level.  We further note, in response to the reviewers, that the method is robust across settings.  We further note, in response to the reviewers, that the method is robust across settings.  We further note, in response to the reviewers, that the method is robust across settings.  We further note, in response to the reviewers, that the method is robust across settings.  We further note, in response to the reviewers, that the method is robust across settings.  We further note, in response to the reviewers, that the method is robust across settings.  We further note, in response to the reviewers, that the method is robust across settings.  We further note, in response to the reviewers, that the method is robust across settings. 
          
          ## Results
          The model discriminated the outcome with an area under the curve of 0.84. The model discriminated the outcome with an area under the curve of 0.84. The model discriminated the outcome with an area under the curve of 0.84. The model discriminated the outcome with an area under the curve of 0.84. The model discriminated the outcome with an area under the curve of 0.84. The model discriminated the outcome with an area under the curve of 0.84. 
          
          ## Discussion
          These findings are preliminary and require external validation. These findings are preliminary and require external validation. These findings are preliminary and require external validation. These findings are preliminary and require external validation. These findings are preliminary and require external validation. These findings are preliminary and require external validation.  We further note, in response to the reviewers, that the method is robust across settings.  We further note, in response to the reviewers, that the method is robust across settings.  We further note, in response to the reviewers, that the method is robust across settings.  We further note, in response to the reviewers, that the method is robust across settings.  We further note, in response to the reviewers, that the method is robust across settings.  We further note, in response to the reviewers, that the method is robust across settings.  We further note, in response to the reviewers, that the method is robust across settings.  We further note, in response to the reviewers, that the method is robust across settings. 
          
          ## References
          1. A. 2020.
          2. B. 2021.
          
        • v21_shorter.md 1.2 KB
          # Title
          
          ## Abstract
          Brief. [1]
          
          ## Introduction
          Screening detects disease early. Screening detects disease early. Screening detects disease early. Screening detects disease early. Screening detects disease early. Screening detects disease early. 
          
          ## Methods
          We assembled a cohort and split it at the patient level. We assembled a cohort and split it at the patient level. We assembled a cohort and split it at the patient level. We assembled a cohort and split it at the patient level. We assembled a cohort and split it at the patient level. We assembled a cohort and split it at the patient level. 
          
          ## Results
          The model discriminated the outcome with an area under the curve of 0.84. The model discriminated the outcome with an area under the curve of 0.84. The model discriminated the outcome with an area under the curve of 0.84. The model discriminated the outcome with an area under the curve of 0.84. The model discriminated the outcome with an area under the curve of 0.84. The model discriminated the outcome with an area under the curve of 0.84. 
          
          ## Discussion
          These findings require external validation. These findings require external validation. These findings require external validation. 
          
          ## References
          1. A. 2020.
          2. B. 2021.
          
        • v_prev.md 1.7 KB
          # Title
          
          ## Abstract
          Brief. [1]
          
          ## Introduction
          Screening detects disease early. Screening detects disease early. Screening detects disease early. Screening detects disease early. Screening detects disease early. Screening detects disease early. Screening detects disease early. Screening detects disease early. Screening detects disease early. Screening detects disease early. Screening detects disease early. Screening detects disease early. 
          
          ## Methods
          We assembled a cohort and split it at the patient level. We assembled a cohort and split it at the patient level. We assembled a cohort and split it at the patient level. We assembled a cohort and split it at the patient level. We assembled a cohort and split it at the patient level. We assembled a cohort and split it at the patient level. 
          
          ## Results
          The model discriminated the outcome with an area under the curve of 0.84. The model discriminated the outcome with an area under the curve of 0.84. The model discriminated the outcome with an area under the curve of 0.84. The model discriminated the outcome with an area under the curve of 0.84. The model discriminated the outcome with an area under the curve of 0.84. The model discriminated the outcome with an area under the curve of 0.84. 
          
          ## Discussion
          These findings are preliminary and require external validation. These findings are preliminary and require external validation. These findings are preliminary and require external validation. These findings are preliminary and require external validation. These findings are preliminary and require external validation. These findings are preliminary and require external validation. 
          
          ## References
          1. A. 2020.
          2. B. 2021.
          
      • verify.sh 3.1 KB
        #!/usr/bin/env bash
        # Deterministic verifier for the density-complaint challenge card.
        #
        # The bug this gate exists to catch is not hypothetical: a revision answers a "too dense" comment
        # point-by-point and comes back LONGER than the version that drew the complaint, every named term
        # higher than before. Point-by-point response rewards adding text, and "too long" is the one
        # comment adding text cannot answer.
        #
        # So the fixtures reproduce that arithmetic:
        #   v_prev       -> what the reviewers saw
        #   v20_longer   -> answered point-by-point, body got LONGER   -> DENSITY_COMPLAINT_UNADDRESSED
        #   v21_shorter  -> actually cut, body got SHORTER             -> OK
        #
        # And the half that keeps the gate honest: a decision letter with NO density complaint must stay
        # silent no matter what the word count did — the gate is not a "shorter is always better" nag.
        set -uo pipefail
        HERE="$(cd "$(dirname "$0")" && pwd)"
        DET="$HERE/../check_density_complaint.py"
        FIX="$HERE/fixture"
        
        pass=0; fail=0
        ck() { if [ "$2" = "$3" ]; then printf '  PASS  %-52s exit=%s\n' "$1" "$3"; pass=$((pass+1));
               else printf '  FAIL  %-52s want=%s got=%s\n' "$1" "$2" "$3"; fail=$((fail+1)); fi; }
        
        # 1) the revision that got LONGER under a density complaint -> fires
        python3 "$DET" --comments "$FIX/decision_letter.md" --previous "$FIX/v_prev.md" \
          --revised "$FIX/v20_longer.md" --strict >/dev/null 2>&1
        ck "point-by-point revision got longer -> UNADDRESSED" 1 "$?"
        
        # ...and it must NAME the verdict, not merely exit nonzero
        python3 "$DET" --comments "$FIX/decision_letter.md" --previous "$FIX/v_prev.md" \
          --revised "$FIX/v20_longer.md" 2>&1 | grep -q "DENSITY_COMPLAINT_UNADDRESSED" \
          && ck "the verdict token is printed" 0 0 || ck "the verdict token is printed" 0 1
        
        # 2) the revision that actually CUT -> silent
        python3 "$DET" --comments "$FIX/decision_letter.md" --previous "$FIX/v_prev.md" \
          --revised "$FIX/v21_shorter.md" --strict >/dev/null 2>&1
        ck "revision got shorter -> OK" 0 "$?"
        
        # 3) NEGATIVE: a decision letter with no density complaint -> silent even if it got longer
        cat > "$FIX/_no_complaint.md" <<'EOF'
        Reviewer 1: Please add a sensitivity analysis and report the calibration slope.
        Reviewer 2: The methods are sound. Add one sentence on generalizability.
        EOF
        python3 "$DET" --comments "$FIX/_no_complaint.md" --previous "$FIX/v_prev.md" \
          --revised "$FIX/v20_longer.md" --strict >/dev/null 2>&1
        ck "no density complaint -> not a shorter-is-better nag" 0 "$?"
        rm -f "$FIX/_no_complaint.md"
        
        # 4) the JSON report carries the arithmetic a downstream consumer needs
        TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
        python3 "$DET" --comments "$FIX/decision_letter.md" --previous "$FIX/v_prev.md" \
          --revised "$FIX/v20_longer.md" --out "$TMP/d.json" >/dev/null 2>&1
        python3 - "$TMP/d.json" <<'PY'
        import json, sys
        d = json.load(open(sys.argv[1]))
        assert d["verdict"] == "DENSITY_COMPLAINT_UNADDRESSED", d["verdict"]
        assert d["delta_words"] > 0 and d["density_complaints"], d
        print("  PASS  JSON report has verdict + delta + complaints")
        PY
        
        echo
        echo "  $pass passed, $fail failed"
        [ "$fail" -eq 0 ] || exit 1
        
    • check_density_complaint.py 8.4 KB
      #!/usr/bin/env python3
      """"Your paper is too dense" is the one comment you cannot address by adding text.
      
      The failure this gate catches, in the shape it takes:
      
          previous   as the reviewers saw it  ->  "too dense / shorten / move to supplement"
          revised    THE revision answering   ->  LONGER.  Every named term goes UP, because each
                     those comments               comment is answered point-by-point, and point-by-point
                                                  response REWARDS adding text.
          next       the cut version          ->  below where it started.  <- what the letter asked for.
      
      Answering "your text is too dense" comment-by-comment makes it denser, because point-by-point
      culture rewards showing you addressed each comment — and length is the one comment adding text
      cannot address. The shrink then slips another round away.
      
      This gate is pure arithmetic. If the reviewer comments contain a density/length complaint AND the
      revised manuscript body did not get SHORTER than the previous version, the complaint was not
      addressed — it was made worse. It fires on the longer revision immediately, and stays silent on the cut one.
      
      It reads:
        --comments   the reviewer decision letter (where the complaint lives)
        --previous   the manuscript as the reviewers saw it   (the word count they complained about)
        --revised    the manuscript you are about to send back (must be shorter, if a complaint was made)
      
      Body word count is measured the way a reviewer counts it: Introduction through Discussion, excluding
      the title page, abstract, references, tables, figure legends, and — so a citation-heavy paragraph is
      not scored as verbose — bracketed/parenthetical citation markers. It is a ratio check, so the exact
      rule matters less than applying it identically to both versions, which it does.
      
      Usage:
          check_density_complaint.py --comments letter.md --previous v_prev.docx --revised v_new.docx \
              [--out qc/density.json] [--strict]
      
      Stdlib only (.docx via python-docx when present).
      """
      
      from __future__ import annotations
      
      import argparse
      import json
      import re
      import sys
      import unicodedata
      from pathlib import Path
      
      # The vocabulary a reviewer reaches for when a manuscript is too long or too dense. Each is a
      # complaint that adding text cannot answer.
      COMPLAINT = re.compile(
          r"\b(too (long|dense|detailed|verbose|wordy)|overly (long|detailed|dense|complex)|"
          r"shorten(ed|ing)?|condens(e|ed|ing)|trim(med|ming)?|cut down|tighten(ed|ing)?|"
          r"reduce (the )?(length|word count|detail)|"
          r"(mov\w+|belongs?).{0,30}?(to (the )?supplement|to supplementary|in (the )?supplement)|"
          r"excessive(ly)? (detail|length|long)|"
          r"(somewhat |very |quite |rather |overly )?(dense|verbose|wordy)\b|"
          r"(is|are|reads?|too) (very )?(repetitive|redundant)|"
          r"difficult to follow|hard to follow|dilut(e|es|ing) the (main )?message|"
          r"streamlin(e|ed|ing)|language editing|more concise|less dense|for (legibility|readability)|"
          r"repeat(ed|s|ing)? (multiple times|throughout|itself))\b",
          re.IGNORECASE,
      )
      
      # Section headings, in reviewer-count order. Body = first Introduction..Introduction-like heading
      # through the end of Discussion / before References.
      BODY_START = re.compile(r"^#{0,4}\s*\**\s*(introduction|background)\b", re.IGNORECASE | re.MULTILINE)
      BODY_END = re.compile(r"^#{0,4}\s*\**\s*(references|acknowledg|funding|conflict|"
                            r"data availability|supplementary|supporting information|figure legends?)\b",
                            re.IGNORECASE | re.MULTILINE)
      ABSTRACT = re.compile(r"^#{0,4}\s*\**\s*abstract\b", re.IGNORECASE | re.MULTILINE)
      
      CITATION = re.compile(r"\[[\d,\s–\-]+\]|\((?:[A-Z][A-Za-z'`-]+(?: et al\.?)?,?\s*\d{4}[a-z]?;?\s*)+\)")
      
      
      def read_text(path: Path) -> str:
          if path.suffix.lower() == ".docx":
              try:
                  from docx import Document  # type: ignore
              except Exception as exc:  # pragma: no cover
                  raise SystemExit(f"python-docx required to read {path}: {exc}")
              doc = Document(str(path))
              parts: list[str] = []
      
              def walk(tbl):
                  for row in tbl.rows:
                      for cell in row.cells:
                          for p in cell.paragraphs:
                              parts.append(p.text)
                          for t in cell.tables:
                              walk(t)
      
              for p in doc.paragraphs:
                  parts.append(p.text)
              for t in doc.tables:
                  walk(t)
              return "\n".join(parts)
          return path.read_text(encoding="utf-8", errors="replace")
      
      
      def body_word_count(text: str) -> int:
          """Introduction..before-References, abstract excluded, citation markers removed."""
          text = unicodedata.normalize("NFKC", text)
          m0 = BODY_START.search(text)
          body = text[m0.start():] if m0 else text
          m1 = BODY_END.search(body)
          if m1:
              body = body[: m1.start()]
          # if an Abstract heading is inside our slice (no Introduction found), drop it
          if not m0:
              a = ABSTRACT.search(body)
              if a:
                  nxt = BODY_START.search(body, a.end())
                  if nxt:
                      body = body[nxt.start():]
          body = CITATION.sub(" ", body)
          body = re.sub(r"^#{1,6}\s.*$", " ", body, flags=re.MULTILINE)  # drop heading lines themselves
          body = re.sub(r"[*_`>|#]", " ", body)                          # markdown punctuation
          words = re.findall(r"[A-Za-z0-9][\w'–-]*", body)
          return len(words)
      
      
      def complaints(text: str) -> list[str]:
          """The density/length complaint sentences in the decision letter."""
          out: list[str] = []
          for sent in re.split(r"(?<=[.!?])\s+|\n", text):
              if COMPLAINT.search(sent):
                  out.append(re.sub(r"\s+", " ", sent).strip()[:160])
          # dedupe preserving order
          seen, uniq = set(), []
          for s in out:
              if s.lower() not in seen:
                  seen.add(s.lower()); uniq.append(s)
          return uniq
      
      
      def main(argv: list[str] | None = None) -> int:
          ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
          ap.add_argument("--comments", required=True, type=Path, help="reviewer decision letter (.md/.txt/.docx)")
          ap.add_argument("--previous", required=True, type=Path, help="manuscript the reviewers saw (.md/.txt/.docx)")
          ap.add_argument("--revised", required=True, type=Path, help="manuscript you are about to send back")
          ap.add_argument("--out", type=Path, help="write JSON report here")
          ap.add_argument("--strict", action="store_true", help="exit 1 if the complaint was not addressed")
          ap.add_argument("--quiet", action="store_true")
          a = ap.parse_args(argv)
      
          for p in (a.comments, a.previous, a.revised):
              if not p.is_file():
                  print(f"error: file not found: {p}", file=sys.stderr)
                  return 2
      
          comp = complaints(read_text(a.comments))
          prev_wc = body_word_count(read_text(a.previous))
          new_wc = body_word_count(read_text(a.revised))
          delta = new_wc - prev_wc
      
          fired = bool(comp) and delta >= 0
          report = {
              "detector": "check_density_complaint",
              "density_complaints": comp,
              "previous_body_words": prev_wc,
              "revised_body_words": new_wc,
              "delta_words": delta,
              "verdict": "DENSITY_COMPLAINT_UNADDRESSED" if fired else "OK",
          }
          if a.out:
              a.out.parent.mkdir(parents=True, exist_ok=True)
              a.out.write_text(json.dumps(report, indent=2), encoding="utf-8")
      
          if not a.quiet:
              if not comp:
                  print(f"OK: no density/length complaint in the decision letter "
                        f"(body {prev_wc} -> {new_wc} words, {delta:+d}).")
              elif not fired:
                  print(f"OK: a density complaint was raised and the body got shorter "
                        f"({prev_wc} -> {new_wc} words, {delta:+d}).")
              else:
                  print(f"DENSITY_COMPLAINT_UNADDRESSED: reviewers said the manuscript was too dense/long, "
                        f"and the body did not get shorter ({prev_wc} -> {new_wc} words, {delta:+d}).\n")
                  for c in comp[:6]:
                      print(f"  reviewer: {c}")
                  print(
                      "\n'Too dense' is the one comment you cannot address by adding text, and point-by-point\n"
                      "response rewards adding it: answering each density comment individually is how a\n"
                      "revision comes back longer than the version that drew the complaint. Cut, or move\n"
                      "detail to the supplement — do not defend length by adding a paragraph that explains\n"
                      "it."
                  )
          return 1 if (fired and a.strict) else 0
      
      
      if __name__ == "__main__":
          sys.exit(main())
      
    • check_response_claims.py 13.2 KB
      #!/usr/bin/env python3
      """Response-letter claim <-> revised-manuscript verification (rule-backed gate).
      
      A response-to-reviewers letter asserts concrete edits: "we added the sentence
      '...'", "we now cite Tariq et al. [15]". The single-source-of-truth is the
      *revised manuscript*, not the response prose — yet a claimed edit can be absent
      from the body (a real incident: an added Discussion citation was described in the
      response but never inserted, and both a reviewer round and the authors missed it
      until a body grep). This gate makes that class deterministic for both sides:
      `/revise` (author, before sending) and `/peer-review` (reviewer, verifying the
      author's claims against the revised manuscript).
      
      It is deliberately conservative — it verifies only claims carrying a strong,
      checkable anchor, so paraphrase and honest rewording do not false-positive:
      
        * RESPONSE_QUOTE_UNVERIFIED (major) — the letter says specific text was
          added / inserted / "now reads" and quotes it verbatim, but that quoted text
          is absent from the revised manuscript body.
        * RESPONSE_QUOTE_UNRESOLVED (minor) — the quoted text IS there in order, but
          only once foreign tokens are allowed between its words, or a word or two is
          missing. That is the signature of a dirty extraction (a bled reference
          column, PDF line numbers, a footnote marker, a hyphen split across a line),
          not of an edit that was never made. Reported so a human looks; never counted
          as drift. A contiguous substring test cannot tell these apart and calls a
          correct quote absent — the failure that once came one step from having two
          accurate verbatim quotes deleted. Matching lives in _quote_match.py.
        * RESPONSE_CITATION_UNVERIFIED (major) — the letter says a citation was added
          / "now cite(d)", but none of the cited tokens ([N] / [@key] / Author et al.)
          appear in the revised manuscript body.
      
      Vague claims with no quote and no citation ("we clarified the Methods") are not
      verifiable and are intentionally NOT flagged. Reviewer-comment blockquotes
      (lines beginning with '>') are excluded so the reviewer's own quoted text is
      never mistaken for an author addition.
      
      Usage:
          check_response_claims.py --response response.md --manuscript revised.md [--strict]
          check_response_claims.py --response r.md --manuscript revised.docx --out qc/response_claims.json
      
      Exit 0 when every anchored claim is verified (or none exist). With --strict,
      exit 1 if any major verdict fires. Stdlib only; .docx read via python-docx when
      available.
      """
      
      from __future__ import annotations
      
      import argparse
      import json
      import re
      import sys
      import unicodedata
      from pathlib import Path
      
      sys.path.insert(0, str(Path(__file__).resolve().parent))
      from _quote_match import match_quality  # noqa: E402  (same-dir helper)
      
      # A claim that asserts an addition/edit to the manuscript.
      CLAIM_VERB = re.compile(
          r"\b("
          r"added the (?:sentence|statement|clause|text|following|phrase)|"
          r"added a (?:sentence|statement|clause|citation|reference|paragraph)|"
          r"we (?:have )?added|have added|now added|"
          r"inserted|included the (?:sentence|statement|text|citation|reference)|"
          r"now (?:reads|read|states|state)|"
          r"(?:revised|changed|reworded|rephrased|amended) [^.\n]{0,60}? to (?:read|state)|"
          r"now cites?|now cited|we (?:now )?cite|added (?:the )?(?:citation|reference)s?"
          r")\b",
          re.IGNORECASE,
      )
      
      # Quoted string: straight or curly, >= 12 chars (a sentence-like assertion).
      QUOTE = re.compile(r"[\"“‘']([^\"“”‘’']{12,600})[\"”’']")
      
      # Citation tokens claimed as added.
      CIT_NUMERIC = re.compile(r"\[(\d{1,3}(?:\s*[,–-]\s*\d{1,3})*)\]")
      CIT_BIBKEY = re.compile(r"\[@([A-Za-z0-9_:.\-]+)\]")
      CIT_AUTHOR = re.compile(r"\b([A-Z][A-Za-zÀ-ſ'-]{2,})\s+et\s+al\.?")
      
      WINDOW = 320  # chars after a claim verb to look for its object
      
      
      def read_text(path: Path) -> str:
          """Return plain text from .md/.txt or .docx (recursive paragraphs + tables)."""
          if path.suffix.lower() == ".docx":
              try:
                  from docx import Document  # type: ignore
                  from docx.document import Document as _Doc  # noqa: F401
              except Exception as exc:  # pragma: no cover
                  raise SystemExit(f"python-docx required to read {path}: {exc}")
              doc = Document(str(path))
              parts: list[str] = []
      
              def walk_table(tbl):
                  for row in tbl.rows:
                      for cell in row.cells:
                          for p in cell.paragraphs:
                              parts.append(p.text)
                          for t in cell.tables:
                              walk_table(t)
      
              for p in doc.paragraphs:
                  parts.append(p.text)
              for t in doc.tables:
                  walk_table(t)
              return "\n".join(parts)
          return path.read_text(encoding="utf-8", errors="replace")
      
      
      def normalize(s: str) -> str:
          """Casefold + collapse whitespace + strip markdown emphasis for substring match."""
          s = unicodedata.normalize("NFKC", s)
          s = s.replace("’", "'").replace("‘", "'")
          s = s.replace("“", '"').replace("”", '"')
          s = re.sub(r"[*_`]", "", s)  # markdown emphasis / code ticks
          s = re.sub(r"\s+", " ", s)
          return s.casefold().strip()
      
      
      def strip_response_blockquotes(text: str) -> str:
          """Drop reviewer-comment blockquote lines (>) so their quotes aren't scanned."""
          keep = [ln for ln in text.splitlines() if not ln.lstrip().startswith(">")]
          return "\n".join(keep)
      
      
      def extract_claims(response: str):
          """Yield (kind, anchor, context) for anchored addition claims in Response prose."""
          prose = strip_response_blockquotes(response)
          claims = []
          for m in CLAIM_VERB.finditer(prose):
              start = m.start()
              window = prose[start : start + WINDOW]
              ctx = re.sub(r"\s+", " ", prose[max(0, start - 20) : start + 120]).strip()
              # quoted additions
              for q in QUOTE.finditer(window):
                  text = q.group(1).strip()
                  if len(text.split()) >= 4:
                      claims.append(("quote", text, ctx))
              # citation additions
              cits = []
              for cm in CIT_NUMERIC.finditer(window):
                  for n in re.split(r"[,–-]", cm.group(1)):
                      if n.strip():
                          cits.append(("num", n.strip()))
              for cm in CIT_BIBKEY.finditer(window):
                  cits.append(("key", cm.group(1)))
              for cm in CIT_AUTHOR.finditer(window):
                  cits.append(("author", cm.group(1)))
              # only treat a verb as a citation claim if the verb itself is citation-ish
              if cits and re.search(r"cit|reference", m.group(0), re.IGNORECASE):
                  claims.append(("citation", cits, ctx))
          return claims
      
      
      def grade_quote(body: str, quote: str) -> dict:
          """Grade the quote's presence in the body via the extraction-tolerant matcher.
      
          A contiguous search alone is not safe here: the manuscript may arrive as a .docx whose
          extraction wedges a footnote marker, a line number, or a bled column of reference text
          into the middle of the very sentence being checked. Those quotes are present and correct,
          and a substring test calls them absent — the failure that once nearly had two accurate
          verbatim quotes deleted. See _quote_match.py."""
          return match_quality(quote, body)
      
      
      def body_has_citation(body: str, norm_body: str, cits) -> bool:
          """True if ANY cited token appears in the body (conservative: any-match passes)."""
          for kind, tok in cits:
              if kind == "num" and re.search(r"\[\s*\d*[,\s–-]*" + re.escape(tok) + r"\b", body):
                  return True
              if kind == "num" and ("[" + tok + "]") in body:
                  return True
              if kind == "key" and ("@" + tok) in body:
                  return True
              if kind == "author" and normalize(tok) in norm_body:
                  return True
          return False
      
      
      def build_report(response_path: Path, manuscript_path: Path) -> dict:
          response = read_text(response_path)
          body = read_text(manuscript_path)
          norm_body = normalize(body)
          findings = []
          for kind, anchor, ctx in extract_claims(response):
              if kind == "quote":
                  g = grade_quote(body, anchor)
                  if g["grade"] == "INTERLEAVED":
                      findings.append(
                          {
                              "verdict": "RESPONSE_QUOTE_UNRESOLVED",
                              "severity": "minor",
                              "claimed_text": anchor,
                              "context": ctx,
                              "match": g,
                              "message": (
                                  f"Every word of the quoted text appears in order, but with {g['inserted']} "
                                  "foreign token(s) wedged in — consistent with a dirty extraction (a bled "
                                  "reference column, line numbers, a footnote marker), not a missing edit. "
                                  "Confirm by eye; do not treat as absent."
                              ),
                          }
                      )
                  elif g["grade"] == "PARTIAL":
                      findings.append(
                          {
                              "verdict": "RESPONSE_QUOTE_UNRESOLVED",
                              "severity": "minor",
                              "claimed_text": anchor,
                              "context": ctx,
                              "match": g,
                              "message": (
                                  f"{g['matched']} of {g['total']} words of the quoted text appear in order "
                                  "— enough to be the same sentence damaged in extraction (a hyphen split "
                                  "across a line, a dropped glyph) rather than an edit that was never made. "
                                  "Confirm by eye."
                              ),
                          }
                      )
                  elif g["grade"] == "ABSENT":
                      findings.append(
                          {
                              "verdict": "RESPONSE_QUOTE_UNVERIFIED",
                              "severity": "major",
                              "claimed_text": anchor,
                              "context": ctx,
                              "match": g,
                              "message": "Response quotes added text that is absent from the revised manuscript body.",
                          }
                      )
              elif kind == "citation":
                  if not body_has_citation(body, norm_body, anchor):
                      findings.append(
                          {
                              "verdict": "RESPONSE_CITATION_UNVERIFIED",
                              "severity": "major",
                              "claimed_citation": [t for _, t in anchor],
                              "context": ctx,
                              "message": "Response claims a citation was added but none of the cited tokens appear in the revised manuscript body.",
                          }
                      )
          n_major = sum(1 for f in findings if f["severity"] == "major")
          return {
              "response": str(response_path),
              "manuscript": str(manuscript_path),
              "findings": findings,
              "summary": {"major": n_major, "unresolved": len(findings) - n_major},
              # An UNRESOLVED quote is a "look at this", not a defect: the words are demonstrably
              # there and only the extraction is suspect. Safety therefore turns on MAJOR findings,
              # which is what --strict has always documented.
              "submission_safe": n_major == 0,
          }
      
      
      def main(argv: list[str] | None = None) -> int:
          ap = argparse.ArgumentParser(description=__doc__)
          ap.add_argument("--response", required=True, type=Path, help="response-to-reviewers letter (.md/.txt/.docx)")
          ap.add_argument("--manuscript", required=True, type=Path, help="revised manuscript (.md/.txt/.docx)")
          ap.add_argument("--out", type=Path, help="write JSON report here")
          ap.add_argument("--strict", action="store_true", help="exit 1 if any major verdict fires")
          ap.add_argument("--quiet", action="store_true")
          args = ap.parse_args(argv)
      
          for p in (args.response, args.manuscript):
              if not p.is_file():
                  print(f"error: file not found: {p}", file=sys.stderr)
                  return 2
      
          report = build_report(args.response, args.manuscript)
          if args.out:
              args.out.parent.mkdir(parents=True, exist_ok=True)
              args.out.write_text(json.dumps({"detector": "check_response_claims", **report}, indent=2, ensure_ascii=False), encoding="utf-8")
      
          if not args.quiet:
              s = report["summary"]
              if not report["findings"]:
                  print("OK: every anchored response claim is verified against the revised manuscript.")
              else:
                  print(f"RESPONSE_CLAIM findings — {s['major']} major, {s['unresolved']} unresolved:")
                  for f in report["findings"]:
                      anchor = f.get("claimed_text") or ", ".join(f.get("claimed_citation", []))
                      print(f"  [{f['verdict']}] ({f['severity']}) {anchor!r}")
                      print(f"      near: {f['context']}")
                      if f["severity"] == "minor":
                          print(f"      {f['message']}")
                  if s["major"] == 0:
                      print("\nNo major drift: the unresolved item(s) are extraction-quality doubts, "
                            "not claims of an edit that was never made.")
      
          if args.strict and not report["submission_safe"]:
              print("\nRESPONSE_CLAIM_UNVERIFIED: a response-letter claim is not reflected in the revised manuscript.", file=sys.stderr)
              return 1
          return 0
      
      
      if __name__ == "__main__":
          sys.exit(main())
      
    • _quote_match.py 7.7 KB
      """Quote matching that survives an extraction layer — the substrate under quote gates.
      
      WHY THIS EXISTS (the failure it removes)
      
      Verifying "the manuscript contains this quoted sentence" by searching a CONTIGUOUS string
      is wrong whenever the haystack came out of an extractor, because extractors interleave
      tokens the source never had. In one submission-day session that single assumption produced
      thirteen false positives, all the same shape:
      
        * a two-column PDF bled reference-list text into the middle of a sentence
          ("learners form independent" | "civile." | "assessments before seeing AI output");
        * a line-numbered supplement PDF put the line number inside the sentence
          ("were" | "86" | "performed");
        * superscript markers and footnote references landed mid-clause;
        * hyphenation across a line break split one word into two ("assess-" + "ments").
      
      Every one of those quotes was CORRECT and present. The contiguous check called them absent.
      It came within one step of instructing an author to delete two accurate verbatim quotes.
      
      THE RULE THIS ENCODES
      
      A quote that cannot be matched contiguously is not thereby "not in the source". It is
      UNRESOLVED until something stronger says otherwise. So this module grades a match instead
      of answering yes/no:
      
        EXACT        the normalized quote is a contiguous substring — verified, no doubt.
        INTERLEAVED  every quote token appears IN ORDER, with only a bounded number of foreign
                     tokens wedged between them — the text is there and the extraction is dirty.
        PARTIAL      most quote tokens appear in order but some are missing — consistent with
                     extraction damage (hyphen splits, dropped glyphs); too weak to call absent.
        ABSENT       not even a partial ordered run — the text really is not there.
      
      Only ABSENT justifies a "you claimed an edit you did not make" verdict. INTERLEAVED and
      PARTIAL are reported as unresolved so a human looks, rather than as a defect.
      
      WHY THE GAPS ARE BOUNDED (the precision that makes this safe)
      
      An unbounded subsequence match is worthless: the tokens of almost any short sentence appear
      "in order" somewhere in a long document if you allow arbitrary distance. The bound that works
      is not a token budget but an INTERRUPTION COUNT, because the two cases differ in shape:
      
          a real extraction artifact interrupts a sentence once or twice, and each interruption can
          be long (a bled reference line is a dozen tokens);
      
          a spurious "match" interrupts at nearly every token, each time by a little.
      
      So the limits are: at most MAX_GAP foreign tokens at any single join, at most
      MAX_INTERRUPTIONS joins that are interrupted at all, and a total-insertion sanity cap. A
      quote whose words are scattered one-by-one across a Discussion section needs an interruption
      at every join and fails, while a quote split once by a column bleed passes.
      
      Not a detector: a helper imported by the gates that need it (leading underscore keeps it out
      of the detector catalog glob). Stdlib only.
      """
      
      from __future__ import annotations
      
      import re
      import unicodedata
      
      # At most this many foreign tokens may sit at ONE join. A bled reference line ("civile. Rev
      # Med Suisse 2019;15:1122.") is around a dozen tokens; a running header a handful.
      MAX_GAP = 25
      # At most this many joins may be interrupted AT ALL. This is the limit that separates a dirty
      # extraction (one or two interruptions) from a spurious scatter (an interruption per token).
      MAX_INTERRUPTIONS = 4
      # Sanity cap on total foreign tokens, so a short quote cannot absorb an entire paragraph.
      MAX_TOTAL_INSERT_FRAC = 5.0
      MIN_TOTAL_INSERT = 20
      # A PARTIAL match must still account for this share of the quote's tokens; below it, ABSENT.
      PARTIAL_COVERAGE = 0.80
      
      _TOKEN_RE = re.compile(r"[0-9a-z]+(?:'[a-z]+)?", re.IGNORECASE)
      
      
      def normalize(s: str) -> str:
          """Casefold, unify quotes/dashes, drop markdown emphasis, repair line-break hyphenation,
          and collapse whitespace. Hyphenation repair matters: an extractor that wraps "assess-
          ments" across a line otherwise destroys the token the quote is looking for."""
          s = unicodedata.normalize("NFKC", s)
          s = s.replace("’", "'").replace("‘", "'")
          s = s.replace("“", '"').replace("”", '"')
          # join a word split by a hyphen at a line break: "assess-\n  ments" -> "assessments"
          s = re.sub(r"(\w)[-‐‑]\s*\n\s*(\w)", r"\1\2", s)
          s = re.sub(r"[*_`]", "", s)
          s = re.sub(r"\s+", " ", s)
          return s.casefold().strip()
      
      
      def tokens(s: str) -> list[str]:
          """Normalized word/number tokens. Punctuation is dropped, so an injected '.' or a stray
          bracket never breaks a match on its own."""
          return _TOKEN_RE.findall(normalize(s))
      
      
      def _ordered_run(needle: list[str], hay: list[str], allow_missing: bool):
          """Best ordered match of `needle` inside `hay`.
      
          Walks every candidate start and consumes needle tokens in order, skipping at most
          MAX_GAP foreign tokens per join, at most MAX_INTERRUPTIONS interrupted joins, and a
          total-insertion sanity cap. With allow_missing, a needle token that cannot be found
          within the gap window is skipped (counted as missing) instead of failing the run.
      
          Returns (matched_count, inserted_count) for the best run, or (0, 0)."""
          if not needle or not hay:
              return (0, 0)
          budget = max(MIN_TOTAL_INSERT, int(len(needle) * MAX_TOTAL_INSERT_FRAC))
          max_missing = len(needle) - int(len(needle) * PARTIAL_COVERAGE)
          best = (0, 0)
          first = needle[0]
          starts = [i for i, t in enumerate(hay) if t == first]
          if allow_missing and not starts:
              # the opening token itself may be the damaged one — try any token of the quote
              wanted = set(needle)
              starts = [i for i, t in enumerate(hay) if t in wanted]
          for start in starts:
              hi = start
              matched = inserted = missing = interruptions = 0
              for tok in needle:
                  found = -1
                  for j in range(hi, min(hi + MAX_GAP + 1, len(hay))):
                      if hay[j] == tok:
                          found = j
                          break
                  if found < 0:
                      if not allow_missing:
                          break
                      missing += 1
                      if missing > max_missing:
                          break
                      continue
                  gap = found - hi
                  if gap:
                      interruptions += 1
                      if interruptions > MAX_INTERRUPTIONS:
                          break
                  inserted += gap
                  if inserted > budget:
                      break
                  matched += 1
                  hi = found + 1
              if matched > best[0]:
                  best = (matched, inserted)
              if matched == len(needle):
                  break
          return best
      
      
      def match_quality(quote: str, haystack: str) -> dict:
          """Grade how well `quote` is present in `haystack`.
      
          Returns {"grade": EXACT|INTERLEAVED|PARTIAL|ABSENT, "matched", "total", "inserted",
                   "coverage"}. Only ABSENT means "this text is not in the document"."""
          nq, nh = normalize(quote), normalize(haystack)
          q_tok = tokens(quote)
          total = len(q_tok)
          if total == 0:
              return {"grade": "ABSENT", "matched": 0, "total": 0, "inserted": 0, "coverage": 0.0}
          if nq and nq in nh:
              return {"grade": "EXACT", "matched": total, "total": total, "inserted": 0, "coverage": 1.0}
      
          h_tok = tokens(haystack)
          matched, inserted = _ordered_run(q_tok, h_tok, allow_missing=False)
          if matched == total:
              return {"grade": "INTERLEAVED", "matched": matched, "total": total,
                      "inserted": inserted, "coverage": 1.0}
      
          matched, inserted = _ordered_run(q_tok, h_tok, allow_missing=True)
          coverage = matched / total
          grade = "PARTIAL" if coverage >= PARTIAL_COVERAGE else "ABSENT"
          return {"grade": grade, "matched": matched, "total": total,
                  "inserted": inserted, "coverage": round(coverage, 3)}
      
  • tests
    • test_response_claims.sh 6.5 KB
      #!/usr/bin/env bash
      # Regression test for skills/revise/scripts/check_response_claims.py — the
      # response-letter <-> revised-manuscript verification gate. Confirms an anchored
      # claim absent from the body fails under --strict, a present one passes, and the
      # false-positive guards (vague claims, reviewer blockquotes) do not fire.
      set -u
      
      REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
      V="$REPO_ROOT/skills/revise/scripts/check_response_claims.py"
      TMP="$(mktemp -d)"
      trap 'rm -rf "$TMP"' EXIT
      
      pass=0
      fail=0
      ck() {
        local label="$1" expected="$2" actual="$3"
        if [ "$expected" = "$actual" ]; then
          printf '  PASS  %-52s exit=%s\n' "$label" "$actual"
          pass=$((pass + 1))
        else
          printf '  FAIL  %-52s expected=%s actual=%s\n' "$label" "$expected" "$actual"
          fail=$((fail + 1))
        fi
      }
      
      # --- manuscript that DOES contain the added sentence + citation ---
      cat > "$TMP/body_good.md" <<'MD'
      ## Methods
      Diabetes was defined by a fasting glucose of at least 126 mg/dL or medication use.
      
      ## Discussion
      Dosing errors are a recognized hazard in this setting, as Tariq et al. [15] reported.
      MD
      
      # --- manuscript that is MISSING both ---
      cat > "$TMP/body_bad.md" <<'MD'
      ## Methods
      Baseline characteristics were summarized descriptively.
      
      ## Discussion
      The findings are consistent with prior work.
      MD
      
      # --- response letter with an anchored quote claim + a citation claim ---
      cat > "$TMP/response.md" <<'MD'
      **Comment 1.**
      > The Methods do not define diabetes.
      
      **Response 1.** Thank you. We added the sentence "Diabetes was defined by a fasting glucose of at least 126 mg/dL or medication use." to the Methods.
      
      **Comment 2.**
      > Please acknowledge dosing-error risk.
      
      **Response 2.** We now cite Tariq et al. [15] in the Discussion.
      MD
      
      # --- response with only a VAGUE claim (no quote, no citation) ---
      cat > "$TMP/response_vague.md" <<'MD'
      **Response.** We clarified the Methods and revised the Introduction for readability.
      MD
      
      # --- response whose ONLY unverifiable quote is inside a reviewer blockquote ---
      cat > "$TMP/response_reviewerquote.md" <<'MD'
      **Comment 1.**
      > The authors claim "a mortality reduction of ninety percent" without support.
      
      **Response 1.** We have tempered this statement and now report the observed range only.
      MD
      
      # 1) anchored claims absent from body -> exit 1 (--strict)
      python3 "$V" --response "$TMP/response.md" --manuscript "$TMP/body_bad.md" --strict > /dev/null 2>&1
      ck "missing added quote + citation fails (--strict)" 1 "$?"
      
      # 2) same claims present in body -> exit 0
      python3 "$V" --response "$TMP/response.md" --manuscript "$TMP/body_good.md" --strict > /dev/null 2>&1
      ck "verified quote + citation passes (--strict)" 0 "$?"
      
      # 3) vague claim (no anchor) -> not flagged, exit 0
      python3 "$V" --response "$TMP/response_vague.md" --manuscript "$TMP/body_bad.md" --strict > /dev/null 2>&1
      ck "vague unanchored claim not flagged" 0 "$?"
      
      # 4) reviewer-blockquote quote (not an author addition) -> not flagged, exit 0
      python3 "$V" --response "$TMP/response_reviewerquote.md" --manuscript "$TMP/body_bad.md" --strict > /dev/null 2>&1
      ck "reviewer blockquote quote not flagged" 0 "$?"
      
      # 5) drift reported but tolerated without --strict -> exit 0
      python3 "$V" --response "$TMP/response.md" --manuscript "$TMP/body_bad.md" > /dev/null 2>&1
      ck "drift tolerated without --strict" 0 "$?"
      
      # 6) the flagged verdicts are the expected two
      OUT="$(python3 "$V" --response "$TMP/response.md" --manuscript "$TMP/body_bad.md" 2>&1)"
      echo "$OUT" | grep -q RESPONSE_QUOTE_UNVERIFIED && echo "$OUT" | grep -q RESPONSE_CITATION_UNVERIFIED
      ck "both expected verdicts present" 0 "$?"
      
      # --- extraction tolerance: a CORRECT quote must survive a dirty extraction ---------------
      # Each manuscript below really does contain the claimed sentence; the variants are what an
      # extractor emits, not what the author wrote. A contiguous substring test calls every one of
      # them "absent" — the false-positive class that once nearly had accurate quotes deleted.
      cat > "$TMP/resp_quote.md" <<'MD'
      **Response 1.** We added the sentence "learners form independent assessments before seeing AI output" to the Discussion.
      MD
      
      # (a) two-column PDF: a reference line bled into the middle of the sentence
      cat > "$TMP/body_bleed.md" <<'MD'
      ## Discussion
      We note that learners form independent civile. Rev Med Suisse 2019;15:1122. assessments before seeing AI output.
      MD
      # (b) line-numbered supplement PDF: line numbers sit inside the sentence
      cat > "$TMP/body_linenum.md" <<'MD'
      ## Discussion
      86 We note that learners form 87 independent assessments 88 before seeing AI output.
      MD
      # (c) a superscript / footnote marker landed mid-clause
      cat > "$TMP/body_supersc.md" <<'MD'
      ## Discussion
      learners form independent 3 assessments before seeing AI output
      MD
      # (d) hyphenation across a line break split one word in two
      cat > "$TMP/body_hyphen.md" <<'MD'
      ## Discussion
      learners form independent assess-
      ments before seeing AI output
      MD
      
      for variant in bleed linenum supersc hyphen; do
        python3 "$V" --response "$TMP/resp_quote.md" --manuscript "$TMP/body_$variant.md" --strict > /dev/null 2>&1
        ck "dirty extraction ($variant) is not drift (--strict)" 0 "$?"
      done
      
      # the interleaved variants must SAY so (unresolved), not pass silently
      for variant in bleed linenum supersc; do
        python3 "$V" --response "$TMP/resp_quote.md" --manuscript "$TMP/body_$variant.md" 2>&1 \
          | grep -q RESPONSE_QUOTE_UNRESOLVED
        ck "dirty extraction ($variant) reports UNRESOLVED" 0 "$?"
      done
      
      # the hyphen split is repaired outright -> no finding at all
      python3 "$V" --response "$TMP/resp_quote.md" --manuscript "$TMP/body_hyphen.md" 2>&1 \
        | grep -q RESPONSE_QUOTE
      ck "line-break hyphenation repaired (no finding)" 1 "$?"
      
      # PRECISION GUARD: tolerance must not excuse a genuine miss. The words below appear in
      # order but scattered a paragraph apart — that is not the claimed sentence.
      {
        echo "## Discussion"
        echo "Some learners were enrolled."
        for _ in $(seq 1 12); do echo "The study reported outcomes across sites and years."; done
        echo "We form working groups."
        for _ in $(seq 1 12); do echo "The study reported outcomes across sites and years."; done
        echo "An independent committee met."
        for _ in $(seq 1 12); do echo "The study reported outcomes across sites and years."; done
        echo "Their assessments were filed before seeing AI output."
      } > "$TMP/body_scattered.md"
      python3 "$V" --response "$TMP/resp_quote.md" --manuscript "$TMP/body_scattered.md" --strict > /dev/null 2>&1
      ck "scattered words are still MAJOR drift (--strict)" 1 "$?"
      
      echo "----"
      echo "test_response_claims: $pass passed, $fail failed"
      [ "$fail" -eq 0 ]
      
  • SKILL.md 31.2 KB
    ---
    name: revise
    description: Parse peer reviewer comments and generate a structured Response to Reviewers document with tracked manuscript changes. Classifies comments as MAJOR/MINOR/REBUTTAL, coordinates new analyses with /analyze-stats and /make-figures, and produces cover letter for editor.
    triggers: revise paper, respond to reviewers, revision letter, reviewer comments, major revision, minor revision, resubmit, R1 revision, revision round, response letter, point-by-point response
    tools: Read, Write, Edit, Bash, Grep, Glob
    model: inherit
    ---
    
    # Revision Skill -- Response to Peer Reviewers
    
    ## Purpose
    
    Parse reviewer decision letters, classify each comment by type, generate a formal Response to Reviewers document, track required manuscript changes, and coordinate with /analyze-stats or /make-figures when new analyses or visuals are needed.
    
    ---
    
    ## Activation
    
    When the user provides reviewer comments (pasted text, PDF, or file path), or requests revision of a manuscript, this skill activates. Before proceeding, confirm:
    
    1. The reviewer decision letter (pasted text or file path)
    2. The current manuscript file (`paper/main.tex` or `paper/main.qmd`)
    3. The revision round number (default: R1)
    4. The journal name (affects cover letter format)
    
    ---
    
    ## Reference Files
    
    - **Response-letter voice gallery**: `${CLAUDE_SKILL_DIR}/references/r2r_voice.md` -- before/after examples, three response skeletons (accept / partial-accept / polite-rebuttal), and a meta-phrase-to-natural conversion table. Read it before drafting the Response to Reviewers document.
    
    ---
    
    ## Step 1: Parse and Number All Comments
    
    Read the full decision letter. Extract every discrete comment from every reviewer and the editor.
    
    ### Numbering Convention
    
    ```
    E-1, E-2, ...       <- Editor comments
    R1-1, R1-2, ...     <- Reviewer 1 comments
    R2-1, R2-2, ...     <- Reviewer 2 comments
    R3-1, R3-2, ...     <- Reviewer 3 (if present)
    ```
    
    If a reviewer groups multiple requests in one paragraph, split them into sub-items: `R1-3a, R1-3b, R1-3c`
    
    ### Classification
    
    | Type | Symbol | Definition |
    |------|--------|------------|
    | **MAJOR** | `[MAJ]` | Requires new experiment, re-analysis, new figure/table, or substantial structural rewrite |
    | **MINOR** | `[MIN]` | Requires text revision, clarification, formatting change, or additional citation |
    | **REBUTTAL** | `[REB]` | Reviewer is factually incorrect, misunderstood the study, or requests something scientifically unjustified |
    
    Output a classified comment list before generating responses:
    
    ```
    E-1   [MIN]  Request to shorten abstract
    R1-1  [MAJ]  Requires subgroup analysis by scanner type
    R1-2  [MIN]  Clarify exclusion criteria rationale
    R1-3  [REB]  Claims our sample size is underpowered (we disagree)
    R2-1  [MAJ]  Requires additional figure showing calibration curve
    R2-2  [MIN]  Add reference to [Author Year]
    ```
    
    **Gate:** Present the classified comment list to the user. Confirm classifications
    (especially REBUTTAL vs MAJOR) before generating responses. A misclassified REBUTTAL
    generates a response that argues with a valid reviewer point.
    
    ---
    
    ## Step 2: Triage -- Flag External Actions Needed
    
    Before writing responses, identify which comments require external action:
    
    **Comments requiring /analyze-stats:** Flag any MAJOR comment that requires new statistical analysis, re-run of existing analysis, additional metric (calibration, NRI, ICC), or sample size recalculation. When the source is a `/self-review` finding, any issue carrying `requires_reanalysis: true` (power/MDE re-simulation under the full model, first-visit / one-record-per-subject dedup, an extended- or reduced-adjustment over-adjustment sensitivity, optimism correction of calibration) is automatically a `/analyze-stats` routing item — it cannot be answered by a prose edit, so it must produce a committed script + CSV whose numbers are then fed back here.
    
    **Comments requiring /make-figures:** Flag any MAJOR comment that requires a new figure or revised figure (calibration plot, subgroup forest plot, Bland-Altman, new panel).
    
    Output: "The following comments require statistical analysis before responses can be finalized: R1-1, R2-3. Run /analyze-stats with these tasks, then return to /revise."
    
    **If `/analyze-stats` or `/make-figures` is not installed in this environment**, do not invent numbers or figures. Emit the same routing list as an explicit checklist for the author to run manually (the named analysis or figure per comment) and hold those responses as `BLOCKED — pending analysis/figure` until the committed script + CSV (or figure file) returns. The reviewer-response numbers must always trace to a produced artifact, never to a model estimate.
    
    ---
    
    ## Step 2.5: Revision Numerical Lineage Check (MANDATORY)
    
    Revision-time is the highest-risk moment for numerical hallucinations. A new analysis script
    written to satisfy a reviewer — typically a comparative arm, a subgroup, or a sensitivity
    check — frequently hand-enters values copied by eye from the original paper's tables, bypassing
    the locked extraction CSV. The resulting numbers then flow into the response letter, the
    revised manuscript, and regenerated figures, and they can be internally consistent everywhere
    while still being wrong at the source.
    
    **The failure pattern — treat as a lived failure, not hypothetical:**
    > An R1 revision introduced a new comparative-arm analysis script to answer a reviewer
    > request. The Fisher exact matrix was hand-typed from the primary source Table, with an
    > adjacent severity-grade column misread as the event count. The script, the revised
    > manuscript, and an accompanying Table all converged on the same direction-reversed
    > numbers relative to what the primary source actually reported.
    
    **Non-negotiable actions when Step 2 flags any `/analyze-stats` re-run:**
    
    1. **Tag every new numerical claim with `[VERIFY-CSV]`** as it is written into the revised
       manuscript, response letter, or new table. The tag is a tripwire — it only comes off at
       Step 7 (Final Verification) after explicit CSV + primary-source back-check.
    
    2. **New analysis scripts must read from the locked extraction CSV.** Hand-typed `matrix()`,
       `c(...)`, or `data.frame(...)` numerical inputs are PROHIBITED when a CSV row exists. If
       hand entry is truly unavoidable (e.g., comparative-arm subset not present in the CSV), the
       line MUST carry a comment citing the CSV coordinate AND the primary-source Table/Figure:
       ```r
       # source: data_extraction_final.csv row <N> (<first-author> <year>, <arm> only),
       #         verified against <primary source> Table <X>, page <P>
       fisher.test(matrix(c(0, 45, 1, 55), nrow = 2, byrow = FALSE))
       ```
    
    3. **Comparative / arm-specific values must enter `extraction_consensus_log.md` as separate
       rows** before the analysis script references them. Do not let a new script invent values
       that never passed through the dual-extraction consensus layer.
    
    4. **Revision-time numerical audit table** — maintain this inside the response document draft
       and copy into the final change log:
    
       | New claim (response + manuscript location) | Source script:line | CSV row/col | Primary source (Table/Fig, page) | Match? |
       |---|---|---|---|---|
    
    5. **Gate before Step 3** — do not generate response prose for a MAJOR comment whose new
       numbers have not yet cleared this check. Prose written around un-audited numbers is very
       hard to unwind cleanly after a mismatch is found.
    
    **Why this matters for reviewer politics:** a numerical reversal caught by the reviewer in R2
    is far more damaging than the same error caught internally in R1 — it implies extraction
    integrity problems to the editor and licenses deeper scrutiny of the rest of the data. Treat
    Step 2.5 as a reputation-preservation gate, not just a QC step.
    
    ---
    
    ## Step 3: Generate Response to Reviewers Document
    
    **Output location:** `revision/R[N]/response_to_reviewers_R[N].md`
    
    ### Document Header
    
    ```
    Response to Reviewers
    
    Manuscript ID: [JOURNAL-XXXXX]
    Manuscript Title: [Full title]
    Authors: [Last name of first author] et al.
    Revision Round: [R1 / R2 / R3]
    Date: [YYYY-MM-DD]
    
    We thank the Editor and reviewers for their careful reading of our manuscript
    and their constructive comments. We have revised the manuscript accordingly
    and provide a point-by-point response below. All changes are shown in the
    revised manuscript with tracked changes (or highlighted in yellow).
    ```
    
    ### Per-Comment Response Block
    
    ```
    ---
    
    **Comment R[X]-[Y]** [MAJ/MIN/REB]
    
    *Reviewer's comment:*
    > [Exact text of the comment, quoted verbatim]
    
    **Response:**
    
    [Response text -- format by type below]
    
    **Manuscript change:**
    - Section: [Methods / Results / Discussion / etc.]
    - Page [X], Line [Y] (in the revised manuscript)
    - [Quote the new or changed sentence if short]
    ```
    
    ---
    
    ## Step 4: Response Formats by Comment Type
    
    ### MINOR Comment
    
    Keep concise (3-8 sentences). Acknowledge, explain the change.
    
    ```
    We thank the reviewer for this observation. We have [describe change] in
    the [section] section. The revised text now reads: "[new sentence]."
    ```
    
    ### MAJOR Comment
    
    Structured response with four parts: acknowledgment -> new analysis -> key result -> location of changes.
    
    ```
    We thank the reviewer for this important suggestion. [State the concern.]
    
    To address this, we [describe new analysis/experiment/rewrite].
    [Key result: metric = value (95% CI, lower-upper; P = exact value)]
    (All new results MUST include 95% CI and exact p-value.)
    
    This finding [supports / strengthens / does not change] our original
    conclusion because [brief interpretation].
    
    Note: New text added to the Results section must contain only factual
    findings. Interpretation belongs in the response letter text or Discussion.
    
    We have added:
    - New [Table X / Figure X / Supplementary Table X] showing [content]
    - Methods revised: Page X, Lines Y-Z
    - Results revised: Page X, Lines Y-Z
    ```
    
    ### REBUTTAL Comment
    
    Polite but firm. Do not capitulate without scientific justification.
    
    ```
    We thank the reviewer for raising this point. We respectfully suggest
    that [restate reviewer's claim], while we [state your position].
    
    [Explanation with supporting evidence. Cite literature if available:
    "This is consistent with [Author et al., Year; PMID XXXXXX], who
    demonstrated that..."]
    
    [If applicable: "We have added the following clarifying sentence to
    [section] (Page X, Line Y): '[new sentence].'"]
    
    We believe this issue does not warrant [the specific change requested]
    because [reason]. We hope the reviewer finds this explanation satisfactory.
    ```
    
    **Voice caution:** The acknowledgment lines in these templates are schematic placeholders, not literal text to paste under every comment. Repeating the same opener ("We thank the reviewer for this important suggestion.") across a dozen responses is itself an AI-tell that careful reviewers notice. Vary the openers and apply the **Response-Letter Voice & AI-Tell Avoidance** section below before finalizing any response.
    
    ---
    
    ## 5-Category Triage Strategy
    
    Before writing individual responses, classify every comment into one of five categories.
    This classification determines the response template and effort level. Process Category 1
    (Simple) comments first — they are the most numerous and clearing them early reduces the
    perceived workload.
    
    ### Category 1: Simple Question (most common)
    
    Reviewer asks for additional description, clarification, or minor data.
    **Response**: Add the requested text and point to the location. Keep the response short.
    **Example**: "Please specify the study period" → add dates, reply "Done. See page X, line Y."
    
    ### Category 2: Misunderstanding
    
    Reviewer misinterpreted the study design, population, or analysis.
    **Response**: Never say "you are wrong." Instead: "We apologize for the lack of clarity"
    → re-explain the intended meaning → revise the manuscript text to prevent future confusion.
    
    ### Category 3: Further Discussion
    
    Reviewer raises a contextual concern (different healthcare system, different clinical practice).
    **Response**: Acknowledge the valid perspective → explain your study context → add a brief
    note in Discussion if appropriate. The full explanation can stay in the response letter
    without bloating the manuscript.
    
    ### Category 4: Additional Results
    
    Reviewer requests new analysis (subgroup, sensitivity, additional metric).
    **Response**: Perform the analysis → add results to Supplementary (or main text if important)
    → describe what was done and what was found. Treat this as a constructive contribution,
    not an attack. **Never ignore these requests** — reviewer engagement is a positive signal.
    
    ### Category 5: Statistical Method Challenge
    
    Reviewer questions or requests changes to statistical methods.
    **Response**: Provide a reasoned justification for the method with references. If the reviewer's
    suggestion is valid, run both analyses and show the results are consistent. If a statistician was
    in fact consulted, say so; do not write that sentence because it *sounds* credible — a claim about
    who reviewed the work is a claim about the world, and this letter goes to an editor.
    
    ### Mapping to MAJ/MIN/REB
    
    | Category | Typical Classification |
    |----------|----------------------|
    | 1. Simple Question | MIN |
    | 2. Misunderstanding | MIN or REB |
    | 3. Further Discussion | MIN (if text change) or REB (if disagree) |
    | 4. Additional Results | MAJ |
    | 5. Statistical Challenge | MAJ |
    
    Use the 5-category triage to inform the MAJ/MIN/REB classification in Step 1, not replace it.
    
    ---
    
    ## Handling Low-Quality Reviews
    
    Reviewer quality varies widely. When facing comments that suggest the reviewer did not
    carefully read the manuscript:
    
    1. **Do not get combative.** Respond with the same professionalism regardless of review quality.
    2. **Address every point**, even trivial or off-topic ones. Skipping a comment signals
       disrespect to the editor.
    3. **For irrelevant comments**: Add a clarifying sentence to Discussion or Methods, and
       reply: "We have added clarification in [section] to address this concern." This shows
       effort without conceding a scientific point.
    4. **For factually incorrect comments**: Provide evidence (with references) politely.
       Frame as "We believe there may be a misunderstanding" rather than "The reviewer is wrong."
    5. **Remember the audience**: The response letter is read by the editor, not just the
       reviewer. A measured, thorough response demonstrates manuscript quality even when
       the review does not.
    
    ---
    
    ## Response-Letter Voice & AI-Tell Avoidance
    
    A response-to-reviewers letter is a reviewer-facing scientific argument, not an internal
    change-log. The dominant AI-tell in machine-drafted letters is the **editing-mechanism
    register**: prose that narrates *how the text was edited* ("the revised Methods adds one
    sentence at line 88", "a grep-and-soften pass replaced six phrases", "no further manuscript
    change") instead of stating, in plain language, what changed and why.
    
    Three principles when drafting (the AI-tell patterns themselves are defined once in humanize
    `references/ai_patterns.md`, patterns 22-24 — this section is the authoring guidance):
    
    1. **Write the change and the science, not the editing mechanism.** Describe what changed and
       why, and quote the new sentence. Never narrate the diff: no version prefixes ("v2 adds..."),
       no "softened N phrases", no grep/verification language, no internal FIX codes, no bare "No
       further manuscript change" stubs. Describing a *new analysis you ran* ("we performed a
       sensitivity analysis and found X") is the science, not a tell — that is welcome.
    2. **No `§` symbols, no internal draft line numbers.** A revised-manuscript page/line
       ("page 7, lines 177-178", stated once as referring to the revised manuscript) is fine;
       only internal draft line numbers that will not match the reviewer's view are banned.
    3. **Format is free.** Free prose, a structured `Response:` / `Changes made:` block, an
       `Original → Revised` pair, or a left-comment/right-response table are all standard human
       conventions. Pick any; strip only the mechanism narration.
    
    ### Reviewer-facing tone
    
    - **Vary openers.** "We thank the reviewer for this point." / "We agree." / "This is an important concern." / "We have addressed this as follows." Do not repeat one acknowledgment sentence down the whole letter.
    - **Calibrate the stance**: full agreement, partial agreement with a bounded clarification, or a polite, evidence-backed rebuttal. Match the register to the substance.
    - **Admit error plainly** when the reviewer is right ("The reviewer is correct; we have corrected this.") — natural humility reads as human and builds editor trust.
    - **Quote the new manuscript text** verbatim in quotation marks, then name its section — what experienced authors do, and the single strongest human signal across real letters.
    
    ### Succinctness & non-defensiveness (especially R2+)
    
    Let the point-by-point prove the work; strip the pre-emptive defence. This matters most on **R2+ rounds**, where over-explaining reads as anxiety rather than rigor.
    
    - **No pre-emptive hand-holding.** Drop "Reviewers 2 and 3 also accepted this," "we left it unchanged because the other reviewers were satisfied," and similar cross-reviewer lobbying. Answer the comment in front of you.
    - **A satisfied reviewer gets one sentence.** If a reviewer is content or offers only praise, "We thank the reviewer." or a single sentence is the whole response. Do not manufacture paragraphs.
    - **Cut defensive meta-comments.** Remove "We confirm this statement is unchanged and not softened," "These passages already make the point, so no further text was added." State plainly where the matter is handled and move on.
    - **Fold methodology disclosure into the comment it answers.** Multiplicity, a SAP deviation, or an analysis caveat goes inside the relevant response — not into a separate "Statistical note" front section. Keep the disclosure (never hide a deviation), but keep it in place.
    - **Split, do not bundle.** When a reviewer packs several points into one paragraph, answer each as its own comment with that reviewer sentence quoted, not one block reply to the whole paragraph. Succinct means short *answers*, not fewer *comments*.
    
    See `${CLAUDE_SKILL_DIR}/references/r2r_voice.md` for the before/after gallery, response
    skeletons, and the meta-phrase conversion table.
    
    ### Mandatory pre-submission scan
    
    Before circulating or uploading the response letter and cover letter, run `/humanize` on
    **both documents**. The R2R AI-tell patterns (22-24) are defined in humanize
    `references/ai_patterns.md`; together with 13 (em dash), 16 (filler), and 19 (`§`) they form
    the response-letter scan. Hold the letter to the same classical-style bar as the manuscript:
    zero `§` symbols and no `(Methods §X)` self-references, em-dash use kept low, and the heading
    style the target journal actually publishes. The enforced item list lives in `/write-paper`
    `references/section_guides/step7_1_classical_qc.md` — these are the marks a senior reviewer
    reads as machine-drafted, and they are as visible in a letter as in a manuscript.
    
    ### Response-claim verification gate (MANDATORY, deterministic)
    
    The single source of truth is the **revised manuscript**, not the response prose. A letter
    that says *"we added the sentence '…'"* or *"we now cite Tariq et al. [15]"* must be
    verifiable in the body — a claimed edit that was never actually inserted is a reputation-fatal
    class that both a reviewer round and the authors have missed. Run the gate before sending:
    
    ```bash
    python3 ${CLAUDE_SKILL_DIR}/scripts/check_response_claims.py \
      --response revision/response_to_reviewers.md \
      --manuscript manuscript/manuscript.md --strict
    ```
    
    It flags `RESPONSE_QUOTE_UNVERIFIED` (a quoted added sentence absent from the body) and
    `RESPONSE_CITATION_UNVERIFIED` (an added citation whose token is nowhere in the body). It is
    conservative — vague, paraphrased claims are not flagged — so a firing verdict is a real
    discrepancy: either insert the promised edit or correct the response wording. This directly
    enforces the *"quote the new manuscript text verbatim"* discipline above, and is the same
    check a reviewer runs against your revision (see `/peer-review`).
    
    A third verdict, `RESPONSE_QUOTE_UNRESOLVED` (**minor**, never drift), exists because the
    manuscript is often read through an extractor. When the quoted words are all present **in
    order** but separated by foreign tokens — a reference column bled into the sentence by a
    two-column PDF, line numbers from a supplement proof, a footnote marker, a hyphen split across
    a line — the text is there and only the extraction is dirty. A contiguous substring test
    cannot tell that from a missing edit and reports the correct quote as absent; that once came
    one step from having two accurate verbatim quotes deleted. So those cases are reported for a
    human to eyeball and do **not** fail `--strict`; only a genuinely absent quote does.
    
    **If a reviewer called the manuscript too long or too dense, prove the body got shorter.** Answering
    a density comment point-by-point is a trap: each point is answered by adding a sentence, so the
    revision that responds to "shorten this" comes back *longer*. Revisions do exactly that: the
    point-by-point answer adds words, the shrink the letter asked for slips another round away, and
    nothing in the response letter records that it happened. This gate is arithmetic: if the decision
    letter contains a density/length complaint and the revised body did not shrink, it fires.
    
    ```bash
    python3 ${CLAUDE_SKILL_DIR}/scripts/check_density_complaint.py \
      --comments revision/decision_letter.md \
      --previous manuscript/manuscript_R0.md \
      --revised manuscript/manuscript.md --strict
    ```
    
    `DENSITY_COMPLAINT_UNADDRESSED` fires only when a complaint was raised AND the body word count
    (Introduction through Discussion, citation markers excluded) did not fall. With no density complaint
    it stays silent — it is not a "shorter is always better" nag. When it fires, cut or move detail to
    the supplement; do not defend the length by adding a paragraph that explains it.
    
    ---
    
    ## Step 5: Cover Letter to Editor
    
    **Output location:** `revision/R[N]/cover_letter_R[N].md`
    
    ```
    [Date]
    
    Dear Dr. [Editor Name / "Editor-in-Chief"],
    
    Thank you for the opportunity to revise our manuscript, "[Full title]"
    (Manuscript ID: XXXX), submitted to [Journal Name]. We have carefully
    reviewed the comments from the Editor and reviewers and have revised
    the manuscript accordingly.
    
    In brief, the principal changes in this revision are: [1) ..., 2) ...,
    3) ...]. A point-by-point response to each comment is provided in the
    accompanying Response to Reviewers document. Revised sections are
    highlighted in yellow in the manuscript.
    
    We believe the revised manuscript addresses all concerns raised in the
    review and is now suitable for publication in [Journal Name].
    
    Sincerely,
    
    [First Author Name], MD/PhD
    [Institution]
    [Email]
    On behalf of all authors
    ```
    
    ### R1 vs R2+ cover-letter protocol
    
    The template above is the **R1** convention: a standalone editor cover letter (200-400 words).
    
    On an **R2+ round (second revision onward), do not write a separate cover letter.** Whatever you would say to the editor — the greeting and the brief change summary — belongs in the **head of the response-to-reviewers letter**, not in a second document. A standalone cover letter that merely restates the response letter's summary reads as redundant and, on later rounds, as boilerplate. If an earlier round already produced a `cover_letter_R1.md`, move it to `_superseded/`, exclude it from the R2+ package, and reuse the response-letter head verbatim in any portal "cover letter" field. (Exception: a journal that explicitly requires a separate cover letter at every round — then keep the head summary and the cover letter from duplicating each other.)
    
    **Response-letter head (R2+)** — placed at the top of `response_to_reviewers_R[N].md`, before the point-by-point:
    
    ```
    Dear Dr. [Editor Name / "Editor-in-Chief"],
    
    Thank you for the opportunity to revise our manuscript once more. In brief, this
    revision [1-2 sentence summary of the principal changes — e.g., "adds the requested
    subgroup analysis and tempers the three comparisons the reviewers flagged as
    over-stated"].
    
    [If applicable: one sentence on a companion paper, a re-analysis, or a verification
    the editor requested.]
    
    All quotations below are from the revised manuscript. A point-by-point response to each
    comment follows.
    
    Sincerely,
    [First Author Name], on behalf of all authors
    ```
    
    Keep the head to a short greeting, a one-paragraph "in brief," an optional companion/verification note, the single line stating quotations are from the revised manuscript, and the signature. Everything else is point-by-point.
    
    ---
    
    ## Step 6: Change Log
    
    **Output location:** `revision/R[N]/change_log_R[N].md`
    
    | Comment | Type | Change Made | Section | Page | Lines |
    |---------|------|-------------|---------|------|-------|
    | R1-1 | MAJ | Added subgroup analysis by scanner type | Results 4.3, Table 3 | 12 | 234-251 |
    | R1-2 | MIN | Clarified exclusion criteria for motion artifact | Methods 2.2 | 6 | 112-115 |
    
    ---
    
    ## Step 7: Final Verification
    
    After all responses are drafted, check:
    
    - [ ] Every reviewer comment has a response (none skipped)
    - [ ] Every MAJOR comment has a corresponding manuscript change with location
    - [ ] Every REBUTTAL is backed by cited evidence or clear scientific reasoning
    - [ ] All new statistics include 95% CI and exact p-values
    - [ ] Page/line number references match the revised manuscript (not the original)
    - [ ] No internal draft line numbers ("(line 43)"); locations point to section names or revised-manuscript page/line
    - [ ] No `§` symbols and no editing-mechanism narration ("v2 adds one sentence", "grep verification", "No further manuscript change")
    - [ ] Acknowledgment openers varied (not one sentence repeated across responses)
    - [ ] Response letter AND cover letter ran through `/humanize` (patterns 22-24 triage hits reviewed; confirmed instances = 0; `§` = 0 hard)
    - [ ] (R2+) No separate cover letter — the editor greeting and "in brief" summary are folded into the response-letter head
    - [ ] (R2+) Satisfied reviewers get ≤1-2 sentences; no pre-emptive hand-holding or cross-reviewer lobbying
    - [ ] Multi-point reviewer paragraphs are split into discrete comments (reviewer sentence quoted + Response N), not answered as a block
    - [ ] Methodology disclosure (multiplicity, SAP deviation) is folded into the relevant response, not a separate front section
    - [ ] Cover letter is addressed to the correct editor
    - [ ] Response letter is 5000-8000 words
    - [ ] The marked manuscript passed the round-trip gate (below) — not merely "tracked changes are on"
    - [ ] All new figures/tables are referenced in the response letter
    
    ### The marked manuscript is gated, not eyeballed
    
    The journal wants the revised paper with tracked changes against **the version the reviewers saw** (R0 — not the previous round). Produce it with Word's Compare, which `/sync-submission` drives from the command line, and verify it with a round trip rather than a spot-check: accepting every revision must reproduce the revised manuscript exactly, and rejecting every revision must reproduce the original. Confirming that "sentence X appears as an insertion" passes even when Compare has dropped a paragraph or attributed half the changes to another author.
    
    ```bash
    python3 <medsci-skills>/skills/sync-submission/scripts/check_marked_manuscript.py \
      --marked revision/R1/manuscript_marked.docx \
      --original submission/R0/manuscript.docx \
      --revised revision/R1/manuscript_clean.docx \
      --author "Submitting Author" --strict
    ```
    
    See `/sync-submission` Phase 10 for the build step and for why the check must be move-aware (`w:moveFrom` / `w:moveTo` are not `w:ins` / `w:del`).
    
    ---
    
    ## Revision Round File Structure
    
    | Round | Folder | Files |
    |-------|--------|-------|
    | R1 | `revision/R1/` | `response_to_reviewers_R1.md`, `cover_letter_R1.md`, `change_log_R1.md` |
    | R2 | `revision/R2/` | `response_to_reviewers_R2.md`, `cover_letter_R2.md`, `change_log_R2.md` |
    
    Revised manuscript: `paper/main_revised_R[N].tex` (or `.qmd`)
    
    For R2+, acknowledge whether R1 concerns were fully resolved. If a reviewer raises a new concern at R2, note: "This comment was not raised in the first review round; we address it as follows."
    
    ---
    
    ## Word Count Guidance
    
    - Response letter total: 5000-8000 words (including quoted reviewer comments)
    - Cover letter: 200-400 words (R1 only; on R2+ there is no separate cover letter — see Step 5)
    - MINOR response: 50-150 words
    - MAJOR response: 150-400 words
    - REBUTTAL response: 200-500 words
    - **R2+ rounds run leaner.** Most R1 concerns are already resolved, so the letter is shorter and a satisfied reviewer's response is 1-2 sentences. Do not pad an R2+ reply to reach the R1 range.
    
    ---
    
    ## Common Mistakes to Avoid
    
    1. Do not agree with every MAJOR comment without providing the actual new data or analysis.
    2. Do not write vague responses ("We have revised the text accordingly") without specifying what changed and where.
    3. Do not skip any comment, even if trivial or addressed elsewhere.
    4. Do not reference page/line numbers from the original manuscript; use the revised version.
    5. Do not begin a rebuttal aggressively; always open with acknowledgment.
    6. Do not promise changes that were not actually made.
    7. Do not forget to renumber figures and tables if new items were inserted.
    
    ## Anti-Hallucination
    
    - **Never fabricate references.** All citations must be verified via `/search-lit` with confirmed DOI or PMID. Mark unverified references as `[UNVERIFIED - NEEDS MANUAL CHECK]`.
    - **Never invent clinical definitions, diagnostic criteria, or guideline recommendations.** If uncertain, flag with `[VERIFY]` and ask the user.
    
    ---
    
    ## Gates
    
    | Gate | Severity | Trigger | Action on fail |
    |---|---|---|---|
    | Comment classification (MAJOR / MINOR / REBUTTAL) | ENFORCED | comment unclassified or classification disputed | ask user; do not silently default |
    | Step 2.5 `[VERIFY-CSV]` tagging on revision-introduced numbers | ENFORCED | new numerical claim added without `[VERIFY-CSV]` tag | tag automatically; HALT until CSV cross-check completes |
    | Reference re-render after revisions touching citations | ENFORCED | any new `[@bibkey]` added in R1+ | route to `/manage-refs` Phase 7.6 re-render before R1 submission |
    | `/verify-refs --strict` post-revision | ENFORCED | FABRICATED / HIGH_MISMATCH_FIRST_AUTHOR > 0 | HALT R1 submission |
    | New analysis coordination | ENFORCED | reviewer asks for new analysis | route to `/analyze-stats` (and `/make-figures` if figure changes); never hand-write new numbers |
    | Body word count vs journal cap (revision-inflation trap) | ENFORCED after every revise pass | resolving majors pushes the body over the target journal's word limit | run `/sync-submission` `scripts/check_wordcount_cap.py` (`--journal-profile` or `--limit`; prefer the rendered DOCX count); `WORDCOUNT_OVER_CAP` blocks submission — relocate methods/sensitivity detail to the Supplement, do not silently exceed |
    | Cover letter to editor | ENFORCED at R1 submission | R1 missing editor cover letter | block submission |
    | R2+ cover-letter handling | ENFORCED at R2+ submission | standalone cover letter present on an R2+ round (not folded into the response-letter head) | move it to `_superseded/`; fold the summary into the head |
    | Response-letter voice / AI-tell | ENFORCED before submission | editing-mechanism narration, internal draft line refs, `§`, tooling leak, or repeated openers in response/cover letter | run `/humanize` (patterns 22-24 as triage; `§` = 0 hard); resolve confirmed tells before submission |
    
  • skill.yml 2.4 KB
    schema_version: 2
    name: revise
    layer: B
    owner_domain: reviewer_response
    maturity: official
    when_to_use:
      - User received reviewer comments and needs a structured response-to-reviewers letter
      - Coordinating new analyses or figures triggered by reviewer requests (delegates to /analyze-stats, /make-figures)
      - Drafting a cover letter for a revision (R1 / R2 / minor / major)
      - Tracking R0→R1 manuscript changes with revision-introduced number `[VERIFY-CSV]` tags
    when_NOT_to_use:
      - Pre-submission self-criticism (use /self-review)
      - Drafting an original manuscript (use /write-paper)
      - Editing tone or AI patterns only (use /humanize)
      - Arguing valid reviewer points as rebuttal without user approval (forbidden)
    inputs:
      - reviewer_comments
      - manuscript/manuscript.md
    outputs:
      - revision/response_to_reviewers.md
      - revision/change_log.md
      - qc/response_claims.json
    deterministic_scripts:
      - scripts/check_response_claims.py
    side_effects:
      - may_edit_manuscript_with_user_approved_changes
    downstream_consumers:
      - sync-submission
      - verify-refs
    forbidden_actions:
      - argue_valid_reviewer_points_as_rebuttal_without_approval
      - reference_original_page_lines_after_revision
    
    # v2.1 quality card
    purpose: "Parse reviewer comments and generate a structured Response to Reviewers with tracked manuscript changes and an editor cover letter."
    safety_boundaries:
      - "Valid reviewer points are not argued away as rebuttal without user approval."
      - "Does not reference original page/line numbers after the manuscript has been revised."
    known_limitations:
      - "Coordinates new analyses/figures via other skills but does not itself produce statistics."
      - "No standalone demo; depends on the actual reviewer comments supplied."
      - "check_response_claims verifies only anchored claims (quoted added text, added citations); paraphrased edits are not machine-verifiable and are not flagged."
    validation_commands:
      - "confirm every reviewer comment maps to a point-by-point response"
      - "/verify-refs --strict on new citations"
      - "python3 scripts/check_response_claims.py --response revision/response_to_reviewers.md --manuscript manuscript/manuscript.md --strict"
      - "bash tests/test_response_claims.sh"
      - "python3 scripts/check_density_complaint.py --comments revision/decision_letter.md --previous manuscript/manuscript_R0.md --revised manuscript/manuscript.md --strict"
      - "bash scripts/density_complaint_challenge/verify.sh"
    evidence_surface: bundled_script
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related