meta-analysis
Systematic review and meta-analysis pipeline for medical research. Covers protocol registration (PROSPERO), search strategy, screening, data extraction, risk of bias assessment (QUADAS-2/ROBINS-I), statistical synthesis (bivariate/HSROC for DTA, random-effects for intervention),
Install
npx skills add https://github.com/Aperivue/medsci-skills/tree/main/skills/meta-analysis
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install aperivue-medsci-skills@llmmart
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
Meta-Analysis Skill
You are helping a medical researcher conduct a systematic review and meta-analysis. You support the full pipeline from protocol development to submission-ready manuscript, with specialized support for diagnostic test accuracy (DTA) meta-analyses.
Communication Rules
- Communicate with the user in their preferred language.
- All output documents, code, and checklists in English.
- Medical terminology always in English.
Reference Files
Built-in References (`$
- PROSPERO template:
${CLAUDE_SKILL_DIR}/references/PROSPERO_template.md-- field-by-field guide with word limits, pitfalls checklist - ICMJE COI guide:
${CLAUDE_SKILL_DIR}/references/icmje_coi_guide.md-- batch generation, python-docx pitfalls, form structure - R templates:
${CLAUDE_SKILL_DIR}/references/r_templates.md - Checklists:
${CLAUDE_SKILL_DIR}/references/checklists/PRISMA_DTA.md-- 27-item checklistQUADAS3.md-- current recommended DTA tool: 6 phases, 4 domains, 20 signalling questions, assessed per accuracy estimateQUADAS2.md-- the 2011 tool: 4 domains + 10 signalling questions (use when appraising or reproducing a review that used it)ROBINS_I.md-- 7 domains + pre-assessment + synthesis recommendationRoB2.md-- 5 domains + signalling questions + overall judgmentPROBAST.md-- 4 domains + AI extension + validation studiesNOS.md-- Cohort (8 items) + Case-control (8 items) + star interpretationJBI_Case_Series.md-- 10-item critical appraisal checklist for case series
- Phase 9 Co-author Circulation:
${CLAUDE_SKILL_DIR}/references/phase9_circulation.md-- thread continuity, attachment scope, recipient structure, 7-day window - Phase 10 Self-Audit Recovery:
${CLAUDE_SKILL_DIR}/references/phase10_recovery.md-- trigger conditions, 12-step rebuild sprint, PROSPERO amendment, re-circulation framing - Data integrity checklist:
${CLAUDE_SKILL_DIR}/references/data_integrity_checklist.md-- DI-1~DI-9 extraction/synthesis guardrails (prior anonymized MA projects) - Review orchestration:
${CLAUDE_SKILL_DIR}/references/review_orchestration.md-- RO-1~RO-5 circulation discipline (extends phase9_circulation.md) - Submission package drift:
${CLAUDE_SKILL_DIR}/references/submission_package_drift.md-- multi-journal folder hygiene,DO_NOT_EDIT_HEREgate,_build.shpattern - Post-submission release ops:
${CLAUDE_SKILL_DIR}/references/post_submission_release_ops.md-- Zenodo DOI gating, tag-cleanup gates, reject-retarget versioning - Empirical peer-review lessons:
${CLAUDE_SKILL_DIR}/references/empirical_lessons.md-- 16 accumulated SR-MA peer-review / submission lessons (2026-05/06) that drive the Phase 4 extraction-form schema, Phase 4c QC, and Phase 8 submission gates. Load before designing the extraction form and before submission.
Built-in Templates (`$
- Extraction Form v2 (
templates/extraction_form_v2.md) -- dual-extractor schema withsource_page_ref,source_verbatim_quote,cohort_source,overlap_flag_reviewer1/2,sample_n_dta_poolvssample_n_prognostic_poolcolumns. Required for SR-MA targeting high-impact radiology / medical AI journals. - Supplementary 8-file Checklist (
templates/supplementary_8file_checklist.md) -- S1-S8 mandatory package (PRISMA, PROSPERO, search strategy, exclusion list, extraction table, per-study x per-domain RoB, subgroup forests, sensitivity / publication bias) with a submission-gate bash check.
Built-in Scripts (`$
screening_reconcile.py-- Phase 3f ID-set screening reconciliation.check_pool_consistency.py-- pool-composition / PRISMA count consistency.cohort_overlap_check.py-- shared-database cohort-overlap detection.extract_assist.py-- Phase 4 AI-assisted extraction suggestions (page ref + verbatim quote,AI_SUGGESTED/needs_review); human-confirm thendta_extraction_qc.py. Challenge card:scripts/extract_assist_challenge/.dta_extraction_qc.py-- 2x2 cell ↔ source sens/spec QC on the confirmed extraction CSV.
Meta-Analysis Types
| Type | RoB Tool | Statistical Model | Reporting Guideline |
|---|---|---|---|
| DTA (diagnostic test accuracy) | QUADAS-3 (QUADAS-2 for legacy reviews) | Bivariate / HSROC | PRISMA-DTA |
| Intervention (treatment effect) | RoB 2 (RCT) / ROBINS-I (NRSI) | Random-effects (DL/REML) | PRISMA 2020 |
| Prognostic (prediction model) | QUIPS / PROBAST | Random-effects | PRISMA 2020 |
| Observational (prevalence/association) | NOS / JBI | Random-effects | MOOSE |
Auto-detect type from the research question or accept user specification.
Workflow Phases
Phase 1: Protocol Development
Goal: Produce a PROSPERO-ready protocol document.
Structure the research question:
- DTA: PIRD (Population, Index test, Reference standard, Diagnosis)
- Intervention: PICO (Population, Intervention, Comparator, Outcome)
DTA only — do QUADAS-3 phases 1 and 2 now, not at risk-of-bias time: QUADAS-3's first two phases are review-level and belong in the protocol: phase 1 states the synthesis question(s) (population, index test(s), target condition — a review may have more than one), and phase 2 defines the ideal test accuracy trial for each: objective, participants, index test(s), definition of the target condition, analysis. Every later risk-of-bias and applicability judgement is made against that trial. Write the review-specific guidance for answering each signalling question here too, with clinical and methodological input, and publish it as a web appendix. Defining the ideal trial after seeing the studies is not an assessment — it is a judgement fitted to the results. See
references/checklists/QUADAS3.md.Define eligibility criteria:
- Study design (cross-sectional DTA, cohort, RCT, etc.)
- Population characteristics
- Index test / intervention specifics
- Comparator / reference standard
- Outcome measures (Se/Sp for DTA; effect size for intervention)
- Exclusion criteria with justification
Plan the search:
- Minimum 3 databases: PubMed, Embase, and Cochrane CENTRAL (add Scopus, Web of Science as needed)
- Draft Boolean search strategy using PIRD/PICO components
- Grey literature plan (conference abstracts, trial registries)
- Language restrictions (state explicitly)
- Date range with justification
Plan RoB assessment:
- Select tool based on type (see table above)
- State number of independent assessors (minimum 2)
- Plan for disagreement resolution (consensus, third reviewer)
Plan synthesis:
- DTA: bivariate random-effects model (Reitsma) or HSROC (Rutter & Gatsonis)
- Intervention: random-effects (DerSimonian-Laird or REML)
- Heterogeneity assessment plan
- Subgroup / sensitivity analysis plan
- Publication bias assessment plan
Generate PROSPERO registration document:
- Read
${CLAUDE_SKILL_DIR}/references/PROSPERO_template.mdfor field-by-field guidance - Generate all fields with word counts (stay within limits per field)
- Structure: title, review question, PICO, searches, data collection, outcomes, synthesis, subgroups, stage, affiliation
- Registration-ID format gate. A PROSPERO ID is
CRD42+ 9 digits (14 characters total), e.g.CRD42024500001. Validate any ID that appears in the manuscript or registration doc withgrep -oE 'CRD42[0-9]+'and assert a 14-character length /^CRD42\d{9}$— a 15-character ID (a stray digit) is a transcription error a reviewer will check against the live record. - Review-type selection. Pick the least-wrong portal review type for the actual design and state any portal constraint in the protocol. A descriptive single-arm proportion synthesis is not an "Intervention review"; choosing "Intervention review" only to satisfy a portal field contradicts a later GRADE / effect-certainty statement. Whatever certainty language the protocol commits to (GRADE vs "evidence statements only") must match the manuscript verbatim — a guideline-style "we recommend" is not licensed by a descriptive review type.
- For mixed designs (comparative + single-arm): explicitly address comparator for both arms
- For RoB: map tool to study design (NOS for comparative, JBI for case series → select "Other" in form)
- Output: Markdown + DOCX (via pandoc) for copy-paste into PROSPERO web form
- Append Common Pitfalls Checklist (HTML entities, word limits, stage constraint)
- Save to project
7_Submission/or equivalent directory
- Read
Phase 2: Search Strategy
Goal: Develop and validate reproducible search strategies.
Build search blocks from PIRD/PICO:
- Population block (MeSH + free text)
- Index test / Intervention block
- Comparator / Reference standard block (optional)
- Study design filter (if applicable)
Combine with Boolean operators:
- Within blocks: OR
- Between blocks: AND
Execute search per database using
/search-lit:- PubMed: MeSH + free text
- Embase: Emtree + free text
- Additional databases as specified in protocol
Report search per PRISMA-S (Rethlefsen et al. 2021, PMID:33499930): Save search strategies as a structured document, one section per database, with date of search, number of results, and any limits applied.
Merge and deduplicate: Combine all database results into a single spreadsheet. Deduplicate by DOI first, then PMID. Save raw counts for PRISMA flow.
Phase 3: Screening & Selection
Goal: Systematic title/abstract and full-text screening with two independent reviewers.
3a. Round 1 — initial title/abstract screening (single reviewer). Define the exclusion codes
from the protocol (E1=Not target population, E2=Not intervention, E3=Ineligible type, E4=Non-human,
E5=Duplicate). Mark every record INCLUDE / EXCLUDE / MAYBE with a reason code → round1_{date}.tsv.
3b. Round 2 — dual independent title/abstract screening. A second independent reviewer (or AI
as a documented second-pass tool with human verification) re-screens all R1 records. Compute
Cohen's κ and report it in Methods. round2_tag = INCLUDE / EXCLUDE / MAYBE, where MAYBE means
disagreement or either reviewer flagged uncertainty → round2_tag, round2_reason columns.
3c. Round 3 — adjudication of disagreements (first reviewer). Build the R3 sheet with all MAYBE
records first, then INCLUDE records for a brief confirmation pass. The first reviewer independently
adjudicates each row (round3_decision, plus round3_reason only when overturning R2). Optional
AI-assisted pre-screening can compress the effort — but AI suggestions are not decisions: the
reviewer independently confirms or overturns every one. Template, sort priority, and the required
Methods boilerplate are in the reference file.
3d. Round 4 — full-text screening. Retrieve full texts for round3_decision = INCLUDE (use
/fulltext-retrieval), apply the full-text exclusion codes (F1=No extractable outcome, F2=No
comparative data, F3=Cannot separate target population, F4=Inadequate sample/follow-up,
F5=Full-text unavailable), with two independent reviewers, Cohen's κ, and consensus or a third
reviewer for disagreements. Flag comparative studies for priority extraction.
3e. PRISMA flow. Track counts at every stage (R1 → R2 → R3 → R4 → final included); generate the
diagram with /make-figures once the numbers are final.
3f. Post-consensus count reconciliation gate (MANDATORY before Phase 5 write-up). Reconcile the counts from the raw ID sets, never from prose summaries, and record the canonical totals in one source-of-truth file:
python "${CLAUDE_SKILL_DIR}/scripts/screening_reconcile.py" \
--screening 2_Screening/fulltext_screening.tsv \
--consensus 2_Screening/consensus_decisions.tsv \
--table1 6_Tables/table1_studies.csv \
--output 2_Screening/screening_consensus.json
Downstream stages consume screening_consensus.json for counts and ID sets; the Markdown consensus
document remains the human explanation. Three hard rules:
- List the narrative-only IDs explicitly. The highest-yield red flag is a numeric claim ("10
narrative-only studies") that does not match the enumerable set
(A ∪ C) \ B \ T. - No "N → M" transition without ID receipts. "k rose from 30 to 32 after FLAG consensus" must cite the added/removed IDs. A transition claim with no enumerable ID set is a P0 and blocks the Phase 5 hand-off.
STAGE_TRANSFER_LOSSis a P0. Exit 1 when a record is included at screening but absent from the consensus artifact altogether — no adjudication was ever recorded. An exclusion is a decision; silence is a gap. Never let it settle into narrative-only (why: reference file).
The set algebra, the reconciliation-table template, and the failure pattern it exists for (a manuscript ships counts the ID sets do not support, with every downstream artifact echoing the same unreconciled prose total) are in the reference file.
3f.5 Pool composition lock (MANDATORY at adjudication freeze). Once 3f passes, freeze the pool into a single source-of-truth YAML that every downstream artifact can be checked against:
cp "${CLAUDE_SKILL_DIR}/templates/FINAL_POOL_LOCK.yaml.template" 2_Data/FINAL_POOL_LOCK.yaml
# fill counts + UID lists from 3f, compute the SHA-256 over the sorted UID list,
# and COMMIT THE LOCK before any Phase 4 extraction
- Never re-derive
k includedfrom the extraction TSV at manuscript build time — always referencefinal_pool_nfrom the lock. - Aggregate patient/lesion totals are locked too, not just study counts. Distinguish arm-separable from both-arm rows: a study contributing one arm must not have its full-cohort count folded into a pooled total. A hand-carried headline total that does not re-derive from the locked per-study values is a P0.
- A late post-freeze change to the pool is a formal PROSPERO amendment: file it, re-freeze as
FINAL_POOL_LOCK_v2.yaml, and propagate to every artifact.
Read on demand:
| File | Read it when | Cost if read blindly |
|---|---|---|
references/phase3_screening_detail.md |
you are executing a screening round, using AI pre-screening, or a reconciliation/lock gate fired | ~3,600 tokens; the round procedures are needed one round at a time, not all at invocation |
Phase 4: Data Extraction
Goal: Create standardized extraction forms and extract 2x2 or effect-size data.
4.0 Entry gate (MANDATORY) — pool composition lock ↔ adjudication TSV. Before any extraction
work begins, confirm the round-3 adjudication TSV and FINAL_POOL_LOCK.yaml (Phase 3f.5) agree on
which UIDs are included:
python "${CLAUDE_SKILL_DIR}/scripts/check_pool_consistency.py" \
--lock 2_Data/FINAL_POOL_LOCK.yaml \
--adjudication-tsv 2_Screening/round3_adjudication.tsv \
--decision-col round3_decision --uid-col uid \
--include-labels "INCLUDE,INCLUDE_MIXED" \
--out qc/pool_consistency.json
The gate fails closed: any UID disagreement blocks extraction. Resolve by re-freezing the lock with the corrected UID set (and propagating downstream) or by correcting a mis-labelled TSV row. Do NOT proceed with a mismatch — the extraction matrix will not align with the locked pool, and the drift surfaces as a fabrication-grade red flag at peer review.
Failure-mode cross-ref →
references/data_integrity_checklist.mdDI-1~DI-5 are mandatory during extraction (2x2 arm-swap, KM audit trail, methodology mismatch, PRISMA 5-way drift, single-source k).
Extraction form. For an SR-MA targeting high-impact radiology / medical AI journals use
${CLAUDE_SKILL_DIR}/templates/extraction_form_v2.md — its dual-extractor, source-page-reference,
and verbatim-quote columns are what close the 2x2 cell-swap and cohort-overlap blind spots. The
DTA and intervention field lists are in the reference file.
AI-drafted starting document — treat as hallucination-suspect. If a mentor or collaborator
shared an AI-drafted study list, 2x2 set, or effect estimates (even flagged "for reference
only"): save it with a _DO_NOT_USE_VERBATIM suffix and re-verify every N, denominator, event
count, OR/CI, and author/year against the source PDF. Trust hierarchy: source PDF + own analysis
stdout > the mentor's direct text > the attached AI draft — never promote a draft up that ladder.
Procedure and precedent: reference file.
4b. Special cases (KM reconstruction, composite exposure). When studies report outcomes only as
Kaplan-Meier curves, or the intervention is a composite of techniques, load
${CLAUDE_SKILL_DIR}/references/phase4_km_composite.md for the WebPlotDigitizer → IPDfromKM
procedure (cite Guyot et al. 2012, doi:10.1186/1471-2288-12-9) and the 4-path composite-exposure
decision tree. Pre-specify a sensitivity analysis excluding composite-exposure studies.
Cross-verification (≥2 independent reviewers). Report inter-reviewer agreement (% or Cohen's
κ) at title/abstract and full-text stages. Verify denominator consistency — the denominator may
differ across outcomes within one study, so for each outcome back-calculate event ÷ denominator
and confirm it reproduces the paper's reported percentage. Distinguish KM-curve estimates from raw
event counts and record the data source (Table / KM / text). Log every consensus decision in
{project}/consensus_log.md, then lock the dataset; later changes need a dated justification.
4c. Extraction QC & cohort overlap. After dual-extractor consensus, run both before locking:
# 2x2 cell integrity: validates TP/FN/TN/FP against source-reported sens/spec (catches arm-swap)
python3 "${CLAUDE_SKILL_DIR}/scripts/dta_extraction_qc.py" \
--input 2_Extraction/extraction.csv --tolerance 0.02 \
--out 2_Extraction/qc/dta_extraction_qc.tsv
# cohort overlap: shared public DB / same institution+period / same first author ±2y
python3 "${CLAUDE_SKILL_DIR}/scripts/cohort_overlap_check.py" \
--input 2_Extraction/studies.csv --enrich \
--out 2_Extraction/qc/cohort_overlap.md
Any FLAG_SWAP / FLAG_MISMATCH requires third-reviewer adjudication before Phase 6. A
confirmed flag is not resolved until the extraction form itself is edited — a flag corrected only
in a review note silently re-enters synthesis, so re-run the QC and confirm zero open flags before
locking. HIGH-confidence overlap pairs require a Limitations acknowledgment plus a sensitivity
analysis excluding one of the pair. Cross-links: /peer-review Phase 2A P1 + P2.
Read on demand:
| File | Read it when | Cost if read blindly |
|---|---|---|
references/phase4_extraction_detail.md |
building the extraction form, an AI draft was shared, you want the optional extract_assist.py scaffolding, or a QC flag fired |
~4,700 tokens; a clean dual-extraction with no AI draft needs none of it |
references/phase4_km_composite.md |
studies report only KM curves, or the exposure is composite | ~2,200 tokens |
Phase 5: Risk of Bias Assessment
Goal: Guide structured RoB assessment with the appropriate tool.
DTA: this phase runs QUADAS-3 phases 3–6 (flow diagram, identify the estimates to assess, assess, overall judgement). Phases 1–2 — the synthesis question and the ideal test accuracy trial — were written in Phase 1 above. If they were not, stop and write them before judging anything; they are the comparator every judgement is made against.
Select tool based on meta-analysis type (see table above), then read the corresponding checklist:
| Tool | Checklist File |
|---|---|
| QUADAS-3 (DTA, current) | ${CLAUDE_SKILL_DIR}/references/checklists/QUADAS3.md |
| QUADAS-2 (DTA, legacy) | ${CLAUDE_SKILL_DIR}/references/checklists/QUADAS2.md |
| RoB 2 (RCT) | ${CLAUDE_SKILL_DIR}/references/checklists/RoB2.md |
| ROBINS-I (NRSI) | ${CLAUDE_SKILL_DIR}/references/checklists/ROBINS_I.md |
| PROBAST (Prediction) | ${CLAUDE_SKILL_DIR}/references/checklists/PROBAST.md |
| NOS (Observational) | ${CLAUDE_SKILL_DIR}/references/checklists/NOS.md |
| JBI (Case Series) | ${CLAUDE_SKILL_DIR}/references/checklists/JBI_Case_Series.md |
For AI/ML prediction models, also apply PROBAST+AI extensions.
Output: Summary table + traffic light plot (use /make-figures).
Phase 6: Statistical Synthesis
Goal: Execute meta-analysis and generate publication-ready outputs.
Failure-mode cross-ref →
references/data_integrity_checklist.mdDI-6/DI-7/DI-9 are the consistency gate (CSV ↔ script ↔ prose; single-source k; 3-way numeric reconciliation before Stage 4).
IMPORTANT: Always use R for meta-analysis (packages: meta, metafor, mada).
See ${CLAUDE_SKILL_DIR}/references/r_templates.md for full code templates.
| Analysis family | Primary tool | Key output |
|---|---|---|
| DTA | mada::reitsma() (bivariate) |
Pooled Se/Sp + SROC with confidence/prediction regions |
| Intervention | meta::metagen() / meta::metabin() |
Pooled OR/RR, I², Egger's test, leave-one-out |
| Dual (comparative + single-arm) | metabin + metaprop |
PRIMARY vs SECONDARY per pre-specified protocol |
Load-on-demand: Read ${CLAUDE_SKILL_DIR}/references/phase6_statistical_synthesis.md
for the full R code templates, the dual-approach decision table (comparative vs
single-arm), practical cautions (method.tau, HK CI, zero-cell correction),
publication-bias test power, sensitivity-analysis menu, and error-handling rules.
Three checks before the pool is written up — each is a Methods sentence, not only a setting. R and detail in the same reference:
- Is the event rare? A pooled event rate < 1%, or any zero-event arm, moves the analysis off the inverse-variance default onto Peto / Mantel-Haenszel without a zero-cell correction / GLMM. Inverse-variance methods including DerSimonian-Laird are to be avoided for rare events, and so are 0.5 continuity corrections with them.
- Why this model? Fixed vs random is a judgment about whether one common true effect exists — never derived from Cochran's Q or I². "A random-effects model was used because I² was 65%" is a reviewer catch, not a rationale.
- Does one study contribute several correlated effect sizes? Multiple outcomes, readers, thresholds, or time points from the same participants need one pre-specified estimate per study, a multivariate model, or robust variance estimation — not independent pooling.
Phase 6b: Post-Analysis Source Fidelity Audit (MANDATORY)
Goal: Catch numerical hallucinations that survived the forward pipeline (CSV → .R → manuscript).
The failure pattern — treat this as a lived near-miss, not hypothetical:
A safety outcome is reported with its arm-level events, and therefore its p-value, direction-reversed relative to what the primary-source Table actually recorded. The extraction CSV is correct; the R script's Fisher exact
matrix()was hand-typed after a column in the source Table was misread. Internal consistency checks passed because every downstream artifact (Abstract, Discussion, Table, forest caption) echoed the same wrong number. The reversal was caught only on a second-pass audit with random extraction sampling against the primary paper.
Non-negotiable rules:
No hand-typed numerical matrices when a CSV exists.
- Use
read.csv(...)+ subset / filter. Never copy a 2x2 table from a paper's Table intomatrix(c(...), ...)by eye. - If hand entry is truly unavoidable (e.g., text-only extraction), the
matrix,c(), ordata.frameline MUST carry a comment citing the exact CSV row + column OR the exact primary-source Table/Page coordinate. Example:# source: data_extraction_final.csv row <N> (<first-author> <year>), cols <event_arm1>=0, <event_arm2>=1 # verified against primary source Table <X>, page <P> fisher.test(matrix(c(0, 45, 1, 55), nrow = 2, byrow = FALSE))
- Use
Comparative-arm subsets are a separate consensus-log row.
- When one study's arm-specific values (e.g., one arm of a multi-arm study) are used in a
comparative analysis while the full cohort of that study appears elsewhere,
extraction_consensus_log.mdmust carry an explicit row for the arm-specific values. Pooled totals and arm-specific values MUST NOT share a row.
- When one study's arm-specific values (e.g., one arm of a multi-arm study) are used in a
comparative analysis while the full cohort of that study appears elsewhere,
Random 3-claim back-check before closing Phase 6.
After the forest/funnel/subgroup outputs stabilize, randomly sample 3 numerical claims from the Results section of the draft manuscript and trace each back to (a) the R output log and (b) the original paper's Table/Figure.
Record the back-check as a small table in
peer_review_<vN>_internal.md:Claim (manuscript line) R output file:line Primary source (paper, Table/Fig, page) Match? A single mismatch is a P0 blocker — do not advance to Phase 7 until resolved.
Revision-introduced numbers must be tagged.
- Any new number added after v1 — including numbers produced by a new comparative / subgroup /
sensitivity script — MUST be wrapped inline as
[VERIFY-CSV]in the manuscript until the Phase 2.5a audit in/self-reviewclears it.
- Any new number added after v1 — including numbers produced by a new comparative / subgroup /
sensitivity script — MUST be wrapped inline as
Sensitivity analyses must be recomputed on the modified data, not copied.
- When you add a sensitivity / leave-one-out / erosion / alternative-model analysis, every reported effect size (Cohen's dz/f, AUC, OR, HR, β, sens/spec, ICC) MUST be re-derived from the modified dataset. If a sensitivity-table effect size is identical to the primary analysis to two decimals across ≥4 values, the recomputation almost certainly did not run and the primary values were transcribed — re-run the script on the modified data.
- The underlying means/SDs/counts will change even when the effect size looks similar; if the effect sizes are byte-identical while the inputs differ, that is the tell. Probability of ≥4 independent values coinciding to 2 decimals by chance is ≈ (0.01)^4 — essentially zero.
- The failure it catches: a sensitivity analysis reports a block of effect-size values byte-identical to the primary tables while the underlying means/SDs differ — the sensitivity analysis was never actually recomputed. Internal consistency cannot see it.
A "fixed" / "resolved" audit note requires re-run evidence, not a claim.
- When a prior audit note records a number as
fixed,resolved, orcorrected, that status is only valid if it carries the re-run evidence: a timestamp and the relevant stdout / output-file line showing the corrected value, or the commit that changed it. A bare "fixed in v10" with no re-run artifact does NOT clear the finding — re-run the script and attach the output. - The forward pipeline can echo a stale value through every artifact while an audit note claims it
was fixed (e.g., a major-comparison N still reading the old total after a "fixed" note). The
outcome-denominator cross-check (
/self-reviewPhase 2.5b, the cohort-arithmetic / pool-lock assertions) must pass against the current outputs before any "fixed" status is accepted.
- When a prior audit note records a number as
When this phase triggers: every time Phase 6 outputs change (first draft, revision, reviewer- requested re-analysis). Not optional on "minor" re-runs — the precedent reversal above occurred inside a "minor" revision-era re-analysis.
Phase 7: GRADE / Certainty of Evidence
Goal: Assess certainty of the body of evidence.
For DTA meta-analysis, apply GRADE-DTA framework:
- Risk of bias (from QUADAS-3, or QUADAS-2 for a legacy review)
- Indirectness (applicability concerns)
- Inconsistency (heterogeneity)
- Imprecision (wide CIs, small sample)
- Publication bias
For intervention meta-analysis, apply standard GRADE.
Certainty is assessed per outcome, not once for the review. The five domains resolve differently for each outcome — an outcome pooled from 12 studies with narrow CIs and one pooled from 3 with a wide CI do not share a rating, and a single review-level "moderate certainty" sentence tells a reader nothing about the outcome they came for. Rate every outcome carried into the Summary of Findings table, and state the reason for each downgrade (which domain, why) rather than the resulting label alone.
Output: Summary of Findings table — one row per outcome, carrying the pooled estimate with its precision alongside the certainty rating (high / moderate / low / very low).
Phase 8: Reporting & Manuscript
Goal: Generate PRISMA-compliant manuscript sections.
Failure-mode cross-ref →
references/submission_package_drift.md— apply the_build.shpattern +DO_NOT_EDIT_HEREgate when staging multi-journal submission folders.
Check reporting compliance: Use
/check-reportingwith PRISMA-DTA or PRISMA 2020, then run it a second time over the abstract with PRISMA 2020 for Abstracts — 12 items, its own denominator. One run does not cover both.Write manuscript: Use
/write-paperwith meta-analysis type selectedFigures: Use
/make-figuresfor:- PRISMA flow diagram
- Forest plots (paired for DTA)
- SROC curve (DTA)
- Funnel plot
- RoB summary (traffic light plot)
Tables:
- Characteristics of included studies
- 2x2 data per study (DTA)
- RoB assessment results
- Summary of findings / GRADE table (one row per outcome — Phase 7)
The items published radiology SR/MA most often drop. Park 2022 (Korean J Radiol; PMID:35213097) scored 24 SR/MAs against PRISMA 2020 and found 24 of 42 items reported by fewer than 80%. The checklist itself lives in
/check-reporting; what follows is where drafts actually fail, so check these by hand before the compliance run rather than after it:PRISMA item What is missing Observed 20a For each synthesis, a brief summary of the contributing studies' characteristics and risk of bias — not one global paragraph covering all pools 0/24 27 Data availability: which of the extraction forms, extracted data, analysis dataset, and analytic code are public, and where 0/24 24a–c Registration number, where the protocol can be read, and any amendment — an explicit "not registered" satisfies 24a 0/24 22 / 15 Certainty of evidence per outcome, and the method used to assess it 9% 13f / 20d Sensitivity analysis: method and result 28% 18 Risk of bias per study, shown study-by-study rather than as a pooled proportion 32% 13d Rationale for the synthesis model (see Phase 6 check 2) 35% 16b Studies that look eligible but were excluded, cited individually with the reason 25% Abstract #3, #12 Eligibility criteria and registration inside the structured abstract 0/24 each The abstract items are the cheapest of these and the most reliably forgotten. PRISMA 2020 devotes a separate 12-item instrument to the abstract — item 2 of the main checklist does nothing but defer to it — so a manuscript can satisfy all 42 main-text items and still fail most of the twelve.
/check-reportingcarries it asPRISMA_2020_Abstracts.md; run it as its own pass and report its score separately, because folding twelve items into a 42-item total is how they stay invisible.Data availability statement: name what is being shared (extraction template, locked dataset, analysis code, RoB judgments) and where — repository, DOI, or supplementary file. "Available from the corresponding author on reasonable request" satisfies few journals now and no longer satisfies item 27. If a Zenodo DOI is minted post-acceptance,
references/post_submission_release_ops.mdcovers propagating it back into this statement.Supplementary & analysis-code pre-submission gate (run before Phase 9 circulation and before portal upload). Presence of the 8-file package (Empirical Lesson 5) is necessary but not sufficient — each item must also be reviewer-ready:
- De-scaffold: strip internal-QC / tool artifacts before bundling — raw
/check-reportingoutput ("Assessed by: - Blind: supplementary goes to reviewers — remove author names/initials and sibling-project cross-references ("Designed by:
- Cross-consistency with the manuscript: every supplementary number must match the main text — PRISMA counts, pool k/N, the Cochrane/CENTRAL search description, RoB counts. A supplement that says "Cochrane — NOT SEARCHED" while Methods report a confirmatory CENTRAL search is a contradiction reviewers catch.
- Submitted analysis code must reproduce and be self-contained: run it from a clean copy of the bundle. It must (a) read the bundled locked dataset (not an out-of-bundle path) and write to the working directory, and (b) regenerate every pool reported in the results table. A hard-coded study-id subset that drifts from the manuscript (e.g., a pool computed over k=7 while the manuscript reports k=9) is a P0 — fix and re-run; never ship stale code or stale figures derived from it.
- Run a supplementary-only review pass — the manuscript self-review/panel does not see the supplement; mirror
/self-reviewPhase 2.5c–2.5d (reference + cross-reference QC) over the supplementary files.
- De-scaffold: strip internal-QC / tool artifacts before bundling — raw
Phase 9: Co-author Circulation
Goal: Standardized pre-submission circulation of the manuscript to co-authors and senior methodologist / reviewer, with a bounded review window and a controlled attachment scope.
Trigger: Phase 8 is complete, and the draft has cleared Phase 6b source-fidelity audit.
Summary: Reply to the prior-version email thread to preserve In-Reply-To continuity
(v1 → v2 → v3 tracked in one place). Attach the manuscript body with figures inline and,
for v≥2, a change summary — exclude graphical abstract, cover letter, COI forms, and
supplementary until the target journal is confirmed. TO = corresponding author + one
senior methodologist; CC = remaining co-authors. Set a 7-day deadline (5 business days +
weekend). Ask the corresponding author for target-journal preference, reviewer candidates,
and cover-letter framing.
Load-on-demand procedural detail (thread continuity, attachment scope rationale,
size-to-method table, journal-undetermined framing, response-tracking log):
${CLAUDE_SKILL_DIR}/references/phase9_circulation.md.
Failure-mode cross-ref →
references/review_orchestration.mdRO-1~RO-5 (dual-rating completeness, defensive-tone bias audit, response-matrix numeric tracking, 2nd-reviewer availability blocking).
Phase 10: Self-Audit Recovery (v
Goal: When an audit uncovers a structural data or protocol-application error, withdraw the current version, rebuild, and re-circulate with a transparent audit trail. Catching the error yourself before a journal reviewer does is the principal trust-building move in this phase.
Trigger conditions (any one):
| # | Trigger | Source |
|---|---|---|
| T1 | Extraction CSV ↔ primary source disagreement for a cell feeding a pooled/subgroup estimate or reported proportion | Phase 6b audit |
| T2 | Included/excluded study violates the pre-specified criteria on re-read | Protocol review |
| T3 | Hand-typed numerical literal in the analysis script traces to a wrong value | Phase 6b audit |
| T4 | PROSPERO protocol ↔ delivered analysis disagreement on outcome, subgroup, or eligibility | Protocol ↔ analysis diff |
| T5 | Dual-reviewer consensus record ↔ locked dataset disagreement on inclusion | Consensus log diff |
Non-negotiable rule: if the trigger fires after Phase 9 circulation but before journal submission, withdraw the current version within 24 hours. Reviewer discovery is a strictly worse failure mode than self-withdrawal.
Sprint outline (12 steps): (10.1) audit log at qc/audit_vN_to_vNplus1.md →
(10.2) CSV re-verification with [VERIFY-CSV] tagging → (10.3) fresh script re-run
(fixed seed, logged) → (10.4) manuscript auto-sync (grep for v residue) → (10.5)
supplementary regeneration (consensus log, RoB, GRADE/SoF, PRISMA flow) → (10.6) figure
regeneration via /make-figures → (10.7) change summary with delta table → (10.8)
PROSPERO amendment (application correction, not criteria change) → (10.9) re-circulation
in the Phase 9 thread with the "On re-review" framing → (10.10) anti-patterns to avoid
(hide-and-submit, "minor revision" reframe, cover-letter-only disclosure) → (10.11) post-
submission escalation path → (10.12) post-recovery loop (Phase 9 restart; tighten Phase
6b if a second sprint is needed).
Load-on-demand procedural detail (exact audit-log fields, delta-table template,
amendment language template, re-circulation paragraph template, anti-pattern rationale):
${CLAUDE_SKILL_DIR}/references/phase10_recovery.md.
Failure-mode cross-ref →
references/post_submission_release_ops.mdGate 4 covers reject/revise Zenodo versioning, tag-cleanup gate, and re-target workflow (avoid "new version" misuse on re-target).
Failure Modes (prior MA projects, anonymized)
Failure patterns observed across three prior MA projects (anonymized). Each topical reference extends the phase it cross-references above — consult alongside phase procedural docs, not in isolation.
| Domain | Phase span | Load-on-demand reference |
|---|---|---|
| Data integrity (2x2 arm-swap, KM audit, methodology mismatch, PRISMA 5-way drift, single-source k) | Phase 3 → 6 | references/data_integrity_checklist.md (DI-1~DI-9) |
| Review orchestration (2nd-reviewer blocking, dual-rating completeness, defensive-tone audit, response-matrix tracking) | Phase 9 circulation (extends phase9_circulation.md) |
references/review_orchestration.md (RO-1~RO-5) |
Submission package drift (multi-journal folder hygiene, DO_NOT_EDIT_HERE gate, build artifact vs master) |
Phase 8 → submission | references/submission_package_drift.md |
| Post-submission release ops (Zenodo DOI timing, tag-cleanup gate, reject-retarget versioning) | Submission → Phase 10 | references/post_submission_release_ops.md |
Automation hooks (invoke at the phase listed)
| When | Script | Gate |
|---|---|---|
| Phase 3f reconciliation (before Phase 5 write-up) | python3 ${CLAUDE_SKILL_DIR}/scripts/check_exclusion_code_validity.py --protocol 0_Protocol/protocol.md --screening 2_Screening/*.tsv --strict |
validates each applied exclusion code against the registered eligibility criteria: CODE_CONTRADICTS_ELIGIBILITY (a code excludes a design the protocol includes — the bulk study-loss defect no arithmetic/inter-rater gate can see), CODE_NOT_REGISTERED (off-protocol code), CODE_RENUMBERED (same code, two meanings). Challenge card: scripts/check_exclusion_code_validity_challenge/. |
| Phase 4 kickoff (before first extraction row) | python3 ${CLAUDE_SKILL_DIR}/../../scripts/extraction_consensus_log_init.py --output 2_Data/extraction_consensus_log.md |
DI-1: creates standalone consensus log so comparative arm-specific rows are never folded into R-script comments. |
| Phase 3f reconciliation + every revision touching PRISMA numbers | python3 ${CLAUDE_SKILL_DIR}/../../scripts/prisma_5way_consistency.py --ssot prisma.yaml |
DI-6: 5-surface drift check (abstract / main text / flow figure / supplement / CSV) against YAML SSOT. Non-zero exit blocks Phase 5 writeup. |
| Phase 8 pre-submission + every journal retarget | bash ${CLAUDE_SKILL_DIR}/../../scripts/tag_cleanup_gate.sh |
DI-8: fails if VERIFY-CSV/TODO/FIXME/XXX survive in 7_Manuscript, supplement, SUBMISSION, etc. |
Phase 8 on first build per journal (--record), then before every re-submission (--verify) |
python3 ${CLAUDE_SKILL_DIR}/../../scripts/verify_package_integrity.py --record --journal <name> then --verify --journal <name> |
SPD: checksum-based drift detection between master manuscript and built SUBMISSION/{journal}/ folder. Journal-editable files (cover letter, response, MANIFEST, DO_NOT_EDIT_HERE.md) are auto-excluded. |
All four scripts are repo-shipped as of 2026-04 (FOLLOWUPS P10). Non-zero exit = gate failure; resolve before proceeding to the next phase.
Empirical Lessons (peer-review cycles)
Sixteen accumulated SR-MA peer-review / submission lessons (2026-05 and 2026-06) — the
drivers behind the Phase 4 extraction-form schema, the Phase 4c QC scripts, and the Phase 8
submission gates. To keep this entry point lean they live load-on-demand in
${CLAUDE_SKILL_DIR}/references/empirical_lessons.md. Load that file when designing the
extraction form (before Phase 4) and before submission (Phase 8) — it covers dual-extractor
2x2 integrity, cohort-overlap clustering, small-k subgroup caution, the supplementary 8-file
bar, PROSPERO ID format, AI-disclosure presence, recompute-don't-copy sensitivity analyses,
outcome harmonization, heterogeneous-RoB κ, survival-specific concerns, supplement blinding /
de-scaffolding, self-contained reproducible analysis scripts, sidecar re-sync, methodological
- software citations, wide-table PDF rendering, and submission-portal journal-identity checks.
DTA-Specific Pitfalls (Always Check)
| Pitfall | Problem | Solution |
|---|---|---|
| Separate pooling of Se/Sp | Ignores correlation | Use bivariate/HSROC model |
| Ignoring threshold effect | False heterogeneity | Check Spearman correlation, SROC plot |
| Standard funnel plot for DTA | Inappropriate | Use Deeks' funnel plot |
| I-squared only for heterogeneity | Doesn't capture threshold effect | Use prediction region on SROC |
| Missing GRADE | Common omission in DTA MA | Apply GRADE-DTA. If <4 studies, assess each domain narratively and state the limitation explicitly |
| Partial verification bias | Inflates sensitivity | QUADAS-3 3.2 (target condition assessed in all participants). QUADAS-3 has no Flow & Timing domain — that was QUADAS-2 |
| Differential verification bias | Distorts both Se and Sp | QUADAS-3 3.3 (target condition assessed the same way in all participants) |
| Unevaluable results excluded | Biases accuracy estimates | Report intent-to-diagnose analysis |
Small Study Considerations
When the number of included studies is small (< 10):
- Bivariate/HSROC model may not converge -- consider univariate random-effects as fallback
- Publication bias tests are underpowered -- state this limitation
- Subgroup/meta-regression analysis not recommended
- Wide prediction regions expected -- emphasize uncertainty in conclusions
- Consider narrative synthesis as alternative/complement
Skill Interactions
| When | Call | Purpose |
|---|---|---|
| Need literature search | /search-lit |
PubMed/Semantic Scholar search with verified citations |
| Need statistical code | /analyze-stats |
Execute R/Python analysis scripts |
| Need figures | /make-figures |
PRISMA flow, forest plots, SROC, funnel plots |
| Need reporting check | /check-reporting |
PRISMA-DTA / PRISMA 2020 compliance (includes Step 4c registration / amendment timing) |
| Need manuscript writing | /write-paper |
Full IMRAD manuscript generation |
| Need self-review | /self-review |
Pre-submission quality check |
| Self-audit recovery entrypoint (Phase 10) | /write-paper Step 7.4a |
Recovery branch for polish pipelines that surface structural audit failures |
/sync-submission SR-MA gate |
/sync-submission |
Before submission, verify supplementary package matches all 8 files in templates/supplementary_8file_checklist.md (PRISMA, PROSPERO, search strategy, exclusion list, extraction table, per-study x per-domain RoB, subgroup forests, sensitivity / publication bias). AI Disclosure presence check (cross-link /peer-review Phase 2A P8). Cite-list duplicate check via /verify-refs Gate 5 (duplicate PMID/DOI). |
Error Handling
- If study type is ambiguous (DTA vs intervention), ask user to clarify before proceeding.
- If fewer than 4 studies for DTA, warn that bivariate model may not converge.
- If data extraction is incomplete (missing 2x2 cells), suggest contacting authors or sensitivity analysis with imputed values.
- If PROSPERO ID is missing, flag as a limitation but continue.
- Always remind user: this is a methodological support tool; final decisions rest with the research team and ideally include a biostatistician/methodologist.
Anti-Hallucination
- Never fabricate variable names, dataset column names, or variable codings. If a variable mapping is uncertain, output
[VERIFY: variable_name]and ask the user to confirm against the data dictionary. - Never fabricate statistical results — no invented p-values, effect sizes, confidence intervals, or sample sizes. All numbers must come from executed code output.
- Never generate references from memory. Use
/search-litfor all citations. - If a function, package, or API does not exist or you are unsure, say so explicitly rather than guessing.
Files (medsci-skills)
-
references
-
checklists
-
JBI_Case_Series.md 2.1 KB
# JBI Critical Appraisal Checklist for Case Series Reference: Munn Z, Barker TH, Moola S, et al. Methodological quality of case series studies: an introduction to the JBI critical appraisal tool. JBI Evid Synth. 2020;18(10):2127-2133. ## 10 Signalling Questions | # | Question | Y/N/U/NA | |---|----------|----------| | Q1 | Were there clear criteria for inclusion in the case series? | | | Q2 | Was the condition measured in a standard, reliable way for all participants included in the case series? | | | Q3 | Were valid methods used for identification of the condition for all participants included in the case series? | | | Q4 | Did the case series have consecutive inclusion of participants? | | | Q5 | Did the case series have complete inclusion of participants? | | | Q6 | Was there clear reporting of the demographics of the participants in the study? | | | Q7 | Was there clear reporting of clinical information of the participants? | | | Q8 | Were the outcomes or follow-up results of cases clearly reported? | | | Q9 | Was there clear reporting of the presenting site(s)/clinic(s) demographic information? | | | Q10 | Was statistical analysis appropriate? | | ## Scoring - **Y** = Yes (criterion met) - **N** = No (criterion not met) - **U** = Unclear - **NA** = Not applicable ## Overall Appraisal | Rating | Criterion | |--------|-----------| | **Include** | ≥7 "Yes" responses | | **Exclude** | <7 "Yes" responses (or critical methodological concerns) | | **Seek further info** | Insufficient information to judge key items | ## Notes - Q4 and Q5 distinguish consecutive vs complete inclusion: - Q4 (consecutive): Were participants enrolled sequentially without selection bias? - Q5 (complete): Were all eligible participants during the study period included? - Q9 applies to multi-site studies; for single-site studies, mark as NA if site demographics are adequately covered by Q6. - When used alongside NOS for mixed comparative + single-arm meta-analyses, map NOS to comparative studies and JBI to case series. In PROSPERO, select "Other" for RoB tool and specify both. -
NOS.md 4.3 KB
# Newcastle-Ottawa Scale (NOS) Assessment Guide Quality assessment tool for non-randomised studies in meta-analyses. Reference: Wells GA et al. Ottawa Hospital Research Institute. Version: Newcastle-Ottawa Scale (current web version) Source: Wells GA, Shea B, O'Connell D, Peterson J, Welch V, Losos M, Tugwell P. The Newcastle-Ottawa Scale (NOS) for assessing the quality of nonrandomised studies in meta-analyses. Ottawa Hospital Research Institute. **No DOI: the tool is distributed from the institute's website and has never been issued one.**. Licence: No formal licence is published with the tool. Verification: both scales were compared item by item and response-option by response-option against the official NOS rating sheet distributed by the Ottawa Hospital Research Institute (`nosgen.pdf`), with the accompanying manual (`nos_manual.pdf`) checked for the scoring rules. The items, their order, the star allocations and the two-star comparability rule all match. **Two numbers did not**: the scale leaves the follow-up percentages blank for the reviewer to set, and this file had hardcoded them; and the Good/Fair/Poor star bands appear nowhere in the NOS documents. Both are corrected below. ## Structure NOS uses a "star system" (maximum 9 stars) across 3 categories. Higher stars = higher quality. ## Cohort Studies (max 9 stars) ### Selection (max 4 stars) 1. **Representativeness of the exposed cohort** (1 star) - a) Truly representative of the average [describe] in the community * - b) Somewhat representative * - c) Selected group of users - d) No description 2. **Selection of the non-exposed cohort** (1 star) - a) Drawn from the same community as the exposed * - b) Drawn from a different source - c) No description 3. **Ascertainment of exposure** (1 star) - a) Secure record (e.g., surgical records) * - b) Structured interview * - c) Written self-report - d) No description 4. **Demonstration that outcome was not present at start** (1 star) - a) Yes * - b) No ### Comparability (max 2 stars) 5. **Comparability of cohorts on the basis of design or analysis** (up to 2 stars) - a) Study controls for [most important factor] * - b) Study controls for any additional factor * ### Outcome (max 3 stars) 6. **Assessment of outcome** (1 star) - a) Independent blind assessment * - b) Record linkage * - c) Self-report - d) No description 7. **Was follow-up long enough for outcomes to occur?** (1 star) - a) Yes (select adequate follow-up period) * - b) No 8. **Adequacy of follow-up of cohorts** (1 star) - a) Complete follow-up (all subjects accounted for) * - b) Subjects lost to follow-up unlikely to introduce bias — small number lost, above a threshold **you select**, or description provided of those lost * - c) Follow-up rate below the threshold **you select**, and no description of those lost - d) No statement The scale prints these two percentages as blanks ("select an adequate %"). NOS does not supply a number; pre-specify yours in the protocol and report it. ## Case-Control Studies (max 9 stars) ### Selection (max 4 stars) 1. Is the case definition adequate? 2. Representativeness of the cases 3. Selection of controls 4. Definition of controls ### Comparability (max 2 stars) 5. Comparability of cases and controls (same 2-star system) ### Exposure (max 3 stars) 6. Ascertainment of exposure 7. Same method of ascertainment for cases and controls 8. Non-response rate ## Interpretation **The NOS does not define quality bands.** Neither the rating sheet nor the manual maps a star total onto "good", "fair" or "poor". The bands below are a widely used external convention (they come from AHRQ-derived practice, not from the scale) and are reproduced here only because reviews so often cite them: | Stars | Convention | |-------|------------| | 7-9 | Good | | 4-6 | Fair | | 0-3 | Poor | If you use them, say where they came from and pre-specify them in the protocol. A different threshold is equally defensible, and the scale's authors leave the choice to you. ## When to Use - Observational cohort studies in intervention or exposure meta-analyses - Case-control studies - Simpler alternative to ROBINS-I when full domain-level assessment is not needed - Note: NOS does not provide domain-level judgments -- only an aggregate score -
PRISMA_DTA.md 10 KB
# PRISMA-DTA Checklist (diagnostic test accuracy systematic reviews) **Preferred Reporting Items for Systematic Reviews and Meta-Analyses — diagnostic test accuracy extension** Version: PRISMA-DTA 2018 — 27 items. Two PRISMA items were **deleted** (15 and 22) and two DTA-specific items were **added** (D1 and D2), so the count matches the original while the numbering skips 15 and 22. A separate abstract checklist accompanies it (below). Source: McInnes MDF, Moher D, Thombs BD, McGrath TA, Bossuyt PM, the PRISMA-DTA Group, et al. Preferred Reporting Items for a Systematic Review and Meta-analysis of Diagnostic Test Accuracy Studies: The PRISMA-DTA Statement. *JAMA* 2018;319(4):388-396 (DOI 10.1001/jama.2017.19163). Correction: *JAMA* 2020;323(6):580 — in item 20, "receiver operating characteristic curve" should read "receiver operating characteristic **plot**"; the correction is reflected below. > **Fidelity and licence.** The statement is published in *JAMA* (© American Medical Association) under > **no open licence** — free to read is not free to reuse. The descriptions below are therefore an > **in-house summary of what each item asks, in our own words — not the published wording**. What *is* > verified against the statement is the structure: which items exist, their numbering (including the > deleted 15 and 22 and the added D1 and D2), their section placement, and their topic labels. > **Complete the official checklist from the statement or EQUATOR for anything you submit.** > **What this file used to be.** Until this revision it listed items 1–27 sequentially — including 15 > and 22, which PRISMA-DTA deletes, and **without D1 and D2, the two DTA-specific items the extension > exists to add**. Its item 2 asked a diagnostic-accuracy review to summarise "interventions". It was > PRISMA 2009 renumbered, presented as PRISMA-DTA. Any assessment made with it scored two items the > guideline had removed and never scored the two it had introduced. ## Main checklist (27 items) ### Title | # | Topic | What to check is reported | |---|-------|---------------------------| | 1 | Title | The report is identified as a systematic review, or meta-analysis, **of DTA studies**. | ### Abstract | # | Topic | What to check is reported | |---|-------|---------------------------| | 2 | Abstract | Assessed with the **abstract checklist below**, not by a single item here. | ### Introduction | # | Topic | What to check is reported | |---|-------|---------------------------| | 3 | Rationale | Why the review was done, set against what is already known. | | **D1** | **Clinical role of index test** | The scientific and clinical background: what the index test is **intended to be used for** and its **role in the care pathway**, plus — where relevant — the reasoning behind a minimally acceptable accuracy (or a minimum difference in accuracy, for a comparative design). **DTA-specific; has no counterpart in PRISMA.** | | 4 | Objectives | An explicit statement of the question in terms of **participants, index test, and target conditions**. | ### Methods | # | Topic | What to check is reported | |---|-------|---------------------------| | 5 | Protocol and registration | Where the protocol can be read (e.g. a web address), and the registration number where one exists. | | 6 | Eligibility criteria | The study characteristics used to decide eligibility — participants, setting, index test, reference standards, target conditions, study design — and the report characteristics (years, language, publication status), **with the reasoning for them**. | | 7 | Information sources | Every source searched, including databases with their coverage dates and any contact with study authors, and the date each was last searched. | | 8 | Search | The **full** search strategy for every database and other source, with any limits, in enough detail to repeat it. | | 9 | Study selection | How studies were selected — screening, eligibility, inclusion in the review, and where applicable inclusion in the meta-analysis. | | 10 | Data collection process | How data were extracted from reports (piloted forms, independent extraction, duplicate extraction) and how data were obtained or confirmed from investigators. | | 11 | Definitions for data extraction | The definitions used when extracting and classifying **target conditions, index tests, reference standards** and other characteristics such as study design and clinical setting. | | 12 | Risk of bias and applicability | The methods used to assess risk of bias in each study **and** concerns about applicability to the review question. | | 13 | Diagnostic accuracy measures | Which accuracy measures are the principal ones (sensitivity, specificity, and so on) **and the unit of assessment** — per patient or per lesion. | | 14 | Synthesis of results | How the data were handled and combined and how between-study variability was described. The statement names six cases to address: multiple definitions of the target condition; multiple positivity thresholds; multiple index-test readers; indeterminate results; grouping and comparing tests; and differing reference standards. | | **D2** | **Meta-analysis** | The statistical methods used for the meta-analysis, where one was performed. **DTA-specific; has no counterpart in PRISMA.** | | 16 | Additional analyses | The methods of any additional analyses — sensitivity, subgroup, meta-regression — **and which of them were prespecified**. | ### Results | # | Topic | What to check is reported | |---|-------|---------------------------| | 17 | Study selection | The count at every stage of selection — records screened, those whose eligibility was assessed, studies entering the review, and studies entering any meta-analysis — together with why studies dropped out at each stage, ideally drawn as a flow diagram. | | 18 | Study characteristics | For each included study, a citation and its key characteristics: participant presentation and prior testing, clinical setting, study design, target-condition definition, index test, reference standard, sample size, and funding source. | | 19 | Risk of bias and applicability | The risk-of-bias and applicability assessment **for each study**. | | 20 | Results of individual studies | For every analysis in every study — each unique combination of index test, reference standard and positivity threshold — the **2 × 2 data (TP, FP, FN, TN)** with accuracy estimates and confidence intervals, ideally shown as a forest plot or a receiver operating characteristic **plot**. | | 21 | Synthesis of results | Test accuracy including its variability; where a meta-analysis was done, the results with confidence intervals. | | 23 | Additional analyses | The results of any additional analyses, including sensitivity, subgroup and meta-regression analyses, and analyses of the index test such as failure rates, the proportion of inconclusive results, and adverse events. | ### Discussion | # | Topic | What to check is reported | |---|-------|---------------------------| | 24 | Summary | The main findings, **with the strength of the evidence**. | | 25 | Limitations | Limitations of the included studies (risk of bias, applicability) **and** limitations of the review process itself, such as incomplete retrieval. | | 26 | Conclusions | A general interpretation against other evidence, and implications for future research and for clinical practice — including the intended use and clinical role of the index test. | ### Funding | # | Topic | What to check is reported | |---|-------|---------------------------| | 27 | Funding | Sources of funding and other support for the review, and the role of the funders. | ## Abstract checklist PRISMA-DTA carries its own abstract checklist; item 2 above defers to it. Score it separately, with its own denominator — the same discipline as `PRISMA_2020_Abstracts.md`. Its items, in our own words: | # | Topic | What to check is reported | |---|-------|---------------------------| | 1 | Title | Identified as a systematic review, or meta-analysis, of DTA studies. | | 2 | Objectives | The research question, with its components — participants, index test, target conditions. | | 3 | Eligibility criteria | The study characteristics used to decide eligibility. | | 4 | Information sources | The key databases searched and the search dates. | | 5 | Risk of bias and applicability | The methods used to assess risk of bias and applicability. | | **A1** | **Synthesis of results** (methods) | The methods used for the data synthesis. **DTA-specific.** | | 6 | Included studies | How many studies and of what type, and the participants and relevant study characteristics, including the reference standard. | | 7 | Synthesis of results (results) | The diagnostic-accuracy results, preferably with the number of studies and participants; accuracy and its variability, and where a meta-analysis was done, summary results with confidence intervals. | The remaining abstract items (interpretation, funding, registration) follow the PRISMA pattern; consult Table 3 of the statement for their exact wording before scoring an abstract you intend to report. ## Notes for Assessors - **Do not score items 15 or 22.** PRISMA-DTA deletes them. An assessment that reports 27 sequential items has not used this instrument. - **D1 and D2 are the point of the extension.** A review that never states the index test's intended clinical role (D1), or that meta-analyses without reporting the statistical methods used (D2), is missing the DTA-specific requirements — and these are exactly the items a generic PRISMA checklist cannot surface. - **Item 13's unit of assessment** (per patient vs per lesion) is the item most often skipped in imaging reviews, and it changes what every downstream accuracy estimate means. - **Item 20 wants the 2 × 2 cells**, not just sensitivity and specificity. Without TP/FP/FN/TN a reader cannot recompute the estimates or pool them. - For the risk-of-bias assessment referenced by items 12 and 19, see `QUADAS2.md`. - For a review that is not diagnostic test accuracy, use `PRISMA_2020.md` with `PRISMA_2020_Abstracts.md`. -
PROBAST.md 5 KB
# PROBAST Assessment Guide Prediction model Risk Of Bias ASsessment Tool. Version: PROBAST 2019 — 20 signalling questions across 4 domains (2 / 3 / 6 / 9). Source: Wolff RF, Moons KGM, Riley RD, Whiting PF, Westwood M, Collins GS, et al. PROBAST: A Tool to Assess the Risk of Bias and Applicability of Prediction Model Studies. *Ann Intern Med* 2019;170(1):51-58 (PMID 30596875; DOI 10.7326/M18-1376). Explanation and Elaboration: *Ann Intern Med* 2019;170(1):W1-W33. > **Fidelity and licence.** PROBAST is published in *Annals of Internal Medicine* (© American College > of Physicians) and carries **no open licence**. The signalling questions below are therefore an > **in-house summary of what each question asks — not the published wording**. The numbering, > the domain structure and the count (2 / 3 / 6 / 9) are verified against the statement; the phrasing > is ours. **Complete the official tool from probast.org or the statement for any assessment you > report.** Use this file to organise a first pass. > **PROBAST 2019 is superseded.** PROBAST+AI (2025) replaces it for all new assessments, covering > regression and AI/ML models alike — see `PROBAST_AI.md`. Use this file only when appraising against > the 2019 instrument specifically (for example, reproducing an earlier review). ## Structure PROBAST assesses 4 domains, each for Risk of Bias AND Applicability. - **Signalling questions**: Yes / Probably yes / No / Probably no / No information - **Domain judgment**: Low / High / Unclear - **Overall judgment**: High if any domain is high; Low only if all domains are low ## Domain 1: Participants ### Signalling questions (risk of bias) — what each asks 1.1 Whether the data source suits the question — a cohort, a randomised trial, or nested case–control data, rather than a design that distorts the sampling. 1.2 Whether every inclusion and exclusion applied to participants was appropriate. ### Applicability - Do the participants and setting match the review question? ## Domain 2: Predictors ### Signalling questions (risk of bias) — what each asks 2.1 Whether predictors were defined and assessed the same way for every participant. 2.2 Whether predictors were assessed without knowledge of the outcome. 2.3 Whether every predictor is available at the moment the model is meant to be used. ### Applicability - Do the predictors, their assessment, and timing match the review question? ## Domain 3: Outcome ### Signalling questions (risk of bias) — what each asks 3.1 Whether the outcome was determined by an appropriate method. 3.2 Whether the outcome definition was prespecified or a standard one. 3.3 Whether predictors were kept out of the outcome definition. 3.4 Whether the outcome was defined and determined the same way for every participant. 3.5 Whether the outcome was determined without knowledge of predictor information. 3.6 Whether the interval between predictor assessment and **outcome determination** was appropriate — the question is about when the outcome was *determined*, not merely when it occurred. ### Applicability - Does the outcome and its definition/timing match the review question? ## Domain 4: Analysis ### Signalling questions (risk of bias) — what each asks 4.1 Whether the number of participants with the outcome was reasonable. 4.2 Whether continuous and categorical predictors were handled appropriately. 4.3 Whether every enrolled participant was included in the analysis. 4.4 Whether participants with missing data were handled appropriately. 4.5 Whether selection of predictors on the basis of univariable analysis was avoided. 4.6 Whether complexities in the data were accounted for — the statement names **censoring, competing risks, and the sampling of control participants** as the cases to look for. 4.7 Whether relevant measures of model performance were evaluated appropriately. 4.8 Whether **overfitting, underfitting, and optimism** in model performance were accounted for. All three, not overfitting alone. 4.9 Whether the predictors and their assigned weights in the final model correspond to the **results from** the reported multivariable analysis. ### For validation studies (additional) - Were the model and its performance evaluated appropriately? ## For AI / machine-learning models Use **`PROBAST_AI.md`** (PROBAST+AI 2025; Moons KGM et al. *BMJ* 2025;388:e082505, DOI 10.1136/bmj-2024-082505), which carries the instrument's own 16 development and 18 evaluation signalling questions. Do not improvise AI addenda on top of the 2019 questions: an earlier version of this file listed four invented bullets under a heading that implied they were part of the extension, which they were not. ## When to Use - Diagnostic prediction models (e.g., AI classifiers for imaging findings) - Prognostic prediction models (e.g., risk scores, survival prediction) - Both development AND validation studies - For any model developed with machine learning or deep learning, and for new assessments generally, use PROBAST+AI (`PROBAST_AI.md`) rather than this file -
QUADAS2.md 8.1 KB
# QUADAS-2 Assessment Guide Quality Assessment of Diagnostic Accuracy Studies, version 2. Version: QUADAS-2 (2011) — 4 domains, **10 signalling questions** (3 / 2 / 2 / 3), risk of bias for every domain and applicability for the first three only. Source: Whiting PF, Rutjes AWS, Westwood ME, Mallett S, Deeks JJ, Reitsma JB, et al. QUADAS-2: a revised tool for the quality assessment of diagnostic accuracy studies. *Ann Intern Med* 2011;155(8):529-536 (DOI 10.7326/0003-4819-155-8-201110180-00009). The tool itself, training material and worked examples were distributed from www.quadas.org; that domain no longer resolves, and the QUADAS group now distributes its tools from https://www.bristol.ac.uk/population-health-sciences/projects/quadas/ > **Fidelity and licence.** QUADAS-2 is published in *Annals of Internal Medicine* (© American College > of Physicians) under **no open licence**. **Verified against the published article**: the four > domains, the 3 / 2 / 2 / 3 split of signalling questions, what each question asks, the answer > options, the judgement levels, and the restriction of applicability to the first three domains all > match. The descriptions below state what each question asks **in our own words** rather than > reproducing the published wording. **Complete the official QUADAS-2 form from www.quadas.org for > any assessment you report.** > > **QUADAS-3 has superseded this tool.** The QUADAS group states that "QUADAS-3 is the current > iteration of the QUADAS tool and is the current recommended version" — it extends QUADAS-2 by > introducing an explicit ideal-test-accuracy-trial comparator and by moving assessment **from the > study level to the level of individual accuracy estimates**. This file documents QUADAS-2, which > remains what most published reviews used. For a new review use **`QUADAS3.md`**; > `QUADAS_C.md` should now be paired with QUADAS-3 rather than with this tool. ## How the tool is applied — four phases QUADAS-2 is not a fixed questionnaire. The statement specifies four phases, and skipping the second is the most common misuse: 1. **State the review question** — patients, index test, reference standard and target condition. Describe patients by setting, the intended use of the index test, presentation, and prior testing, because accuracy depends on where in the diagnostic pathway the test sits. 2. **Tailor the tool to the review.** Add or omit signalling questions and write review-specific guidance on how each is to be judged. (For an objective index test, for example, the question about blinding the interpreter to the reference standard may not apply.) Avoid piling on extra questions. At least two people should pilot the tailored tool; refine it if agreement is poor. 3. **Draw the study's flow diagram** — use the published one, or draw it yourself if it is absent or inadequate. It need not be reported; it exists to make the flow-and-timing judgements possible. 4. **Judge bias and applicability**, recording the information each judgement rests on. ## Answers and judgements - **Signalling questions**: Yes / No / Unclear, phrased so that **Yes indicates low risk of bias**. - **Risk of bias**: Low / High / Unclear. - **Applicability concern** (domains 1–3 only): Low / High / Unclear. - **Unclear is only for insufficient reporting**, not for a difficult judgement. **How a "No" is handled.** If every signalling question in a domain is Yes, risk of bias can be judged low. A **No does not automatically make the domain High** — it establishes that *potential for bias exists*, and the reviewer then applies the review-specific guidance written in phase 2 to reach the judgement. A file or workflow that maps "any No → High" has removed the judgement the tool asks for. ## Domain 1: Patient Selection **Risk of bias — could patient selection have introduced bias?** 1. Whether enrolment took a consecutive or random sample of eligible patients. 2. Whether a case–control design was avoided. 3. Whether the study avoided inappropriate exclusions. *Why it matters*: enrolling patients whose diagnosis is already confirmed, or excluding the difficult-to-diagnose, inflates apparent accuracy; excluding patients with obvious signs of the target condition can deflate it. **Applicability** — whether the included patients and the setting differ from the review question (severity, demographics, comorbidity and differential diagnosis, setting, prior testing). ## Domain 2: Index Test **Risk of bias — could the conduct or interpretation of the index test have introduced bias?** 1. Whether the index test was interpreted without knowledge of the reference standard result. 2. Whether a threshold, if one was used, was prespecified. *Why it matters*: this is the diagnostic equivalent of blinding, and its force depends on how subjective the index test is and on the order of testing. Choosing the threshold after the fact to maximise sensitivity or specificity overstates performance that will not hold in a new sample. **Applicability** — whether the index test, how it was carried out, or how it was interpreted differs from the review question. ## Domain 3: Reference Standard **Risk of bias — could the reference standard, its conduct or its interpretation have introduced bias?** 1. Whether the reference standard is likely to classify the target condition correctly. 2. Whether the reference standard was interpreted without knowledge of the index test result. *Why it matters*: accuracy estimates assume the reference standard is correct, so that any disagreement is the index test's error. **Applicability** — whether the target condition **as the reference standard defines it** differs from the one in the review question. ## Domain 4: Flow and Timing **Risk of bias — could patient flow have introduced bias?** 1. Whether the interval between index test and reference standard was appropriate. 2. Whether all patients received the same reference standard. 3. Whether all patients were included in the analysis. *Why it matters*: an interval long enough for the condition to change causes misclassification, and how long is too long depends on the condition. Verifying only some patients, or verifying different patients with different reference standards, biases the estimate. Patients lost between enrolment and the 2 × 2 table differ systematically from those who remain. **This domain has no applicability judgement.** ## Reporting the assessment - **Do not produce a summary quality score.** The statement is explicit about this, for the reasons well established in the literature on quality scores. - A study low on every domain may be called low risk of bias, or low concern for applicability, overall. High or unclear on one or more domains means it is at risk of bias, or of concern. - At minimum, report the assessment across all included studies — how many were low, high or unclear per domain — and consider highlighting signalling questions on which studies consistently do badly. - Restricting the primary analysis to low-risk studies is legitimate, but it is often better to include everything and then investigate heterogeneity — by subgroup, sensitivity analysis, or by entering domains as covariates in meta-regression. - QUADAS-2 does **not** cover studies comparing multiple index tests. The development group considered it and concluded the evidence base was insufficient. ## Common Issues in DTA Studies - **Partial verification bias**: Not all patients receive the reference standard (especially when invasive, e.g., biopsy) - **Differential verification**: Different reference standards used for different patients - **Incorporation bias**: Index test forms part of the reference standard - **Review bias**: Knowledge of index test results influences reference standard interpretation - **Clinical review bias**: Additional clinical information available during index test interpretation - **Uninterpretable results**: Exclusion of technically inadequate or indeterminate results ## Related - `PRISMA_DTA.md` items 12 and 19 are the reporting counterparts: the methods used for this assessment, and its result presented **for each study**. -
QUADAS3.md 12.9 KB
# QUADAS-3 Assessment Guide Quality Assessment of Diagnostic Accuracy Studies, version 3 — **the current recommended version**. Version: QUADAS-3 tool v1.2 — 6 phases, 4 domains, **20 signalling questions** (4 / 4 / 8 / 4). Source: Whiting PF, Tomlinson E, Rutjes AWS, Davenport C, Yang B, Westwood M, et al. QUADAS-3: a revised tool for the quality assessment of diagnostic test accuracy studies. *Ann Intern Med* 2026;179(4):548-555 (DOI 10.7326/ANNALS-25-02104). The tool itself, the Explanation & Elaboration report and an introductory video are distributed by the QUADAS group at https://www.bristol.ac.uk/population-health-sciences/projects/quadas/quadas-3/ Explanation & Elaboration: Davenport CF, Rutjes AWS, Mallett S, Tomlinson E, Yang B, et al. QUADAS-3 explanation and elaboration: guidance for quality assessment of diagnostic test accuracy studies. *Ann Intern Med* 2026;179(4):e2504943 (DOI 10.7326/ANNALS-25-04943). Resource site: **www.quadas.info** (the older www.quadas.org no longer resolves). > **Fidelity and licence.** QUADAS-3 is published in *Annals of Internal Medicine* (© American > College of Physicians) under **no open licence** — Crossref returns only ACP's text-and-data-mining > policy. The descriptions below state what each question asks **in our own words** rather than > reproducing the published wording. **Complete the official QUADAS-3 form (`QUADAS-3 1.2.docx`) > from the page above for any assessment you report, and read the E&E report before using it.** > > Verification: the six phases and when each is completed, the four domains, all 20 signalling > questions, the response options, the domain-level rule, which domains carry an applicability > judgement, and the overall-judgement rules were compared against **the official tool document > v1.2** distributed by the QUADAS group. All matched. The *Using QUADAS-C with QUADAS-3*, > *Tailoring* and *no "moderate" grade* sections below are taken from the **Explanation and > Elaboration paper**, read directly. ## QUADAS-3 supersedes QUADAS-2 The QUADAS group states QUADAS-3 "is the current version of QUADAS and the tool that we recommend." For a new review, use this file. `QUADAS2.md` documents the 2011 tool, which is what most published reviews used and what you will still be reading in them. What changed, in the group's own framing: | | QUADAS-2 | QUADAS-3 | |---|---|---| | Unit of assessment | the **study** | **each set of accuracy estimates** | | Comparator for judging | implicit | an explicit **ideal test accuracy trial**, defined per synthesis question | | Synthesis questions | one, implicit | **multiple, defined up front** | | Overall judgment | none | **a formal phase (6)** | | Phases | 4 | **6** | | Domains | Patient Selection, Index Test, Reference Standard, Flow and Timing | **Participants, Index Test, Target Condition, Analysis** | | Signalling questions | 10 (3/2/2/3) | **20 (4/4/8/4)** | | Third judgement level | "unclear" | **"insufficient information" (II)** | Note the domain rename: QUADAS-2's *Flow and Timing* is gone. Timing moved into **Target Condition** (the index-test-to-reference-standard interval), and participant exclusions, missing data and the unit of analysis moved into the new **Analysis** domain. **Comparative accuracy reviews**: the group recommends using **QUADAS-C in addition to QUADAS-3** (`QUADAS_C.md`). QUADAS-C was written against QUADAS-2 and needs adaptation — see the next section. ## Using QUADAS-C with QUADAS-3 For comparative accuracy studies — where two or more index tests are compared — the guideline says to use **QUADAS-C alongside QUADAS-3**, because such studies carry additional sources of bias (confounding between tests, and interference of one test with another). QUADAS-C was written as an extension of QUADAS-2, so it needs adapting. **An updated QUADAS-C is in development**; what follows are the E&E's own *preliminary* modifications, not a finished tool. ### What you assess The unit is a **comparative measure** — for example the difference in sensitivity or in specificity between two tests. Most primary studies report only the separate estimate for each index test, so **you will usually have to compute the comparative measure yourself.** Specify which estimate a QUADAS-C assessment refers to, exactly as you do in QUADAS-3 phase 4. ### Domains are renamed onto QUADAS-3's | QUADAS-C (as published) | becomes | |---|---| | Patient Selection | **Participants** | | Index Test | Index Test *(unchanged)* | | Reference Standard | **Target Condition** | | Flow and Timing | **Analysis** | ### Three signalling questions change (E&E Table 8) | QUADAS-C question | Change | |---|---| | **C3.2** Did the reference standard avoid incorporating any of the index tests? | **Removed** — it overlaps QUADAS-3 item 3.4 | | **C4.2** Was there an appropriate interval between the index tests? | **Moved to domain 2** (Index Test) | | **C4.3** Was the same reference standard used for all index tests? | **Moved to domain 3** (Target Condition) | Everything else carries over unchanged. ### Answers and judgements follow QUADAS-3 Signalling questions take **Y / PY / PN / N / NI**; each domain is judged **low / high / insufficient information**. The overall judgement for the comparative estimate: - **low** if all domains are low - **high** if at least one domain is high - **insufficient information** if at least one domain is II and none is high ## The six phases | Phase | What | How often | |---|---|---| | 1 | State the systematic review synthesis question(s) | once per review | | 2 | Define the **ideal test accuracy trial** for each synthesis question | once per review | | 3 | Draw a flow diagram | once per study | | 4 | Identify which accuracy estimates to assess | once per study | | 5 | Assess risk of bias and applicability | for each selected estimate | | 6 | Overall judgment | for each selected estimate | Phases 1 and 2 are review-level and **belong in the review protocol**. Phases 3–4 are study-level. Phases 5–6 run once per selected set of estimates. **Phase 1** — a review may address more than one synthesis question. Specify each with its population, index test(s) and target condition, and pre-specify them in the protocol. **Phase 2** — the ideal test accuracy trial is the study that would answer the synthesis question with minimum bias and maximum applicability. Define it per question across: objective, participants, index test(s), definition of the target condition, and analysis. Every later judgement is made **against this trial**, not against an unstated ideal. **Phase 4** — a single primary study usually yields several two-by-two tables. Assess only the estimates relevant to a synthesis question. Record, for each: the synthesis question, the numerical result, participants, index test and threshold, target condition, reference standard, unit of analysis, and the analysis method. After the first estimate, **only the domains whose characteristics differ between estimates need reassessing**. ## Phase 5 — signalling questions Signalling questions: **Y / PY / PN / N / NI**. Domain risk-of-bias judgement: **low / high / insufficient information (II)**. ### Domain 1: Participants (4) | # | Signalling question | |---|---------------------| | 1.1 | Was a single-gate design used? | | 1.2 | Were participants prospectively enrolled? | | 1.3 | Was a consecutive or random sample of participants included? | | 1.4 | Is the study group a representative sample of the intended-use population? | *Applicability*: does the included population match the ideal trial's? Participants who dropped out or were excluded because they did not receive the index test or the reference standard belong in **Domain 4 (Analysis)**, not here. ### Domain 2: Index Test (4) | # | Signalling question | |---|---------------------| | 2.1 | Was the index test conducted and interpreted according to the recommended instructions? | | 2.2 | Were the index test results interpreted without knowledge of the reference standard results? | | 2.3 | Were the index test results interpreted with the same information that would be available when the test is used in practice? | | 2.4 | If an index test threshold was used, was it standard or pre-specified? | *Applicability*: does the index test, its conduct and its interpretation match the ideal trial's? 2.3 is new relative to QUADAS-2 and cuts both ways — a reader given **more** information than they would have in practice is as much a problem as one given less. ### Domain 3: Target Condition (8) | # | Signalling question | |---|---------------------| | 3.1 | Does the reference standard adequately identify those with and without the target condition? | | 3.2 | Was the target condition assessed in all participants? | | 3.3 | Was the target condition assessed in the same way in all participants? | | 3.4 | Did the reference standard avoid incorporating the index test? | | 3.5 | Was the reference standard conducted and interpreted according to the recommended instructions? | | 3.6 | Were the reference standard results interpreted without knowledge of the index test results? | | 3.7 | If a reference standard threshold was used, was it standard or pre-specified? | | 3.8 | Was there an appropriate time interval between index test and reference standard? | *Applicability*: does the target condition as defined by the reference standard match the ideal trial's? This domain absorbs QUADAS-2's Reference Standard domain **and** its verification and timing questions. 3.2 and 3.3 are partial and differential verification; 3.8 is the interval that used to sit in Flow and Timing. ### Domain 4: Analysis (4) | # | Signalling question | |---|---------------------| | 4.1 | Were all participants included in the analysis? | | 4.2 | Were missing data handled appropriately? | | 4.3 | Does the unit of analysis match the ideal test accuracy trial? | | 4.4 | Were the estimates of sensitivity and specificity calculated appropriately? | **No applicability judgement** — applicability is assessed for the first three domains only. 4.3 is where a lesion-level or sample-level analysis meets a participant-level synthesis question. That mismatch had no home in QUADAS-2. ## Judgement rules **Domain level.** If all signalling questions in a domain are answered *yes* or *probably yes*, risk of bias can be judged **low**. A *no* or *probably no* **flags potential** for bias — it does not settle it. Reviewers then apply their judgement and their review-specific guidance to decide whether the issue is likely to have influenced the accuracy estimates. > **A study can still be at low risk of bias with one or more signalling questions answered "no."** > The tool says this explicitly. Do not implement "any No → High" as a rule; that replaces the > judgement the tool asks for. Use **insufficient information** only when too little is reported to permit a judgement. It is not a middle rating between low and high. **Overall (phase 6)**, per estimate, done separately for risk of bias and for applicability: - any domain **high** → overall **high** - all domains **low** → overall **low** - any domain **insufficient information** and none high → overall **insufficient information** Record a rationale naming the major limitations behind the overall judgement. **Do not add a "moderate" grade.** Reviewers sometimes want one, to separate a study that is high risk in a single domain from one that is high risk in several. The E&E says plainly that the authors *do not support* this: if an estimate is high risk for one domain, it is high risk, whatever the other domains say. ## Tailoring the tool to your review Phase 2's tailoring is the step most often skipped, and the E&E is specific about it. - Do it **at the protocol stage, alongside phases 1 and 2** — not when you reach the studies. - Write review-specific guidance on how to answer each signalling question, adapting the general guidance tables. Publish it as a web appendix so the application is auditable. - Draw on **both clinical and methodological** expertise in the review area. - **Do not remove signalling questions.** Keep them even when they cannot bite: if two-gate designs were excluded, every study answers "yes" to 1.1, and recording that shows the issue was considered. Deleting the question hides that. - If you add a question, it must: address **one** issue only; concern **risk of bias, not reporting quality**; and be **factual**, phrased so that "yes"/"probably yes" means bias is absent. ## When to Use - Systematic reviews assessing the accuracy of tests used for **diagnosis, screening or staging** - New reviews — QUADAS-3 is the current recommended version - Alongside **QUADAS-C** when the review compares the accuracy of two or more index tests - Read the **Explanation & Elaboration report** before first use, and tailor the signalling questions and their guidance to your review (that tailoring is the step most often skipped) - Not for prediction models (PROBAST), non-randomised intervention studies (ROBINS-I), or randomised trials (RoB 2) -
RoB2.md 8.1 KB
# RoB 2 Assessment Guide Revised Cochrane Risk-of-Bias tool for Randomised Trials. Version: RoB 2 (22 August 2019). Full guidance and the current tool: https://www.riskofbias.info Source: Sterne JAC, Savović J, Page MJ, Elbers RG, Blencowe NS, Boutron I, et al. RoB 2: a revised tool for assessing risk of bias in randomised trials. *BMJ* 2019;366:l4898 (DOI 10.1136/bmj.l4898; PMID 31462531). > **Fidelity and licence.** **No open licence was found for the article** — Crossref returns only > BMJ's text-and-data-mining policy, and `LICENSES.md` previously claimed CC BY for it on no > evidence. The signalling questions below were checked against the **official RoB 2 template and > full guidance document** distributed at riskofbias.info (version of 22 August 2019); their > wording is short-form and paraphrased. **Complete the official RoB 2 form for any assessment you > report.** > > Verification: every signalling question, its conditionality, the response options and the > domain- and overall-judgement rules were compared against that template and guidance. Four > problems were found and corrected: two questions were **missing** from the assignment variant of > domain 2 (2.5, 2.7), question 2.5 of the adherence variant had **inverted polarity** (the tool > asks about *non-adherence*), domain 5's three questions had been **collapsed into two**, and the > missing-data judgement carried a **">95%" threshold that the tool does not define** — the > guidance defines "nearly all" qualitatively and gives no percentage. Reference: Sterne JAC et al. BMJ 2019;366:l4898. PMID: 31462531. ## Structure RoB 2 is applied **to a specific result** — one outcome, one numerical result, in one trial. Record the experimental and comparator interventions, the outcome, and the numerical result before you start. - **Signalling question responses**: Yes / Probably yes / Probably no / No / No information (and **NA** where a question is conditional and its condition was not met) - **Domain judgement**: Low risk of bias / Some concerns / High risk of bias - **Optional, per domain and overall**: the predicted **direction** of bias — NA, favours experimental, favours comparator, towards null, away from null, or unpredictable Questions written as "If … to N.n" are **conditional**: ask them only when the stated answer was given to the earlier question. ## Before domain 2: state the effect of interest The review team must declare whether the aim for this result is to assess **the effect of assignment** to intervention (the intention-to-treat effect) or **the effect of adhering** to intervention. Domain 2 has a different set of questions for each; do not mix them. ## Domain 1: Bias arising from the randomisation process | # | Signalling question | |---|---------------------| | 1.1 | Was the allocation sequence random? | | 1.2 | Was the allocation sequence concealed until participants were enrolled and assigned to interventions? | | 1.3 | Did baseline differences between intervention groups suggest a problem with the randomisation process? | The tool does not aim to identify baseline imbalances that arose by chance; a small number of "statistically significant" differences at 0.05 is usually compatible with chance. ## Domain 2: Bias due to deviations from the intended interventions ### Variant A — effect of **assignment** to intervention | # | Signalling question | |---|---------------------| | 2.1 | Were participants aware of their assigned intervention during the trial? | | 2.2 | Were carers and people delivering the interventions aware of participants' assigned intervention during the trial? | | 2.3 | *If Y/PY/NI to 2.1 or 2.2:* Were there deviations from the intended intervention that arose because of the trial context? | | 2.4 | *If Y/PY to 2.3:* Were these deviations likely to have affected the outcome? | | 2.5 | *If Y/PY/NI to 2.4:* Were these deviations from intended intervention balanced between groups? | | 2.6 | Was an appropriate analysis used to estimate the effect of assignment to intervention? | | 2.7 | *If N/PN/NI to 2.6:* Was there potential for a substantial impact (on the result) of the failure to analyse participants in the group to which they were randomised? | ### Variant B — effect of **adhering** to intervention | # | Signalling question | |---|---------------------| | 2.1 | Were participants aware of their assigned intervention during the trial? | | 2.2 | Were carers and people delivering the interventions aware of participants' assigned intervention during the trial? | | 2.3 | *If applicable, and if Y/PY/NI to 2.1 or 2.2:* Were important non-protocol interventions balanced across intervention groups? | | 2.4 | *If applicable:* Were there failures in implementing the intervention that could have affected the outcome? | | 2.5 | *If applicable:* Was there **non-adherence** to the assigned intervention regimen that could have affected participants' outcomes? | | 2.6 | *If N/PN/NI to 2.3, or Y/PY/NI to 2.4 or 2.5:* Was an appropriate analysis used to estimate the effect of adhering to the intervention? | ## Domain 3: Bias due to missing outcome data | # | Signalling question | |---|---------------------| | 3.1 | Were data for this outcome available for all, or nearly all, participants randomised? | | 3.2 | *If N/PN/NI to 3.1:* Is there evidence that the result was not biased by missing outcome data? | | 3.3 | *If N/PN to 3.2:* Could missingness in the outcome depend on its true value? | | 3.4 | *If Y/PY/NI to 3.3:* Is it likely that missingness in the outcome depended on its true value? | **"Nearly all" is not a percentage.** The guidance defines it as: the number of participants with missing outcome data is so small that their outcomes, whatever they were, could have made no important difference to the estimated effect. Do not substitute a 95% or 80% rule — RoB 2 states none. **Low risk** requires any one of: (i) outcome data available for all, or nearly all, randomised participants; **or** (ii) evidence that the result was not biased by missing outcome data; **or** (iii) missingness in the outcome could not depend on its true value. ## Domain 4: Bias in measurement of the outcome | # | Signalling question | |---|---------------------| | 4.1 | Was the method of measuring the outcome inappropriate? | | 4.2 | Could measurement or ascertainment of the outcome have differed between intervention groups? | | 4.3 | *If N/PN/NI to 4.1 and 4.2:* Were outcome assessors aware of the intervention received by study participants? | | 4.4 | *If Y/PY/NI to 4.3:* Could assessment of the outcome have been influenced by knowledge of intervention received? | | 4.5 | *If Y/PY/NI to 4.4:* Is it likely that assessment of the outcome was influenced by knowledge of intervention received? | ## Domain 5: Bias in selection of the reported result | # | Signalling question | |---|---------------------| | 5.1 | Were the data that produced this result analysed in accordance with a pre-specified analysis plan that was finalised before unblinded outcome data were available for analysis? | | 5.2 | Is the numerical result being assessed likely to have been selected, on the basis of the results, from multiple eligible outcome **measurements** (e.g. scales, definitions, time points) within the outcome domain? | | 5.3 | Is the numerical result being assessed likely to have been selected, on the basis of the results, from multiple eligible **analyses** of the data? | 5.2 and 5.3 are separate questions. Selecting a measurement and selecting an analysis are different acts, and a result can be at risk from one and not the other. ## Overall risk of bias - **Low risk of bias**: low risk of bias for all domains - **Some concerns**: some concerns in at least one domain, but not high risk of bias in any domain - **High risk of bias**: high risk of bias in at least one domain, **or** some concerns for multiple domains in a way that substantially lowers confidence in the result ## When to Use - Use for **individually randomised, parallel-group trials** (default) - Variants are published for cluster-randomised trials and crossover trials — use the matching variant - Do NOT use for non-randomised studies (use ROBINS-I instead) -
ROBINS_I.md 5.6 KB
# ROBINS-I Assessment Guide Risk Of Bias In Non-randomised Studies - of Interventions. Version: ROBINS-I (2016), the original version. Tool home: https://www.riskofbias.info Source: Sterne JAC, Hernán MA, Reeves BC, Savović J, Berkman ND, Viswanathan M, et al. ROBINS-I: a tool for assessing risk of bias in non-randomised studies of interventions. *BMJ* 2016;355:i4919 (DOI 10.1136/bmj.i4919). > **Fidelity and licence.** The source article is **CC BY-NC 3.0** — non-commercial. This repository > is MIT-licensed and redistributed without restriction, so the tool's wording cannot be carried > verbatim here. This file is an **in-house summary of the tool's structure**: the seven domains, > their order, the answer options and the judgement levels, all of which were checked against the > article. The per-domain questions below are abbreviated and are **not** the tool's signalling > questions. **Complete the official ROBINS-I form from riskofbias.info for any assessment you > report.** > > **Verification.** The seven domains, their order, their pre-/at-/post-intervention grouping and > the five judgement levels with their across-domain criteria were compared against Tables 1 and 2 > of the article (Europe PMC full text, PMC5062054). All matched. The **No information** overall > judgement, which the file omitted, has been added. > > **A version 2 exists and is still in draft.** ROBINS-I V2 adds algorithms mapping signalling-question > answers onto domain judgements, and covers bias due to immortal time, which the 2016 version omits. > A revised draft was posted in November 2025 and is subject to change. Check riskofbias.info before > choosing which version to appraise against; this file documents the 2016 version. Reference: Sterne JAC et al. BMJ 2016;355:i4919. ## Structure ROBINS-I assesses 7 domains + overall judgment. The article groups them by when the bias arises: **pre-intervention** (domains 1–2, where assessment is mainly distinct from randomised trials), **at intervention** (domain 3, also mainly distinct), and **post-intervention** (domains 4–7, which overlap substantially with assessments of randomised trials). - **Signalling questions**: Yes / Probably yes / Probably no / No / No information - **Domain judgment**: Low / Moderate / Serious / Critical / No information - **Overall judgment**: Lowest of all domain judgments (most conservative) ## Pre-assessment Requirements Before applying ROBINS-I, specify: 1. The target trial (what RCT would ideally answer this question?) 2. The effect of interest (assignment to intervention vs starting and adhering) 3. Confounders to be controlled ## Domain 1: Bias Due to Confounding ### Key Questions - Is there potential for confounding not accounted for? - Did the authors use appropriate methods to control confounding (matching, regression, propensity score)? ### Judgment - **Low**: All critical confounders appropriately controlled - **Moderate**: Minor concerns about residual confounding - **Serious**: Important confounders not adequately controlled - **Critical**: Confounding so severe that no useful estimate possible ## Domain 2: Bias in Selection of Participants into the Study ### Key Questions - Was selection into the study related to both intervention and outcome? - Was start of follow-up and intervention aligned? - Were adjustments made for different start times? ## Domain 3: Bias in Classification of Interventions ### Key Questions - Were intervention groups clearly defined? - Was information used to classify interventions recorded at the start of the intervention? - Could classification of intervention status have been affected by knowledge of the outcome? ## Domain 4: Bias Due to Deviations from Intended Interventions ### Key Questions - Were there deviations from intended intervention beyond what would be expected? - Were these deviations unbalanced between groups and likely to affect outcomes? - Were important co-interventions balanced across groups? ## Domain 5: Bias Due to Missing Data ### Key Questions - Were outcome data available for all or nearly all participants? - Were participants excluded due to missing data on intervention or other variables? - Was the proportion of missing data similar across groups? - Were appropriate methods used to handle missing data? ## Domain 6: Bias in Measurement of Outcomes ### Key Questions - Could outcome measurement have been influenced by knowledge of intervention? - Were outcome assessors blinded? - Were outcome measures comparable across groups? ## Domain 7: Bias in Selection of the Reported Result ### Key Questions - Were multiple outcome measurements reported? - Were multiple analyses performed? - Is the reported result likely selected from among multiple measurements or analyses? ## Overall Risk of Bias The overall judgment follows the criteria in the article's Table 2: - **Low**: the study is judged at low risk of bias for all domains — comparable to a well performed randomised trial - **Moderate**: low or moderate for all domains — sound evidence for a non-randomised study, but not comparable to a well performed randomised trial - **Serious**: serious in at least one domain, but not critical in any - **Critical**: critical in at least one domain — too problematic to provide useful evidence, and should not be included in any synthesis - **No information**: no clear indication that the study is at serious or critical risk of bias, and information is lacking in one or more key domains ## Recommendation for Synthesis - Studies at **critical** risk of bias should be excluded from meta-analysis - Present critical studies in a separate table for completeness - Conduct sensitivity analysis excluding serious risk of bias studies
-
-
ai_pre_screening_template.py 9.8 KB
"""AI-Assisted Pre-Screening Template for R3 Adjudication. PURPOSE ------- For systematic review screening, the methodologically sound workflow requires TWO independent human reviewers with Cohen's kappa, followed by adjudication of disagreements (R3) by the first reviewer. R3 commonly involves 100+ records and is the bottleneck. This template generates AI-assisted *suggestions* (NOT decisions). The first reviewer must still confirm or overturn each suggestion. This preserves methodological integrity while compressing R3 from 2-3 hours x 2-3 days to ~30-60 minutes. NOT a replacement for human reviewer. Methods boilerplate is provided below. INPUT ----- TSV with at minimum these columns: uid, title, abstract_preview, journal, doi, round2_tag, round2_reason `round2_tag` values expected: INCLUDE -> AI default = CONFIRM-INCLUDE; flag review/conf abstract patterns MAYBE -> AI requires per-uid manual decisions (UNCERTAIN if not pre-coded) EXCLUDE -> typically not in R3 sheet; if present, AI default = CONFIRM-EXCLUDE OUTPUT ------ Same TSV plus columns: ai_suggestion : INCLUDE / EXCLUDE / UNCERTAIN / CONFIRM-INCLUDE / CONFIRM-EXCLUDE ai_reason : one-sentence justification USAGE ----- 1. Copy this file to your project's `1_Code/` directory. 2. Customize: - `PROJECT_PECOS` (eligibility criteria summary) - `MAYBE_DECISIONS` (per-uid manual judgments after reading each abstract) - `EXCLUDE_PATTERNS` (project-specific keyword patterns) 3. Run; review priority-sorted output (use companion `make_review_priority_view.py`). METHODS BOILERPLATE ------------------- "Round 3 adjudication of records flagged as 'maybe' by either reviewer was performed by the first reviewer (initials) with AI-assisted pre-screening (model name and version). The AI model was prompted with the prespecified PECOS criteria and produced a suggestion plus brief justification for each record; the first reviewer independently confirmed or overturned every suggestion against the title, abstract, and (when needed) full text. AI suggestions were not used as final inclusion decisions." CITATION -------- Inspired by emerging AI-assisted screening practice (e.g., Khalil et al., Syst Rev 2022). Document model and version in the manuscript. """ from __future__ import annotations import csv from pathlib import Path # ============================================================================ # 1. PROJECT CONFIGURATION — customize per project # ============================================================================ PROJECT_PECOS = """ Replace this string with a one-paragraph summary of your PECOS criteria. Used as documentation only; the AI logic below must be coded explicitly. """ # Per-uid MAYBE decisions. Fill after reading each MAYBE abstract. # Format: uid -> (suggestion, one-sentence reason) # Suggestion values: INCLUDE / EXCLUDE / UNCERTAIN MAYBE_DECISIONS: dict[str, tuple[str, str]] = { # "study_0042": ("INCLUDE", "Title confirms target population + intervention; design = RCT."), # "study_0099": ("EXCLUDE", "Conference abstract without full publication."), # "study_0123": ("UNCERTAIN", "Abstract truncated; full text needed to confirm intervention arm."), } # ============================================================================ # 2. INCLUDE-row heuristic classifier — common patterns; tune for your project # ============================================================================ # Hard EXCLUDE keyword patterns (title or first 300 chars of abstract) REVIEW_KEYWORDS = [ "systematic review", "meta-analysis", "narrative review", " review of ", "scoping review", "review article", "editorial", "commentary", "letter to the editor", ] # Conference abstract / supplement detection CONF_JOURNAL_KEYWORDS = ["abstracts", "supplement"] # Common DOI markers for supplements (tune to your field) CONF_DOI_PATTERNS = ["suppl_", "/circ.144.suppl", "/circ.146.suppl", ".abstract.", "-snis.", "-esmint."] # Single case report markers CASE_KEYWORDS = ["case report", "case series"] def classify_include_row(uid: str, title: str, abstract: str, journal: str, doi: str, reason: str, topic_keywords: list[str] | None = None) -> tuple[str, str]: """Heuristic AI suggestion for INCLUDE-tagged rows. Default = CONFIRM-INCLUDE. Returns EXCLUDE for clear review/conference/ case-report patterns, or when no topic keyword appears in title or abstract. Args: topic_keywords: list of lowercase keywords; if NONE found in title or abstract, suggest EXCLUDE. Pass project topic terms (e.g., for an aneurysm SR: ["aneurysm", "intracranial", "cerebral", "vasc"]). """ t = title.lower() a = abstract.lower() j = journal.lower() d = doi.lower() # Review / editorial filter if (any(k in t for k in REVIEW_KEYWORDS) or any(k in a[:300] for k in REVIEW_KEYWORDS)): if "primary" not in a[:500] and "we developed" not in a[:500]: return ("EXCLUDE", "Likely review/editorial based on title/abstract keywords.") # Conference abstract filter if (any(k in j for k in CONF_JOURNAL_KEYWORDS) or any(k in d for k in CONF_DOI_PATTERNS)): return ("EXCLUDE", "Conference abstract / supplement; likely no full peer-reviewed publication.") # Single case report if any(k in t for k in CASE_KEYWORDS): if "single" in t or "single case" in a[:300]: return ("EXCLUDE", "Single case report -- excluded by study design criterion.") # Off-topic check if topic_keywords: if (not any(k in t for k in topic_keywords) and not any(k in a[:500] for k in topic_keywords)): return ("EXCLUDE", "No topic keyword in title or abstract.") return ("CONFIRM-INCLUDE", "Title/abstract consistent with PECOS; default include, verify at full-text extraction.") # ============================================================================ # 3. Main pipeline # ============================================================================ def run_pre_screening(src_tsv: Path, dst_tsv: Path, topic_keywords: list[str] | None = None) -> None: """Read screening TSV, add ai_suggestion/ai_reason columns, write output.""" rows = list(csv.DictReader(src_tsv.open(encoding="utf-8"), delimiter="\t")) if not rows: raise SystemExit(f"No rows in {src_tsv}") in_fields = list(rows[0].keys()) extra = [c for c in ("ai_suggestion", "ai_reason") if c not in in_fields] out_fields = ["uid", "ai_suggestion", "ai_reason"] + [ c for c in in_fields if c not in ("uid", "ai_suggestion", "ai_reason") ] counts: dict[str, int] = {} with dst_tsv.open("w", encoding="utf-8", newline="") as f: w = csv.DictWriter(f, fieldnames=out_fields, delimiter="\t", quoting=csv.QUOTE_MINIMAL) w.writeheader() for r in rows: uid = r["uid"] tag = r.get("round2_tag", "").upper() if tag == "MAYBE": if uid in MAYBE_DECISIONS: sug, why = MAYBE_DECISIONS[uid] else: sug, why = ("UNCERTAIN", "MAYBE row without pre-coded decision; full review needed.") elif tag == "INCLUDE": sug, why = classify_include_row( uid, r.get("title", ""), r.get("abstract_preview", ""), r.get("journal", ""), r.get("doi", ""), r.get("round2_reason", ""), topic_keywords=topic_keywords, ) elif tag == "EXCLUDE": sug, why = ("CONFIRM-EXCLUDE", "Round 2 EXCLUDE; reviewer to confirm with brief check.") else: sug, why = ("UNCERTAIN", f"Unexpected round2_tag={tag!r}") counts[sug] = counts.get(sug, 0) + 1 r["ai_suggestion"] = sug r["ai_reason"] = why w.writerow({k: r.get(k, "") for k in out_fields}) print(f"Wrote {dst_tsv} ({len(rows)} rows)") print("AI suggestion distribution:") for k in sorted(counts): print(f" {k}: {counts[k]}") # ============================================================================ # 4. Companion: priority-sorted view # ============================================================================ PRIORITY_ORDER = { "UNCERTAIN": 0, "EXCLUDE": 1, "INCLUDE": 2, "CONFIRM-EXCLUDE": 3, "CONFIRM-INCLUDE": 4, } def write_priority_view(src_tsv: Path, dst_tsv: Path) -> None: """Sort AI-pre-screened TSV by review priority.""" rows = list(csv.DictReader(src_tsv.open(encoding="utf-8"), delimiter="\t")) rows.sort(key=lambda r: (PRIORITY_ORDER.get(r["ai_suggestion"], 9), r["uid"])) fields = list(rows[0].keys()) with dst_tsv.open("w", encoding="utf-8", newline="") as f: w = csv.DictWriter(f, fieldnames=fields, delimiter="\t", quoting=csv.QUOTE_MINIMAL) w.writeheader() w.writerows(rows) print(f"Wrote priority view: {dst_tsv}") # ============================================================================ # 5. Example invocation # ============================================================================ if __name__ == "__main__": # Customize these paths and topic keywords for your project. PROJECT_ROOT = Path(__file__).resolve().parent SRC = PROJECT_ROOT / "round3_adjudication_TEMPLATE.tsv" AI = PROJECT_ROOT / "round3_adjudication_AI_TEMPLATE.tsv" PRIORITY = PROJECT_ROOT / "round3_priority_TEMPLATE.tsv" TOPIC_KEYWORDS = ["TOPIC1", "TOPIC2"] # e.g., ["aneurysm", "intracranial"] run_pre_screening(SRC, AI, topic_keywords=TOPIC_KEYWORDS) write_priority_view(AI, PRIORITY) -
data_integrity_checklist.md 5.4 KB
# Data Integrity Checklist **Applies to**: the full span of Phase 3 (Extraction) ~ Phase 6 (Statistical synthesis) ~ Phase 9 (Circulation). Blocks the numerical-consistency risks specific to meta-analysis. ## 1. Extraction stage ### DI-1. Formalize the extraction consensus log - **Problem pattern**: Comparative extraction results kept only as inline comments inside the R analysis script — no standalone consensus log → arm-specific numbers become irretraceable once the script is edited. - **Rule**: At the start of every MA project, create `2_Data/extraction_consensus_log.md` or `3_Extraction/extraction_consensus_log.md`. Columns: study_id, arm, numerator, denominator, source_page, source_type (text/table/figure/KM-reconstruction), extractor_initials, second_reviewer_initials, timestamp, notes. Comparative extractions must live only as formal rows, never as script comments. ### DI-2. Mandatory double-check of 2x2 cell counts - **Problem pattern**: Hand-typed 2x2 cells with arm order swapped against the source paper, or with numerator/denominator misread from a KM-derived subgroup rather than the raw table. Both fail silently until a third reviewer back-calculates the proportion. - **Rule**: Every 2x2 / comparative extraction requires (a) a first extraction + (b) an independent second re-extraction + (c) source re-check on any mismatch + a consensus-log row. Perform the Phase 6b "numerical safety gate". ### DI-3. Complete the KM-reconstruction audit trail - **Problem pattern**: Subgroup cell counts reconstructed from a published KM curve without preserving the WebPlotDigitizer trace or the IPDfromKM reconstruction log → numbers cannot be re-derived if a reviewer challenges them. - **Rule**: KM-reconstruction outputs must be kept as a set in `3_Extraction/km_reconstruction/{study_id}/`: (a) the WebPlotDigitizer JSON, (b) the IPDfromKM CSV, and (c) metadata for tool version + coordinate values + parameters + date. Link them in the consensus log as type "km-reconstruction". ### DI-4. Denominator change = source page citation + consensus-log row - **Problem pattern**: Denominator correction (e.g., treatment-naive subset) entered only as an R-script comment without citing the source paper's page/table → the correction's rationale is lost at revision. - **Rule**: Every denominator change requires (a) a source page/table citation, (b) a sentence stating the rationale, and (c) one consensus-log row. Reject the change if any of the three is missing. ### DI-5. Methodology mismatch random spot-check - **Problem pattern**: A source paper reports per-protocol analysis while the SR framework is ITT/ITD (or vice versa). Without a methodology spot-check, the study's effect estimate is silently re-used under a different analysis framework. - **Rule**: Include a "methodology flag" in the extraction spot-check scope — whether each study's source analysis unit (per-protocol / ITT / ITD) matches our SR framework. Re-extract on mismatch. ## 2. 3~5-way consistency ### DI-6. PRISMA flow 5-way consistency - **Problem pattern**: PRISMA flow numbers drift across the search CSV, the screening log, the Methods prose, the Results prose, and the Figure 1 caption — five surfaces reach submission in three mutually inconsistent states (reversed database order, divergent full-text-assessed counts, stale caption numbers). - **Rule**: Verify the PRISMA flow numbers as consistent across five places simultaneously: 1. `1_Search/*.csv` original (source of truth, no edits) 2. `2_Screening/prisma_flow_final.md` 3. Manuscript Methods prose 4. Manuscript Results prose (where mentioned again) 5. Figure 1 caption (both `5_Figures/_captions.md` and the short DOCX caption) - **ID-Set Gate Rule 5**: fix the prose first → then render the diagram. If the diagram is built first, editing the prose causes drift. - **Automation candidate**: manage `k`, `n`, and the search numbers in a single YAML source and substitute them into the prose/diagram templates. ### DI-7. Single source for k = `4_Analysis/*.csv` - **Problem pattern**: Included-study count `k` quoted as two different values across consecutive manuscript versions because it was hand-typed into prose rather than derived from the analysis CSV. - **Rule**: Derive k only from the `4_Analysis/*.csv` row count. When entering k into the manuscript / MANIFEST / PROSPERO, also state the CSV path + row count. ## 3. Pre-submission cleanup ### DI-8. Remove every tag / TODO - **Problem pattern**: `TODO`, `[VERIFY-CSV]`, "to be regenerated" strings survive into the submission package — found in R scripts, figure captions, and supplementary material banners. - **Rule**: 0 hits for the following grep before submission day: ```bash rg -n "VERIFY-CSV|TODO|FIXME|XXX|to be regenerated|PH TODO|to-do" \ 7_Manuscript/ supplement/ 5_Figures/ 6_Tables/ 1_Code/ ``` - **`[VERIFY-CSV]` lifecycle**: attach (v6) → verify (v7) → mark (v8+) → remove (submission). Record each stage in the MANIFEST. ### DI-9. State bias-driven homogeneity interpretation - **Problem pattern**: In a DTA pool where every included study is retrospective with differential verification, Sp I²=0% reflects universalized bias compression rather than true between-study homogeneity — easy to over-interpret as robust agreement. - **Rule**: When I²=0% and QUADAS-2 Domain 4 risk are both high/unclear → state "universalized bias compression" explicitly and frame as an "upper-bound estimate". Bias context is mandatory in the Discussion. -
empirical_lessons.md 7.4 KB
# Empirical Lessons — SR-MA peer-review cycles Accumulated, dated lessons from real systematic-review / meta-analysis peer-review and submission cycles. They drive the Phase 4 extraction-form schema, the Phase 4c QC scripts, and the Phase 8 submission gates. **Load this file when designing the extraction form (before Phase 4) and before submission (Phase 8).** > Note: the 2026-05 list below carries a duplicate "9." (the "Prognostic / survival-outcome" > item) inherited from the source; it is the 11th lesson of that batch. Left as-is here to > avoid a renumber that would desync any external cross-reference; treat the numbers as labels. ## Empirical Lessons (2026-05) Synthesized from recent SR-MA peer-review cycles. Drives the Phase 4 extraction form schema, Phase 4c QC scripts, and submission-gate enhancements documented in the skill. 1. **Dual-extractor + source-page-reference + verbatim quote** is mandatory for 2x2 cell integrity. Single-extractor without source-page citation invites sens/spec swap that is invisible to forest-plot-level review. 2. **Cohort overlap detection** must cluster by shared public database + institution + author. Independent-cohort assumption for MA pooling fails when multiple included studies use the same public ICU/EHR cohort with overlapping enrollment windows. Sensitivity analysis excluding overlap is the minimum acknowledgment. 3. **Diagnostic subset N transparency** in mixed DTA + prognostic MAs: report `sample_n_dta_pool` separately from `sample_n_prognostic_pool` with explicit prevalence. Aggregate N in Abstract misleads readers about diagnostic-subset power. 4. **Small-k subgroups are not robust (k < 4)**: a subgroup test driven by a single study (k=1) is descriptive-only, and the same caution extends to k=2–3 — heterogeneity and the trend are not estimable from so few strata. Any subgroup with k < 4 must be labelled descriptive / exploratory rather than entered into a formal subgroup interaction test. Post-hoc subgroups require a PROSPERO amendment with a visible record. 5. **Supplementary 8-file package** is the minimum bar for high-impact journals: PRISMA checklist, PROSPERO PDF, full search strategy, full-text exclusion list with reasons, per-study extraction table, per-study x per-domain RoB, subgroup forests, sensitivity / publication-bias analyses. See `templates/supplementary_8file_checklist.md`. 6. **PROSPERO 14-char ID format** (`^CRD42\d{9}$` = `CRD42` + 4-digit year + 5-digit sequence, e.g. `CRD42024500001`). A 15-character ID is a stray-digit transcription error; pre-2020 IDs may be shorter. Validate with `grep -oE 'CRD42[0-9]+'` + length assert, and request the live registration URL in the cover letter for protocol cross-check. 7. **AI Disclosure presence** for SR-MA submissions to RYAI / Radiology / RSNA / Lancet / JAMA / BMJ / Nature families. Absence triggers MINOR-to-MAJOR finding at peer review. 8. **Sensitivity analyses are recomputed, not copied** (Phase 6b rule 5). Leave-one-out / erosion / alternative-model effect sizes identical to the primary analysis to 2 decimals across ≥4 values means the recomputation did not run. Re-derive from the modified dataset; the inputs (means/SDs/counts) change even when the effect size is close. 9. **Outcome harmonization before pooling.** Studies that report the same-named outcome under different definitions (an imaging-detected event vs a clinically diagnosed one; different thresholds) must not be presented as a single pooled range or pooled estimate. Split by ascertainment method (or pool only the harmonizable subset) and state the definition per stratum — a "6.9–46%" range that silently mixes imaging-detected and clinical events is a heterogeneity artifact, not a finding. 10. **Heterogeneous RoB instruments → no single pooled κ.** When studies are assessed with different risk-of-bias tools (QUADAS-2 for DTA + NOS for cohorts, etc.), do not report one pooled inter-rater κ across the mixed set. Report agreement per instrument, and use an ordinal weighted κ when the domain judgments are ordered (low/some/high). A single κ over a heterogeneous instrument set is uninterpretable. 9. **Prognostic / survival-outcome MAs carry survival-specific concerns** beyond the DTA pitfalls: censoring handling, competing risks (cause-specific vs Fine-Gray), cutoff-derivation optimism, comparator time-horizon alignment, C-index variant transparency (Harrell vs Uno vs IPCW), and calibration beyond discrimination. When pooling prognostic models, pre-specify these in the protocol and report them per study; for the reviewing counterpart see the survival/prognostic 7-probe in `/peer-review`. ## Empirical Lessons (2026-06) Submission-stage lessons from an SR-MA cycle on an Editorial Manager journal; complements the 2026-05 lessons. 11. **Supplementary materials need the same blinding + de-scaffolding + cross-consistency pass as the manuscript.** The largest source of pre-submission defects this cycle was the supplement shipping as raw internal artifacts — `/check-reporting` output carrying an "Assessed by: <AI tool>" line and a JSON verdict block, and a pre-search planning doc with the author's real name, sibling-project cross-references, unresolved `[Check on execution]` placeholders, and estimate tables that contradicted the actual PRISMA counts. Presence (Lesson 5) is not enough; apply the Phase 8 supplementary gate. 12. **A submitted analysis script must reproduce the manuscript and be self-contained.** A hard-coded study-id subset silently drifted (a pool was k=7 in the script vs k=9 in the results table — the manuscript was correct, the script was stale) and the script read a path outside the bundle. Run the bundled code from a clean copy before submission: it must read the bundled dataset, write to cwd, and regenerate every reported pool. Remove stale figures produced by an out-of-sync script. 13. **Re-sync sidecars (cover letter, title-page Word-Counts table) whenever the reference or word count changes.** Adding methodological/software citations took the list from 12 to 24, but the cover letter and title page still said "12 references" — a contradiction visible in the built PDF. Reference/word-count changes are sidecar drift targets (mirror `submission-portal-verification` cover-letter drift). 14. **Methodological + software citations are a routine SR-MA gap.** The reporting standard (PRISMA 2020), each risk-of-bias tool (JBI, ROBINS-I, …), the pooling method (random-effects GLMM / logit, Hartung-Knapp, the choice over Freeman-Tukey arcsine), the certainty framework (GRADE), and the analysis software (R `meta`, `metafor`) should each be cited where named in Methods. Frequently missing from an early draft and an easy reviewer comment to pre-empt. Verify every added citation via PubMed/CrossRef with a first-author cross-check — never from memory. 15. **Wide characteristics tables (≥ ~10 columns) render as character-wrapped gibberish in the journal's built PDF** when the docx uses fixed narrow columns. Put the table in a landscape section with autofit layout and a smaller font, and verify by converting the docx to PDF (`soffice --headless --convert-to pdf`) and viewing the page — the docx alone does not reveal the problem. 16. **Verify the submission portal's journal identity before entering metadata.** A classification taxonomy that does not match the target journal's scope (e.g., a liver/hepatology list at an interventional-radiology journal) is the tell that you are in the wrong journal's Editorial Manager instance. -
icmje_coi_guide.md 5.9 KB
# ICMJE COI Form Generation Guide ## Overview Most journals (Springer/CVIR, Lancet, npj, etc.) require ICMJE Conflict of Interest disclosure forms from all authors. This guide documents how to batch-generate pre-filled forms using `python-docx`. ## Template Location Use any existing blank ICMJE form as template. Suggested locations: - A previously-used filled form from one of your manuscripts (under `<project>/submission/{journal}/icmje_forms/`). - Download a blank from https://www.icmje.org/disclosure-of-interest/. ## ICMJE Form Structure (docx) ``` Table 0 (Header): 6 rows × 2 cols Row 0: Title ("ICMJE DISCLOSURE FORM") Row 1: Date Row 2: Author name ← PRE-FILL THIS Row 3: Manuscript title ← PRE-FILL THIS Row 4: Manuscript number ← Leave blank or fill if known Row 5: Instructions text Table 1 (Disclosures): 19 rows × 5 cols Row 0: Column headers Row 1: Time frame header (since initial planning) Row 2: Item 1 — Support for present manuscript Row 3: Time frame header (past 36 months) Rows 4-15: Items 2-13 (grants, royalties, consulting, etc.) → Col 3: Entity name (leave blank if no conflict) → Col 4: Specifications (leave blank if no conflict) Row 16: Empty Row 17: Certification header Row 18: Certification text ← ADD "X" to certify ``` ## Python Script: Batch Generate ```python #!/usr/bin/env python3 """Batch-generate ICMJE COI forms from a template.""" import shutil from pathlib import Path from docx import Document def generate_coi_forms( template_path: str, output_dir: str, manuscript_title: str, authors: list[tuple[str, str]], # [(name, email), ...] manuscript_number: str = "", date_str: str = "", ): """ Generate one ICMJE COI form per author. KNOWN ISSUES (python-docx + ICMJE form): 1. Merged cells: Table 0 Row 0 and Table 1 headers use merged cells. Writing to cell.text replaces ALL merged cell content. → Use cell.paragraphs[0].runs approach instead. 2. Formatting loss: Direct cell.text assignment strips bold/italic. → Preserve runs and only modify run.text. 3. XML namespace errors: Some ICMJE forms have custom XML. python-docx may warn about "lxml.etree.XMLSyntaxError". → These warnings are usually harmless; the output is still valid. """ out = Path(output_dir) out.mkdir(parents=True, exist_ok=True) for i, (name, email) in enumerate(authors, 1): # Copy template to preserve all formatting fname = f"ICMJE_COI_{i:02d}_{name.replace(' ', '_')}.docx" dst = out / fname shutil.copy2(template_path, dst) doc = Document(str(dst)) t0 = doc.tables[0] # Pre-fill header fields # IMPORTANT: Access the cell, find existing text, append to it # Don't replace — some cells have label + value in same cell # Row 1: Date if date_str: _safe_append(t0.rows[1].cells[0], f" {date_str}") # Row 2: Author name _safe_append(t0.rows[2].cells[0], f" {name}") # Row 3: Manuscript title _safe_append(t0.rows[3].cells[0], f" {manuscript_title}") # Row 4: Manuscript number if manuscript_number: _safe_append(t0.rows[4].cells[0], f" {manuscript_number}") doc.save(str(dst)) print(f" Created: {fname}") print(f"\nGenerated {len(authors)} COI forms in {output_dir}") def _safe_append(cell, text): """Append text to a cell without destroying existing formatting. This avoids the common python-docx pitfall where cell.text = "new" destroys all runs and formatting in merged cells. """ if cell.paragraphs: p = cell.paragraphs[0] if p.runs: # Append to last run to preserve formatting p.runs[-1].text += text else: # No runs — add one run = p.add_run(text) else: cell.text = text # === Example usage === if __name__ == "__main__": # Replace with the actual author roster for your manuscript. authors = [ ("Author One", "author1@example.com"), ("Author Two", "author2@example.com"), ("Author Three", "author3@example.com"), # ... ] generate_coi_forms( template_path="path/to/blank_ICMJE_template.docx", output_dir="submission/{journal}/icmje_coi_forms/", manuscript_title="Your Manuscript Title Here", authors=authors, date_str="YYYY-MM-DD", ) ``` ## Common Pitfalls ### 1. Merged Cell Destruction **Problem**: `cell.text = "new value"` on a merged cell deletes content in ALL merged cells. **Fix**: Use `_safe_append()` or access `cell.paragraphs[0].runs[-1].text`. ### 2. XML/SQL-like Errors **Problem**: `lxml.etree.XMLSyntaxError` when opening some ICMJE forms. This happens because ICMJE's official template contains custom XML namespaces that python-docx doesn't fully support. **Fix**: These warnings are cosmetic. The output file is still valid Word doc. If errors persist, use `shutil.copy2()` first, then open the copy. ### 3. Formatting Loss on Save **Problem**: Bold/italic disappears after saving. **Fix**: Never assign to `cell.text` directly. Always work through `cell.paragraphs[0].runs` to preserve formatting. ### 4. Empty Cells for "No Conflict" **Problem**: Some journals want explicit "None" vs empty cell. **Fix**: Check journal-specific requirements. CVIR (Springer) accepts empty cells. ## Workflow for New Projects 1. Copy any existing ICMJE form as template (or download blank from icmje.org) 2. Update `authors` list with names and emails 3. Update `manuscript_title` 4. Run script → generates N individual docx files 5. Send to co-authors for review/signature 6. Collect signed forms before submission ## Author Email List (JSON format) Store alongside COI forms for reference: ```json [ ["Author Name", "email@example.com"], ... ] ``` -
LICENSES.md 1.2 KB
# Checklist Licenses Attribution for bundled reporting guideline checklists. | File | Guideline | Reference | License | |------|-----------|-----------|---------| | STROBE.md | STROBE 2007 | von Elm E et al. PLoS Med 2007 | CC BY | | STARD.md | STARD 2015 | Bossuyt PM et al. BMJ 2015 | CC BY 4.0 | | TRIPOD_AI.md | TRIPOD+AI 2024 | Collins GS et al. BMJ 2024 | CC BY 4.0 | | PRISMA_2020.md | PRISMA 2020 | Page MJ et al. BMJ 2021 | CC BY | | ARRIVE_2.md | ARRIVE 2.0 | Percie du Sert N et al. PLoS Biol 2020 | CC0 | | PRISMA_DTA.md | PRISMA-DTA 2018 | McInnes MDF et al. JAMA 2018 | CC BY | | QUADAS2.md | QUADAS-2 | Whiting PF et al. Ann Intern Med 2011 | CC BY | | RoB2.md | RoB 2 | Sterne JAC et al. BMJ 2019 | CC BY | | ROBINS_I.md | ROBINS-I | Sterne JAC et al. BMJ 2016 | CC BY | | PROBAST.md | PROBAST | Wolff RF et al. Ann Intern Med 2019 | CC BY | | NOS.md | Newcastle-Ottawa Scale | Wells GA et al. Ottawa Hospital Research Institute | Public Domain | These files are educational summaries of published assessment tools. The original checklist documents should be cited in any manuscript that uses them. All files are compatible with the MIT license of this repository. -
phase10_recovery.md 6 KB
# Phase 10 Reference — Self-Audit Recovery (v{N} → v{N+1} Sprint) Load this reference when any Phase 10 trigger fires during `/meta-analysis`. The SKILL.md body carries only the trigger table and summary pointer; all procedural detail lives here. ## Goal When an audit — internal, or triggered by an incoming co-author comment — uncovers a structural data or protocol-application error, withdraw the current version, rebuild, and re-circulate with a transparent audit trail. Catching the error yourself before a journal reviewer does is the principal trust-building move in this phase. ## Trigger Conditions (any one) - Extraction CSV disagrees with the primary source for a cell that materially feeds a pooled estimate, subgroup estimate, or a reported proportion. - A study that was excluded (or included) by the pre-specified criteria is found to violate those criteria on re-read. - A hand-typed numerical literal in the analysis script traces to a wrong value (see the Phase 6b precedent failure pattern). - The PROSPERO (or equivalent) protocol and the delivered analysis disagree on a pre-specified outcome, subgroup, or eligibility rule. - A dual-reviewer consensus record shows a study was supposed to be excluded, but the locked dataset still contains it (or vice versa). ## Non-Negotiable Rule If the trigger fires after Phase 9 circulation but before journal submission, withdraw the current version within 24 hours. Reviewer discovery is a strictly worse failure mode than self-withdrawal. ## 10.1 Audit Log Create `qc/audit_vN_to_vNplus1.md`. For each flagged item, record: - Affected study (first author, year). - Cell or claim (e.g., TP/FP/FN/TN, k, pooled estimate, CI, subgroup count). - Source of the error (extraction, application of criteria, script literal, protocol mismatch, reviewer-consensus mismatch). - Proposed resolution (exclude study, re-extract cell, correct script literal, amend protocol, reinstate study). ## 10.2 CSV Re-Verification For every flagged cell, re-verify against the primary source (paper Table/Figure, page number). Any revision-introduced value must carry the `[VERIFY-CSV]` tag until the Phase 6b audit clears it. ## 10.3 Re-Run the Analysis Script Re-execute the full synthesis pipeline with a fixed random seed. Save complete output to `analysis/vNplus1_run.log`. Do not re-use partial v{N} output even for subsections that "should be unchanged" — prove it with a fresh run. ## 10.4 Manuscript Auto-Sync Every number the manuscript reports — k, pooled estimate, 95% CI, τ², I², subgroup counts, sensitivity-analysis rows, GRADE levels — must be pulled from the fresh output. Grep the manuscript for old v{N} numerical values to confirm no residue survives. ## 10.5 Supplementary Regeneration Re-emit (do not patch): - Extraction consensus log. - RoB tables. - GRADE / Summary of Findings table. - PRISMA flow diagram (counts shift when studies are excluded/reinstated). ## 10.6 Figure Regeneration Regenerate forest plots, funnel plots, subgroup plots, and the PRISMA flow via `/make-figures`. Re-embed at the original manuscript positions. Confirm figure captions match the new values (captions often contain pooled estimates). ## 10.7 Change Summary Produce `v{N+1}_change_summary.md` with an explicit delta table: | Item | v{N} | v{N+1} | Reason | |------|------|--------|--------| | k | | | | | Primary pooled estimate (95% CI) | | | | | Heterogeneity (I², τ²) | | | | | Studies added | | | | | Studies removed | | | | | Subgroup re-stratification | | | | This file is the external audit trail. It must be circulated with the v{N+1} manuscript. ## 10.8 PROSPERO (or Equivalent) Amendment If eligibility, analysis population, or outcome definitions changed, file an amendment to the registered protocol in parallel with the re-build: - Keep revision notes within the registry's word limit (often 250 words). - Frame the amendment as application correction, not criteria change: > "The pre-specified inclusion criteria are unchanged. The amended application excludes > {N} study(ies) whose characteristics were confirmed on independent re-review to fall > outside the pre-specified criteria." - Submit the amendment before — or simultaneously with — re-circulation, so co-authors see an amendment already in flight rather than only a draft change. ## 10.9 Re-Circulation Framing Use the Phase 9 thread. State the situation plainly in the first paragraph: > "On re-review of v{N}, we identified {N} study(ies) whose data did not meet the > pre-specified inclusion criteria. These were excluded and the analysis regenerated. > The manuscript body, supplementary, and all figures have been re-rendered. A change > summary is attached." Tonal anchors worth preserving: - "On re-review" — signals internal audit, not external pressure. - "Pre-specified criteria" — the protocol itself is unchanged; application is corrected. - "Change summary" — an external audit trail is available for scrutiny. ## 10.10 Anti-Patterns (do not) - Hide the error and submit v{N} as-is. This converts a recoverable finding into a retraction-class incident if the reviewer catches it. - Reframe the fix as a "minor revision" with no audit trail. Senior reviewers read audit trails; absence of one signals concealment. - Disclose only in the cover letter while leaving Methods silent. Methods and the cover letter must agree. ## 10.11 When the Trigger Fires Post-Submission If the audit trigger is detected after the journal submission but before an editorial decision, notify the editorial office immediately and submit a corrected manuscript alongside the disclosure. Do not wait for the reviewer to surface it; editors prefer author-initiated correction. ## 10.12 Post-Recovery Loop After v{N+1} is circulated, Phase 9 restarts. If a further audit trigger emerges during the new review window, a second recovery sprint (v{N+1} → v{N+2}) is acceptable — but each recovery costs co-author goodwill, so the incoming audit in Phase 6b should be tightened each cycle rather than relying on Phase 10 as a routine catch. -
phase3_screening_detail.md 10.7 KB
# Phase 3 — Screening & Selection: round procedures and gate detail Load-on-demand companion to `/meta-analysis` Phase 3. SKILL.md keeps the four screening rounds, the two MANDATORY gates (3f reconciliation, 3f.5 pool lock), and their hard rules; this file carries the detail. Read the block you need: - **Rounds 3a–3d** — exclusion-code sets, the AI-assisted pre-screening template and its Methods boilerplate, the full-text exclusion codes. - **3f reconciliation** — the ID-set algebra, the reconciliation-table template, and the precedent incident where four downstream artifacts echoed one unreconciled prose total. - **3f.5 pool lock** — why the lock exists, how to build it, and every downstream gate that reads it. **Goal**: Systematic title/abstract and full-text screening with two independent reviewers. #### 3a. Round 1 — Initial Title/Abstract Screening (single reviewer) 1. Define exclusion codes from protocol (e.g., E1=Not target population, E2=Not intervention, E3=Ineligible type, E4=Non-human, E5=Duplicate). 2. For each record, screen title+abstract against eligibility criteria. 3. Mark each record as INCLUDE / EXCLUDE / MAYBE with reason code. 4. Output: `round1_{date}.tsv` with color-coded decisions. #### 3b. Round 2 — Dual Independent Title/Abstract Screening 1. A second independent reviewer (or AI as a documented second-pass tool with human verification) re-screens all R1 records. 2. Compute Cohen's kappa at title/abstract stage; report in Methods. 3. Tag each record's `round2_tag` as INCLUDE / EXCLUDE / MAYBE based on R1+R2 agreement (MAYBE = disagreement OR either reviewer flagged uncertain). 4. Output: `round2_{date}.tsv` (adds `round2_tag`, `round2_reason` columns). #### 3c. Round 3 — Adjudication of Disagreements (first reviewer) 1. Build R3 sheet: all MAYBE records first, followed by INCLUDE records (which receive a brief confirmation pass). 2. The **first reviewer** independently adjudicates each row, recording `round3_decision` (INCLUDE/EXCLUDE) and `round3_reason` (only when overturning R2). 3. **Optional AI-assisted pre-screening** to compress R3 effort: - Use `references/ai_pre_screening_template.py` (customize per project). - Pre-screen produces `ai_suggestion` (INCLUDE/EXCLUDE/UNCERTAIN/CONFIRM-INCLUDE) + `ai_reason` columns. - Sort priority: UNCERTAIN → EXCLUDE → INCLUDE → CONFIRM-INCLUDE. - First reviewer must independently confirm or overturn every AI suggestion against the title, abstract, and (when needed) full text. AI suggestions are **not** final decisions. - Methods boilerplate: "Round 3 adjudication was performed by the first reviewer with AI-assisted pre-screening ({model name and version}). The AI was prompted with the prespecified PECOS criteria and produced a suggestion plus brief justification for each record; the first reviewer independently confirmed or overturned every suggestion. AI suggestions were not used as final inclusion decisions." 4. Output: `round3_{date}.tsv` with finalized `round3_decision`. #### 3d. Round 4 — Full-text Screening 1. For records with `round3_decision = INCLUDE`, retrieve full-text PDFs (use `/fulltext-retrieval`). 2. Apply full-text exclusion criteria (F1=No extractable outcome, F2=No comparative data, F3=Cannot separate target population data, F4=Inadequate sample/follow-up, F5=Full-text unavailable). 3. Two independent reviewers; compute Cohen's kappa at full-text stage. 4. Resolve disagreements by consensus or third reviewer. 5. Flag comparative studies for priority extraction. #### 3e. PRISMA Flow Track numbers at each stage for PRISMA flow diagram (R1 → R2 → R3 → R4 → final included). Use `/make-figures` to generate PRISMA flow diagram when numbers are finalized. #### 3f. Post-Consensus Count Reconciliation Gate (MANDATORY before Phase 5 write-up) Before handing the screening artifacts to Phase 5 (statistical synthesis) or to `/write-paper` / `/self-review`, run an explicit ID-set reconciliation and record the canonical totals in a single source-of-truth file (typically `2_Screening/screening_consensus.md` §Net Impact or equivalent): Use the deterministic helper when TSV/CSV artifacts are available: ```bash python "${CLAUDE_SKILL_DIR}/scripts/screening_reconcile.py" \ --screening 2_Screening/fulltext_screening.tsv \ --consensus 2_Screening/consensus_decisions.tsv \ --table1 6_Tables/table1_studies.csv \ --output 2_Screening/screening_consensus.json ``` Downstream stages should consume `screening_consensus.json` for counts and ID sets. The Markdown consensus document remains the human explanation. 1. **Enumerate ID sets from raw artifacts (not from prose summaries):** - A = screening TSV INCLUDE IDs - B = consensus spreadsheet Exclude IDs - C = consensus spreadsheet Include-qualitative IDs (FLAG-resolved additions) - T = Table 1 / bivariate-eligible IDs (2×2-extractable studies) 2. **Compute canonical totals via set algebra:** - k_qualitative = |A \ B| + |C| - k_bivariate = |T| - k_narrative-only = k_qualitative − k_bivariate - k_FT-excluded = |full-text reviewed| − k_qualitative 3. **List the narrative-only IDs explicitly.** The highest-yield red flag is a numeric claim ("10 narrative-only studies") that does not match the enumerable ID set (A ∪ C) \ B \ T. 4. **Prohibit "N → M" transitions without ID receipts.** Any sentence of the form "k rose from 30 to 32 after FLAG consensus" must cite the specific added/removed IDs. A transition claim with no enumerable ID set is a P0 error and blocks the Phase 5 hand-off. 5. **Record in a reconciliation table** inside the screening-consensus document: | Quantity | v_prev draft | v_current (ID-verified) | Derivation | |---|---|---|---| | k_full-text | ... | ... | ... | | k_FT-excluded | ... | ... | |TSV EXCLUDE| + |consensus-downgrades| | | k_qualitative | ... | ... | |A \ B| + |C| | | k_bivariate | ... | ... | |T| | | k_narrative-only | ... | ... (explicit IDs listed) | (A ∪ C) \ B \ T | **The failure pattern:** a late-revision manuscript ships with qualitative / narrative-only / full-text-excluded counts that ID-set reconciliation later moves in every column. An early-draft prose total is carried forward without ever being reconciled against the screening TSV intersected with the consensus spreadsheet, and the downstream artifacts echo the same wrong total back to each other. This gate catches the drift at the Phase 5 hand-off, rather than at a post-Stage 4 audit. ##### Why `STAGE_TRANSFER_LOSS` needs its own verdict The set algebra above reconciles *counts*. On its own it does not distinguish the two ways an id can be missing from set `B`: | Case | Meaning | Where it lands | |---|---|---| | consensus says **exclude** | a decision was made and recorded | out of `qualitative` — correct | | consensus **has no row at all** | the record fell out of the pipeline; nobody adjudicated it | into `qualitative`, then `narrative_only` — **wrong, and silent** | Both leave the id out of `B`, so both look identical to a count-based check. And because a diagnostic-accuracy review legitimately contains narrative-only studies (eligible, but no extractable 2×2), a lost record parked in that set is indistinguishable from a real one. The totals still reconcile — they were recomputed from the downstream artifact, which is exactly where the record is already absent. `screening_reconcile.py` therefore reports `stage_transfer_loss` (= `screening_include` − `consensus_ids`) as a blocking issue, and splits `narrative_only` into `_adjudicated` (a decision exists) and `_unadjudicated` (none does). Only the first is a legitimate category. **The failure pattern:** a review reaches journal submission with eligible studies missing from its pool that were sitting **inside its own retrieved records** — none of them search failures. One passes both title/abstract screening passes and is never entered into the consensus stage; others sit under an exclusion code that contradicts the registered eligibility criteria (a design the protocol declared eligible, coded as though it were not). A pre-specified sensitivity analysis can cross its own significance threshold once the pool is corrected, which is how much can ride on this. Note the shape: every count in the submitted manuscript reconciles, because each was recomputed from an artifact the lost studies had already dropped out of. #### 3f.5 Pool composition lock (MANDATORY at adjudication freeze) After Phase 3f reconciliation passes, freeze the pool composition into a single source-of-truth YAML so every downstream artifact (extraction TSV, manuscript prose counts, PRISMA flow caption, supplementary INDEX, cover letter free-text) can be checked against it. Why this lock exists ^^^^^^^^^^^^^^^^^^^^ Cross-project precedent (anonymized): an LLM reporting-quality SR carried five documents that disagreed on INCLUDE (63 vs 64) and EXCLUDE (108/109/111). Three EXCLUDE rows existed in the extraction sheet without matching INCLUDE. The drift traced to a late round-3 adjudication whose result was applied to some artifacts and not others — there was no single canonical post-freeze count to reference. How to lock ^^^^^^^^^^^ 1. Copy the template: ```bash cp "${CLAUDE_SKILL_DIR}/templates/FINAL_POOL_LOCK.yaml.template" \ 2_Data/FINAL_POOL_LOCK.yaml ``` 2. Fill in counts and UID lists from the reconciliation in Phase 3f. 3. Compute the SHA-256 integrity hash from the sorted UID list. 4. Commit the lock to git BEFORE starting Phase 4 extraction. Downstream gates ^^^^^^^^^^^^^^^^ - `/meta-analysis` Phase 4 entry: extraction TSV's UID set MUST equal `include_uids` ∪ `mixed_uids` from the lock. See Phase 4 entry gate. - `/sync-submission` Phase 5 (`scripts/cross_document_n_check.py --pool-lock`): every numeric claim in manuscript / abstract / supplementary that maps to a locked category must match the locked value. - Manuscript prose: NEVER re-derive `k included` from extraction TSV at manuscript build time. Always reference `final_pool_n` from the lock. - **Aggregate patient/lesion totals are locked too, not just study counts.** The Abstract/Results aggregate denominators ("a total of 483 patients / 531 lesions") are derived from the lock, never hand-carried. Lock them as explicit fields and distinguish **arm-separable** from **both-arm** rows: a study contributing one arm to a comparison must not have its full-cohort patient count folded into a pooled total. A hand-carried headline total that does not re-derive from the locked per-study values is a P0 (the analysis-side mirror of `/self-review` `check_cohort_arithmetic.py` partition checks). If a late post-freeze decision changes the pool, treat it as a formal PROSPERO amendment: file the amendment, re-freeze the lock as a new file (`FINAL_POOL_LOCK_v2.yaml`), and propagate to every artifact. -
phase4_extraction_detail.md 11 KB
# Phase 4 — Data Extraction: gate detail and extraction forms Load-on-demand companion to `/meta-analysis` Phase 4. SKILL.md keeps the mandatory entry gate, the two QC commands, and the fail-closed rules; this file carries the detail behind them. Read the block you need: - **AI-drafted starting document gate** — only when a mentor/collaborator has shared an AI-drafted study list, 2x2 set, or effect estimates. - **AI-assisted extraction suggestions** (`extract_assist.py`) — optional scaffolding; suggestions, never decisions. - **Extraction form fields** — the DTA and intervention field lists. - **Cross-verification** — the six dual-reviewer checks, in full. - **QC rationale** — what `dta_extraction_qc.py` and `cohort_overlap_check.py` catch, and the flag → form-edit forced transition. **Goal**: Create standardized extraction forms and extract 2x2 or effect size data. #### 4.0 Entry gate (MANDATORY): pool composition lock ↔ adjudication TSV Before any extraction work begins, run the deterministic UID-set check to confirm that the round-3 adjudication TSV and `FINAL_POOL_LOCK.yaml` (produced in Phase 3f.5) agree on which UIDs are included. ```bash python "${CLAUDE_SKILL_DIR}/scripts/check_pool_consistency.py" \ --lock 2_Data/FINAL_POOL_LOCK.yaml \ --adjudication-tsv 2_Screening/round3_adjudication.tsv \ --decision-col round3_decision \ --uid-col uid \ --include-labels "INCLUDE,INCLUDE_MIXED" \ --out qc/pool_consistency.json ``` Output `qc/pool_consistency.json`: ```json { "submission_safe": false, "match": false, "lock_include_n": 42, "tsv_include_n": 43, "in_lock_not_tsv": ["UID_007"], "in_tsv_not_lock": ["UID_055"] } ``` The gate fails closed: any UID disagreement blocks extraction. To resolve, either (a) re-freeze the lock with the corrected set of UIDs and propagate to downstream artifacts, or (b) correct the adjudication TSV if a row was mis-labeled. Do NOT proceed to Phase 4 with a mismatch — the resulting extraction matrix will not align with the locked pool, and the drift surfaces as a fabrication-grade red flag at peer review. > **Failure-mode cross-ref** → `references/data_integrity_checklist.md` DI-1~DI-5 are mandatory during extraction (2x2 arm-swap, KM audit trail, methodology mismatch, PRISMA 5-way drift, single-source k). **Recommended extraction form**: For SR-MA targeting high-impact radiology / medical AI journals, use `${CLAUDE_SKILL_DIR}/templates/extraction_form_v2.md`. Dual-extractor + source-page-reference + verbatim-quote columns prevent the 2x2 cell-swap and cohort-overlap blind spots surfaced in recent SR-MA peer-review cycles. New required columns: `cohort_source`, `source_page_ref`, `source_verbatim_quote`, `extraction_consensus_status`, `overlap_flag_reviewer1/2`, `sample_n_dta_pool` vs `sample_n_prognostic_pool`. #### 4.0 AI-drafted starting document gate Before opening the extraction form: if a senior mentor or collaborator has shared an AI-drafted starting document (Claude / ChatGPT / Gemini draft of the study list, 2x2 cells, or effect estimates) — even when the sender flags it as "for reference only" — apply the following before any of its values enter the extraction form: - Save the file with a `_DO_NOT_USE_VERBATIM` (or `_AI_DRAFT_REFERENCE_ONLY`) filename suffix. - Treat every per-study N, denominator, event count, OR/CI, and author/year as **hallucination-suspect** until re-verified against the source PDF + own analysis script. AI-drafts collapse multiple denominator definitions (treatment-naïve / full-cohort / per-arm) into one and silently mis-route counts. - Record any reconciled discrepancy in `extraction_consensus_log.md` with a verbatim quote of the AI-draft value and the corrected value with PDF page coordinate. - Trust hierarchy for this phase: **SSOT (source PDF + own analysis stdout) > mentor's direct text (email / track-changes) > attached AI-draft**. Do not promote an AI-draft from tier 3 to tier 2. The failure it prevents: an AI-drafted directive presents a study as a two-arm comparison ("x/n vs y/m") when the source PDF reports a single arm with a different denominator entirely. Verbatim absorption would produce a denominator-hallucinated meta-analysis. #### 4.0.1 AI-assisted extraction suggestions (optional, suggestions not decisions) To scaffold (not replace) manual extraction from a full-text paper, use the deterministic helper `scripts/extract_assist.py`. It scans a Markdown full text (e.g. `/fulltext-retrieval`'s PDF→MD output) for schema-defined fields and emits **candidate values, each with a `source_page_ref` and a verbatim source quote** — the extraction-stage analog of the screening-stage `ai_pre_screening_template.py`. ```bash python3 scripts/extract_assist.py \ --md paper.md --schema schema.yaml --study-id StudyA_2021 --out suggestions.tsv ``` - **Suggestions, never decisions.** Every row is `extraction_consensus_status = AI_SUGGESTED` and `needs_review = true`. The tool invents nothing — values and quotes are copied literally from the text; absent fields become explicit `not_found` rows; unit-ambiguous values (e.g. `92%` vs `0.92`) are emitted as multiple candidates side by side so the reviewer reconciles them. - **Human confirmation is mandatory.** Apply the 4.0 gate: treat every candidate N / denominator / 2x2 cell / effect estimate as hallucination-suspect until confirmed against the source PDF, recording reconciliations in `extraction_consensus_log.md`. Confirm or overturn each suggestion into the `extraction_form_v2.md` columns. - **Then, and only then, QC.** Build the confirmed DTA CSV and run `dta_extraction_qc.py` on **that** table — never on the suggestion TSV. Passing QC is not extract-assist's acceptance criterion; per-cell human confirmation is. A deterministic, network-free challenge card demonstrating the full suggestions → confirm → QC pipeline lives in `scripts/extract_assist_challenge/` (synthetic paper + schema + expected output + `verify.sh`). #### DTA Meta-Analysis: Generate a data extraction form with: - Study ID (first author, year) - Study characteristics (country, design, setting, enrollment period) - Population (n, age, sex, disease prevalence) - Index test details (technique, threshold, manufacturer, reader experience) - Reference standard details - 2x2 table (TP, FP, FN, TN) - Additional outcomes (AUC per study, if reported) - Notes on partial verification, differential verification, uninterpretable results #### Intervention Meta-Analysis: Generate a data extraction form with: - Study ID - Study characteristics - Population - Intervention / comparator details - Outcome data (means, SDs, event counts, sample sizes) - Effect measures (OR, RR, HR, MD, SMD as appropriate) Output: Excel/CSV template for data entry. #### 4b. Special cases (KM reconstruction, composite exposure) When studies report outcomes only as Kaplan-Meier curves (no raw event counts) or when the intervention is a composite of multiple techniques, load `${CLAUDE_SKILL_DIR}/references/phase4_km_composite.md` for the WebPlotDigitizer → `IPDfromKM` reconstruction procedure (cite Guyot et al. 2012, doi:10.1186/1471-2288-12-9) and the 4-path composite-exposure disaggregation decision tree. Pre-specify a sensitivity analysis excluding composite-exposure studies and document extraction strategy in the form's Notes column. #### Data Extraction Cross-Verification When comparing extraction results between independent reviewers (minimum 2), check: 0. **Inter-reviewer agreement**: Calculate and report screening agreement: % agreement or Cohen's kappa at title/abstract and full-text stages. If kappa was not calculated, report the exact number of discrepant records and the resolution method. 1. **Denominator consistency**: Verify sample sizes match between reviewers. Watch for per-patient vs per-lesion/per-tumor unit confusion. **CRITICAL**: The denominator may differ across outcomes within the same study (e.g., LTP assessed only among treatment-naive nodules, but complications assessed among all treated tumors). For each outcome, back-calculate: `event ÷ denominator` must equal the percentage reported in the paper's Tables. If it does not match, investigate the analysis population definition in the Methods section. If denominators differ, return to the original paper's Tables/Flow diagram. 2. **Arithmetic verification**: Back-calculate proportions from event/total counts and cross-check against original text (e.g., 78/91 = 85.7%). 3. **Kaplan-Meier estimate distinction**: KM curve estimates differ from raw event counts. Always record the data source (Table vs KM curve vs text) during extraction. 4. **Discrepancy resolution**: List all discrepancies → verify against original text → reach consensus → if consensus fails, use third reviewer. Log all consensus decisions in `{project}/consensus_log.md`. 5. **Dataset lock**: After resolving all discrepancies, lock the final dataset. Any subsequent changes require documented justification with date. #### Phase 4c: Extraction QC & Cohort Overlap Detection After dual-extractor consensus, run two QC scripts before locking the extraction table for statistical synthesis. **1. 2x2 Cell Integrity Check** -- `scripts/dta_extraction_qc.py`: Validates manuscript forest-plot cells (TP / FN / TN / FP) against source-paper-reported sens/spec within a tolerance (default 0.02). Catches sens/spec swap at extraction stage -- a common error pattern where a single-study k=1 subgroup outlier flips conclusions due to cell-assignment swap. ```bash python3 "${CLAUDE_SKILL_DIR}/scripts/dta_extraction_qc.py" \ --input 2_Extraction/extraction.csv \ --tolerance 0.02 \ --out 2_Extraction/qc/dta_extraction_qc.tsv ``` Any `FLAG_SWAP` or `FLAG_MISMATCH` row requires third-reviewer adjudication before Phase 6 statistical synthesis. **Flag → form-edit forced transition.** A confirmed flag is not resolved until the extraction form itself is edited. Track each flag through `confirmed → acted`: after the adjudicator confirms a `FLAG_SWAP`/`FLAG_MISMATCH`/unit-of-analysis violation, the extraction CSV row MUST be corrected and the QC re-run to clear it. A flag that is "confirmed" but whose form row is unchanged (the correction lived only in a review note) silently re-enters synthesis. Verify the form's mtime advanced and the re-run QC shows zero open flags before locking. **2. Cohort Overlap Check** -- `scripts/cohort_overlap_check.py`: Clusters included studies by (a) shared public ICU/EHR database (MIMIC-IV, eICU, MIMIC-III, KNHIS, UK Biobank, Optum, MarketScan, TriNetX, IBM), (b) same institution + overlapping enrollment period, (c) shared first-author surname + ±2y year proximity. Flags HIGH / MEDIUM overlap confidence. ```bash python3 "${CLAUDE_SKILL_DIR}/scripts/cohort_overlap_check.py" \ --input 2_Extraction/studies.csv \ --enrich \ --out 2_Extraction/qc/cohort_overlap.md ``` HIGH-confidence overlap pairs require Limitations acknowledgment + sensitivity analysis excluding one of the pair. Cross-links: `/peer-review` Phase 2A P1 (cell integrity) + P2 (cohort overlap). -
phase4_km_composite.md 2.6 KB
# Phase 4 Reference — KM Reconstruction & Composite Exposure Disaggregation Load this reference when `/meta-analysis` Phase 4 data extraction encounters either of two special cases: (a) studies that report outcomes only as Kaplan-Meier curves without raw event counts, or (b) studies whose intervention is a composite of multiple techniques. The main Phase 4 body of SKILL.md lists the standard extraction-form fields and the cross-verification checklist; this reference holds the procedural detail for these two scenarios. --- ## 4b. KM Curve Reconstruction (when raw events not reported) When studies report outcomes only as Kaplan-Meier curves without raw event counts: 1. **Digitise the KM curve**: Use WebPlotDigitizer (https://automeris.io/WebPlotDigitizer/) - Calibrate X/Y axes carefully — verify output range matches the original axis labels. - If coordinates come out in 0–1 range, multiply X by the actual time range (e.g., ×30 for months). - Clip negative Y values to 0 (digitisation artifact). - Export as CSV: `time, cumulative_event_rate` (or survival). 2. **Extract number-at-risk**: Record from the table below the KM plot at each time point. 3. **Reconstruct IPD**: Use the R `IPDfromKM` package (Guyot et al. 2012 method): ```r library(IPDfromKM) dat <- read.csv("digitised_curve.csv") preproc <- preprocess(dat, trisk, nrisk, totalpts, maxy = 1) ipd <- getIPD(preproc, armID = 1) # armID starts at 1, NOT 0 ``` - ⚠️ `preprocess()` does NOT accept a `mateflag` parameter (common error). - ⚠️ `armID` starts at 1 (not 0). 4. **Verify**: Generate a reconstructed KM plot and visually compare to the original figure. 5. **Report in Methods**: Cite Guyot et al. 2012 (doi:10.1186/1471-2288-12-9) and state which studies required reconstruction. **Alternative — Text-based extraction**: When no subgroup-specific KM curve exists but the text reports "0% LTP at 12 months" or similar, extract directly from text. Document the page number and exact quote. --- ## Composite Exposure Disaggregation When a study's intervention is a composite of multiple techniques: 1. **Subgroup-specific KM curve** → use KM reconstruction (section 4b above). 2. **Component-specific Table/multivariate** → extract per-component data from Tables. 3. **Text-based subgroup report** → extract from narrative (e.g., "APE arm: 0% LTP"). 4. **None available** → include as composite; flag in sensitivity analysis for exclusion. Always pre-specify a sensitivity analysis excluding composite-exposure studies. Document the extraction strategy in the data extraction form Notes column. -
phase6_statistical_synthesis.md 12.5 KB
# Phase 6 Reference — Statistical Synthesis Load this reference when `/meta-analysis` Phase 6 begins executing the pooled analysis. It contains the R code templates for DTA and intervention meta-analysis, the dual-approach (comparative + single-arm pooled proportion) decision table, the rare-event branch, the model-choice rationale required by PRISMA item 13d, the handling of correlated effect sizes from the same participants, and the practical cautions (method.tau, HK CI, zero-cell correction, publication-bias test power). **Always use R** (packages: `meta`, `metafor`, `mada`). Companion templates: `${CLAUDE_SKILL_DIR}/references/r_templates.md`. --- ## DTA Meta-Analysis ```r library(mada) # bivariate model, forest/SROC plots library(meta) # general meta-analysis utilities library(metafor) # advanced models # Bivariate model (recommended for DTA) fit <- reitsma(data, formula = cbind(tsens, tfpr) ~ 1) summary(fit) # SROC curve with confidence and prediction regions plot(fit, sroclwd = 2, main = "SROC Curve") # Forest plot (paired: sensitivity + specificity) forest(fit, type = "sens") forest(fit, type = "spec") ``` ### Key outputs for DTA - Pooled sensitivity (95% CI) - Pooled specificity (95% CI) - Pooled positive LR, negative LR - Pooled DOR - SROC curve with AUC, confidence region, prediction region - Heterogeneity: I-squared for sensitivity and specificity separately - Threshold effect: Spearman correlation between sensitivity and FPR --- ## Intervention Meta-Analysis ```r library(meta) library(metafor) res <- metagen(TE, seTE, data = dat, studlab = study, method.tau = "REML", sm = "OR") forest(res) funnel(res) summary(res) # I-squared, tau-squared, Q test metabias(res, method.bias = "Egger") metainf(res, pooled = "random") # leave-one-out ``` --- ## Dual Approach: Comparative + Single-Arm Pooled Proportion When both comparative and single-arm studies are available, use dual analysis (precedent: Lin 2025 PMID:41419890, Su 2026 PMID:41653198). The assignment of PRIMARY vs SECONDARY depends on the research question and available evidence: | Scenario | Primary | Secondary | Rationale | |----------|---------|-----------|-----------| | Enough comparative studies (k≥8) | Comparative OR/RR | Pooled proportion | Direct comparison answers efficacy | | Limited comparative (k<6), many single-arm | Pooled proportion | Comparative OR/RR | Insufficient power for comparative; pooled proportion provides descriptive evidence | | Mixed (moderate k, each) | Discuss with co-authors | — | PI/methodologist decision | The choice should be pre-specified in the PROSPERO protocol and remain consistent throughout the manuscript. ```r # Comparative MA (binary outcomes) — NON-RARE events only. # If the outcome is rare (see "Rare Events" below), this specification is the # one Cochrane tells you to avoid: do not reach for it by default. res_comp <- metabin(ei, ni, ec, nc, data = dat, studlab = study, sm = "OR", method = "Inverse", method.tau = "DL", common = FALSE, random = TRUE, method.random.ci = "HK", incr = 0.5) # Single-arm pooled proportion res_prop <- metaprop(event, n, data = dat_single, studlab = study, sm = "PLOGIT", method.tau = "DL", method.ci = "CP") ``` ### Key points - Comparative answers "is adjunct effective?"; single-arm answers "what outcomes to expect?" - Single-arm uses `metaprop()` with logit transformation + Clopper-Pearson CI - GRADE certainty lower for single-arm — state explicitly - Report both in Results: label PRIMARY/SECONDARY per pre-specified assignment - **Selection bias warning**: Single-arm case series may introduce selection bias (experienced centres, favourable patients). When pooling with comparative arms, report both pooled estimates separately and discuss any numerically lower event rate in single-arm studies as a potential selection effect. --- ## Practical R Notes - For **non-rare** binary outcomes, use `method = "Inverse"`, not `"MH"`, to avoid a method.tau conflict. For **rare** events this reverses — see "Rare Events" below. - Use `method.tau = "DL"` (DerSimonian-Laird) — REML may not converge with sparse data. Not for rare events (below). - Use `method.random.ci = "HK"` (Hartung-Knapp) instead of the deprecated `hakn = TRUE`. - Use `common = FALSE, random = TRUE` instead of deprecated `comb.fixed/comb.random`. - For zero cells in **non-rare binary 2×2 outcomes** (OR/RR), apply `incr = 0.5` continuity correction. **Do NOT** apply a continuity correction when the event is **rare** (below) or when pooling **single-arm proportions**: use `metaprop(..., method = "GLMM", sm = "PLOGIT")`, which handles zero-event studies natively. See `single_arm_proportion_ma.md`. - Egger's test is underpowered for k < 10 — note this in results. **Egger/funnel tests are invalid for pooled proportions** (the SE is a deterministic function of the proportion); see `single_arm_proportion_ma.md`. --- ## Rare Events (sparse 2×2 data) The default specification above (`method = "Inverse"` + `method.tau = "DL"` + `incr = 0.5`) is chosen for convergence convenience, and it is the wrong tool once the event is rare. Inverse-variance weights are derived from a large-sample normal approximation that fails with few events, and adding 0.5 to every cell biases the estimate toward the null and distorts its variance — Cochrane Handbook §10.4.4.1, restated for radiology SR/MA in Park 2022 (Korean J Radiol; PMID:35213097): *inverse-variance methods (including the DerSimonian and Laird method) should be avoided in meta-analyses of rare events.* **Trigger** — treat the outcome as rare and branch when any holds: | Signal | Threshold | |---|---| | Pooled event rate | < 1% (Cochrane's working definition; < 5% warrants a sensitivity check) | | Zero-event arms | any study with a structural zero in either arm | | Double-zero studies | present (these carry no information for OR and are dropped by MH/Peto — say so) | **Use instead** (pre-specify one; the others become the sensitivity analysis): ```r # 1. Peto OR — a FIXED-effect estimator. Best when events are very rare, arms are # of similar size, and the true effect is not large. No continuity correction. res_peto <- metabin(ei, ni, ec, nc, data = dat, studlab = study, sm = "OR", method = "Peto", common = TRUE, random = FALSE) # 2. Mantel-Haenszel without a zero-cell correction — also FIXED-effect. # MH.exact = TRUE suppresses the continuity correction in the pooled estimate # (the 0.5 still appears in the per-study estimates drawn on the forest plot, # which is what RevMan does too — say so in the legend). res_mh <- metabin(ei, ni, ec, nc, data = dat, studlab = study, sm = "OR", method = "MH", MH.exact = TRUE, common = TRUE, random = FALSE) # 3. Binomial-normal GLMM — the route to a RANDOM-effects rare-event pool. # Exact likelihood, handles zero cells natively, tolerates unbalanced arms. res_glmm <- metabin(ei, ni, ec, nc, data = dat, studlab = study, sm = "OR", method = "GLMM", common = FALSE, random = TRUE) ``` **Do not layer random effects onto Peto or MH.** Both are fixed-effect estimators, and `meta` produces their random-effects companions by inverse-variance weighting the per-study estimates with τ² added — which puts back exactly the weighting the branch was taken to escape. If between-study heterogeneity has to be modelled for a rare outcome, that is what the GLMM is for. **Reporting**: name the method and why the rare-event branch was taken, state how double-zero studies were handled, and report the alternative specification as a sensitivity analysis. A rare-event pool reported without this sentence reads as an unconsidered default. **Caveat on Peto**: its advantage disappears when arm sizes are markedly unbalanced or the effect is large — Peto ORs are biased in both cases. Check both before pre-specifying it; if either fails, GLMM is the safer primary. --- ## Choosing the Model — and Reporting Why (PRISMA item 13d) Item 13d asks for the *rationale* behind the synthesis method, and it is among the most under-reported items in published radiology SR/MA (35% in Park 2022, PMID:35213097). The failure is rarely that no model was chosen — it is that the stated reason is the wrong kind of reason. **The choice between fixed- and random-effects is not a test result.** It is a judgment about whether the studies estimate one identical true effect or a distribution of true effects. Cochran's Q and Higgins' I² measure how much the observed effects scatter; they do not answer that question, and Q is underpowered at small k while I² is not a measure of the *amount* of heterogeneity at all. **Forbidden phrasings** (a methods reviewer catches these on the first pass): - "A random-effects model was used because I² was 65%." - "Because I² was 0% and the Q test was non-significant, a fixed-effect model was used." - "The model was selected according to the level of statistical heterogeneity." **Write instead** — the reason lives in the studies, not in the output: > A random-effects model was used because the included studies differed in scanner > platform, reader experience, and positivity threshold, so a single common true effect > could not be assumed. Between-study variance was estimated with [method], and > heterogeneity is reported as I² and τ² with a 95% prediction interval. **Default for radiology**: random-effects. Design, population, and threshold heterogeneity is close to universal in imaging meta-analyses, which is exactly the condition that makes the identical-true-effect assumption untenable. A fixed-effect primary analysis needs an argument, not a p-value. Methods must also carry, per 13d: the model, the between-study variance estimator, the heterogeneity statistics reported, and the software package **with version**. --- ## Multiple Outcomes from the Same Participants (within-study covariance) When one study contributes more than one effect size to the same synthesis — several outcomes, several readers, several thresholds, several time points — those estimates are correlated because they come from the same patients. Pooling them as if they were independent counts the same participants more than once, understates the standard error, and narrows every confidence interval in the forest plot. Choose one, and say which: | Approach | When | R | |---|---|---| | One effect size per study, pre-specified | Simplest; the outcome hierarchy must be in the protocol, not chosen after seeing results | filter before `metabin`/`metagen` | | Multivariate / multilevel model | Several outcomes genuinely belong in one synthesis | `metafor::rma.mv` with a block-diagonal V built by `metafor::vcalc(..., rho = )` | | Robust variance estimation | Correlation is unknown and k is reasonable | `clubSandwich::coef_test(..., vcov = "CR2")` | The correlation ρ is usually not reported by the primary studies. State the assumed value, and re-run across a plausible range (e.g. 0.4 / 0.6 / 0.8) as a sensitivity analysis rather than presenting one assumed ρ as if it were measured. For DTA this is already handled: sensitivity and specificity from the same 2×2 are correlated by construction, which is why the bivariate/HSROC models are the requirement and separate univariate pooling of sensitivity and specificity is not acceptable. --- ## Subgroup / Meta-Regression - Subgroup analysis for pre-specified covariates - Meta-regression for continuous moderators - Report interaction test p-value, not just within-subgroup p-values --- ## Publication Bias - DTA: Deeks' funnel plot asymmetry test (standard funnel plots are inappropriate for DTA). - Intervention: Funnel plot + Egger's or Peters' test. - Note: tests are underpowered for <10 studies. --- ## Sensitivity Analysis - Leave-one-out analysis (`metainf()`) - Excluding high RoB studies - Excluding overlapping populations (same institution + enrollment period) - Including/excluding borderline studies (sensitivity to inclusion criteria) - Alternative model specifications --- ## Error Handling - If an R script fails, capture the error message, diagnose the likely cause (missing package, data format mismatch, convergence failure), and present a fix. Do not silently re-run. - When reporting R output, separate statistical results (pooled estimates, heterogeneity metrics, I-squared) from interpretation. Present numbers first in a "Statistical Results" block, then interpretation guidance in a separate "Interpretation Notes" block. -
phase9_circulation.md 3.4 KB
# Phase 9 Reference — Co-author Circulation Load this reference when Phase 9 is entered (after Phase 8 is complete and Phase 6b source-fidelity audit has cleared). The SKILL.md body carries only the goal + summary pointer; all procedural detail lives here. ## Goal Standardized pre-submission circulation of the manuscript to co-authors and senior methodologists / reviewers, with a bounded review window and a controlled attachment scope. ## Trigger Phase 8 is complete, and the draft has cleared Phase 6b source-fidelity audit. ## 9.1 Thread Continuity - If a prior version (v1, v2, ...) of this manuscript was already circulated to the same author team, reply to the same email thread. Preserve `In-Reply-To` and `References` headers so the thread tracks v1 → v2 → v3 in one place. - Open a new thread only for the first circulation, or when the author team / target journal has fundamentally changed. ## 9.2 Attachment Scope **Include:** - Manuscript body with figures embedded inline (single DOCX/PDF). - Change summary — for v≥2, a delta table vs. the prior circulated version. **Exclude (circulate separately later):** - Graphical abstract — after the body is locked. - Cover letter — after the target journal is confirmed. - COI forms — after journal format is known. - Supplementary appendices — share via a drive link, not attached. Rationale: bundling all submission artifacts before the body is locked forces co-authors to review multiple moving targets at once and telegraphs premature commitment to a specific journal (especially when the GA carries journal-specific branding). ## 9.3 Attachment Method | Total attachment size | Method | |---|---| | < 5 MB | Direct email attachment | | 5 – 25 MB | Direct attachment (within the common 25 MB mailbox limit), but verify at the draft-API level — some clients fail well below the stated message limit | | > 25 MB or grey-list formats | Shared drive link only | A manuscript DOCX with inline figures is typically 0.5 – 1 MB — direct attach is safe. ## 9.4 Recipient Structure - **TO**: Corresponding author + one senior methodologist reviewer (e.g., a protocol assessor or statistician external to the writing team). - **CC**: All remaining co-authors. Include every alternate email address a co-author has used on the thread to avoid dropping them. Ask the corresponding author explicitly for: - target journal preference, - 2 – 3 reviewer candidates, - any framing adjustments to carry into the cover letter. ## 9.5 Journal-Undetermined Framing If the target journal is not yet confirmed, state this explicitly: > "The manuscript is currently formatted to [placeholder journal]'s guidelines. Please > suggest an appropriate target journal and we will re-format accordingly." Do not include the graphical abstract in this round if it carries journal-specific branding that would require rework on re-targeting. ## 9.6 Deadline - Set `(send date + 7 days)` = 5 business days + 1 weekend. - Record the deadline in a task tracker with expected responses per recipient. - Informal follow-up is acceptable after 4 – 5 days of silence. A formal second message before the 7-day window closes is not appropriate unless an external submission deadline forces it. ## 9.7 Response Tracking For each recipient, log: date of response, issues raised, and whether the issues trigger Phase 10 (Self-Audit Recovery) or are minor in-place revisions. -
post_submission_release_ops.md 2.7 KB
# Post-Submission Release Operations **When**: between the end of Phase 9 (circulation) and journal submission. Separate from Phase 10 (recovery). **Why it's hard**: during circulation (v7~v18 coexisting), the numbers keep shifting. Mint the DOI too early and a content mismatch forces a re-issue; too late and you are scrambling on submission day. ## Checklist ### Gate 1 — When to issue the release - [ ] Internal circulation closed: every reviewer (internal PI, external peer) has signed off. - [ ] `[VERIFY-CSV]` / `TODO` / `FIXME` / `(to be regenerated)` tags fully removed from manuscript / supplement / figures / code (`rg -n` → 0 hits). - [ ] k (number of included studies) matches across the `4_Analysis/*.csv` row count, the manuscript prose, the PRISMA flow, and every figure caption. - [ ] Author order / ICMJE COI finalized (just before reflecting it in the Zenodo author metadata). ### Gate 2 — GitHub repo - [ ] Journal-target bundle regenerates successfully via `_build.sh`. - [ ] Repo includes the raw analysis code, extraction_consensus_log.md, PROSPERO amendments tracker, and methodology. - [ ] README has a DOI placeholder (replaced after the Zenodo DOI is issued). - [ ] `.gitignore` confirmed to exclude raw PDFs / copyrighted material. - [ ] LICENSE stated (CC-BY 4.0 or the journal-required license). ### Gate 3 — Zenodo DOI - [ ] Zenodo record metadata: author order, affiliation, ORCID, keywords, related identifiers (PROSPERO registration number). - [ ] Add the GitHub repo release tag URL to `related_identifiers` as `isSupplementTo`. - [ ] The submission package (.tar.gz) is uploaded to Zenodo — the journal submission bundle, not the circulation package (vN). - [ ] After the DOI is issued, reflect it in the "data availability" section of the manuscript / cover letter / submission portal. ### Gate 4 — Handling re-targeting after rejection - [ ] Resubmission of identical content to another journal: **do NOT mint a new Zenodo version** (the DOI attaches to the content). Use `_build.sh --journal {new}` to create only a new SUBMISSION folder. - [ ] Resubmission after revision: content changed → mint a new Zenodo version. The concept DOI is preserved. - [ ] On author changes: redistribute the ICMJE COI + mint a new Zenodo version. ## Common failures - **F1**: Zenodo DOI minted while `k` (included study count) is still oscillating between versions → content-DOI mismatch forces re-issue. **Blocked by**: Gate 1. - **F2**: Journal-specific folders edited by hand without a `_build.sh` → the journal copies drift from master. **Blocked by**: see `submission_package_drift.md`. - **F3**: `TODO` / `FIXME` tag left in an R analysis script surfaces only after the repo is pushed to GitHub. **Blocked by**: Gate 1 `rg` scope including code. -
PROSPERO_template.md 8.1 KB
# PROSPERO Registration Template ## Overview PROSPERO (International Prospective Register of Systematic Reviews) requires completion of a structured web form. This template provides field-by-field guidance with word limits and common pitfalls from real registration experience. **Key constraint**: Initial registrations are NOT allowed after data extraction is completed. Searching and screening may be completed. --- ## Form Fields Reference ### REVIEW TITLE AND BASIC DETAILS #### 1. Review title (max 50 words) Include: study design (systematic review, meta-analysis, pooled analysis), condition, intervention, population qualifier if relevant. #### 2. Review question (max 250 words) Structure around PICO/PIRD. State both primary and secondary objectives clearly. For pooled analysis designs, distinguish between pooled proportion and comparative questions. #### 3. Condition/domain being studied (max 200 words) Define the condition. Explain why it matters. Note the clinical gap. #### 4. Keywords Semicolon-separated. Include MeSH-equivalent terms. --- ### ELIGIBILITY CRITERIA #### 5. Population (max 200 words) State included and excluded populations. Include age, disease status, anatomical specifics. #### 6. Intervention/exposure (max 200 words) Define all eligible interventions with specifics (technique, device, imaging guidance). State minimum reporting requirement ("must report at least one clinical outcome"). #### 7. Comparator/control (max 200 words) If both comparative and single-arm studies are included, state this explicitly: "For the comparative analysis, [comparator description]. For the pooled proportion analysis, single-arm studies without a comparator are also included." #### 8. Type of study to be included (max 150 words) List included and excluded study designs separately. Be specific about minimum sample size (e.g., "case reports with fewer than 5 patients excluded"). --- ### SEARCHING AND SCREENING #### 9. Searches (max 300 words) Name databases, date range, language restrictions. Reference supplementary search strategies. #### 10. Search strategy (max 300 words) Show actual search blocks with Boolean structure. No need for full line-by-line syntax here (reference supplementary materials for that). #### Sub-fields in the web form: - **Search for unpublished studies**: Yes/No (select based on protocol) - **Main databases**: Select from checklist (PubMed, Embase.com vs Embase via Ovid — distinguish!) - **Language restrictions**: State explicitly - **Date restrictions**: State explicitly - **Other methods**: Check ONLY what was actually done: - reference list checking (backward citation) - contacting authors - conference proceedings - trial registers - forward citation searching (snowballing) - **WARNING**: Do not check methods you did not perform — reviewers may ask for results - **Link to search strategy**: Reference protocol or supplementary materials - **Selection process**: "Two reviewers independently..." with disagreement resolution - **Other info about screening**: PRISMA flow diagram reference (optional) --- ### DATA COLLECTION PROCESS #### 11. Data extraction (max 300 words) List extracted variables by category: study characteristics, population, intervention, outcomes. State unit of analysis policy (patient vs tumor level). If Kaplan-Meier reconstruction is used, cite methods (Tierney 2007, Guyot 2012). #### Sub-fields: - **Extraction method**: Select "independently by at least two people" - **Author contact**: Yes/No - **IPD**: Usually No for standard MA #### 12. Risk of bias/Quality assessment (max 200 words) Name tool(s) per study design. If using tools not in the PROSPERO checklist (e.g., JBI), select "Other" and describe in text. Common tool mapping: | Study design | Tool | |---|---| | RCT | Cochrane RoB 2 | | Non-randomized comparative | Newcastle-Ottawa Scale (NOS) or ROBINS-I | | Single-arm / case series | JBI Critical Appraisal Checklist (select "Other") | | DTA | QUADAS-2 | | Prediction model | PROBAST | State number of assessors and disagreement resolution. #### 13. Reporting bias assessment For pooled proportion (k>=10): funnel plot + Egger's regression test. For comparative (k<10): funnel plot visual inspection only. Do NOT promise trim-and-fill or contour-enhanced funnel unless actually planned. #### 14. Certainty assessment Select "No" unless GRADE or equivalent is explicitly planned in the protocol. If "Yes": requires Summary of Findings table — significant additional work. --- ### OUTCOMES TO BE ANALYSED #### 15. Primary outcomes (max 300 words) Define each outcome precisely: what it measures, how it is defined, at what time points. State the effect measure (proportion, OR, HR, etc.). #### 16. Secondary outcomes (max 300 words) List all secondary outcomes. Include exploratory outcomes that will be "extracted if reported." --- ### PLANNED DATA SYNTHESIS #### 17. Strategy for data synthesis (max 400 words) Structure as: 1. **Primary analysis**: model, estimator, transformation 2. **Secondary analysis**: model, estimator, CI adjustment 3. **Heterogeneity**: I-squared, Q test, interpretation thresholds 4. **Publication bias**: methods (reference field 13) 5. **Software**: R packages with versions #### 18. Subgroups/subsets (max 250 words) Number and list all pre-specified subgroup analyses. Number and list all pre-specified sensitivity analyses. State interaction test method. Mention leave-one-out if planned. --- ### CURRENT REVIEW STAGE #### 19. Review stage checklist | Stage | Allowed at registration | |---|---| | Pilot work | Started or Completed OK | | Formal searching | Started or Completed OK | | Screening | Started or Completed OK | | **Data extraction** | **Not started or Started ONLY** | | Risk of bias | Not started | | Data synthesis | Not started | **CRITICAL**: If data extraction is "Completed", registration will be REJECTED. If protocol restructuring invalidates prior extraction → defensible to mark "Not started." #### 20. Review status Usually: "The review is planned or ongoing" --- ### REVIEW AFFILIATION, FUNDING AND PEER REVIEW #### 21. Review team members All authors with ORCID and affiliation. One must be Guarantor + Named Contact. Guarantor = typically first or corresponding author. #### 22. Funding - If no funding: "This review received no specific funding from any agency in the public, commercial, or not-for-profit sectors." - Do NOT write "supported by academic institutions" unless there is actual institutional funding. #### 23. Peer review Usually "Not peer reviewed" for initial registration. --- ### ADDITIONAL INFORMATION #### 24. Additional info (max 250 words) Standard text: "Any important protocol amendments will be documented in the PROSPERO record and clearly reported in the final publication." #### 25. MeSH terms Auto-generated but editable. Review and add missing terms (e.g., Microwaves for MWA studies). --- ## Common Pitfalls Checklist Before submitting, verify: - [ ] **HTML entities**: Preview PDF and check all `>=`, `<`, `&` symbols display correctly (common breakage: `>`, `<`, `&` appearing literally) - [ ] **Word limits**: Each field within stated maximum - [ ] **Database names**: Embase.com vs Embase via Ovid (different options in form) - [ ] **Other methods**: Only checked items actually performed - [ ] **RoB tool**: JBI requires "Other" selection (not in standard list) - [ ] **Certainty/GRADE**: "No" unless explicitly planned with SoF table - [ ] **Funding text**: Matches actual funding status - [ ] **Data extraction stage**: Not marked as "Completed" - [ ] **Similar records**: All reviewed and confirmed "not similar" - [ ] **All authors**: Listed with correct ORCID and affiliation - [ ] **End date**: In the future (update if past) - [ ] **Comparator field**: Addresses both comparative and single-arm inclusion if applicable --- ## Output Format When generating PROSPERO content for the user: 1. Produce a single Markdown file with all fields, word counts per field 2. Convert to DOCX via pandoc for copy-paste convenience 3. Flag any fields requiring user decision (e.g., author contact, unpublished search) 4. Include the Common Pitfalls Checklist at the end -
review_orchestration.md 3.7 KB
# Peer Review Orchestration (3-tier) **Extends**: the existing `phase9_circulation.md` (circulation email template). This document covers the full internal → external → journal three-round orchestration. ## 3-tier structure | Tier | Reviewer | Output | Blocking point | |---|---|---|---| | 1. Internal | Clinical PI (second dual rater) + methodology lead | dual-rating consensus, QUADAS-2 domain table | PI availability | | 2. External | Adversarial GPT review + independent peer (optional) | critique memo, defensive-tone audit | — | | 3. Journal | Editorial + peer reviewers | reject/revise/accept, Response matrix | — | ## Core rules ### RO-1. Dual-rating work ships as a trio: "figure + per-study table + supplementary", together - **Problem pattern**: A QUADAS-2 traffic-light figure ships without an accompanying per-study domain table → PRISMA-DTA Item 19 remains PARTIAL. The supplementary table must be planned alongside the figure, not added one revision later. - **Rule**: Plan dual-rating outputs such as QUADAS-2 / RoB2 / GRADE as a three-part set at the design stage. A figure alone does not complete PRISMA-DTA Item 19. ### RO-2. Secure the "second-reviewer lead time" before sending the circulation package - **Problem pattern**: Manuscript circulated after only first-reviewer screening → the second-reviewer dual rating then blocks the entire revision cycle for weeks, leaving PRISMA Item 9 PARTIAL. - **Rule**: Confirm the clinical PI's second-rating availability window before writing the circulation email. Put blocking items as a bullet at the very top of the email body. Add a "PI availability window" field to the circulation template. ### RO-3. External review specializes in "defensive tone / bias-inflation / upper-bound framing" - **Problem pattern**: External adversarial review surfaces over-defensive Methods prose (multi-paragraph justifications for pre-specified protocol deviations) and flags that a homogeneity statistic (e.g., Sp I²=0%) is driven by universalized bias rather than true between-study agreement. - **Rule**: Include in the external adversarial-review prompt: (a) a defensive-tone audit (check that protocol-deviation explanations in Methods are ≤3 sentences), (b) a bias-driven homogeneity interpretation check, and (c) an upper-bound/lower-bound framing check. Offload detailed rationale to the Supplement. ### RO-4. The Response matrix tracks reviewer reply + manuscript edit + supplement together - **Problem pattern**: During revision, a numeric value (e.g., a subgroup p-value) changes between versions with no before/after diff recorded in the Response matrix → the reviewer's re-review cannot verify the update. - **Rule**: For each comment row in the Response matrix, record (a) the reviewer's original text, (b) a response summary, (c) the manuscript edit locator (section/line), and (d) whether a number changed, tagged `[VERIFY-CSV]`. Remove all tags on submission day. ### RO-5. Consolidate protocol deviations into a single PROSPERO amendment - **Problem pattern**: Analysis-framework deviation, subgroup deviation, and search-amendment notes split across three manuscript sections and three separate tracker files → the PROSPERO amendment loses provenance. - **Rule**: One file at the project root, `protocol_deviations_tracker.md`. Bundle them under one ID when submitting the PROSPERO amendment. Use a single citation in the manuscript too. ## Templates - Circulation email: see the existing `phase9_circulation.md`. Per RO-2 of this document, propose adding a "PI availability window" field. - Internal peer review memo: `internal_peer_review_MA{n}_v{N}_{date}.md` - External GPT review memo: `external_review_gpt_MA{n}_{date}.md` - Response matrix: the existing `/revise` skill template. Per RO-4, add a `[VERIFY-CSV]` field. -
r_templates.md 3.3 KB
# R Code Templates for Meta-Analysis ## Required Packages ```r # DTA meta-analysis library(mada) # bivariate model, forest/SROC plots library(meta) # general meta-analysis utilities library(metafor) # advanced models # Intervention meta-analysis library(meta) library(metafor) ``` ## DTA Meta-Analysis ### Bivariate Model (Recommended) ```r # Bivariate model (Reitsma et al.) fit <- reitsma(data, formula = cbind(tsens, tfpr) ~ 1) summary(fit) # SROC curve with confidence and prediction regions plot(fit, sroclwd = 2, main = "SROC Curve") # Forest plot (paired: sensitivity + specificity) forest(fit, type = "sens") forest(fit, type = "spec") ``` ### Key Outputs for DTA - Pooled sensitivity (95% CI) - Pooled specificity (95% CI) - Pooled positive LR, negative LR - Pooled DOR - SROC curve with AUC, confidence region, prediction region - Heterogeneity: I-squared for sensitivity and specificity separately - Threshold effect: Spearman correlation between sensitivity and FPR ### Publication Bias (DTA) - Use Deeks' funnel plot asymmetry test (standard funnel plots are inappropriate for DTA) ## Intervention Meta-Analysis ### Random-Effects Model ```r # Random-effects model res <- metagen(TE, seTE, data = dat, studlab = study, method.tau = "REML", sm = "OR") forest(res) funnel(res) # Heterogeneity summary(res) # I-squared, tau-squared, Q test # Publication bias metabias(res, method.bias = "Egger") # Sensitivity analysis: leave-one-out metainf(res, pooled = "random") ``` ### Publication Bias (Intervention) - Funnel plot + Egger's or Peters' test - Note: Tests underpowered for <10 studies ## Subgroup / Meta-Regression - Subgroup analysis for pre-specified covariates - Meta-regression for continuous moderators - Report interaction test p-value, not just within-subgroup p-values ## KM Curve Reconstruction (Guyot et al. 2012) ```r library(IPDfromKM) # Read digitised KM curve (time, cumulative event rate) dat <- read.csv("digitised_curve.csv") # Number-at-risk from figure trisk <- c(0, 6, 12, 18, 24, 30) # time points nrisk <- c(51, 41, 30, 15, 7, 4) # at-risk counts totalpts <- 51 # Reconstruct IPD preproc <- preprocess(dat, trisk, nrisk, totalpts, maxy = 1) ipd <- getIPD(preproc, armID = 1) # armID = 1 (NOT 0) # Count events for meta-analysis input events <- sum(ipd$status == 1) total <- nrow(ipd) cat(sprintf("Events: %d / %d\n", events, total)) ``` Key pitfalls: - `preprocess()` does NOT accept a `mateflag` parameter - `armID` starts at 1, not 0 - Verify reconstructed KM visually against original figure ## Pooled Proportion (Single-arm or combined) ```r res_prop <- metaprop(event, n, data = dat, studlab = study, sm = "PLOGIT", # logit transformation method.tau = "ML", # maximum likelihood method.ci = "CP", # Clopper-Pearson for individual studies common = FALSE, random = TRUE) # Forest plot forest(res_prop, xlim = c(0, 1), leftcols = c("studlab", "event", "n"), leftlabs = c("Study", "Events", "Total")) # Egger's test (k >= 10) metabias(res_prop, method.bias = "Egger") ``` ## Sensitivity Analysis - Leave-one-out analysis - Excluding high RoB studies - Excluding outliers (identified via influence diagnostics) - Alternative model specifications -
single_arm_proportion_ma.md 5.8 KB
# Single-Arm Proportion Meta-Analysis — Conventions Pooling a single proportion (event rate) across single-arm studies — common when randomized comparators are scarce (interventional radiology, surgery, device/technique case series). These conventions prevent the failure modes a methodological reviewer reliably flags. They are distinct from DTA (sensitivity/specificity) pooling; see also `dta_meta_analysis.R`. ## 1. Model: GLMM logit, not inverse-variance with continuity correction Pool with a **random-effects generalized linear mixed model (binomial-normal, logit link)** — e.g. R `meta::metaprop(..., method = "GLMM", sm = "PLOGIT")`. The GLMM uses the exact binomial likelihood and **handles zero-event studies natively — do NOT add a 0.5 continuity correction** (continuity corrections bias proportion estimates and are unnecessary under the GLMM). Inverse-variance pooling of raw or arcsine-transformed proportions with `incr = 0.5` is an older default to avoid here. Report between-study variance τ² on the **logit scale** and a **95% prediction interval** for every pool. ## 2. Lead with dispersion, not a single pooled point estimate Thin, heterogeneous single-arm evidence does not yield a generalizable benchmark. Foreground: - the **per-study range** and the **95% prediction interval** (often very wide, e.g. a pooled 18% with a 3–59% PI), and - frame pooled values as **descriptive summaries of the existing evidence base**, not as effect estimates or as a guidance/technique "advantage". Do not let an Abstract/Key-Points headline a precise pooled percentage that the body then disclaims. ## 3. Boundary-degenerate pools (near 0% or 100%) When most studies sit at the proportion boundary (e.g. 5/6 studies at 100%), the GLMM is near-degenerate: the **Q-based I² can read 0% while the logit-scale τ² and the prediction interval are large** — these are not contradictory, they reflect boundary estimation. Also, Hartung-Knapp intervals are unstable at small k and can give an uninformatively wide CI (e.g. 69–100%). Handling: - present such an outcome **descriptively** (e.g. "five of six studies reported 100%; one reported 97.5%"), - keep the pooled value only "for completeness" with an explicit footnote explaining the I²=0 vs τ²>0 artifact, - do not call the heterogeneity "negligible" on the strength of I²=0 alone. ## 4. Report the crude rate alongside the pooled estimate With sparse events the logit-scale random-effects estimate **shrinks below the crude rate** (Σevents/Σdenominator). Report both and say so (e.g. "crude 3.3%; pooled 2.2%, the pooled value lower because of logit-scale shrinkage with sparse events"), so a reviewer does not read pooled < crude as an error. ## 5. Symmetric handling of zero-event studies Decide one inclusion rule for an outcome and apply it **symmetrically**. The common defect: including narrative-zero studies (a study that says "no major complications" with a borrowed denominator) while **excluding a study that explicitly reported 0/N** — this drops zeros asymmetrically and inflates the rate. Default: **include every study that reported the outcome's status, zero or not**; exclude only studies whose arm-specific count is genuinely not separable from a comparator. State borrowed denominators explicitly. ## 6. Pre-specify the event definition and verify it against each source For composite/graded outcomes (e.g. "major complication"), state an **a-priori definition** (e.g. SIR major / CTCAE Grade ≥3) in Methods, and **verify each study's count against that study's own grading table in the primary PDF** — do not trust a structured-extraction cell that disagrees with the paper's narrative. A single high-contributing study can drive the pooled estimate, so its grading must be source-confirmed. When a structured value and an extraction note conflict, the primary source decides. ## 7. Publication-bias / small-study tests are invalid for proportions The standard error of a proportion is a deterministic function of the proportion, so funnel-plot asymmetry and Egger/Begg regression are **uninterpretable for pooled proportions** and must not be reported as evidence of (no) publication bias. If shown at all, a contour-enhanced funnel plot is descriptive only, with a caption stating it cannot infer small-study effects. Do not write "no publication bias was found." ## 8. Unit of analysis Single-arm series mix per-patient, per-lesion, and per-session denominators. Use the most granular available unit per outcome, **disclose the mixing**, and treat the pooled CI as a descriptive-precision statement (study-level independence assumed), not an inferential interval. Where feasible, a within-study single-unit sensitivity analysis strengthens the key efficacy outcome. ## 9. Certainty of evidence Formal GRADE is not standardized for single-arm proportion syntheses; use a **GRADE-informed** narrative — start at low certainty (non-comparative observational), rate down for inconsistency and imprecision (wide PIs, I²) — and **report the per-outcome rating** (commonly "very low"), not merely a statement that certainty "was not assessed". ## 10. Risk of bias Use an instrument matched to the design (e.g. JBI Critical Appraisal Checklist for case series; ROBINS-I for the non-randomized comparative subset). When two instruments are used, **report agreement per instrument** (do not pool a single κ across incompatible rating scales), and get the item denominator right (Σ items per instrument). ## Reviewer-facing summary A defensible single-arm proportion MA: pre-registered; GLMM logit with τ² + prediction intervals; crude reported alongside pooled; symmetric zero handling; source-verified event definitions; no Egger on proportions; descriptive (non-comparative) framing; per-outcome GRADE-informed certainty; comprehensive search with honest disclosure of database scope. -
submission_package_drift.md 2.9 KB
# Submission Package Drift Control **Applies to**: situations with multiple coexisting journal targets (academic radiology / DIR / BJR / MDPI Diagnostics, etc.). ## The problem When 4~5 per-journal `SUBMISSION/{journal}/` folders coexist and each holds the full body/supplement/figures, there is a drift risk after rebuilds: it becomes unclear which folder is the master, and a typo fixed in only one folder gets re-propagated to the other targets. ## Rules ### SPD-1. Single master + build script - **Rule**: There is exactly one `7_Manuscript/` master. Per-journal conversion is handled by `SUBMISSION/_build.sh`. - **Build output**: `SUBMISSION/{journal}/{manuscript.docx, supplement.docx, figures/, tables/}` — these are **build artifacts; do not edit by hand**. ### SPD-2. `DO_NOT_EDIT_HERE.md` gate - **Rule**: Place a `DO_NOT_EDIT_HERE.md` file in each per-journal folder. When this file is present, the body/supplement/figure in that directory must not be edited. Allowed exception files: - `cover_letter.docx` - `title_page.docx` - `highlights.txt` - `checklist.md` (journal-specific reporting checklist) - `response_to_reviewers.docx` (during revision) - These exception files are needed per-journal, so direct editing is allowed. ### SPD-3. Record build time in `MANIFEST.md` - **Rule**: Four lines in each `SUBMISSION/{journal}/MANIFEST.md`: 1. `_build.sh` run timestamp 2. Source: master manuscript commit / `v{N}` 3. List of files allowed to be edited in the journal folder 4. Confirmation that all `[VERIFY-CSV]` tags were removed (timestamp of `rg` → 0 hits) ### SPD-4. Handling re-targeting after rejection - **Same content → different journal**: `_build.sh --journal {new}` for a new folder. No Zenodo DOI re-issue needed. Move the old journal folder to `_archive/`. - **After revision, same journal**: `_build.sh --journal {same} --revision {n}`. Build the Response matrix + the changed manuscript together. - **Major revision → different journal**: new folder + new Zenodo version (content changed). ## Templates ### `_build.sh` base structure ```bash #!/bin/bash # SUBMISSION/_build.sh # Usage: ./_build.sh --journal {academic_radiology|dir|bjr|mdpi_diagnostics|...} [--revision N] set -euo pipefail JOURNAL="$1" # Per-journal config: word limit, figure limit, reference style, supplement rules source "configs/${JOURNAL}.sh" # Build manuscript pandoc "../7_Manuscript/master.md" \ --reference-doc "configs/${JOURNAL}_template.docx" \ --citeproc --csl "configs/${JOURNAL}.csl" \ -o "${JOURNAL}/manuscript.docx" # Build supplement # ... figures, tables, DO_NOT_EDIT_HERE.md touch, MANIFEST.md update ``` ### `DO_NOT_EDIT_HERE.md` contents ``` This directory holds build artifacts from SUBMISSION/_build.sh. Do not edit the body/supplement/figures here. The master is /7_Manuscript/. Files allowed to be edited: - cover_letter.docx - title_page.docx - highlights.txt - checklist.md - response_to_reviewers.docx (during revision) ```
-
-
scripts
-
check_exclusion_code_validity_challenge
-
fixture
-
protocol_negative.md 617 B
# Protocol — Comparator-Restricted Systematic Review (registered) ## Eligibility criteria Included study designs: randomised controlled trials and comparative cohort studies were eligible. A concurrent comparator group was required for inclusion. Single-arm and non-comparative studies were excluded (code F2 below), as were studies of a wrong target population. ## Exclusion-code legend | Code | Reason | |---|---| | F1 | Duplicate publication | | F2 | No comparative data (single-arm / uncontrolled) | | F3 | Full-text unavailable | | F4 | Wrong target population | | F5 | Insufficient sample or follow-up | -
protocol_positive.md 611 B
# Protocol — Adjunctive Ablation Systematic Review (registered) ## Eligibility criteria Included study designs: single-arm case series and non-comparative cohort studies were eligible, provided they reported the primary outcome at the study level. Comparative studies were also eligible but a concurrent comparator was not required. Studies were excluded if they enrolled a non-target population or reported no extractable outcome. ## Exclusion-code legend | Code | Reason | |---|---| | F1 | Duplicate publication | | F2 | No comparative data (single-arm / uncontrolled) | | F3 | Full-text unavailable | -
screening_negative.tsv 186 B · in bundle
-
screening_positive.tsv 270 B · in bundle
-
-
problem.md 2.1 KB
# Challenge — exclusion-code validity vs registered eligibility ## The defect this gate catches A screening sheet can be internally perfect — two reviewers agree, Cohen's kappa is high, every PRISMA count reconciles — and still delete eligible studies in bulk, because the *exclusion code itself* is wrong. The code excludes a study design the registered protocol explicitly **includes**. No consistency, arithmetic, or inter-rater gate can see this: they all operate on the cells, and the defect is in the legend, above the cells. Real instance: a protocol admitted single-arm case series, but three studies were removed under a "not comparative" code. The code was applied consistently and the sheet was coherent; the code should not have existed. ## Fixtures **Positive** (`protocol_positive.md` + `screening_positive.tsv`): the protocol admits single-arm / non-comparative designs, yet the screening applies - `F2` "single-arm / no comparator" — excludes an **eligible** design → `CODE_CONTRADICTS_ELIGIBILITY` (Major, the study-loss defect); - `F9` — a code **absent from the registered legend** → `CODE_NOT_REGISTERED` (Major); - `F3` — registered as "Full-text unavailable" but applied as "Overlapping cohort", the same number meaning two things → `CODE_RENUMBERED` (Minor, a documentation defect at a lower severity). Valid codes (`F1` = duplicate) fire nothing. Exit 1 under `--strict`. **Negative** (`protocol_negative.md` + `screening_negative.tsv`): the protocol **requires** a comparator and excludes single-arm studies. The very same "no comparative data" code (`F2`) is now correct — it excludes a design the protocol excludes — and every code is registered and consistent. Zero verdicts, exit 0. The discrimination between the two fixtures is the whole point: an exclusion reason contradicts eligibility only when the protocol's own **non-negated** eligibility text names the excluded design as eligible. ## Verify `bash verify.sh` — deterministic, network-free. Runs the detector on both fixtures and asserts the three positive verdicts (exit 1) and a clean negative (exit 0). -
verify.sh 2 KB
#!/usr/bin/env bash # Deterministic verifier for the exclusion-code-validity challenge card. # Positive: a code excludes a design the protocol INCLUDES (single-arm), plus an # unregistered code and a renumbered code -> 3 verdicts, exit 1. # Negative: the SAME "no comparative data" code is correct because the protocol # REQUIRES a comparator -> 0 verdicts, exit 0. # No network. Exit 0 = both stages match expectations. set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" DET="$HERE/../check_exclusion_code_validity.py" tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT # --- Positive: must flag the study-loss code, the unregistered code, the rename --- set +e python3 "$DET" \ --protocol "$HERE/fixture/protocol_positive.md" \ --screening "$HERE/fixture/screening_positive.tsv" \ --strict --quiet --out "$tmp/pos.json" pos_rc=$? set -e for v in CODE_CONTRADICTS_ELIGIBILITY CODE_NOT_REGISTERED CODE_RENUMBERED; do if ! grep -q "\"verdict\": \"$v\"" "$tmp/pos.json"; then echo "FAIL: positive fixture did not emit $v" >&2 cat "$tmp/pos.json" >&2 exit 1 fi done if [ "$pos_rc" -ne 1 ]; then echo "FAIL: positive fixture must exit 1 under --strict (2 Major); got $pos_rc" >&2 exit 1 fi if ! grep -q '"detector": "check_exclusion_code_validity"' "$tmp/pos.json"; then echo "FAIL: JSON envelope does not self-identify the detector" >&2 exit 1 fi # --- Negative: the same code is correct here; nothing must fire --- set +e python3 "$DET" \ --protocol "$HERE/fixture/protocol_negative.md" \ --screening "$HERE/fixture/screening_negative.tsv" \ --strict --quiet --out "$tmp/neg.json" neg_rc=$? set -e if [ "$neg_rc" -ne 0 ]; then echo "FAIL: negative fixture must exit 0; got $neg_rc" >&2 cat "$tmp/neg.json" >&2 exit 1 fi if grep -qE '"verdict": "CODE_' "$tmp/neg.json"; then echo "FAIL: negative fixture emitted a claim verdict (false positive)" >&2 cat "$tmp/neg.json" >&2 exit 1 fi echo "PASS: positive flags CONTRADICTS+NOT_REGISTERED+RENUMBERED (exit 1); negative is clean (exit 0)."
-
-
extract_assist_challenge
-
expected
-
suggestions.tsv 1.5 KB · in bundle
-
-
fixture
-
confirmed_example.csv 200 B · in bundle
-
paper.md 534 B
<!-- page: 1 --> # Synthetic Diagnostic Accuracy Study This was a retrospective single-center study conducted in Synthetica. A total of 120 patients were enrolled between 2018 and 2021. <!-- page: 2 --> ## Methods The index test was evaluated against the reference standard in all 120 patients. <!-- page: 3 --> ## Results The sensitivity was 92% and the specificity was 85% in the primary analysis. The 2x2 table yielded TP = 55, FP = 9, FN = 5, and TN = 51. A sensitivity of 0.92 was also reported in the sensitivity analysis. -
schema.yaml 502 B
study_id: StudyA_2021 fields: - {name: study_design, type: study_design} - {name: country, type: regex, pattern: "conducted in ([A-Z][A-Za-z]+)"} - {name: sample_n, type: sample_n} - {name: source_sens, type: sensitivity} - {name: source_spec, type: specificity} - {name: extracted_tp, type: tp} - {name: extracted_fp, type: fp} - {name: extracted_fn, type: fn} - {name: extracted_tn, type: tn} - {name: comparator_design, type: regex, pattern: "double-blinded RCT of ([A-Za-z]+)"}
-
-
problem.md 3 KB
# Challenge card — AI-assisted extraction suggestions (meta-analysis Phase 4) ## Problem Data extraction is the bottleneck and a top source of SR/MA errors (2x2 cell-swaps, denominator confusion, % vs decimal mix-ups). Tools like Elicit and SciSpace auto-fill extraction columns from full text — but a raw auto-fill is hallucination-prone and unauditable. Before this gate, `meta-analysis` shipped a manual extraction form (`extraction_form_v2.md`) and a confirmed-table QC (`dta_extraction_qc.py`), but **nothing to scaffold candidate values from the full text with provenance** for a human to confirm. ## What the new gate does `scripts/extract_assist.py` scans a full-text Markdown paper for schema-defined fields and emits **AI_SUGGESTED** candidates — each with a `source_page_ref` and a **verbatim_quote** copied literally from the text. It invents nothing: values and quotes are pulled from the document; missing fields become explicit `not_found` rows. Every row is `needs_review = true`. It is the extraction-stage analog of `ai_pre_screening_template.py`: **suggestions, never decisions.** ## Pipeline (suggestions → human confirm → QC) ``` extract_assist.py ──► AI_SUGGESTED candidates (+page +quote) │ human reconciles vs source PDF ▼ (e.g., picks 0.92 over the "92%" duplicate) confirmed DTA CSV ──► dta_extraction_qc.py ──► OK ``` `dta_extraction_qc.py` is **only** run on the human-confirmed CSV — never on the suggestion TSV. Passing QC is not extract-assist's acceptance criterion. ## Fixture (synthetic only — no real paper/PII) - `fixture/paper.md` — synthetic DTA paper with `<!-- page: N -->` markers. - `fixture/schema.yaml` — fields incl. 2x2 cells, a custom-regex country, and a deliberately-absent `comparator_design` (→ `not_found`). - `fixture/confirmed_example.csv` — the human-confirmed table after reconciling the unit-ambiguous sensitivity (`92%` vs `0.92`). ## Expected - `expected/suggestions.tsv` — 11 candidates + 1 `not_found`; `source_sens` surfaces **two** candidates (`92%`, `0.92`) so the reviewer must reconcile the unit; all rows `AI_SUGGESTED` / `needs_review=true`. - Confirmed CSV then yields `DTA QC: OK=1 | SWAP=0 | MISMATCH=0`. ## Baseline vs new gate | | Baseline | New extract-assist gate | |---|---|---| | Candidate values from full text | manual typing | scaffolded suggestions | | Per-cell provenance | author discipline | page_ref + verbatim_quote on every row | | Hallucination posture | — | literal-only; `AI_SUGGESTED`/`needs_review`; not_found explicit | | Unit-ambiguity surfacing | — | multiple candidates emitted side by side | ## Verifier (deterministic, no network) ```bash bash verify.sh ``` ## Acknowledgement The "fixture + expected + deterministic verifier" packaging is inspired by public reproducible-audit layouts such as [EinsteinArena](https://einsteinarena.com/) (design inspiration only; no code, solutions, or data were copied). -
verify.sh 1.8 KB
#!/usr/bin/env bash # Deterministic verifier for the extract-assist challenge card. # Demonstrates the full Phase-4 pipeline: # (1) extract_assist.py emits AI_SUGGESTED candidates with page + verbatim quote # (including a unit-ambiguous source_sens: "92%" vs "0.92", and a not_found # field) — diffed against expected/suggestions.tsv. # (2) AFTER a human reconciles the candidates into a confirmed DTA CSV, that CSV # (NOT the suggestion TSV) passes dta_extraction_qc.py. # No network. Exit 0 = both stages match expectations. set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" SCRIPTS="$HERE/.." # --- Stage 1: suggestions are AI_SUGGESTED, with provenance, never decisions --- actual="$(python3 "$SCRIPTS/extract_assist.py" \ --md "$HERE/fixture/paper.md" \ --schema "$HERE/fixture/schema.yaml" 2>/dev/null)" if ! diff -u "$HERE/expected/suggestions.tsv" <(printf '%s\n' "$actual"); then echo "FAIL: extract_assist suggestions drifted from expected/suggestions.tsv" >&2 exit 1 fi # Every non-header row must be AI_SUGGESTED + needs_review=true (suggestions, not decisions). bad=$(printf '%s\n' "$actual" | tail -n +2 | grep -cv $'\tAI_SUGGESTED$' || true) if [ "$bad" -ne 0 ]; then echo "FAIL: $bad suggestion row(s) not labeled AI_SUGGESTED" >&2 exit 1 fi # --- Stage 2: human-confirmed CSV then passes the downstream QC gate --- qc_out="$(mktemp)" summary="$(python3 "$SCRIPTS/dta_extraction_qc.py" \ --input "$HERE/fixture/confirmed_example.csv" \ --out "$qc_out" 2>/dev/null | grep '^DTA QC:')" rm -f "$qc_out" if printf '%s' "$summary" | grep -q "OK=1 | SWAP=0 | MISMATCH=0"; then echo "PASS: 11 AI_SUGGESTED candidates with page+quote (1 not_found); human-confirmed CSV clears dta_extraction_qc ($summary)." else echo "FAIL: confirmed CSV did not pass dta_extraction_qc cleanly: $summary" >&2 exit 1 fi
-
-
check_exclusion_code_validity.py 16.7 KB
#!/usr/bin/env python3 """Exclusion-code validity gate — a code that excludes a design the protocol INCLUDES removes eligible studies in bulk while passing every downstream check (meta-analysis Phase 3f). The screening sheet can be internally perfect — every reviewer agrees, every count reconciles, Cohen's kappa is high — and still be wrong at the source: the *code itself* excludes a study design or population that the registered protocol's eligibility criteria explicitly admit. Nothing else can see this. Consistency, arithmetic, and inter-rater gates all operate on the cells; the defect is in the legend, above the cells, and it deletes eligible studies quietly and in bulk. Three verdicts, each computed by comparing the exclusion codes ACTUALLY APPLIED in the screening artifacts against the code legend + eligibility text REGISTERED in the protocol: 1. CODE_CONTRADICTS_ELIGIBILITY (Major) a used code's stated reason excludes a design/population on an axis (comparator, randomisation) that the protocol's own eligibility text names as ELIGIBLE. e.g. the protocol admits single-arm case series, and the screening excludes studies as "not comparative". This is the study-loss defect: the code is applied consistently and the sheet is coherent, but the code should not exist. 2. CODE_NOT_REGISTERED (Major) a code applied in the artifacts is absent from the registered legend — an off-protocol exclusion reason with no documented basis (also the PRISMA item 16a registered-vs-used drift). 3. CODE_RENUMBERED (Minor) a code present in BOTH the legend and the artifacts carries a DIFFERENT meaning in each (disjoint reason wording): the same number means two things. A documentation defect, not a study-loss defect — reported at a lower severity, which is the discrimination that matters. Deterministic and conservative — it stays silent unless it can prove the defect: * CONTRADICTS fires only when the exclusion reason AND an affirmative, NON-negated eligibility sentence match the SAME design axis. A protocol that *excludes* single-arm studies and a code that excludes "not comparative" do NOT contradict — the eligibility text has to say the excluded design is eligible. * NOT_REGISTERED fires only when a legend was actually found in the protocol (no legend -> cannot assess -> silent, never a false positive on absence). * RENUMBERED fires only when both sides carry a reason and their content words are disjoint. * Missing/blank inputs degrade to a clean run rather than firing. INPUT --protocol PATH registered protocol / PROSPERO markdown or text (required). --screening PATH ... one or more screening artifact TSV/CSV files (required). --code-col NAME exclusion-code column override (else auto-detected). --reason-col NAME exclusion-reason column override (else auto-detected). OUTPUT (--out path) {"detector": "check_exclusion_code_validity", "protocol", "screening", "claims": [{verdict, severity, code, detail, where}], "summary": {...}} CODE_CONTRADICTS_ELIGIBILITY and CODE_NOT_REGISTERED are Major candidates; CODE_RENUMBERED is Minor. Stdlib-only (csv / re / json / argparse / pathlib). Exit codes: 0 clean (or report-only), 1 a Major claim exists (with --strict), 2 input/usage error. """ from __future__ import annotations import argparse import csv import json import re import sys from pathlib import Path # --- code token: F1, E12, X2 (1-3 letters + 1-2 digits) -------------------- CODE_TOKEN_RE = re.compile(r"\b([A-Z]{1,3}\d{1,2})\b") CODE_COL_CANDS = [ "exclusion_code", "excl_code", "exclusion code", "reason_code", "reason code", "code", "exclude_code", "exclusioncode", "exclusion", ] REASON_COL_CANDS = [ "exclusion_reason", "excl_reason", "exclusion reason", "reason_text", "reason", "rationale", "justification", "notes", ] # --- legend line forms in a protocol --------------------------------------- # "F1 = duplicate", "F2: no comparative data", "F3 — cannot separate", "F4) x". # Plain hyphen is NOT a separator (it collides with ranges like "F1-F5"). The # meaning must contain a letter and must not itself begin with a code token. LEGEND_LINE_RE = re.compile(r"\b([A-Z]{1,3}\d{1,2})\s*(?:=|:|—|–|\))\s*([^,;|\n]{3,})") # --- eligibility signals --------------------------------------------------- ELIG_INCLUDE_RE = re.compile( r"\b(eligible|were\s+included|are\s+included|will\s+be\s+included|" r"we\s+included|included\s+if|permitted|admitted)\b", re.I) ELIG_NEGATION_RE = re.compile( r"\b(not\s+eligible|ineligible|not\s+included|were\s+excluded|are\s+excluded|" r"excluded\s+if|were\s+not|are\s+not|no\s+longer)\b", re.I) INCLUDE_HEADING_RE = re.compile( r"^#{0,6}\s*\**\s*(?:inclusion\s+criteria|eligibility\s+criteria|eligible\s+stud(?:y|ies)|" r"included\s+stud(?:y|ies)|types?\s+of\s+stud(?:y|ies)(?:\s+to\s+be\s+included)?|study\s+designs?)\b", re.I | re.M) NEXT_HEADING_RE = re.compile(r"^#{1,6}\s", re.M) # prefix match on purpose: "exclu" must fire on exclude/excluded/exclusion, so no # trailing \b (there is no word boundary between "exclu" and "ded"). EXCLUDE_CUE_RE = re.compile(r"(?im)^.*\b(?:exclu|not\s+eligible|ineligible)") # --- design axes: (exclusion reason) vs (protocol says it is eligible) ------ # Both sides must match the SAME axis. Extensible: add an axis with an exclusion # regex and the affirmative-eligibility regex for the same design/population. # Only comparator + randomisation ship — the axes with a real study-loss failure. AXES = [ { "name": "comparator design", "exclusion": re.compile( r"not\s+comparative|non-?comparative|no\s+(?:comparator|control(?:\s+group|\s+arm)?|" r"comparison\s+group)|single[-\s]?arm|case\s+series|uncontrolled|" r"lack(?:ed|ing|s)?\s+(?:a\s+)?(?:comparator|control)", re.I), "inclusion": re.compile( r"single[-\s]?arm|case\s+series|non-?comparative|uncontrolled|" r"comparator\s+(?:was\s+|is\s+)?not\s+required|without\s+(?:a\s+)?(?:comparator|control)", re.I), }, { "name": "randomisation", "exclusion": re.compile( r"not\s+randomi[sz]ed|non-?randomi[sz]ed|not\s+(?:an?\s+)?RCTs?|" r"observational\s+(?:stud(?:y|ies)|designs?)\s+(?:were\s+)?excluded", re.I), "inclusion": re.compile( r"observational\s+stud|non-?randomi[sz]ed\s+stud|" r"cohort\s+(?:stud(?:y|ies)|designs?)\s+(?:were\s+|are\s+)?(?:eligible|included|permitted)", re.I), }, ] _STOP = { "the", "a", "an", "of", "to", "no", "not", "non", "or", "and", "study", "studies", "data", "design", "for", "with", "without", "full", "text", "paper", "were", "was", "are", "is", "target", "type", "types", } def read_table(path: Path) -> list[dict[str, str]]: delimiter = "\t" if path.suffix.lower() in {".tsv", ".tab"} else "," with path.open(encoding="utf-8-sig", newline="") as fh: return [{(k or "").strip(): (v or "").strip() for k, v in row.items()} for row in csv.DictReader(fh, delimiter=delimiter)] def find_col(rows: list[dict[str, str]], candidates: list[str]) -> str | None: if not rows: return None lower = {k.lower(): k for k in rows[0].keys()} for cand in candidates: # exact match first if cand.lower() in lower: return lower[cand.lower()] for key in rows[0].keys(): # then substring lk = key.lower() if any(cand.lower() in lk for cand in candidates): return key return None def norm_code(raw: str) -> str: m = CODE_TOKEN_RE.search((raw or "").upper()) return m.group(1) if m else "" def content_tokens(s: str) -> set[str]: return {w for w in re.findall(r"[a-z]+", (s or "").lower()) if w not in _STOP and len(w) > 2} def collect_used_codes(paths: list[Path], code_col_arg: str | None, reason_col_arg: str | None) -> dict[str, set[str]]: """code -> set of reason strings actually applied in the screening artifacts.""" used: dict[str, set[str]] = {} for path in paths: rows = read_table(path) if not rows: continue code_col = code_col_arg or find_col(rows, CODE_COL_CANDS) reason_col = reason_col_arg or find_col(rows, REASON_COL_CANDS) for row in rows: raw_code = row.get(code_col, "") if code_col else "" raw_reason = row.get(reason_col, "") if reason_col else "" code = norm_code(raw_code) or norm_code(raw_reason) if not code: continue reason = raw_reason.strip() if not reason and raw_code: # salvage a reason from the code column ("F2 - not comparative") reason = CODE_TOKEN_RE.sub("", raw_code, count=1).strip(" -:—–\t") used.setdefault(code, set()) if reason: used[code].add(reason) return used def extract_legend(text: str) -> dict[str, str]: """Registered code -> meaning, from markdown-table rows and inline forms.""" legend: dict[str, str] = {} for line in text.splitlines(): s = line.strip() if s.startswith("|"): cells = [c.strip() for c in s.strip("|").split("|")] if len(cells) >= 2: c = norm_code(cells[0]) meaning = cells[1] if (c and cells[0].strip().upper() == c and meaning and re.search(r"[A-Za-z]", meaning) and not set(meaning) <= set("-| ") and not CODE_TOKEN_RE.match(meaning.upper())): legend.setdefault(c, meaning) continue for m in LEGEND_LINE_RE.finditer(s): c, meaning = m.group(1).upper(), m.group(2).strip() if (meaning and re.search(r"[A-Za-z]", meaning) and not CODE_TOKEN_RE.match(meaning.upper()) and c not in legend): legend[c] = meaning return legend def inclusion_text(protocol: str) -> str: """Text where the protocol AFFIRMATIVELY names an eligible design/population: non-negated inclusion sentences + inclusion-heading blocks truncated at the first exclusion cue (so an Excluded sub-list under an Eligibility heading does not leak in).""" parts: list[str] = [] for s in re.split(r"(?<=[.!?])\s+|\n", protocol): if ELIG_INCLUDE_RE.search(s) and not ELIG_NEGATION_RE.search(s): parts.append(s) for m in INCLUDE_HEADING_RE.finditer(protocol): start = m.end() nxt = NEXT_HEADING_RE.search(protocol, start) block = protocol[start: nxt.start() if nxt else len(protocol)] cut = EXCLUDE_CUE_RE.search(block) if cut: block = block[:cut.start()] parts.append(block) return "\n".join(parts) def check(protocol_text: str, used: dict[str, set[str]]) -> list[dict]: legend = extract_legend(protocol_text) inc_text = inclusion_text(protocol_text) claims: list[dict] = [] for code in sorted(used): reasons = used[code] reason_blob = ((legend.get(code, "") + " ; " + " ; ".join(sorted(reasons)))).strip(" ;") # 1. CODE_CONTRADICTS_ELIGIBILITY — same design axis on both sides. for axis in AXES: if axis["exclusion"].search(reason_blob) and axis["inclusion"].search(inc_text): elig = axis["inclusion"].search(inc_text) claims.append({ "verdict": "CODE_CONTRADICTS_ELIGIBILITY", "severity": "Major", "code": code, "detail": (f"exclusion code {code} ({reason_blob[:80]!r}) excludes on the " f"{axis['name']} axis, but the protocol's eligibility text names that " f"design as eligible ({elig.group(0)!r}); this code deletes studies the " f"protocol includes — remove the code or amend the registered criteria"), "where": reason_blob[:120], }) break # 2. CODE_NOT_REGISTERED — used but absent from the registered legend. if legend and code not in legend: claims.append({ "verdict": "CODE_NOT_REGISTERED", "severity": "Major", "code": code, "detail": (f"code {code} is applied in the screening artifacts but is absent from the " f"registered exclusion-code legend ({sorted(legend)}); an off-protocol reason " f"with no documented basis — register it or reclassify the affected records"), "where": (reason_blob or code)[:120], }) # 3. CODE_RENUMBERED — same code, disjoint meaning in legend vs use. if code in legend and reasons: legend_toks = content_tokens(legend[code]) used_toks: set[str] = set() for r in reasons: used_toks |= content_tokens(r) if legend_toks and used_toks and not (legend_toks & used_toks): claims.append({ "verdict": "CODE_RENUMBERED", "severity": "Minor", "code": code, "detail": (f"code {code} is registered as {legend[code][:50]!r} but applied as " f"{sorted(reasons)[0][:50]!r} — the same code number carries two meanings; " f"realign the legend and the artifacts"), "where": f"{code}: {legend[code][:60]}", }) return claims def analyze(protocol: str, screening: list[str], code_col: str | None, reason_col: str | None) -> dict: ppath = Path(protocol) if not ppath.is_file(): sys.stderr.write(f"ERROR: protocol not found: {protocol}\n") sys.exit(2) spaths = [Path(s) for s in screening] for sp in spaths: if not sp.is_file(): sys.stderr.write(f"ERROR: screening file not found: {sp}\n") sys.exit(2) used = collect_used_codes(spaths, code_col, reason_col) claims = check(ppath.read_text(encoding="utf-8"), used) n_major = sum(1 for c in claims if c["severity"] == "Major") return { "protocol": str(ppath), "screening": [str(s) for s in spaths], "codes_used": sorted(used), "claims": claims, "summary": { "n_claims": len(claims), "n_major": n_major, "n_minor": len(claims) - n_major, "verdict": "MAJOR_CANDIDATE" if n_major else "OK", }, } def render(result: dict) -> str: lines = ["| Verdict | Severity | Code | Detail |", "|---|---|---|---|"] for c in result["claims"]: lines.append(f"| {c['verdict']} | {c['severity']} | {c['code']} | {c['detail']} |") if len(lines) == 2: lines.append("| (none) | — | — | every applied code is registered and consistent with eligibility |") return "\n".join(lines) def main() -> int: ap = argparse.ArgumentParser(description="Exclusion-code validity gate (Phase 3f).") ap.add_argument("--protocol", required=True, help="registered protocol / PROSPERO markdown or text") ap.add_argument("--screening", required=True, nargs="+", help="screening artifact TSV/CSV file(s)") ap.add_argument("--code-col", help="exclusion-code column override") ap.add_argument("--reason-col", help="exclusion-reason column override") ap.add_argument("--out", help="write JSON artifact to this path") ap.add_argument("--strict", action="store_true", help="exit 1 if any Major claim exists") ap.add_argument("--quiet", action="store_true", help="suppress stdout table") args = ap.parse_args() result = analyze(args.protocol, args.screening, args.code_col, args.reason_col) if not args.quiet: print("=" * 44) print(" Exclusion-Code Validity (Phase 3f)") print("=" * 44) print(render(result)) print() s = result["summary"] if s["n_major"]: print(f"MAJOR candidate: {s['n_major']} code(s) unregistered or contradicting eligibility.") elif s["n_minor"]: print(f"MINOR: {s['n_minor']} renumbered code(s); no study-loss defect.") else: print("OK: every applied exclusion code is registered and consistent with the protocol.") if args.out: Path(args.out).parent.mkdir(parents=True, exist_ok=True) Path(args.out).write_text( json.dumps({"detector": "check_exclusion_code_validity", **result}, indent=2, ensure_ascii=False), encoding="utf-8") if not args.quiet: print(f"\nwrote {args.out}") return 1 if (args.strict and result["summary"]["n_major"]) else 0 if __name__ == "__main__": sys.exit(main()) -
check_pool_consistency.py 7 KB
#!/usr/bin/env python3 """ check_pool_consistency.py — Phase 4 entry gate. Asserts UID-set equality between (a) the frozen `FINAL_POOL_LOCK.yaml` and (b) the actual round-3 adjudication TSV that feeds extraction. Blocks Phase 4 (data extraction) until the two agree. Why this gate exists ==================== Cross-project precedent (anonymized): an LLM reporting-quality SR carried five documents that disagreed on INCLUDE/EXCLUDE counts. Three EXCLUDE rows existed in the downstream extraction sheet without matching INCLUDE decisions. The drift traced to a post-freeze adjudication change that propagated to the extraction TSV but not the lock — or the other way around. Either direction is fatal at peer review. The gate fails CLOSED: if the lock and the extraction sheet disagree on even one UID, extraction is blocked. Inputs ====== --lock PATH FINAL_POOL_LOCK.yaml (Phase 3f.5 artifact) --adjudication-tsv PATH round3_adjudication.tsv (Phase 3c artifact) --decision-col NAME column holding the decision label (default: "round3_decision") --uid-col NAME column holding the UID (default: "uid") --include-labels LIST decisions counted as INCLUDE (default: "INCLUDE,INCLUDE_MIXED") --out PATH JSON report (default: qc/pool_consistency.json) Output JSON =========== { "submission_safe": false, "lock_include_n": 42, "tsv_include_n": 43, "in_lock_not_tsv": ["UID_007"], "in_tsv_not_lock": ["UID_055"], "match": false } Exit codes ========== 0 lock and TSV agree on the UID set 1 disagreement (PR T1-5 blocks extraction) 2 invocation error (missing files, missing columns) Read-only script. No file modification. """ from __future__ import annotations import argparse import csv import json import sys from pathlib import Path def load_lock_uids(lock_path: Path, label_set: list[str]) -> set[str]: try: import yaml # type: ignore except ImportError: print( "ERROR: PyYAML required for --lock parsing. pip install PyYAML", file=sys.stderr, ) sys.exit(2) data = yaml.safe_load(lock_path.read_text(encoding="utf-8")) if not isinstance(data, dict): print(f"ERROR: lock file not a mapping: {lock_path}", file=sys.stderr) sys.exit(2) # INCLUDE_MIXED maps to mixed_uids in the lock template. uids: set[str] = set() if "INCLUDE" in label_set: uids.update(str(u) for u in (data.get("include_uids") or [])) if "INCLUDE_MIXED" in label_set: uids.update(str(u) for u in (data.get("mixed_uids") or [])) if "MIXED" in label_set: uids.update(str(u) for u in (data.get("mixed_uids") or [])) if "EXCLUDE" in label_set: uids.update(str(u) for u in (data.get("exclude_uids") or [])) return uids def load_tsv_uids( tsv_path: Path, decision_col: str, uid_col: str, label_set: set[str], ) -> set[str]: # Allow .tsv or .csv (sniff by extension). delim = "," if tsv_path.suffix.lower() == ".csv" else "\t" with tsv_path.open("r", encoding="utf-8", newline="") as fh: reader = csv.DictReader(fh, delimiter=delim) if reader.fieldnames is None: print(f"ERROR: empty TSV: {tsv_path}", file=sys.stderr) sys.exit(2) if uid_col not in reader.fieldnames: print( f"ERROR: uid column {uid_col!r} not in TSV columns " f"{reader.fieldnames!r}", file=sys.stderr, ) sys.exit(2) if decision_col not in reader.fieldnames: print( f"ERROR: decision column {decision_col!r} not in TSV columns " f"{reader.fieldnames!r}", file=sys.stderr, ) sys.exit(2) uids: set[str] = set() for row in reader: decision = (row.get(decision_col) or "").strip() if decision in label_set: uid = (row.get(uid_col) or "").strip() if uid: uids.add(uid) return uids def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser( description=( "Phase 4 entry gate: asserts UID-set equality between the frozen " "FINAL_POOL_LOCK.yaml and the round-3 adjudication TSV." ) ) parser.add_argument("--lock", type=Path, required=True) parser.add_argument("--adjudication-tsv", type=Path, required=True) parser.add_argument("--decision-col", default="round3_decision") parser.add_argument("--uid-col", default="uid") parser.add_argument( "--include-labels", default="INCLUDE,INCLUDE_MIXED", help="Comma-separated decision labels counted as included.", ) parser.add_argument("--out", type=Path, default=Path("qc/pool_consistency.json")) parser.add_argument("--quiet", action="store_true") args = parser.parse_args(argv) if not args.lock.is_file(): print(f"ERROR: lock not found: {args.lock}", file=sys.stderr) return 2 if not args.adjudication_tsv.is_file(): print(f"ERROR: TSV not found: {args.adjudication_tsv}", file=sys.stderr) return 2 labels = [s.strip() for s in args.include_labels.split(",") if s.strip()] label_set = set(labels) lock_uids = load_lock_uids(args.lock, labels) tsv_uids = load_tsv_uids( args.adjudication_tsv, args.decision_col, args.uid_col, label_set ) in_lock_only = sorted(lock_uids - tsv_uids) in_tsv_only = sorted(tsv_uids - lock_uids) match = not in_lock_only and not in_tsv_only report = { "submission_safe": match, "match": match, "lock_include_n": len(lock_uids), "tsv_include_n": len(tsv_uids), "in_lock_not_tsv": in_lock_only, "in_tsv_not_lock": in_tsv_only, "include_labels": labels, } args.out.parent.mkdir(parents=True, exist_ok=True) args.out.write_text(json.dumps({"detector": "check_pool_consistency", **report}, indent=2), encoding="utf-8") if not args.quiet: if match: print(f"PASS: lock and TSV agree ({len(lock_uids)} UIDs).") else: print( f"FAIL: lock includes {len(lock_uids)} UIDs, TSV includes " f"{len(tsv_uids)} UIDs." ) if in_lock_only: print(f" In lock but not TSV ({len(in_lock_only)}):") for u in in_lock_only[:10]: print(f" - {u}") if len(in_lock_only) > 10: print(f" ... and {len(in_lock_only) - 10} more") if in_tsv_only: print(f" In TSV but not lock ({len(in_tsv_only)}):") for u in in_tsv_only[:10]: print(f" - {u}") if len(in_tsv_only) > 10: print(f" ... and {len(in_tsv_only) - 10} more") return 0 if match else 1 if __name__ == "__main__": sys.exit(main()) -
cohort_overlap_check.py 9 KB
#!/usr/bin/env python3 """ Cohort Overlap Detection — flag included-study pairs sharing same data source. Motivation: recent SR-MA peer-review cycles found included studies using overlapping public ICU/EHR cohorts (MIMIC-IV, eICU, KNHIS, UK Biobank) without explicit acknowledgment. Independent-cohort assumption for MA pooling is then violated. This script reads a study list with PMID + extracted cohort metadata, fetches missing fields from PubMed efetch when needed, and reports clusters by (a) shared public database, (b) shared institution + overlapping enrollment period, (c) shared author surname + year proximity. Usage: python3 cohort_overlap_check.py --input studies.csv --out overlap_report.md Input CSV schema (recommended columns): study_id, pmid, country, institution, database_source, enrollment_period_start, enrollment_period_end, first_author, year, sample_n `database_source` examples: "MIMIC-IV", "eICU", "KNHIS", "UK Biobank", "institutional", "multi-center prospective". Use empty string if unknown — script will attempt PubMed fetch to infer. Outputs a Markdown report listing: - HIGH-CONFIDENCE OVERLAP pairs (same database + overlapping period) - MEDIUM-CONFIDENCE candidates (same institution OR same author surname + year ±2) - UNDETERMINED (insufficient metadata) """ import argparse import csv import json import sys import urllib.parse import urllib.request import xml.etree.ElementTree as ET from collections import defaultdict from pathlib import Path from typing import Optional PUBLIC_DBS = [ "MIMIC-IV", "MIMIC-III", "eICU", "MIMIC", "MarketScan", "Optum", "NHANES", "KNHANES", "KNHIS", "UK Biobank", "Biobank Japan", "TriNetX", "All of Us", "VA Corporate Data Warehouse", "PCORnet", "SEER", "NSQIP", ] def fetch_pubmed_affiliation(pmid: str, timeout: int = 10) -> dict: """Fetch first-author affiliation + abstract Methods snippet via efetch.""" url = ( "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi" f"?db=pubmed&id={pmid}&retmode=xml" ) try: with urllib.request.urlopen(url, timeout=timeout) as resp: data = resp.read() except Exception as e: return {"pmid": pmid, "error": str(e)} root = ET.fromstring(data) art = root.find(".//Article") if art is None: return {"pmid": pmid, "error": "no Article element"} title = art.findtext("ArticleTitle", "") or "" aff = root.findtext(".//Author/AffiliationInfo/Affiliation", "") or "" abstract = " ".join( (ab.text or "") for ab in root.findall(".//AbstractText") ) # Heuristic database detection from abstract db_hit = "" for db in PUBLIC_DBS: if db.lower() in abstract.lower(): db_hit = db break return { "pmid": pmid, "title": title, "affiliation": aff, "database_hit": db_hit, } def periods_overlap(s1: str, e1: str, s2: str, e2: str) -> Optional[bool]: """Return True/False/None (None = insufficient data). YYYY-MM strings.""" if not (s1 and e1 and s2 and e2): return None try: return not (e1 < s2 or e2 < s1) except Exception: return None def cluster_by_db(rows: list[dict]) -> dict[str, list[dict]]: """Group rows by database_source (non-empty).""" clusters = defaultdict(list) for r in rows: db = (r.get("database_source") or "").strip() if db and db.lower() != "institutional": clusters[db].append(r) return {k: v for k, v in clusters.items() if len(v) >= 2} def cluster_by_institution(rows: list[dict]) -> dict[str, list[dict]]: clusters = defaultdict(list) for r in rows: inst = (r.get("institution") or "").strip() if inst: # Normalize: lowercase, strip "Department of X, " prefix key = inst.lower().split(",")[0].strip() clusters[key].append(r) return {k: v for k, v in clusters.items() if len(v) >= 2} def cluster_by_author_year(rows: list[dict]) -> dict[str, list[dict]]: clusters = defaultdict(list) for r in rows: surname = (r.get("first_author") or "").strip().split()[0].lower() if r.get("first_author") else "" if surname: clusters[surname].append(r) return {k: v for k, v in clusters.items() if len(v) >= 2} def write_report(out_path: Path, db_clusters, inst_clusters, author_clusters, enriched: list[dict]) -> None: lines = ["# Cohort Overlap Report", ""] lines.append(f"Total studies analyzed: **{len(enriched)}**") lines.append("") lines.append("## HIGH-CONFIDENCE: Same public database") if db_clusters: for db, rows in db_clusters.items(): lines.append(f"\n### Database: **{db}** ({len(rows)} studies)") for r in rows: period = ( f"{r.get('enrollment_period_start','?')}" f"-{r.get('enrollment_period_end','?')}" ) lines.append( f"- {r.get('study_id','?')} (PMID {r.get('pmid','?')}, " f"N={r.get('sample_n','?')}, period {period})" ) # Period overlap check (pairwise) for i in range(len(rows)): for j in range(i + 1, len(rows)): o = periods_overlap( rows[i].get("enrollment_period_start", ""), rows[i].get("enrollment_period_end", ""), rows[j].get("enrollment_period_start", ""), rows[j].get("enrollment_period_end", ""), ) if o is True: lines.append( f" - WARN Period overlap: {rows[i]['study_id']} <-> {rows[j]['study_id']}" ) elif o is None: lines.append( f" - QUERY Period overlap undetermined: " f"{rows[i]['study_id']} <-> {rows[j]['study_id']}" ) else: lines.append("None.") lines.append("\n## MEDIUM: Same institution (single-center cluster)") if inst_clusters: for inst, rows in inst_clusters.items(): lines.append(f"\n### Institution: **{inst}** ({len(rows)} studies)") for r in rows: lines.append(f"- {r.get('study_id','?')} (PMID {r.get('pmid','?')})") else: lines.append("None.") lines.append("\n## LOWER: Same author surname +/-2y year") if author_clusters: for surname, rows in author_clusters.items(): years = [int(r["year"]) for r in rows if str(r.get("year","")).isdigit()] year_span = max(years) - min(years) if years else None tag = "(within +/-2y)" if year_span is not None and year_span <= 2 else "(>2y span)" lines.append(f"\n### Surname: **{surname}** {tag} - {len(rows)} studies") for r in rows: lines.append(f"- {r.get('study_id','?')} ({r.get('year','?')})") else: lines.append("None.") lines.append("\n## Recommendation") lines.append( "- For HIGH-CONFIDENCE overlap pairs: sensitivity analysis excluding one, " "explicit Limitations acknowledgment, cohort-source column in Table 1." ) lines.append( "- For MEDIUM: request author institution + ORCID to verify cohort independence." ) out_path.write_text("\n".join(lines), encoding="utf-8") def main() -> int: p = argparse.ArgumentParser(description="Cohort overlap detector for SR-MA") p.add_argument("--input", required=True, type=Path, help="Input CSV") p.add_argument("--out", required=True, type=Path, help="Output Markdown report") p.add_argument("--enrich", action="store_true", help="Fetch PubMed efetch for missing database/affiliation") args = p.parse_args() if not args.input.exists(): print(f"ERROR: input not found: {args.input}", file=sys.stderr) return 1 with args.input.open(newline="", encoding="utf-8") as fin: rows = list(csv.DictReader(fin)) if args.enrich: for r in rows: if not r.get("database_source") and r.get("pmid"): info = fetch_pubmed_affiliation(r["pmid"]) if info.get("database_hit"): r["database_source"] = info["database_hit"] if not r.get("institution") and info.get("affiliation"): r["institution"] = info["affiliation"][:80] db_clusters = cluster_by_db(rows) inst_clusters = cluster_by_institution(rows) author_clusters = cluster_by_author_year(rows) write_report(args.out, db_clusters, inst_clusters, author_clusters, rows) n_db = sum(len(v) for v in db_clusters.values()) n_inst = sum(len(v) for v in inst_clusters.values()) print( f"Cohort overlap: {len(db_clusters)} DB clusters ({n_db} studies), " f"{len(inst_clusters)} institution clusters ({n_inst} studies)" ) print(f"Report: {args.out}") return 1 if db_clusters else 0 if __name__ == "__main__": sys.exit(main()) -
dta_extraction_qc.py 5.3 KB
#!/usr/bin/env python3 """ DTA Extraction QC — cross-check forest plot 2x2 cells against source paper sens/spec. Motivation: recent SR-MA peer-review cycles found extracted (TP, FN, TN, FP) cells with sensitivity/specificity values swapped versus the source paper. In single-study k=1 subgroups this can invert reported p-values. This script validates extracted cells against source-reported sens/spec with a tolerance and FLAGs mismatches for manual review. It does NOT auto-correct. Usage: python3 dta_extraction_qc.py --input extraction.csv --tolerance 0.02 --out qc_report.tsv Input CSV schema (required columns): study_id - e.g., "StudyA_2021_1" source_pmid - PubMed ID (string) source_sens - decimal 0-1 (e.g., 0.978) source_spec - decimal 0-1 (e.g., 0.554) extracted_tp - int (TP cell) extracted_fn - int (FN cell) extracted_tn - int (TN cell) extracted_fp - int (FP cell) source_cohort - e.g., "external_test_set" (which cohort the source values come from) source_page_ref - e.g., "Table 3 page 7" (audit trail) Output: Tab-separated table with one row per study, columns: study_id | extracted_sens | extracted_spec | source_sens | source_spec | sens_diff | spec_diff | status | swap_suspected | message """ import argparse import csv import sys from pathlib import Path def compute_sens_spec(tp: int, fn: int, tn: int, fp: int) -> tuple[float, float]: """Return (sensitivity, specificity).""" sens = tp / (tp + fn) if (tp + fn) > 0 else float("nan") spec = tn / (tn + fp) if (tn + fp) > 0 else float("nan") return sens, spec def check_row(row: dict, tol: float) -> dict: """Return augmented row with status/diff fields.""" try: tp = int(row["extracted_tp"]) fn = int(row["extracted_fn"]) tn = int(row["extracted_tn"]) fp = int(row["extracted_fp"]) source_sens = float(row["source_sens"]) source_spec = float(row["source_spec"]) except (KeyError, ValueError) as e: return {**row, "status": "PARSE_ERROR", "message": str(e)} ext_sens, ext_spec = compute_sens_spec(tp, fn, tn, fp) sens_diff = abs(ext_sens - source_sens) spec_diff = abs(ext_spec - source_spec) # Swap detection: extracted_sens ≈ source_spec AND extracted_spec ≈ source_sens swap_sens_match = abs(ext_sens - source_spec) <= tol swap_spec_match = abs(ext_spec - source_sens) <= tol swap_suspected = swap_sens_match and swap_spec_match if sens_diff <= tol and spec_diff <= tol: status = "OK" message = "Match within tolerance." elif swap_suspected: status = "FLAG_SWAP" message = ( f"Possible sens/spec SWAP. " f"Extracted (sens={ext_sens:.3f}, spec={ext_spec:.3f}) matches " f"source swapped (source_sens={source_sens:.3f}, source_spec={source_spec:.3f})." ) else: status = "FLAG_MISMATCH" message = ( f"Discrepancy beyond tolerance ({tol:.3f}). " f"Check source_page_ref={row.get('source_page_ref','?')}." ) return { **row, "extracted_sens": f"{ext_sens:.4f}", "extracted_spec": f"{ext_spec:.4f}", "sens_diff": f"{sens_diff:.4f}", "spec_diff": f"{spec_diff:.4f}", "swap_suspected": "YES" if swap_suspected else "NO", "status": status, "message": message, } def main() -> int: p = argparse.ArgumentParser(description="DTA extraction sens/spec QC") p.add_argument("--input", required=True, type=Path, help="Input CSV file") p.add_argument("--tolerance", type=float, default=0.02, help="Absolute tolerance for sens/spec match (default 0.02 = 2 percentage points)") p.add_argument("--out", required=True, type=Path, help="Output TSV report") args = p.parse_args() if not args.input.exists(): print(f"ERROR: input file not found: {args.input}", file=sys.stderr) return 1 with args.input.open(newline="", encoding="utf-8") as fin: reader = csv.DictReader(fin) rows = [check_row(r, args.tolerance) for r in reader] fieldnames = [ "study_id", "source_pmid", "source_cohort", "source_page_ref", "extracted_tp", "extracted_fn", "extracted_tn", "extracted_fp", "extracted_sens", "extracted_spec", "source_sens", "source_spec", "sens_diff", "spec_diff", "swap_suspected", "status", "message", ] with args.out.open("w", newline="", encoding="utf-8") as fout: writer = csv.DictWriter(fout, fieldnames=fieldnames, delimiter="\t", extrasaction="ignore") writer.writeheader() for r in rows: writer.writerow(r) # Summary stats n = len(rows) n_ok = sum(1 for r in rows if r.get("status") == "OK") n_swap = sum(1 for r in rows if r.get("status") == "FLAG_SWAP") n_mismatch = sum(1 for r in rows if r.get("status") == "FLAG_MISMATCH") n_err = sum(1 for r in rows if r.get("status") == "PARSE_ERROR") print(f"DTA QC: {n} studies | OK={n_ok} | SWAP={n_swap} | MISMATCH={n_mismatch} | PARSE_ERROR={n_err}") print(f"Report: {args.out}") return 1 if (n_swap + n_mismatch + n_err) > 0 else 0 if __name__ == "__main__": sys.exit(main()) -
extract_assist.py 7.7 KB
#!/usr/bin/env python3 """ AI-assisted extraction *suggestions* for SR/MA data extraction (Phase 4). Scans a full-text paper (Markdown, e.g. produced by /fulltext-retrieval's PDF->MD step) for schema-defined fields and emits SUGGESTED candidate values, each carrying a page reference and a verbatim source quote. This is the extraction-stage analog of the screening-stage `ai_pre_screening_template.py`: it produces *suggestions, never decisions*. CRITICAL — suggestions, not decisions ------------------------------------- Every emitted row is labeled `extraction_consensus_status = AI_SUGGESTED` and `needs_review = true`. A human extractor MUST confirm or overturn each value against the source PDF before it enters the confirmed extraction table. Only the human-confirmed DTA CSV is fed to `dta_extraction_qc.py` — this tool's output is NOT a QC-acceptable table. Per the Phase 4.0 gate, treat every candidate N / denominator / 2x2 cell / effect estimate as hallucination-suspect until reconciled against the source. The script never invents values: every `value` and `verbatim_quote` is copied literally from the Markdown, and `source_page_ref` comes from a literal page marker in the text. If a field is not found, it emits a `not_found` row so the gap is explicit rather than silently dropped. Usage ----- python3 extract_assist.py --md paper.md --schema schema.yaml --out suggestions.tsv python3 extract_assist.py --md paper.md --schema schema.json --study-id StudyA_2021 Schema (YAML or JSON) --------------------- study_id: StudyA_2021 # optional; --study-id overrides fields: - {name: study_design, type: study_design} - {name: sample_n, type: sample_n} - {name: source_sens, type: sensitivity} - {name: source_spec, type: specificity} - {name: extracted_tp, type: tp} - {name: country, type: regex, pattern: "conducted in ([A-Z][A-Za-z]+)"} Built-in field types: sensitivity, specificity, sample_n, study_design, year, tp, fp, fn, tn, regex (requires `pattern` with one capture group). Page markers recognized: `<!-- page: N -->`, `<!-- page N -->`, `[page N]`, `## Page N`, `===PAGE N===` (case-insensitive). """ import argparse import json import re import sys from pathlib import Path OUT_COLUMNS = [ "study_id", "field", "value", "source_page_ref", "verbatim_quote", "confidence", "needs_review", "extraction_consensus_status", ] PAGE_RE = re.compile( r"(?:<!--\s*page:?\s*(\d+)\s*-->|\[page\s+(\d+)\]|^#{1,3}\s*page\s+(\d+)\b|===\s*page\s+(\d+)\s*===)", re.I, ) # Built-in extractors: type -> compiled regex with one numeric/text capture group. BUILTIN = { "sensitivity": re.compile(r"sensitivit(?:y|ies)\s*(?:was|were|of|=|:|,)?\s*(\d{1,3}(?:\.\d+)?\s*%|0?\.\d+|\d{1,3}(?:\.\d+)?)", re.I), "specificity": re.compile(r"specificit(?:y|ies)\s*(?:was|were|of|=|:|,)?\s*(\d{1,3}(?:\.\d+)?\s*%|0?\.\d+|\d{1,3}(?:\.\d+)?)", re.I), "sample_n": re.compile(r"(?:\bn\s*=\s*(\d+)|\b(\d+)\s+(?:patients|participants|subjects|cases|nodules|lesions|images|scans|examinations))", re.I), "study_design": re.compile(r"\b(retrospective|prospective|cross-sectional|case-control|cohort)\b", re.I), "year": re.compile(r"\b(19\d{2}|20\d{2})\b"), "tp": re.compile(r"(?:true[\s-]?positives?|TP)\s*(?:=|:|of|was|were)?\s*(\d+)", re.I), "fp": re.compile(r"(?:false[\s-]?positives?|FP)\s*(?:=|:|of|was|were)?\s*(\d+)", re.I), "fn": re.compile(r"(?:false[\s-]?negatives?|FN)\s*(?:=|:|of|was|were)?\s*(\d+)", re.I), "tn": re.compile(r"(?:true[\s-]?negatives?|TN)\s*(?:=|:|of|was|were)?\s*(\d+)", re.I), } def load_schema(path: Path) -> dict: text = path.read_text(encoding="utf-8") if path.suffix.lower() == ".json": return json.loads(text) try: import yaml # PyYAML; used across medsci-skills scripts except ImportError: # pragma: no cover sys.exit("ERROR: PyYAML required for .yaml schemas (or use a .json schema).") return yaml.safe_load(text) def page_for_line(line_pages: list[int], idx: int) -> str: """Page number in effect at 0-based line index idx ('?' before first marker).""" return str(line_pages[idx]) if line_pages[idx] is not None else "?" def first_group(m: re.Match) -> str: for g in m.groups(): if g is not None: return g.strip() return m.group(0).strip() def find_field(field: dict, lines: list[str], line_pages: list[int]) -> list[dict]: """Return candidate rows (dicts) for one schema field, in document order.""" name = field["name"] ftype = field.get("type", "regex") if ftype == "regex": pat = field.get("pattern") if not pat: sys.exit(f"ERROR: field '{name}' type=regex requires a 'pattern'.") rx = re.compile(pat) else: rx = BUILTIN.get(ftype) if rx is None: sys.exit(f"ERROR: unknown field type '{ftype}' for '{name}'.") hits = [] for i, line in enumerate(lines): for m in rx.finditer(line): val = first_group(m) if m.groups() else m.group(0).strip() hits.append({ "field": name, "value": re.sub(r"\s+", "", val) if ftype in {"sensitivity", "specificity"} else val, "source_page_ref": page_for_line(line_pages, i), "verbatim_quote": line.strip(), }) n = len(hits) if n == 0: return [{ "field": name, "value": "", "source_page_ref": "?", "verbatim_quote": "", "confidence": "not_found", }] for k, h in enumerate(hits, 1): h["confidence"] = "single" if n == 1 else f"candidate_{k}_of_{n}" return hits def main(argv=None) -> int: ap = argparse.ArgumentParser(description="AI-assisted extraction suggestions (Phase 4).") ap.add_argument("--md", required=True, help="full-text paper in Markdown") ap.add_argument("--schema", required=True, help="extraction schema (.yaml or .json)") ap.add_argument("--study-id", default=None, help="overrides schema study_id") ap.add_argument("--out", default=None, help="output TSV (default: stdout)") args = ap.parse_args(argv) schema = load_schema(Path(args.schema)) study_id = args.study_id or schema.get("study_id", "UNKNOWN") fields = schema.get("fields", []) if not fields: sys.exit("ERROR: schema has no 'fields'.") raw = Path(args.md).read_text(encoding="utf-8") lines = raw.splitlines() # Build per-line current-page map from literal page markers. line_pages: list[int] = [] current = None for line in lines: m = PAGE_RE.search(line) if m: current = int(next(g for g in m.groups() if g is not None)) line_pages.append(current) rows = [] for field in fields: for cand in find_field(field, lines, line_pages): cand["study_id"] = study_id cand["needs_review"] = "true" cand["extraction_consensus_status"] = "AI_SUGGESTED" rows.append(cand) out_lines = ["\t".join(OUT_COLUMNS)] for r in rows: out_lines.append("\t".join(str(r.get(c, "")) for c in OUT_COLUMNS)) blob = "\n".join(out_lines) + "\n" if args.out: Path(args.out).write_text(blob, encoding="utf-8") else: sys.stdout.write(blob) n_found = sum(1 for r in rows if r.get("confidence") != "not_found") n_missing = sum(1 for r in rows if r.get("confidence") == "not_found") sys.stderr.write( f"[extract_assist] study={study_id}: {n_found} suggestion(s), " f"{n_missing} field(s) not found. ALL rows are AI_SUGGESTED / needs_review=true — " f"a human must confirm against the source PDF before building the DTA CSV for " f"dta_extraction_qc.py.\n" ) return 0 if __name__ == "__main__": raise SystemExit(main()) -
screening_reconcile.py 7.8 KB
#!/usr/bin/env python3 """Reconcile meta-analysis screening ID sets into a canonical JSON artifact.""" from __future__ import annotations import argparse import csv import json import re import sys from pathlib import Path INCLUDE_VALUES = {"include", "included", "yes", "y", "1", "true", "eligible", "include-qualitative"} EXCLUDE_VALUES = {"exclude", "excluded", "no", "n", "0", "false", "ineligible"} def read_table(path: Path) -> list[dict[str, str]]: if not path.exists(): raise FileNotFoundError(path) delimiter = "\t" if path.suffix.lower() in {".tsv", ".tab"} else "," with path.open(encoding="utf-8-sig", newline="") as fh: return [{k.strip(): (v or "").strip() for k, v in row.items()} for row in csv.DictReader(fh, delimiter=delimiter)] def find_col(rows: list[dict[str, str]], candidates: list[str]) -> str | None: if not rows: return None lower = {k.lower(): k for k in rows[0].keys()} for cand in candidates: if cand.lower() in lower: return lower[cand.lower()] for key in rows[0].keys(): lk = key.lower() if any(cand.lower() in lk for cand in candidates): return key return None def norm_id(value: str) -> str: value = value.strip() if not value: return "" match = re.search(r"\d+", value) return match.group(0) if match else value def decision_kind(value: str) -> str: v = value.strip().lower() if any(token in v for token in INCLUDE_VALUES): return "include" if any(token in v for token in EXCLUDE_VALUES): return "exclude" return "unknown" def ids_from_table(path: Path, id_col_arg: str | None, decision_col_arg: str | None, include_only: bool) -> tuple[set[str], dict[str, str]]: rows = read_table(path) id_col = id_col_arg or find_col(rows, ["id", "record_id", "study_id", "ref_id"]) if not id_col: raise ValueError(f"Could not identify ID column in {path}") decision_col = decision_col_arg or find_col(rows, ["decision", "verdict", "include", "screening", "consensus", "outcome"]) ids: set[str] = set() decisions: dict[str, str] = {} for row in rows: rid = norm_id(row.get(id_col, "")) if not rid: continue decision = row.get(decision_col, "") if decision_col else "" kind = decision_kind(decision) decisions[rid] = decision if include_only: if kind == "include": ids.add(rid) else: ids.add(rid) return ids, decisions def main() -> int: parser = argparse.ArgumentParser(description="Reconcile MA screening ID sets.") parser.add_argument("--screening", required=True, help="TSV/CSV with screening decisions") parser.add_argument("--consensus", help="TSV/CSV with final consensus decisions") parser.add_argument("--table1", help="TSV/CSV containing bivariate/Table 1 study IDs") parser.add_argument("--output", default="2_Screening/screening_consensus.json") parser.add_argument("--screening-id-col") parser.add_argument("--screening-decision-col") parser.add_argument("--consensus-id-col") parser.add_argument("--consensus-decision-col") parser.add_argument("--table1-id-col") args = parser.parse_args() screening_path = Path(args.screening) screening_include, screening_decisions = ids_from_table( screening_path, args.screening_id_col, args.screening_decision_col, include_only=True ) if args.consensus: consensus_ids, consensus_decisions = ids_from_table( Path(args.consensus), args.consensus_id_col, args.consensus_decision_col, include_only=False ) consensus_exclude = {rid for rid, dec in consensus_decisions.items() if decision_kind(dec) == "exclude"} consensus_include = {rid for rid, dec in consensus_decisions.items() if decision_kind(dec) == "include"} else: consensus_ids = set() consensus_exclude = set() consensus_include = set() if args.table1: table1_ids, _ = ids_from_table(Path(args.table1), args.table1_id_col, None, include_only=False) else: table1_ids = set() qualitative = (screening_include - consensus_exclude) | consensus_include bivariate = table1_ids narrative_only = qualitative - bivariate # A record that passed screening and was EXCLUDED at consensus carries a decision. # A record that passed screening and is ABSENT from the consensus artifact carries # none -- it fell out of the pipeline. Both leave `consensus_exclude` empty for that # id, so without this split the second case flows into `qualitative` and then into # `narrative_only`, where it is indistinguishable from a study legitimately lacking # extractable data. That is how an eligible study is lost silently. if args.consensus: stage_transfer_loss = screening_include - consensus_ids else: stage_transfer_loss = set() narrative_only_unadjudicated = narrative_only & stage_transfer_loss narrative_only_adjudicated = narrative_only - stage_transfer_loss payload = { "schema_version": 2, "sources": { "screening": str(screening_path), "consensus": args.consensus, "table1": args.table1, }, "sets": { "screening_include": sorted(screening_include, key=lambda x: (len(x), x)), "consensus_exclude": sorted(consensus_exclude, key=lambda x: (len(x), x)), "consensus_include": sorted(consensus_include, key=lambda x: (len(x), x)), "qualitative": sorted(qualitative, key=lambda x: (len(x), x)), "bivariate": sorted(bivariate, key=lambda x: (len(x), x)), "narrative_only": sorted(narrative_only, key=lambda x: (len(x), x)), "narrative_only_adjudicated": sorted(narrative_only_adjudicated, key=lambda x: (len(x), x)), "narrative_only_unadjudicated": sorted(narrative_only_unadjudicated, key=lambda x: (len(x), x)), "stage_transfer_loss": sorted(stage_transfer_loss, key=lambda x: (len(x), x)), }, "totals": { "k_screening_include": len(screening_include), "k_consensus_exclude": len(consensus_exclude), "k_consensus_include": len(consensus_include), "k_qualitative": len(qualitative), "k_bivariate": len(bivariate), "k_narrative_only": len(narrative_only), "k_narrative_only_adjudicated": len(narrative_only_adjudicated), "k_narrative_only_unadjudicated": len(narrative_only_unadjudicated), "k_stage_transfer_loss": len(stage_transfer_loss), }, "blocking_issues": [], } if bivariate and not bivariate <= qualitative: payload["blocking_issues"].append( { "code": "TABLE1_NOT_IN_QUALITATIVE", "ids": sorted(bivariate - qualitative, key=lambda x: (len(x), x)), } ) if stage_transfer_loss: payload["blocking_issues"].append( { "code": "STAGE_TRANSFER_LOSS", "ids": sorted(stage_transfer_loss, key=lambda x: (len(x), x)), "detail": ( "Included at screening but absent from the consensus artifact -- neither " "included nor excluded, so no adjudication is recorded. Either restore these " "records to the consensus stage, or record an explicit exclusion decision for " "each. Do not leave them to flow into the narrative-only set." ), } ) output = Path(args.output) output.parent.mkdir(parents=True, exist_ok=True) output.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8") print(json.dumps(payload["totals"], indent=2)) return 1 if payload["blocking_issues"] else 0 if __name__ == "__main__": sys.exit(main())
-
-
templates
-
extraction_form_v2.md 6.2 KB
# SR-MA Data Extraction Form v2 **Version**: v2.0 **Motivation**: Recent SR-MA peer-review cycles surfaced three recurring extraction-stage failure modes — diagnostic 2×2 cell sens/spec swap, undisclosed cohort overlap via shared public databases, and undocumented diagnostic vs prognostic subset N. v2 adds three column families to prevent recurrence. ## Coverage Use this form for any SR-MA with diagnostic accuracy and/or prognostic prediction outcomes. Compatible with QUADAS-AI / PROBAST-AI / METRICS frameworks. ## Column schema (CSV / Google Sheets) ### Identity (always required) | Column | Description | Example | |---|---|---| | `study_id` | Internal ID (`<Surname>_<Year>_<seq>`) | `StudyA_2021_1` | | `pmid` | PubMed ID | `00000000` | | `doi` | DOI | `10.xxxx/example.YYYY.NNNNNN` | | `first_author_lastname` | LastName from PubMed | `Surname` | | `first_author_forename` | ForeName from PubMed | `Forename` | | `year` | Publication year | `2021` | | `country` | Per affiliation (use country of cohort, not publisher) | `[Country]` | | `journal` | Full journal name | `[Journal Full Title]` | | `study_design` | retrospective / prospective / case-control / cross-sectional | `retrospective` | ### Cohort source (NEW in v2 — prevents undisclosed overlap) | Column | Description | Example | |---|---|---| | `cohort_source` | One of: `institutional` / `multi-center prospective` / `MIMIC-IV` / `eICU` / `MIMIC-III` / `KNHIS` / `UK Biobank` / `TriNetX` / `Optum` / `MarketScan` / `SEER` / etc. | `MIMIC-IV` | | `institution_primary` | Verbatim institution name from corresponding-author affiliation | `Tertiary Academic Medical Center, [City]` | | `institutions_additional` | Pipe-separated additional centers | `Other Affiliated Hospital` | | `enrollment_period_start` | YYYY-MM | `2017-01` | | `enrollment_period_end` | YYYY-MM | `2019-12` | | `data_sharing_statement` | Yes / No / NotReported | `NotReported` | | `overlap_flag_reviewer1` | Reviewer 1 suspects cohort overlap with which study_id (or empty) | `StudyB_YYYY` | | `overlap_flag_reviewer2` | Reviewer 2's overlap flag | `StudyB_YYYY` | ### Sample sizes (diagnostic subset N transparency) | Column | Description | Example | |---|---|---| | `sample_n_total` | Total study N | `452` | | `sample_n_train` | Training set N | `273` | | `sample_n_internal_test` | Internal test N | `68` | | `sample_n_external_test` | External test N | `111` | | `sample_n_dta_pool` | N contributing to DTA bivariate (sens/spec extraction) | `111` | | `sample_n_prognostic_pool` | N contributing to prognostic AUC pool | `0` | | `prevalence_in_dta_pool` | (TP+FN) / sample_n_dta_pool | `0.414` | ### DTA outcome extraction (2×2 cell integrity) For each cohort that contributes to bivariate pool, extract: | Column | Description | Example | |---|---|---| | `dta_cohort_label` | Which cohort the values come from (must match `sample_n_external_test` or similar) | `external_test` | | `tp` | True positive count | `45` | | `fn` | False negative count | `1` | | `tn` | True negative count | `36` | | `fp` | False positive count | `29` | | `extracted_sens` | TP / (TP+FN), decimal | `0.978` | | `extracted_spec` | TN / (TN+FP), decimal | `0.554` | | **`source_sens_reported`** | Sensitivity as reported in source paper (decimal) | `0.978` | | **`source_spec_reported`** | Specificity as reported in source paper (decimal) | `0.554` | | **`source_page_ref`** | Page + Table/Figure number in source paper | `Table 3, p.7` | | **`source_verbatim_quote`** | Verbatim sentence containing the sens/spec values | `"The deep-integrated model achieved a sensitivity of 0.978..."` | | `extractor1_initials` | First extractor | `R1` | | `extractor2_initials` | Second extractor | `R2` | | `extraction_consensus_status` | Resolved / DiscussNeeded / Conflict | `Resolved` | **QC**: Run `scripts/dta_extraction_qc.py` with `--tolerance 0.02` after dual-extractor entry. Any FLAG_SWAP or FLAG_MISMATCH requires third-reviewer adjudication. ### Prognostic outcome extraction (PROBAST-AI compatible) | Column | Description | Example | |---|---|---| | `prognostic_outcome_type` | mortality / AKI / disease_progression / readmission / etc. | `mortality` | | `prognostic_endpoint_definition` | Verbatim definition | `In-hospital all-cause death within index admission` | | `auc_point` | Reported AUC, decimal | `0.87` | | `auc_ci_lower` | 95% CI lower | `0.83` | | `auc_ci_upper` | 95% CI upper | `0.90` | | `validation_type` | internal / external / leave-one-out / k-fold | `external` | | `calibration_reported` | Yes / No | `No` | | `dca_reported` | Decision curve analysis Yes/No | `No` | ### Risk of bias (per-study × per-domain) QUADAS-AI (for DTA studies): | Column | Description | |---|---| | `quadasai_d1_patient_selection` | Low / High / Unclear | | `quadasai_d2_index_test` | Low / High / Unclear | | `quadasai_d3_reference_standard` | Low / High / Unclear | | `quadasai_d4_flow_timing` | Low / High / Unclear | | `quadasai_d1_justification` | Verbatim short reason | | `quadasai_d2_justification` | ... | | ... | (same for D3, D4) | | `quadasai_overall` | Low / High / Unclear (consensus) | PROBAST-AI (for prognostic studies): D1 Participants / D2 Predictors / D3 Outcome / D4 Analysis — same Low/High/Unclear + justification. METRICS: 4 domains (data, model dev, validation, reporting) per study. ### Authors of extraction record | Column | Description | |---|---| | `extraction_date_initial` | YYYY-MM-DD | | `extraction_date_consensus` | YYYY-MM-DD | | `cohens_kappa_d1` | Pre-adjudication inter-rater κ for domain D1 | | ... | (record κ for each pre-adjudication discrepancy domain) | ## Workflow (dual-extractor consensus) 1. Extractor 1 + Extractor 2 independently fill all cells, source_page_ref, source_verbatim_quote 2. Run `dta_extraction_qc.py --tolerance 0.02` → flag mismatches 3. Pre-adjudication κ recorded for D1-D4 4. Discrepancies → third-reviewer adjudication, consensus_status updated 5. Run `cohort_overlap_check.py --enrich` → populate database_source via PubMed if missing 6. Manual review of HIGH/MEDIUM clusters → confirm or override 7. Lock extraction (read-only) before statistical analysis ## Related - `scripts/dta_extraction_qc.py` - `scripts/cohort_overlap_check.py` - `templates/supplementary_8file_checklist.md` -
FINAL_POOL_LOCK.yaml.template 2.8 KB · in bundle
-
supplementary_8file_checklist.md 3.8 KB
# SR-MA Supplementary Package — 8-File Standard **Motivation**: Recent SR-MA peer reviews surfaced supplementary packages that contained only figure captions (≈90 lines), missing PRISMA checklist, exclusion list, extraction table, per-study RoB — despite Methods citing "Supplementary Table S1". Below is the minimum package to avoid this pattern. **Apply to**: every SR-MA submission. Radiology / Radiology-AI / European Radiology / JCSM / Lancet family / JAMA family / BMJ family all expect this level of supplementary. ## 8 mandatory files ### S1 — PRISMA / PRISMA-DTA / PRISMA-S checklist - 27-item PRISMA 2020 (general) or PRISMA-DTA (diagnostic accuracy SR-MA) - Each item filled with main-manuscript page reference (`p.3, lines 45-48`) - Use checklist template from your institutional checklist store - For pure search reporting: PRISMA-S 16-item additional ### S2 — PROSPERO protocol PDF (snapshot + amendments) - Download from prospero.york.ac.uk at registration AND at submission - Include all amendments with date + rationale - **If subgroup analyses changed post-registration**: explicit amendment record with date ### S3 — Full search strategy verbatim (per database) - Each database (PubMed / Embase / Cochrane / Scopus / Web of Science) with: - Search date (YYYY-MM-DD) - Final string verbatim (copy-paste from interface) - Filter use (date, language, study type) - Number of records retrieved per database ### S4 — Full-text exclusion list with reasons (PRISMA item 16b) - Every full-text article excluded with: - First author, year, title - PMID / DOI - Exclusion reason (one of pre-specified categories) - Use Rayyan export or equivalent ### S5 — Per-study data extraction table - One row per included study × all extracted variables (use `extraction_form_v2.md` schema) - Include `source_page_ref` + `source_verbatim_quote` for outcome cells (DTA 2×2 or AUC) - Locked read-only file (CSV + PDF snapshot) ### S6 — Per-study × per-domain risk-of-bias table - For DTA: QUADAS-AI (4 domains × N_diagnostic studies) + applicability concerns - For prognostic: PROBAST-AI (4 domains × N_prognostic studies) - Per-cell: Low / High / Unclear + 1-line justification - Optionally: METRICS framework parallel column - Include pre-adjudication Cohen's κ per domain ### S7 — Subgroup forest plots (all pre-specified + clearly labeled exploratory) - Each forest plot with: - Subgroup label - "Pre-specified in PROSPERO" or "Exploratory (post-hoc)" tag - Heterogeneity (I², τ²) per stratum - **k=1 strata excluded from formal test OR flagged as descriptive only** ### S8 — Sensitivity analyses + publication bias - Leave-one-out for primary outcome (forest plot or table) - Cohort overlap sensitivity analysis (exclude one of HIGH-confidence overlap pair) - Deeks' funnel asymmetry test for DTA studies (mada::funnel) - Funnel plot + Egger / Begg for prognostic AUC pooling - Trim-and-fill if asymmetry detected ## Submission gate check Before submitting, verify: ```bash ls -la submission/supplementary/ # Expected: at minimum 8 files (S1-S8) # Each file size > 5 KB (figure caption only is ~3 KB) for f in submission/supplementary/S*.{md,pdf,csv,docx}; do size=$(stat -f%z "$f" 2>/dev/null || stat -c%s "$f" 2>/dev/null) echo "$f: $size bytes" done ``` If any S-file is missing or only contains figure captions → **NOT READY for submission**. ## Cross-link to AI Disclosure (`/sync-submission` gate) The supplementary checklist should be combined with the AI Disclosure presence check. Most major imaging journals require an affirmative or negative disclosure statement either in manuscript text or in S-file metadata. ## Related - `templates/extraction_form_v2.md` - `scripts/dta_extraction_qc.py` - `scripts/cohort_overlap_check.py` - `/peer-review` Phase 2A P5 (supplementary completeness probe)
-
-
tests
-
test_pool_consistency.sh 3.6 KB
#!/usr/bin/env bash # Regression tests for meta-analysis check_pool_consistency.py. set -uo pipefail REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" SCRIPT="$REPO_ROOT/skills/meta-analysis/scripts/check_pool_consistency.py" TMP="$(mktemp -d -t pool_consist.XXXXXX)" trap 'rm -rf "$TMP"' EXIT [[ -f "$SCRIPT" ]] || { echo "ENV-ERR: script missing" >&2; exit 2; } python3 -c "import yaml" 2>/dev/null || { echo "SKIP: pyyaml unavailable"; exit 0; } fail=0 ran=0 assert_exit() { local label="$1" expected="$2" actual="$3" ran=$((ran + 1)) if [[ "$expected" == "$actual" ]]; then printf ' PASS %-50s exit=%s\n' "$label" "$actual" else printf ' FAIL %-50s expected=%s actual=%s\n' "$label" "$expected" "$actual" fail=$((fail + 1)) fi } # -------------------------------------------------------------------------- # Case 1: lock and TSV agree on 3 UIDs => PASS # -------------------------------------------------------------------------- mkdir -p "$TMP/c1" cat > "$TMP/c1/lock.yaml" <<'EOF' freeze_date: "2026-01-01" final_pool_n: 3 include_count: 3 exclude_count: 0 mixed_count: 0 include_uids: [UID_001, UID_002, UID_003] exclude_uids: [] mixed_uids: [] EOF cat > "$TMP/c1/r3.tsv" <<'EOF' uid round3_decision notes UID_001 INCLUDE ok UID_002 INCLUDE ok UID_003 INCLUDE ok UID_004 EXCLUDE out of scope EOF python3 "$SCRIPT" --lock "$TMP/c1/lock.yaml" \ --adjudication-tsv "$TMP/c1/r3.tsv" \ --out "$TMP/c1/report.json" --quiet assert_exit "case 1: lock and TSV agree" 0 $? # -------------------------------------------------------------------------- # Case 2: TSV has extra UID => FAIL # -------------------------------------------------------------------------- mkdir -p "$TMP/c2" cat > "$TMP/c2/lock.yaml" <<'EOF' include_uids: [UID_001, UID_002] exclude_uids: [] mixed_uids: [] EOF cat > "$TMP/c2/r3.tsv" <<'EOF' uid round3_decision UID_001 INCLUDE UID_002 INCLUDE UID_009 INCLUDE EOF python3 "$SCRIPT" --lock "$TMP/c2/lock.yaml" \ --adjudication-tsv "$TMP/c2/r3.tsv" \ --out "$TMP/c2/report.json" --quiet assert_exit "case 2: TSV extra UID (FAIL)" 1 $? python3 - "$TMP/c2/report.json" <<'PY' || fail=$((fail + 1)) import json, sys with open(sys.argv[1]) as fh: r = json.load(fh) assert "UID_009" in r["in_tsv_not_lock"], r assert not r["in_lock_not_tsv"], r PY # -------------------------------------------------------------------------- # Case 3: lock has extra UID => FAIL # -------------------------------------------------------------------------- mkdir -p "$TMP/c3" cat > "$TMP/c3/lock.yaml" <<'EOF' include_uids: [UID_001, UID_002, UID_999] mixed_uids: [] exclude_uids: [] EOF cat > "$TMP/c3/r3.tsv" <<'EOF' uid round3_decision UID_001 INCLUDE UID_002 INCLUDE EOF python3 "$SCRIPT" --lock "$TMP/c3/lock.yaml" \ --adjudication-tsv "$TMP/c3/r3.tsv" \ --out "$TMP/c3/report.json" --quiet assert_exit "case 3: lock extra UID (FAIL)" 1 $? python3 - "$TMP/c3/report.json" <<'PY' || fail=$((fail + 1)) import json, sys with open(sys.argv[1]) as fh: r = json.load(fh) assert "UID_999" in r["in_lock_not_tsv"], r PY # -------------------------------------------------------------------------- # Case 4: missing decision column => exit 2 # -------------------------------------------------------------------------- mkdir -p "$TMP/c4" cat > "$TMP/c4/lock.yaml" <<'EOF' include_uids: [UID_001] mixed_uids: [] exclude_uids: [] EOF cat > "$TMP/c4/r3.tsv" <<'EOF' uid notes UID_001 whatever EOF python3 "$SCRIPT" --lock "$TMP/c4/lock.yaml" \ --adjudication-tsv "$TMP/c4/r3.tsv" \ --out "$TMP/c4/report.json" --quiet 2>/dev/null assert_exit "case 4: missing decision col (exit 2)" 2 $? echo "" echo "ran=$ran fail=$fail" [[ $fail -eq 0 ]] -
test_screening_reconcile.sh 4 KB
#!/usr/bin/env bash # Regression test for screening_reconcile.py STAGE_TRANSFER_LOSS. # # The positive fixture reproduces the defect this check exists to stop: a record that # passed title/abstract screening, was never entered into the consensus stage, and is # absent from Table 1. Before the check existed, that record flowed into `qualitative` # and then into `narrative_only` -- where it is indistinguishable from a study that is # legitimately narrative-only -- and the script exited 0. # # The negative fixture is the case that must NOT fire: a genuine narrative-only study # (adjudicated at consensus as an include, simply lacking extractable 2x2 data). A # diagnostic-accuracy review normally has one or two of these, so a check that flags # them is useless. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" RECONCILE="${SCRIPT_DIR}/../scripts/screening_reconcile.py" TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT fail() { echo "FAIL: $*" >&2; exit 1; } # ---------------------------------------------------------------- positive # id 99: include at screening, absent from consensus entirely, absent from Table 1. printf 'id\tdecision\n1\tinclude\n2\tinclude\n99\tinclude\n3\texclude\n' > "$TMP/screening.tsv" printf 'id\tdecision\n1\tinclude\n2\tinclude\n3\texclude\n' > "$TMP/consensus.tsv" printf 'id\n1\n2\n' > "$TMP/table1.csv" set +e python3 "$RECONCILE" --screening "$TMP/screening.tsv" --consensus "$TMP/consensus.tsv" \ --table1 "$TMP/table1.csv" --output "$TMP/pos.json" > /dev/null rc=$? set -e [ "$rc" -eq 1 ] || fail "positive fixture: expected exit 1, got $rc" python3 - "$TMP/pos.json" <<'PY' || fail "positive fixture: STAGE_TRANSFER_LOSS not reported for id 99" import json, sys d = json.load(open(sys.argv[1])) codes = {i["code"] for i in d["blocking_issues"]} assert "STAGE_TRANSFER_LOSS" in codes, codes ids = [i["ids"] for i in d["blocking_issues"] if i["code"] == "STAGE_TRANSFER_LOSS"][0] assert ids == ["1", "2", "99"][2:], ids assert d["totals"]["k_stage_transfer_loss"] == 1 assert d["sets"]["narrative_only_unadjudicated"] == ["99"] assert d["sets"]["narrative_only_adjudicated"] == [] PY # ---------------------------------------------------------------- negative # id 7: adjudicated as an include at consensus, but has no extractable 2x2 -> it is # legitimately narrative-only. Must not fire. printf 'id\tdecision\n1\tinclude\n2\tinclude\n7\tinclude\n3\texclude\n' > "$TMP/screening_n.tsv" printf 'id\tdecision\n1\tinclude\n2\tinclude\n7\tinclude\n3\texclude\n' > "$TMP/consensus_n.tsv" printf 'id\n1\n2\n' > "$TMP/table1_n.csv" set +e python3 "$RECONCILE" --screening "$TMP/screening_n.tsv" --consensus "$TMP/consensus_n.tsv" \ --table1 "$TMP/table1_n.csv" --output "$TMP/neg.json" > /dev/null rc=$? set -e [ "$rc" -eq 0 ] || fail "negative fixture: expected exit 0, got $rc (false positive)" python3 - "$TMP/neg.json" <<'PY' || fail "negative fixture: genuine narrative-only misclassified" import json, sys d = json.load(open(sys.argv[1])) assert d["blocking_issues"] == [], d["blocking_issues"] assert d["totals"]["k_stage_transfer_loss"] == 0 assert d["sets"]["narrative_only"] == ["7"] assert d["sets"]["narrative_only_adjudicated"] == ["7"] assert d["sets"]["narrative_only_unadjudicated"] == [] PY # ------------------------------------------------- negative: no consensus supplied # With no consensus artifact there is nothing to reconcile against; the check must # stay silent rather than flag every screened include. set +e python3 "$RECONCILE" --screening "$TMP/screening.tsv" --output "$TMP/noc.json" > /dev/null rc=$? set -e [ "$rc" -eq 0 ] || fail "no-consensus fixture: expected exit 0, got $rc" python3 - "$TMP/noc.json" <<'PY' || fail "no-consensus fixture: fired without a consensus artifact" import json, sys d = json.load(open(sys.argv[1])) assert d["totals"]["k_stage_transfer_loss"] == 0 assert d["blocking_issues"] == [] PY echo "PASS: test_screening_reconcile.sh (positive + 2 negatives)"
-
-
SKILL.md 45.8 KB
--- name: meta-analysis description: Systematic review and meta-analysis pipeline for medical research. Covers protocol registration (PROSPERO), search strategy, screening, data extraction, risk of bias assessment (QUADAS-2/ROBINS-I), statistical synthesis (bivariate/HSROC for DTA, random-effects for intervention), and PRISMA-compliant reporting. Supports both DTA and intervention meta-analyses. triggers: meta-analysis, systematic review, PROSPERO, QUADAS-3, forest plot, funnel plot, PRISMA, QUADAS, ROBINS, HSROC, bivariate model, pooled sensitivity, pooled specificity, search strategy, study selection, data extraction form tools: Read, Write, Edit, Bash, Grep, Glob model: inherit --- # Meta-Analysis Skill You are helping a medical researcher conduct a systematic review and meta-analysis. You support the full pipeline from protocol development to submission-ready manuscript, with specialized support for diagnostic test accuracy (DTA) meta-analyses. ## Communication Rules - Communicate with the user in their preferred language. - All output documents, code, and checklists in English. - Medical terminology always in English. ## Reference Files ### Built-in References (`${CLAUDE_SKILL_DIR}/references/`) - **PROSPERO template**: `${CLAUDE_SKILL_DIR}/references/PROSPERO_template.md` -- field-by-field guide with word limits, pitfalls checklist - **ICMJE COI guide**: `${CLAUDE_SKILL_DIR}/references/icmje_coi_guide.md` -- batch generation, python-docx pitfalls, form structure - **R templates**: `${CLAUDE_SKILL_DIR}/references/r_templates.md` - **Checklists**: `${CLAUDE_SKILL_DIR}/references/checklists/` - `PRISMA_DTA.md` -- 27-item checklist - `QUADAS3.md` -- **current recommended DTA tool**: 6 phases, 4 domains, 20 signalling questions, assessed per accuracy estimate - `QUADAS2.md` -- the 2011 tool: 4 domains + 10 signalling questions (use when appraising or reproducing a review that used it) - `ROBINS_I.md` -- 7 domains + pre-assessment + synthesis recommendation - `RoB2.md` -- 5 domains + signalling questions + overall judgment - `PROBAST.md` -- 4 domains + AI extension + validation studies - `NOS.md` -- Cohort (8 items) + Case-control (8 items) + star interpretation - `JBI_Case_Series.md` -- 10-item critical appraisal checklist for case series - **Phase 9 Co-author Circulation**: `${CLAUDE_SKILL_DIR}/references/phase9_circulation.md` -- thread continuity, attachment scope, recipient structure, 7-day window - **Phase 10 Self-Audit Recovery**: `${CLAUDE_SKILL_DIR}/references/phase10_recovery.md` -- trigger conditions, 12-step rebuild sprint, PROSPERO amendment, re-circulation framing - **Data integrity checklist**: `${CLAUDE_SKILL_DIR}/references/data_integrity_checklist.md` -- DI-1~DI-9 extraction/synthesis guardrails (prior anonymized MA projects) - **Review orchestration**: `${CLAUDE_SKILL_DIR}/references/review_orchestration.md` -- RO-1~RO-5 circulation discipline (extends phase9_circulation.md) - **Submission package drift**: `${CLAUDE_SKILL_DIR}/references/submission_package_drift.md` -- multi-journal folder hygiene, `DO_NOT_EDIT_HERE` gate, `_build.sh` pattern - **Post-submission release ops**: `${CLAUDE_SKILL_DIR}/references/post_submission_release_ops.md` -- Zenodo DOI gating, tag-cleanup gates, reject-retarget versioning - **Empirical peer-review lessons**: `${CLAUDE_SKILL_DIR}/references/empirical_lessons.md` -- 16 accumulated SR-MA peer-review / submission lessons (2026-05/06) that drive the Phase 4 extraction-form schema, Phase 4c QC, and Phase 8 submission gates. Load before designing the extraction form and before submission. ### Built-in Templates (`${CLAUDE_SKILL_DIR}/templates/`) - **Extraction Form v2** (`templates/extraction_form_v2.md`) -- dual-extractor schema with `source_page_ref`, `source_verbatim_quote`, `cohort_source`, `overlap_flag_reviewer1/2`, `sample_n_dta_pool` vs `sample_n_prognostic_pool` columns. Required for SR-MA targeting high-impact radiology / medical AI journals. - **Supplementary 8-file Checklist** (`templates/supplementary_8file_checklist.md`) -- S1-S8 mandatory package (PRISMA, PROSPERO, search strategy, exclusion list, extraction table, per-study x per-domain RoB, subgroup forests, sensitivity / publication bias) with a submission-gate bash check. ### Built-in Scripts (`${CLAUDE_SKILL_DIR}/scripts/`) - **`screening_reconcile.py`** -- Phase 3f ID-set screening reconciliation. - **`check_pool_consistency.py`** -- pool-composition / PRISMA count consistency. - **`cohort_overlap_check.py`** -- shared-database cohort-overlap detection. - **`extract_assist.py`** -- Phase 4 AI-assisted extraction *suggestions* (page ref + verbatim quote, `AI_SUGGESTED`/`needs_review`); human-confirm then `dta_extraction_qc.py`. Challenge card: `scripts/extract_assist_challenge/`. - **`dta_extraction_qc.py`** -- 2x2 cell ↔ source sens/spec QC on the **confirmed** extraction CSV. --- ## Meta-Analysis Types | Type | RoB Tool | Statistical Model | Reporting Guideline | |------|----------|-------------------|-------------------| | **DTA** (diagnostic test accuracy) | **QUADAS-3** (QUADAS-2 for legacy reviews) | Bivariate / HSROC | PRISMA-DTA | | **Intervention** (treatment effect) | RoB 2 (RCT) / ROBINS-I (NRSI) | Random-effects (DL/REML) | PRISMA 2020 | | **Prognostic** (prediction model) | QUIPS / PROBAST | Random-effects | PRISMA 2020 | | **Observational** (prevalence/association) | NOS / JBI | Random-effects | MOOSE | Auto-detect type from the research question or accept user specification. --- ## Workflow Phases ### Phase 1: Protocol Development **Goal**: Produce a PROSPERO-ready protocol document. 1. **Structure the research question**: - DTA: PIRD (Population, Index test, Reference standard, Diagnosis) - Intervention: PICO (Population, Intervention, Comparator, Outcome) 2. **DTA only — do QUADAS-3 phases 1 and 2 now, not at risk-of-bias time**: QUADAS-3's first two phases are **review-level and belong in the protocol**: phase 1 states the **synthesis question(s)** (population, index test(s), target condition — a review may have more than one), and phase 2 defines the **ideal test accuracy trial** for each: objective, participants, index test(s), definition of the target condition, analysis. Every later risk-of-bias and applicability judgement is made against that trial. Write the review-specific guidance for answering each signalling question here too, with clinical **and** methodological input, and publish it as a web appendix. Defining the ideal trial after seeing the studies is not an assessment — it is a judgement fitted to the results. See `references/checklists/QUADAS3.md`. 3. **Define eligibility criteria**: - Study design (cross-sectional DTA, cohort, RCT, etc.) - Population characteristics - Index test / intervention specifics - Comparator / reference standard - Outcome measures (Se/Sp for DTA; effect size for intervention) - Exclusion criteria with justification 4. **Plan the search**: - Minimum 3 databases: PubMed, Embase, and Cochrane CENTRAL (add Scopus, Web of Science as needed) - Draft Boolean search strategy using PIRD/PICO components - Grey literature plan (conference abstracts, trial registries) - Language restrictions (state explicitly) - Date range with justification 5. **Plan RoB assessment**: - Select tool based on type (see table above) - State number of independent assessors (minimum 2) - Plan for disagreement resolution (consensus, third reviewer) 6. **Plan synthesis**: - DTA: bivariate random-effects model (Reitsma) or HSROC (Rutter & Gatsonis) - Intervention: random-effects (DerSimonian-Laird or REML) - Heterogeneity assessment plan - Subgroup / sensitivity analysis plan - Publication bias assessment plan 7. **Generate PROSPERO registration document**: - Read `${CLAUDE_SKILL_DIR}/references/PROSPERO_template.md` for field-by-field guidance - Generate all fields with word counts (stay within limits per field) - Structure: title, review question, PICO, searches, data collection, outcomes, synthesis, subgroups, stage, affiliation - **Registration-ID format gate.** A PROSPERO ID is `CRD42` + 9 digits (14 characters total), e.g. `CRD42024500001`. Validate any ID that appears in the manuscript or registration doc with `grep -oE 'CRD42[0-9]+'` and assert a 14-character length / `^CRD42\d{9}$` — a 15-character ID (a stray digit) is a transcription error a reviewer will check against the live record. - **Review-type selection.** Pick the *least-wrong* portal review type for the actual design and state any portal constraint in the protocol. A descriptive single-arm proportion synthesis is not an "Intervention review"; choosing "Intervention review" only to satisfy a portal field contradicts a later GRADE / effect-certainty statement. Whatever certainty language the protocol commits to (GRADE vs "evidence statements only") must match the manuscript verbatim — a guideline-style "we recommend" is not licensed by a descriptive review type. - For mixed designs (comparative + single-arm): explicitly address comparator for both arms - For RoB: map tool to study design (NOS for comparative, JBI for case series → select "Other" in form) - Output: Markdown + DOCX (via pandoc) for copy-paste into PROSPERO web form - Append Common Pitfalls Checklist (HTML entities, word limits, stage constraint) - Save to project `7_Submission/` or equivalent directory ### Phase 2: Search Strategy **Goal**: Develop and validate reproducible search strategies. 1. **Build search blocks** from PIRD/PICO: - Population block (MeSH + free text) - Index test / Intervention block - Comparator / Reference standard block (optional) - Study design filter (if applicable) 2. **Combine with Boolean operators**: - Within blocks: OR - Between blocks: AND 3. **Execute search per database** using `/search-lit`: - PubMed: MeSH + free text - Embase: Emtree + free text - Additional databases as specified in protocol 4. **Report search per PRISMA-S** (Rethlefsen et al. 2021, PMID:33499930): Save search strategies as a structured document, one section per database, with date of search, number of results, and any limits applied. 5. **Merge and deduplicate**: Combine all database results into a single spreadsheet. Deduplicate by DOI first, then PMID. Save raw counts for PRISMA flow. ### Phase 3: Screening & Selection **Goal**: Systematic title/abstract and full-text screening with two independent reviewers. **3a. Round 1 — initial title/abstract screening (single reviewer).** Define the exclusion codes from the protocol (E1=Not target population, E2=Not intervention, E3=Ineligible type, E4=Non-human, E5=Duplicate). Mark every record INCLUDE / EXCLUDE / MAYBE with a reason code → `round1_{date}.tsv`. **3b. Round 2 — dual independent title/abstract screening.** A second independent reviewer (or AI as a *documented* second-pass tool with human verification) re-screens all R1 records. Compute Cohen's κ and report it in Methods. `round2_tag` = INCLUDE / EXCLUDE / MAYBE, where MAYBE means disagreement **or** either reviewer flagged uncertainty → `round2_tag`, `round2_reason` columns. **3c. Round 3 — adjudication of disagreements (first reviewer).** Build the R3 sheet with all MAYBE records first, then INCLUDE records for a brief confirmation pass. The first reviewer independently adjudicates each row (`round3_decision`, plus `round3_reason` only when overturning R2). Optional AI-assisted pre-screening can compress the effort — but **AI suggestions are not decisions**: the reviewer independently confirms or overturns every one. Template, sort priority, and the required Methods boilerplate are in the reference file. **3d. Round 4 — full-text screening.** Retrieve full texts for `round3_decision = INCLUDE` (use `/fulltext-retrieval`), apply the full-text exclusion codes (F1=No extractable outcome, F2=No comparative data, F3=Cannot separate target population, F4=Inadequate sample/follow-up, F5=Full-text unavailable), with two independent reviewers, Cohen's κ, and consensus or a third reviewer for disagreements. Flag comparative studies for priority extraction. **3e. PRISMA flow.** Track counts at every stage (R1 → R2 → R3 → R4 → final included); generate the diagram with `/make-figures` once the numbers are final. **3f. Post-consensus count reconciliation gate (MANDATORY before Phase 5 write-up).** Reconcile the counts from the **raw ID sets, never from prose summaries**, and record the canonical totals in one source-of-truth file: ```bash python "${CLAUDE_SKILL_DIR}/scripts/screening_reconcile.py" \ --screening 2_Screening/fulltext_screening.tsv \ --consensus 2_Screening/consensus_decisions.tsv \ --table1 6_Tables/table1_studies.csv \ --output 2_Screening/screening_consensus.json ``` Downstream stages consume `screening_consensus.json` for counts and ID sets; the Markdown consensus document remains the human explanation. Three hard rules: 1. **List the narrative-only IDs explicitly.** The highest-yield red flag is a numeric claim ("10 narrative-only studies") that does not match the enumerable set `(A ∪ C) \ B \ T`. 2. **No "N → M" transition without ID receipts.** "k rose from 30 to 32 after FLAG consensus" must cite the added/removed IDs. A transition claim with no enumerable ID set is a **P0** and blocks the Phase 5 hand-off. 3. **`STAGE_TRANSFER_LOSS` is a P0.** Exit 1 when a record is included at screening but **absent from the consensus artifact altogether** — no adjudication was ever recorded. An exclusion is a decision; silence is a gap. Never let it settle into narrative-only (why: reference file). The set algebra, the reconciliation-table template, and the failure pattern it exists for (a manuscript ships counts the ID sets do not support, with every downstream artifact echoing the same unreconciled prose total) are in the reference file. **3f.5 Pool composition lock (MANDATORY at adjudication freeze).** Once 3f passes, freeze the pool into a single source-of-truth YAML that every downstream artifact can be checked against: ```bash cp "${CLAUDE_SKILL_DIR}/templates/FINAL_POOL_LOCK.yaml.template" 2_Data/FINAL_POOL_LOCK.yaml # fill counts + UID lists from 3f, compute the SHA-256 over the sorted UID list, # and COMMIT THE LOCK before any Phase 4 extraction ``` - **Never re-derive `k included` from the extraction TSV at manuscript build time** — always reference `final_pool_n` from the lock. - **Aggregate patient/lesion totals are locked too**, not just study counts. Distinguish **arm-separable** from **both-arm** rows: a study contributing one arm must not have its full-cohort count folded into a pooled total. A hand-carried headline total that does not re-derive from the locked per-study values is a **P0**. - A late post-freeze change to the pool is a **formal PROSPERO amendment**: file it, re-freeze as `FINAL_POOL_LOCK_v2.yaml`, and propagate to every artifact. **Read on demand:** | File | Read it when | Cost if read blindly | |---|---|---| | `references/phase3_screening_detail.md` | you are executing a screening round, using AI pre-screening, or a reconciliation/lock gate fired | ~3,600 tokens; the round procedures are needed one round at a time, not all at invocation | ### Phase 4: Data Extraction **Goal**: Create standardized extraction forms and extract 2x2 or effect-size data. **4.0 Entry gate (MANDATORY) — pool composition lock ↔ adjudication TSV.** Before any extraction work begins, confirm the round-3 adjudication TSV and `FINAL_POOL_LOCK.yaml` (Phase 3f.5) agree on which UIDs are included: ```bash python "${CLAUDE_SKILL_DIR}/scripts/check_pool_consistency.py" \ --lock 2_Data/FINAL_POOL_LOCK.yaml \ --adjudication-tsv 2_Screening/round3_adjudication.tsv \ --decision-col round3_decision --uid-col uid \ --include-labels "INCLUDE,INCLUDE_MIXED" \ --out qc/pool_consistency.json ``` **The gate fails closed: any UID disagreement blocks extraction.** Resolve by re-freezing the lock with the corrected UID set (and propagating downstream) or by correcting a mis-labelled TSV row. Do NOT proceed with a mismatch — the extraction matrix will not align with the locked pool, and the drift surfaces as a fabrication-grade red flag at peer review. > **Failure-mode cross-ref** → `references/data_integrity_checklist.md` DI-1~DI-5 are mandatory > during extraction (2x2 arm-swap, KM audit trail, methodology mismatch, PRISMA 5-way drift, > single-source k). **Extraction form.** For an SR-MA targeting high-impact radiology / medical AI journals use `${CLAUDE_SKILL_DIR}/templates/extraction_form_v2.md` — its dual-extractor, source-page-reference, and verbatim-quote columns are what close the 2x2 cell-swap and cohort-overlap blind spots. The DTA and intervention field lists are in the reference file. **AI-drafted starting document — treat as hallucination-suspect.** If a mentor or collaborator shared an AI-drafted study list, 2x2 set, or effect estimates (*even* flagged "for reference only"): save it with a `_DO_NOT_USE_VERBATIM` suffix and re-verify **every** N, denominator, event count, OR/CI, and author/year against the source PDF. Trust hierarchy: **source PDF + own analysis stdout > the mentor's direct text > the attached AI draft** — never promote a draft up that ladder. Procedure and precedent: reference file. **4b. Special cases (KM reconstruction, composite exposure).** When studies report outcomes only as Kaplan-Meier curves, or the intervention is a composite of techniques, load `${CLAUDE_SKILL_DIR}/references/phase4_km_composite.md` for the WebPlotDigitizer → `IPDfromKM` procedure (cite Guyot et al. 2012, doi:10.1186/1471-2288-12-9) and the 4-path composite-exposure decision tree. Pre-specify a sensitivity analysis excluding composite-exposure studies. **Cross-verification (≥2 independent reviewers).** Report inter-reviewer agreement (% or Cohen's κ) at title/abstract and full-text stages. Verify denominator consistency — **the denominator may differ across outcomes within one study**, so for each outcome back-calculate `event ÷ denominator` and confirm it reproduces the paper's reported percentage. Distinguish KM-curve estimates from raw event counts and record the data source (Table / KM / text). Log every consensus decision in `{project}/consensus_log.md`, then **lock the dataset**; later changes need a dated justification. **4c. Extraction QC & cohort overlap.** After dual-extractor consensus, run both before locking: ```bash # 2x2 cell integrity: validates TP/FN/TN/FP against source-reported sens/spec (catches arm-swap) python3 "${CLAUDE_SKILL_DIR}/scripts/dta_extraction_qc.py" \ --input 2_Extraction/extraction.csv --tolerance 0.02 \ --out 2_Extraction/qc/dta_extraction_qc.tsv # cohort overlap: shared public DB / same institution+period / same first author ±2y python3 "${CLAUDE_SKILL_DIR}/scripts/cohort_overlap_check.py" \ --input 2_Extraction/studies.csv --enrich \ --out 2_Extraction/qc/cohort_overlap.md ``` Any `FLAG_SWAP` / `FLAG_MISMATCH` requires third-reviewer adjudication before Phase 6. **A confirmed flag is not resolved until the extraction form itself is edited** — a flag corrected only in a review note silently re-enters synthesis, so re-run the QC and confirm zero open flags before locking. HIGH-confidence overlap pairs require a Limitations acknowledgment plus a sensitivity analysis excluding one of the pair. Cross-links: `/peer-review` Phase 2A P1 + P2. **Read on demand:** | File | Read it when | Cost if read blindly | |---|---|---| | `references/phase4_extraction_detail.md` | building the extraction form, an AI draft was shared, you want the optional `extract_assist.py` scaffolding, or a QC flag fired | ~4,700 tokens; a clean dual-extraction with no AI draft needs none of it | | `references/phase4_km_composite.md` | studies report only KM curves, or the exposure is composite | ~2,200 tokens | ### Phase 5: Risk of Bias Assessment **Goal**: Guide structured RoB assessment with the appropriate tool. **DTA**: this phase runs QUADAS-3 **phases 3–6** (flow diagram, identify the estimates to assess, assess, overall judgement). Phases 1–2 — the synthesis question and the ideal test accuracy trial — were written in Phase 1 above. If they were not, stop and write them before judging anything; they are the comparator every judgement is made against. Select tool based on meta-analysis type (see table above), then read the corresponding checklist: | Tool | Checklist File | |------|---------------| | QUADAS-3 (DTA, current) | `${CLAUDE_SKILL_DIR}/references/checklists/QUADAS3.md` | | QUADAS-2 (DTA, legacy) | `${CLAUDE_SKILL_DIR}/references/checklists/QUADAS2.md` | | RoB 2 (RCT) | `${CLAUDE_SKILL_DIR}/references/checklists/RoB2.md` | | ROBINS-I (NRSI) | `${CLAUDE_SKILL_DIR}/references/checklists/ROBINS_I.md` | | PROBAST (Prediction) | `${CLAUDE_SKILL_DIR}/references/checklists/PROBAST.md` | | NOS (Observational) | `${CLAUDE_SKILL_DIR}/references/checklists/NOS.md` | | JBI (Case Series) | `${CLAUDE_SKILL_DIR}/references/checklists/JBI_Case_Series.md` | For AI/ML prediction models, also apply PROBAST+AI extensions. **Output**: Summary table + traffic light plot (use `/make-figures`). ### Phase 6: Statistical Synthesis **Goal**: Execute meta-analysis and generate publication-ready outputs. > **Failure-mode cross-ref** → `references/data_integrity_checklist.md` DI-6/DI-7/DI-9 are the consistency gate (CSV ↔ script ↔ prose; single-source k; 3-way numeric reconciliation before Stage 4). **IMPORTANT**: Always use R for meta-analysis (packages: `meta`, `metafor`, `mada`). See `${CLAUDE_SKILL_DIR}/references/r_templates.md` for full code templates. | Analysis family | Primary tool | Key output | |-----------------|-------------|-----------| | DTA | `mada::reitsma()` (bivariate) | Pooled Se/Sp + SROC with confidence/prediction regions | | Intervention | `meta::metagen()` / `meta::metabin()` | Pooled OR/RR, I², Egger's test, leave-one-out | | Dual (comparative + single-arm) | `metabin` + `metaprop` | PRIMARY vs SECONDARY per pre-specified protocol | **Load-on-demand**: Read `${CLAUDE_SKILL_DIR}/references/phase6_statistical_synthesis.md` for the full R code templates, the dual-approach decision table (comparative vs single-arm), practical cautions (method.tau, HK CI, zero-cell correction), publication-bias test power, sensitivity-analysis menu, and error-handling rules. **Three checks before the pool is written up** — each is a Methods sentence, not only a setting. R and detail in the same reference: 1. **Is the event rare?** A pooled event rate < 1%, or any zero-event arm, moves the analysis off the inverse-variance default onto Peto / Mantel-Haenszel without a zero-cell correction / GLMM. Inverse-variance methods including DerSimonian-Laird are to be *avoided* for rare events, and so are 0.5 continuity corrections with them. 2. **Why this model?** Fixed vs random is a judgment about whether one common true effect exists — never derived from Cochran's Q or I². "A random-effects model was used because I² was 65%" is a reviewer catch, not a rationale. 3. **Does one study contribute several correlated effect sizes?** Multiple outcomes, readers, thresholds, or time points from the same participants need one pre-specified estimate per study, a multivariate model, or robust variance estimation — not independent pooling. ### Phase 6b: Post-Analysis Source Fidelity Audit (MANDATORY) **Goal**: Catch numerical hallucinations that survived the forward pipeline (CSV → .R → manuscript). **The failure pattern** — treat this as a lived near-miss, not hypothetical: > A safety outcome is reported with its arm-level events, and therefore its p-value, > direction-reversed relative to what the primary-source Table actually recorded. > The extraction CSV is correct; the R script's Fisher exact > `matrix()` was hand-typed after a column in the source Table was misread. Internal > consistency checks passed because every downstream artifact (Abstract, Discussion, > Table, forest caption) echoed the same wrong number. The reversal was caught only on > a second-pass audit with random extraction sampling against the primary paper. **Non-negotiable rules:** 1. **No hand-typed numerical matrices when a CSV exists.** - Use `read.csv(...)` + subset / filter. Never copy a 2x2 table from a paper's Table into `matrix(c(...), ...)` by eye. - If hand entry is truly unavoidable (e.g., text-only extraction), the `matrix`, `c()`, or `data.frame` line MUST carry a comment citing the exact CSV row + column OR the exact primary-source Table/Page coordinate. Example: ```r # source: data_extraction_final.csv row <N> (<first-author> <year>), cols <event_arm1>=0, <event_arm2>=1 # verified against primary source Table <X>, page <P> fisher.test(matrix(c(0, 45, 1, 55), nrow = 2, byrow = FALSE)) ``` 2. **Comparative-arm subsets are a separate consensus-log row.** - When one study's arm-specific values (e.g., one arm of a multi-arm study) are used in a comparative analysis while the full cohort of that study appears elsewhere, `extraction_consensus_log.md` must carry an explicit row for the arm-specific values. Pooled totals and arm-specific values MUST NOT share a row. 3. **Random 3-claim back-check before closing Phase 6.** - After the forest/funnel/subgroup outputs stabilize, randomly sample 3 numerical claims from the Results section of the draft manuscript and trace each back to (a) the R output log and (b) the original paper's Table/Figure. - Record the back-check as a small table in `peer_review_<vN>_internal.md`: | Claim (manuscript line) | R output file:line | Primary source (paper, Table/Fig, page) | Match? | |---|---|---|---| - A single mismatch is a P0 blocker — do not advance to Phase 7 until resolved. 4. **Revision-introduced numbers must be tagged.** - Any new number added after v1 — including numbers produced by a new comparative / subgroup / sensitivity script — MUST be wrapped inline as `[VERIFY-CSV]` in the manuscript until the Phase 2.5a audit in `/self-review` clears it. 5. **Sensitivity analyses must be recomputed on the modified data, not copied.** - When you add a sensitivity / leave-one-out / erosion / alternative-model analysis, every reported effect size (Cohen's dz/f, AUC, OR, HR, β, sens/spec, ICC) MUST be re-derived from the modified dataset. If a sensitivity-table effect size is **identical to the primary analysis to two decimals across ≥4 values**, the recomputation almost certainly did not run and the primary values were transcribed — re-run the script on the modified data. - The underlying means/SDs/counts will change even when the effect size looks similar; if the effect sizes are byte-identical while the inputs differ, that is the tell. Probability of ≥4 independent values coinciding to 2 decimals by chance is ≈ (0.01)^4 — essentially zero. - The failure it catches: a sensitivity analysis reports a block of effect-size values byte-identical to the primary tables while the underlying means/SDs differ — the sensitivity analysis was never actually recomputed. Internal consistency cannot see it. 6. **A "fixed" / "resolved" audit note requires re-run evidence, not a claim.** - When a prior audit note records a number as `fixed`, `resolved`, or `corrected`, that status is only valid if it carries the re-run evidence: a timestamp and the relevant stdout / output-file line showing the corrected value, or the commit that changed it. A bare "fixed in v10" with no re-run artifact does NOT clear the finding — re-run the script and attach the output. - The forward pipeline can echo a stale value through every artifact while an audit note claims it was fixed (e.g., a major-comparison N still reading the old total after a "fixed" note). The outcome-denominator cross-check (`/self-review` Phase 2.5b, the cohort-arithmetic / pool-lock assertions) must pass against the *current* outputs before any "fixed" status is accepted. **When this phase triggers:** every time Phase 6 outputs change (first draft, revision, reviewer- requested re-analysis). Not optional on "minor" re-runs — the precedent reversal above occurred inside a "minor" revision-era re-analysis. ### Phase 7: GRADE / Certainty of Evidence **Goal**: Assess certainty of the body of evidence. For DTA meta-analysis, apply GRADE-DTA framework: 1. Risk of bias (from QUADAS-3, or QUADAS-2 for a legacy review) 2. Indirectness (applicability concerns) 3. Inconsistency (heterogeneity) 4. Imprecision (wide CIs, small sample) 5. Publication bias For intervention meta-analysis, apply standard GRADE. **Certainty is assessed per outcome, not once for the review.** The five domains resolve differently for each outcome — an outcome pooled from 12 studies with narrow CIs and one pooled from 3 with a wide CI do not share a rating, and a single review-level "moderate certainty" sentence tells a reader nothing about the outcome they came for. Rate every outcome carried into the Summary of Findings table, and state the reason for each downgrade (which domain, why) rather than the resulting label alone. Output: Summary of Findings table — one row per outcome, carrying the pooled estimate with its precision alongside the certainty rating (high / moderate / low / very low). ### Phase 8: Reporting & Manuscript **Goal**: Generate PRISMA-compliant manuscript sections. > **Failure-mode cross-ref** → `references/submission_package_drift.md` — apply the `_build.sh` pattern + `DO_NOT_EDIT_HERE` gate when staging multi-journal submission folders. 1. **Check reporting compliance**: Use `/check-reporting` with PRISMA-DTA or PRISMA 2020, then run it a second time over the **abstract** with PRISMA 2020 for Abstracts — 12 items, its own denominator. One run does not cover both. 2. **Write manuscript**: Use `/write-paper` with meta-analysis type selected 3. **Figures**: Use `/make-figures` for: - PRISMA flow diagram - Forest plots (paired for DTA) - SROC curve (DTA) - Funnel plot - RoB summary (traffic light plot) 4. **Tables**: - Characteristics of included studies - 2x2 data per study (DTA) - RoB assessment results - Summary of findings / GRADE table (one row per outcome — Phase 7) 5. **The items published radiology SR/MA most often drop.** Park 2022 (Korean J Radiol; PMID:35213097) scored 24 SR/MAs against PRISMA 2020 and found 24 of 42 items reported by fewer than 80%. The checklist itself lives in `/check-reporting`; what follows is where drafts actually fail, so check these by hand before the compliance run rather than after it: | PRISMA item | What is missing | Observed | |---|---|---| | **20a** | For **each** synthesis, a brief summary of the contributing studies' characteristics and risk of bias — not one global paragraph covering all pools | 0/24 | | **27** | Data availability: which of the extraction forms, extracted data, analysis dataset, and analytic code are public, and where | 0/24 | | **24a–c** | Registration number, where the protocol can be read, and any amendment — an explicit "not registered" satisfies 24a | 0/24 | | **22 / 15** | Certainty of evidence per outcome, and the method used to assess it | 9% | | **13f / 20d** | Sensitivity analysis: method and result | 28% | | **18** | Risk of bias **per study**, shown study-by-study rather than as a pooled proportion | 32% | | **13d** | Rationale for the synthesis model (see Phase 6 check 2) | 35% | | **16b** | Studies that look eligible but were excluded, cited individually with the reason | 25% | | Abstract **#3, #12** | Eligibility criteria and registration inside the structured abstract | 0/24 each | The abstract items are the cheapest of these and the most reliably forgotten. PRISMA 2020 devotes a **separate 12-item instrument** to the abstract — item 2 of the main checklist does nothing but defer to it — so a manuscript can satisfy all 42 main-text items and still fail most of the twelve. `/check-reporting` carries it as `PRISMA_2020_Abstracts.md`; run it as its own pass and report its score separately, because folding twelve items into a 42-item total is how they stay invisible. 6. **Data availability statement**: name what is being shared (extraction template, locked dataset, analysis code, RoB judgments) and where — repository, DOI, or supplementary file. "Available from the corresponding author on reasonable request" satisfies few journals now and no longer satisfies item 27. If a Zenodo DOI is minted post-acceptance, `references/post_submission_release_ops.md` covers propagating it back into this statement. 7. **Supplementary & analysis-code pre-submission gate** (run before Phase 9 circulation and before portal upload). Presence of the 8-file package (Empirical Lesson 5) is necessary but not sufficient — each item must also be reviewer-ready: - **De-scaffold**: strip internal-QC / tool artifacts before bundling — raw `/check-reporting` output ("Assessed by: <tool>", JSON blocks, "READY FOR SUBMISSION" verdicts, action-item lists), search-development planning docs (decision logs, expected-yield estimates, `[Check on execution]` placeholders, version-history dev notes), and stale version stamps. Ship a clean PRISMA 2020 checklist (27-item / 42-subitem table only) and an executed-method search-strategy doc, not the working drafts. - **Blind**: supplementary goes to reviewers — remove author names/initials and sibling-project cross-references ("Designed by: <name>", "identical to a sibling review"). Same standard as the blinded manuscript. - **Cross-consistency with the manuscript**: every supplementary number must match the main text — PRISMA counts, pool k/N, the Cochrane/CENTRAL search description, RoB counts. A supplement that says "Cochrane — NOT SEARCHED" while Methods report a confirmatory CENTRAL search is a contradiction reviewers catch. - **Submitted analysis code must reproduce and be self-contained**: run it from a clean copy of the bundle. It must (a) read the bundled locked dataset (not an out-of-bundle path) and write to the working directory, and (b) regenerate every pool reported in the results table. A hard-coded study-id subset that drifts from the manuscript (e.g., a pool computed over k=7 while the manuscript reports k=9) is a P0 — fix and re-run; never ship stale code or stale figures derived from it. - **Run a supplementary-only review pass** — the manuscript self-review/panel does not see the supplement; mirror `/self-review` Phase 2.5c–2.5d (reference + cross-reference QC) over the supplementary files. --- ### Phase 9: Co-author Circulation **Goal**: Standardized pre-submission circulation of the manuscript to co-authors and senior methodologist / reviewer, with a bounded review window and a controlled attachment scope. **Trigger**: Phase 8 is complete, and the draft has cleared Phase 6b source-fidelity audit. **Summary**: Reply to the prior-version email thread to preserve `In-Reply-To` continuity (v1 → v2 → v3 tracked in one place). Attach the manuscript body with figures inline and, for v≥2, a change summary — exclude graphical abstract, cover letter, COI forms, and supplementary until the target journal is confirmed. TO = corresponding author + one senior methodologist; CC = remaining co-authors. Set a 7-day deadline (5 business days + weekend). Ask the corresponding author for target-journal preference, reviewer candidates, and cover-letter framing. **Load-on-demand procedural detail** (thread continuity, attachment scope rationale, size-to-method table, journal-undetermined framing, response-tracking log): `${CLAUDE_SKILL_DIR}/references/phase9_circulation.md`. > **Failure-mode cross-ref** → `references/review_orchestration.md` RO-1~RO-5 (dual-rating completeness, defensive-tone bias audit, response-matrix numeric tracking, 2nd-reviewer availability blocking). --- ### Phase 10: Self-Audit Recovery (v{N} → v{N+1} sprint) **Goal**: When an audit uncovers a structural data or protocol-application error, withdraw the current version, rebuild, and re-circulate with a transparent audit trail. Catching the error yourself before a journal reviewer does is the principal trust-building move in this phase. **Trigger conditions (any one):** | # | Trigger | Source | |---|---------|--------| | T1 | Extraction CSV ↔ primary source disagreement for a cell feeding a pooled/subgroup estimate or reported proportion | Phase 6b audit | | T2 | Included/excluded study violates the pre-specified criteria on re-read | Protocol review | | T3 | Hand-typed numerical literal in the analysis script traces to a wrong value | Phase 6b audit | | T4 | PROSPERO protocol ↔ delivered analysis disagreement on outcome, subgroup, or eligibility | Protocol ↔ analysis diff | | T5 | Dual-reviewer consensus record ↔ locked dataset disagreement on inclusion | Consensus log diff | **Non-negotiable rule**: if the trigger fires after Phase 9 circulation but before journal submission, withdraw the current version within 24 hours. Reviewer discovery is a strictly worse failure mode than self-withdrawal. **Sprint outline (12 steps)**: (10.1) audit log at `qc/audit_vN_to_vNplus1.md` → (10.2) CSV re-verification with `[VERIFY-CSV]` tagging → (10.3) fresh script re-run (fixed seed, logged) → (10.4) manuscript auto-sync (grep for v{N} residue) → (10.5) supplementary regeneration (consensus log, RoB, GRADE/SoF, PRISMA flow) → (10.6) figure regeneration via `/make-figures` → (10.7) change summary with delta table → (10.8) PROSPERO amendment (application correction, not criteria change) → (10.9) re-circulation in the Phase 9 thread with the "On re-review" framing → (10.10) anti-patterns to avoid (hide-and-submit, "minor revision" reframe, cover-letter-only disclosure) → (10.11) post- submission escalation path → (10.12) post-recovery loop (Phase 9 restart; tighten Phase 6b if a second sprint is needed). **Load-on-demand procedural detail** (exact audit-log fields, delta-table template, amendment language template, re-circulation paragraph template, anti-pattern rationale): `${CLAUDE_SKILL_DIR}/references/phase10_recovery.md`. > **Failure-mode cross-ref** → `references/post_submission_release_ops.md` Gate 4 covers reject/revise Zenodo versioning, tag-cleanup gate, and re-target workflow (avoid "new version" misuse on re-target). --- ## Failure Modes (prior MA projects, anonymized) Failure patterns observed across three prior MA projects (anonymized). Each topical reference extends the phase it cross-references above — consult alongside phase procedural docs, not in isolation. | Domain | Phase span | Load-on-demand reference | |---|---|---| | Data integrity (2x2 arm-swap, KM audit, methodology mismatch, PRISMA 5-way drift, single-source k) | Phase 3 → 6 | `references/data_integrity_checklist.md` (DI-1~DI-9) | | Review orchestration (2nd-reviewer blocking, dual-rating completeness, defensive-tone audit, response-matrix tracking) | Phase 9 circulation (extends `phase9_circulation.md`) | `references/review_orchestration.md` (RO-1~RO-5) | | Submission package drift (multi-journal folder hygiene, `DO_NOT_EDIT_HERE` gate, build artifact vs master) | Phase 8 → submission | `references/submission_package_drift.md` | | Post-submission release ops (Zenodo DOI timing, tag-cleanup gate, reject-retarget versioning) | Submission → Phase 10 | `references/post_submission_release_ops.md` | ### Automation hooks (invoke at the phase listed) | When | Script | Gate | |---|---|---| | Phase 3f reconciliation (before Phase 5 write-up) | `python3 ${CLAUDE_SKILL_DIR}/scripts/check_exclusion_code_validity.py --protocol 0_Protocol/protocol.md --screening 2_Screening/*.tsv --strict` | validates each applied exclusion code against the *registered* eligibility criteria: `CODE_CONTRADICTS_ELIGIBILITY` (a code excludes a design the protocol includes — the bulk study-loss defect no arithmetic/inter-rater gate can see), `CODE_NOT_REGISTERED` (off-protocol code), `CODE_RENUMBERED` (same code, two meanings). Challenge card: `scripts/check_exclusion_code_validity_challenge/`. | | Phase 4 kickoff (before first extraction row) | `python3 ${CLAUDE_SKILL_DIR}/../../scripts/extraction_consensus_log_init.py --output 2_Data/extraction_consensus_log.md` | DI-1: creates standalone consensus log so comparative arm-specific rows are never folded into R-script comments. | | Phase 3f reconciliation + every revision touching PRISMA numbers | `python3 ${CLAUDE_SKILL_DIR}/../../scripts/prisma_5way_consistency.py --ssot prisma.yaml` | DI-6: 5-surface drift check (abstract / main text / flow figure / supplement / CSV) against YAML SSOT. Non-zero exit blocks Phase 5 writeup. | | Phase 8 pre-submission + every journal retarget | `bash ${CLAUDE_SKILL_DIR}/../../scripts/tag_cleanup_gate.sh` | DI-8: fails if `VERIFY-CSV`/`TODO`/`FIXME`/`XXX` survive in `7_Manuscript`, `supplement`, `SUBMISSION`, etc. | | Phase 8 on first build per journal (`--record`), then before every re-submission (`--verify`) | `python3 ${CLAUDE_SKILL_DIR}/../../scripts/verify_package_integrity.py --record --journal <name>` then `--verify --journal <name>` | SPD: checksum-based drift detection between master manuscript and built `SUBMISSION/{journal}/` folder. Journal-editable files (cover letter, response, MANIFEST, `DO_NOT_EDIT_HERE.md`) are auto-excluded. | All four scripts are repo-shipped as of 2026-04 (FOLLOWUPS P10). Non-zero exit = gate failure; resolve before proceeding to the next phase. --- ## Empirical Lessons (peer-review cycles) Sixteen accumulated SR-MA peer-review / submission lessons (2026-05 and 2026-06) — the drivers behind the Phase 4 extraction-form schema, the Phase 4c QC scripts, and the Phase 8 submission gates. To keep this entry point lean they live load-on-demand in `${CLAUDE_SKILL_DIR}/references/empirical_lessons.md`. **Load that file when designing the extraction form (before Phase 4) and before submission (Phase 8)** — it covers dual-extractor 2x2 integrity, cohort-overlap clustering, small-k subgroup caution, the supplementary 8-file bar, PROSPERO ID format, AI-disclosure presence, recompute-don't-copy sensitivity analyses, outcome harmonization, heterogeneous-RoB κ, survival-specific concerns, supplement blinding / de-scaffolding, self-contained reproducible analysis scripts, sidecar re-sync, methodological + software citations, wide-table PDF rendering, and submission-portal journal-identity checks. --- ## DTA-Specific Pitfalls (Always Check) | Pitfall | Problem | Solution | |---------|---------|----------| | Separate pooling of Se/Sp | Ignores correlation | Use bivariate/HSROC model | | Ignoring threshold effect | False heterogeneity | Check Spearman correlation, SROC plot | | Standard funnel plot for DTA | Inappropriate | Use Deeks' funnel plot | | I-squared only for heterogeneity | Doesn't capture threshold effect | Use prediction region on SROC | | Missing GRADE | Common omission in DTA MA | Apply GRADE-DTA. If <4 studies, assess each domain narratively and state the limitation explicitly | | Partial verification bias | Inflates sensitivity | QUADAS-3 **3.2** (target condition assessed in all participants). QUADAS-3 has no Flow & Timing domain — that was QUADAS-2 | | Differential verification bias | Distorts both Se and Sp | QUADAS-3 **3.3** (target condition assessed the same way in all participants) | | Unevaluable results excluded | Biases accuracy estimates | Report intent-to-diagnose analysis | --- ## Small Study Considerations When the number of included studies is small (< 10): - Bivariate/HSROC model may not converge -- consider univariate random-effects as fallback - Publication bias tests are underpowered -- state this limitation - Subgroup/meta-regression analysis not recommended - Wide prediction regions expected -- emphasize uncertainty in conclusions - Consider narrative synthesis as alternative/complement --- ## Skill Interactions | When | Call | Purpose | |------|------|---------| | Need literature search | `/search-lit` | PubMed/Semantic Scholar search with verified citations | | Need statistical code | `/analyze-stats` | Execute R/Python analysis scripts | | Need figures | `/make-figures` | PRISMA flow, forest plots, SROC, funnel plots | | Need reporting check | `/check-reporting` | PRISMA-DTA / PRISMA 2020 compliance (includes Step 4c registration / amendment timing) | | Need manuscript writing | `/write-paper` | Full IMRAD manuscript generation | | Need self-review | `/self-review` | Pre-submission quality check | | Self-audit recovery entrypoint (Phase 10) | `/write-paper` Step 7.4a | Recovery branch for polish pipelines that surface structural audit failures | | `/sync-submission` SR-MA gate | `/sync-submission` | Before submission, verify supplementary package matches all 8 files in `templates/supplementary_8file_checklist.md` (PRISMA, PROSPERO, search strategy, exclusion list, extraction table, per-study x per-domain RoB, subgroup forests, sensitivity / publication bias). AI Disclosure presence check (cross-link `/peer-review` Phase 2A P8). Cite-list duplicate check via `/verify-refs` Gate 5 (duplicate PMID/DOI). | --- ## Error Handling - If study type is ambiguous (DTA vs intervention), ask user to clarify before proceeding. - If fewer than 4 studies for DTA, warn that bivariate model may not converge. - If data extraction is incomplete (missing 2x2 cells), suggest contacting authors or sensitivity analysis with imputed values. - If PROSPERO ID is missing, flag as a limitation but continue. - Always remind user: this is a methodological support tool; final decisions rest with the research team and ideally include a biostatistician/methodologist. ## Anti-Hallucination - **Never fabricate variable names, dataset column names, or variable codings.** If a variable mapping is uncertain, output `[VERIFY: variable_name]` and ask the user to confirm against the data dictionary. - **Never fabricate statistical results** — no invented p-values, effect sizes, confidence intervals, or sample sizes. All numbers must come from executed code output. - **Never generate references from memory.** Use `/search-lit` for all citations. - If a function, package, or API does not exist or you are unsure, say so explicitly rather than guessing. -
skill.yml 2.6 KB
schema_version: 2 name: meta-analysis layer: B owner_domain: meta_analysis_integrity maturity: official when_to_use: - Running an end-to-end systematic review or meta-analysis (PROSPERO → search → screen → extract → synthesis → PRISMA) - Diagnostic test accuracy meta-analysis (bivariate / HSROC) or intervention meta-analysis (random-effects) - Screening reconciliation across two reviewers with Cohen's kappa and adjudication round - Risk of bias batch (QUADAS-2 / ROBINS-I) tied to extraction CSV ID receipts - Phase 6b post-analysis source-fidelity audit when CSV ↔ R output ↔ manuscript drift suspected when_NOT_to_use: - Single-study analysis (use /analyze-stats) - Topic discovery before a protocol exists (use /ma-scout) - Reporting checklist generation only (use /check-reporting) - Manuscript prose drafting after synthesis (use /write-paper) inputs: - protocol/search_strategies.md - screening/*.tsv - extraction/*.csv outputs: - 2_Screening/screening_consensus.json - analysis/meta_analysis_outputs.json - manuscript/manuscript.md deterministic_scripts: - scripts/screening_reconcile.py - scripts/extract_assist.py # Phase 4 AI-assisted extraction suggestions (provenance + human gate); challenge card in scripts/extract_assist_challenge/ - scripts/check_exclusion_code_validity.py # Phase 3f exclusion-code vs registered-eligibility gate; challenge card in scripts/check_exclusion_code_validity_challenge/ side_effects: - writes_project_artifacts downstream_consumers: - write-paper - self-review - make-figures forbidden_actions: - report_counts_without_id_receipts - continue_after_p0_reconciliation_mismatch # v2.1 quality card purpose: "Run the SR/MA pipeline: PROSPERO registration, search, screening, extraction, risk of bias, synthesis (bivariate/HSROC or random-effects), and PRISMA reporting." safety_boundaries: - "Study counts are never reported without ID-level receipts; halts on a P0 reconciliation mismatch." - "Pool composition is locked to a single source of truth; downstream counts are re-derived, not copied." known_limitations: - "Synthesis validity depends on correct extraction; the skill enforces process, not clinical correctness." - "DTA pooling assumes adequate per-study 2x2 / threshold data." validation_commands: - "python3 scripts/screening_reconcile.py" - "python3 scripts/check_pool_consistency.py" - "bash scripts/extract_assist_challenge/verify.sh # deterministic, network-free" - "bash scripts/check_exclusion_code_validity_challenge/verify.sh # deterministic, network-free" evidence_surface: demo
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.