
Litreview
- 2 installs
- 23.8k repo stars
- Updated July 17, 2026
- alirezarezvani/claude-skills
Litreview is a Claude Code skill that searches academic papers via Consensus and synthesizes them into a formatted Word literature-orientation guide.
About
Litreview is an academic literature orientation skill that searches papers through the Consensus MCP and builds a strategic search plan using frameworks like PICO or SPIDER. A researcher uses it when starting a literature review on an unfamiliar topic to get an orientation guide rather than a finished review. It runs a grill-me intake, executes rate-limited searches, and synthesizes findings into a formatted Word document with strict source and counting discipline.
- Builds a PICO/SPIDER/Decomposition search plan then synthesizes to a .docx guide
- Grill-me intake plus a post-Phase-2 checkpoint before searches spend budget
- Configurable depth of 5, 10, or 20 queries controls coverage vs speed
Litreview by the numbers
- 2 all-time installs (skills.sh)
- Ranked #1,294 of 1,879 Documentation skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
litreview capabilities & compatibility
- Capabilities
- research · documentation
- Use cases
- research · documentation
- Pricing
- Free
What litreview says it does
Academic literature orientation skill that searches papers via Consensus, builds a strategic search plan using PICO (default) or SPIDER / Decomposition / hybrid as fallbacks
Requires a Consensus MCP connection, Node.js with `docx` package for document generation, and (in CLI) `bash_tool`.
npx skills add https://github.com/alirezarezvani/claude-skills --skill litreviewAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 23.8k |
| Last updated | July 17, 2026 |
| Repository | alirezarezvani/claude-skills ↗ |
What it does
Search academic papers via Consensus and synthesize them into a formatted Word literature-orientation guide.
Who is it for?
A researcher starting a literature review or paper on an unfamiliar topic who needs an orientation launching pad.
Skip if: A single one-off paper search for a quick list, which is just a plain Consensus search.
When should I use this skill?
The user says litreview on a topic, literature review on a topic, or help me research X.
What you get
A formatted Word orientation guide with a framework-based search plan and cited Consensus papers.
- formatted Word (.docx) literature-orientation guide
- framework-based search plan
By the numbers
- 3 grill-me forcing questions
- depth of 5, 10, or 20 queries
- Consensus rate limit 1 query/sec
Files
Litreview — Academic Literature Orientation
Portability: Requires a Consensus MCP connection, Node.js withdocxpackage for document generation, and (in CLI)bash_tool. Works in Claude Code CLI natively. In Claude.ai with Consensus MCP + Code Execution, the workflow is supported.
Produce a launching pad — not a finished literature review, but an orientation document that gives a researcher entering an unfamiliar field everything they need to start reading and searching with confidence. Think: what a generous colleague who knows the field would tell you over coffee.
Agent Integrity Rules (Research-Pack Convention)
Inherited from the research-pack convention; locked verbatim per PR #657's cross-skill consistency audit.
- Source discipline. Only cite Consensus-returned papers from THIS session. Training knowledge labeled
[Not from Consensus — model knowledge]and excluded from cited count. Sparse results stated explicitly, never silently filled. - Counting discipline. Three numbers tracked: searches executed / unique papers received (deduplicated) / papers cited. Every cited paper has a retrievable Consensus URL from this session. Use
scripts/citation_tracker.pyfor deterministic counts. - Tool constraints. Consensus per-query cap depends on plan tier. Detect at first search, report at checkpoint. Rate limit is 1 query/sec — sequential execution mandatory.
- Retry policy. On failure → wait 3s → retry once → log. After 3 consecutive failures: stop, alert user, share what was collected.
- Plan-tier detection. Parse first-search response for "Showing top 10" / "upgrade" → free tier (10/search). 20 returned → Pro (20/search). Calculate theoretical ceiling and surface at checkpoint so user can recalibrate.
See `references/search_budget_allocation.md` for the sequential-execution rationale + plan-tier signals.
Error Handling
| Failure | Behavior |
|---|---|
| Consensus rate-limit hit | Wait 3s, retry once, log outcome |
| Search returns 0 results | Note explicitly; "either niche terminology or genuine gap"; never silently fill |
| Plan-tier cap detected | Log tier; report at checkpoint; surface in audit |
| 3 consecutive failures | Stop searching, alert user, share what's collected, ask how to proceed |
| Sub-area returns thin results (<5 papers) | Flag in audit; suggest manual PubMed/Scholar supplementation |
| User wants to adjust sub-areas | Update table, re-confirm before searching |
| DOCX validation fails | Unpack XML, fix, repack |
Phase 0: Grill-Me Intake (3 forcing questions, one at a time)
Each question carries explicit "why I'm asking". Stop condition: max 3 before Phase 1.
Q1 (root) — Research question specificity
State the research question in 1–2 sentences. Specific is better — "How do LLMs perform on clinical reasoning tasks compared to physicians?" beats "AI in medicine". Vague questions produce vague reviews.
>
Why I'm asking: The reconnaissance search hinges on precise terminology. Vague questions produce thin recon results that don't yield a useful framework breakdown.
Refuse mush. Re-ask once with examples if user is too broad. If still vague, deliver with explicit "broad-scope orientation, not depth review" caveat.
Q2 (depends on Q1) — Framework hint
Framework — pick one or say "you pick":
>
1. PICO (Population / Intervention / Comparison / Outcome — most clinical questions)
2. SPIDER (Sample / Phenomenon / Design / Evaluation / Research-type — social/qualitative)
3. Decomposition (Problem / Solution / Evaluation / Limitations — technology-focused)
4. Hybrid (you pick which components from which framework)
5. You pick — analyze Q1 and recommend
>
Why I'm asking: PICO is the default for ~70% of clinical questions but maps poorly to qualitative work or technology evaluation. Picking upfront saves the recon search from suggesting a misaligned framework.
Forcing choice with default ("you pick"). The skill surfaces its own framework recommendation after the recon search so user can override. Use scripts/framework_recommender.py for the heuristic.
See `references/framework_selection.md` for PICO / SPIDER / Decomposition canon.
Q3 (depends on Q1) — Tentative depth
Tentative depth — pick one. Final confirmation comes after the framework breakdown:
>
1. Quick scan (5 searches)
2. Standard review (10 searches)
3. Deep dive (20 searches)
>
Why I'm asking: I ask this twice — once now to calibrate the recon search emphasis, once after the framework breakdown to confirm. Tentative answer affects which sub-areas to surface first; final answer drives search budget allocation.
Forcing choice. Re-asked at the post-Phase-2 checkpoint after the user has seen the framework breakdown.
Stop condition: 3 questions max before Phase 1. The post-Phase-2 checkpoint is its own grill-me moment (framework table + sub-area-adjustment + depth-reconfirmation).
Phase 1: Initial Reconnaissance
One broad Consensus search to map themes, terminology, methodological distinctions.
- Query: broad version of Q1 (terminology variants are okay; first search casts wide)
- Record:
citation_tracker.py --action record_search --session NAME --query "..." - Record received count:
citation_tracker.py --action record_papers_received --session NAME --count N - Detect plan tier from response: "Showing top 10" / "upgrade" → free; 20 returned → Pro
Synthesize for the checkpoint:
- Themes that surfaced
- Terminology variations (e.g., "LLM" vs "large language model" vs "GPT-style model")
- Methodological distinctions (clinical trials vs benchmark eval vs case study)
- Coverage gaps (sub-questions absent from recon results)
Phase 2: Framework Selection + Sub-area Generation
Choose framework (from Q2 OR override based on recon):
- PICO — most clinical questions (~70% default)
- SPIDER — social / qualitative
- Decomposition — technology focus (Problem / Solution / Evaluation / Limitations)
- Hybrid — explicit cross-framework mapping
Generate 4-5 sub-area questions mapped to framework components. Each becomes a targeted Phase 3 search.
Checkpoint (grill-me forcing-options moment)
After Phase 2, halt and present:
3-4 sentence recon summary
- What themes surfaced
- Terminology landscape
- Evidence landscape characterization
Framework breakdown table
| Framework Component | How It Maps to This Topic | Proposed Sub-area to Explore |
|---|---|---|
| (Component 1) | ... | Sub-area 1 |
| (Component 2) | ... | Sub-area 2 |
| (Component 3) | ... | Sub-area 3 |
| (Component 4) | ... | Sub-area 4 |
| Cross-cutting theme | ... | Sub-area 5 |
Depth re-confirmation (forcing choice)
Surface the practical constraint: detected plan tier + theoretical ceiling.
- Quick scan (5 searches × ~10 results each = ~50 papers max)
- Standard review (10 searches × ~10 = ~100 papers)
- Deep dive (20 searches × ~10 = ~200 papers)
Sub-area forcing options
- "Looks good — proceed with these sub-areas"
- "Adjust: add sub-area on [X]"
- "Adjust: remove and replace [Y] with [Z]"
- "Restart with different framework"
Why I'm asking (the rationale)
A wrong framework or sub-area set wastes the search budget. This is the last cheap moment to correct course.
Wait for user response before Phase 3. Refuse to start Phase 3 without explicit user choice.
Phase 3: Targeted Searches
Sequential (1 query/sec), budget per depth tier. See `references/search_budget_allocation.md` for full canon.
Quick scan (5 searches)
- 5 sub-area searches (one per sub-area)
- Skip era-gated + review-specific
Standard review (10 searches)
- 5 sub-area searches
- 2 review article searches (top 2 sub-areas):
"systematic review [topic]"/"meta-analysis [topic]" - 2 era-gated searches (most important sub-area):
year_max: 2015+year_min: 2021 - 1 follow-up on highest-cited paper using its key terms +
year_minafter publication
Deep dive (20 searches)
- 5 sub-area searches
- 5 review article searches (one per sub-area)
- 4 era-gated searches (top 2 sub-areas, old + new each)
- 3 follow-ups on top 3 highest-cited papers
- 3 spare for emerging threads (surprising findings to chase)
Throughout: 1 q/sec rate limit. Sequential. Confirm response before next call. Record each via citation_tracker.py.
Cross-Search Intelligence
Three trackers across ALL search results — run scripts/cross_search_aggregator.py --session NAME after Phase 3 completes:
1. Repeat-hit papers — same paper appearing in 3+ sub-area searches = likely foundational 2. Recurring authors — same author in multiple searches = dominant research group; top 3-5 most frequent matter 3. Citation-per-year heuristic — a 2023 paper with 150 citations >> 2008 paper with 150 citations. Use for seminal-work identification.
These feed the "Start Here" + "Key Research Groups" + "Bibliography" DOCX sections.
Phase 4: DOCX Research Guide
Generate via Node.js + docx library. 8 sections (see `references/docx_8_sections.md` for full spec):
1. Topic Overview — single tight paragraph (4-6 sentences) 2. Start Here — Priority Reading Order — 5-7 papers ordered: best recent review → foundational → 2-3 frontier → gap/controversy. Each: hyperlinked title + authors/year + 1-sentence contribution + 1-sentence "what to look for" 3. How the Field Got Here — chronological narrative (1-2 paragraphs) + timeline table (5-8 milestones: Year / Milestone / Significance) + terminology evolution note 4. Sub-area Guides (one per sub-area, 4 parts each)
- 4a. What the Research Shows (2-3 sentence synthesis with inline citations)
- 4b. Key Papers (3-5 hyperlinked papers with citation count, year, 1-sentence importance)
- 4c. Key Search Terms (6-10 keywords, synonyms, MeSH, historical terms)
- 4d. Boolean Search Strings (2-3 ready-to-paste strings)
5. Key Research Groups — top 3-5 authors/groups with affiliations, sub-area coverage, representative paper link (from cross-search aggregator) 6. Open Questions & Gaps — three categories: methodological / population-context / conceptual-theoretical. Each gap explains why it matters. 7. Bibliography — alphabetical by first author. Every entry has clickable "View on Consensus" link. Every inline citation matches a bibliography entry. 8. Audit Log — search summary table (#, query, filters, papers returned, status), counts block, coverage notes including detected tier and theoretical ceiling
DOCX Technical Requirements
Document the key docx library patterns:
- Page: US Letter, 1-inch margins
- Lists:
LevelFormat.BULLET(never unicode bullets) - Hyperlinks:
ExternalHyperlinkwithstyle: "Hyperlink", full URL (never truncated) - Tables: dual widths (
columnWidths+ cellwidth),ShadingType.CLEAR - Validation step after save (zip-integrity check:
python3 -c "import zipfile,sys; zipfile.ZipFile(sys.argv[1]).testzip()" output.docx— no output = intact — then confirm the required sections are present)
Reference the docx skill for setup patterns and best practices.
Output
research_guide_<topic-slug>_<YYYY-MM-DD>.docxPlus:
- Chat summary block: "Saved: <path>. Audit: N searches × M unique papers / K cited. Plan tier: <tier>."
- Audit log printed inline if user asks for it
Tooling
| Script | Role |
|---|---|
scripts/citation_tracker.py | JSON-backed three-count audit at ~/.litreview_sessions/<session>.json |
scripts/framework_recommender.py | Heuristic PICO/SPIDER/Decomposition suggestion from research question |
scripts/cross_search_aggregator.py | Repeat-hits + recurring-authors + citation-per-year ranking after Phase 3 |
References
- `references/framework_selection.md` — PICO / SPIDER / Decomposition canon (7+ sources)
- `references/search_budget_allocation.md` — depth tiers + cross-search intelligence + sequential execution rationale (7+ sources)
- `references/docx_8_sections.md` — research guide DOCX spec + technical requirements (7+ sources)
Anti-Patterns To Reject
- Parallelizing Consensus calls
- Skipping the interactive checkpoint (running all searches without user confirmation)
- Padding thin results with training knowledge
- Defaulting to non-PICO framework without justification
- Citing papers in chat that didn't come from Consensus this session
- Hardcoding plan tier instead of detecting from first response
- Skipping era-gated searches in standard/deep budgets
- Skipping cross-search intelligence (repeat-hits, recurring authors)
- Truncating Consensus URLs in hyperlinks
---
Version: 1.0.0 Source spec: `megaprompts/09-litreview-megaprompt.md` Build pattern: Path B (direct conversion). Sibling of pulse (research-pack shape).
DOCX Research Guide — 8 Sections + Technical Requirements
This reference answers exactly one decision: what are the 8 sections of the litreview research guide, and what does each contain to function as a "launching pad" for a researcher entering an unfamiliar field?
The Core Frame
The output is a launching pad, not a finished review. Frame each section as: "what would a generous colleague tell you over coffee if they knew the field and you didn't?"
That framing rules out:
- Exhaustive coverage (a launch pad is finite)
- Comprehensive synthesis (the user will read the papers)
- Defensible-publishable form (this is orientation, not submission-ready)
And rules in:
- Clear ordering (read these papers in this order)
- Honest gaps (here's what's underdeveloped)
- Practical entry points (here's how to keep searching)
Section 1: Topic Overview
Length: 4-6 sentences, single tight paragraph.
Contents:
- What the field is (1 sentence)
- Why it matters (1 sentence)
- Framework used (PICO / SPIDER / Decomposition / hybrid) (1 sentence)
- Characterization of the evidence landscape (1-2 sentences)
- Honest caveat or limitation (1 sentence) — e.g., "mostly Western data" or "RCTs are scarce"
Tone: Confident but caveated. A colleague summarizing, not a textbook authority.
Section 2: Start Here — Priority Reading Order
Length: 5-7 papers, ordered.
Order: 1. Best recent review (sets the field context) 2. Foundational paper(s) — 1-2, ranked by repeat-hits + cited-per-year 3. Frontier papers — 2-3 (most-recent that surfaced multiple times) 4. Gap / controversy paper — 1 (surfaces what's contested)
Per paper:
- Hyperlinked title (clickable to Consensus)
- Authors + year
- One sentence: contribution
- One sentence: "what to look for"
Example entry:
1. [A systematic review of LLM clinical reasoning](https://consensus.app/...) — Singhal et al. 2024 — Most comprehensive synthesis of LLM diagnostic performance through 2023. Look for: section on prompting strategy (the field's main tunable variable).
Section 3: How the Field Got Here
Length: 1-2 paragraphs narrative + timeline table.
Narrative: chronological story of the field's evolution. 3-5 sentences. What changed, when, why.
Timeline table: 5-8 milestones.
| Year | Milestone | Significance |
|---|---|---|
| 2015 | First paper applying X to Y | Established the question |
| 2018 | Method Z introduced | Made evaluation tractable |
| 2020 | Large-scale dataset W released | Enabled benchmarking |
| 2023 | Breakthrough result by Group A | Set current state-of-the-art |
Terminology evolution note: "Field used 'X' through 2018; now standardly called 'Y'. Older searches must include the older term."
This section is what makes a literature review for the researcher: the linear story plus the moments of inflection. Build it from era-gated search results.
Section 4: Sub-area Guides
Length: One per sub-area (4-5 total), 4 parts each.
4a. What the Research Shows
2-3 sentence synthesis with inline citations.
Example:
LLMs achieve 70-85% accuracy on clinical reasoning benchmarks (Singhal et al. 2023, Liévin et al. 2024) but performance degrades sharply on novel case presentations (Toma et al. 2024). The variance across model families and prompting strategies is the field's central open question.
Every fact is hyperlinked. Every inline citation matches a bibliography entry (Section 7).
4b. Key Papers
3-5 hyperlinked papers. Per paper:
- Title (hyperlinked)
- Citation count + year
- One-sentence importance
4c. Key Search Terms
6-10 keywords for the sub-area:
- Modern preferred terms
- Synonyms (especially historical)
- MeSH headings if applicable
- Domain-specific terms (e.g., "USMLE-style" for clinical reasoning)
4d. Boolean Search Strings
2-3 ready-to-paste strings:
("clinical reasoning" OR "diagnostic reasoning") AND ("large language model" OR LLM OR GPT) AND (evaluation OR benchmark)User pastes into Consensus / PubMed / Scopus to continue searching beyond what the skill ran.
Section 5: Key Research Groups
Length: 3-5 groups.
Source: scripts/cross_search_aggregator.py recurring-authors output.
Per group:
- Lead author (or 2-3 authors if collaborative)
- Affiliation (institution)
- Sub-areas they cover (from cross-search analysis)
- Representative paper (hyperlinked, with year)
- Why they matter (1 sentence)
Example:
Singhal, K. et al. (Google DeepMind / Med-PaLM) — Coverage: clinical reasoning, multimodal medical AI. Representative: "Towards Generalist Biomedical AI" (2023). Why they matter: built the Med-PaLM line; their benchmark methodology defines current state-of-the-art evaluation.
Section 6: Open Questions & Gaps
Length: 3 categories, each with 1-3 gaps.
Categories:
1. Methodological gaps — what's hard to measure, what we don't have good methods for 2. Population / context gaps — who isn't being studied, where the data isn't 3. Conceptual / theoretical gaps — what we don't understand about the underlying mechanism
Per gap:
- One sentence stating the gap
- One sentence on why it matters — what's downstream of this gap being filled
Example:
Methodological gap: No standardized benchmark for novel-case clinical reasoning (only retrospective USMLE-style). Why it matters: current "85% accuracy" claims may not generalize to real practice where novel cases dominate.
The "why it matters" sentence is what distinguishes a gap list from a complaint list.
Section 7: Bibliography
Length: All cited papers, alphabetical by first author.
Per entry:
- Full citation (author list, title, journal, year, volume/issue, pages)
- Hyperlinked "View on Consensus" link (full URL, never truncated)
- Inline-citation key matching Section 4 references (e.g., "Singhal et al. 2024")
Discipline:
- Every inline citation in Sections 1-6 appears in Bibliography
- Every Bibliography entry is cited at least once
- No phantom entries (cited but no bib) or orphan entries (bib but never cited)
- Consensus URLs preserved in full (never
...truncation)
Section 8: Audit Log
Length: Search summary table + counts block + coverage notes.
Search summary table:
| # | Query | Filters | Results | Status |
|---|---|---|---|---|
| 1 | broad recon | none | 10 | OK |
| 2 | sub-area 1 | year_min: 2018 | 10 | OK |
| ... | ... | ... | ... | ... |
| 10 | follow-up on Singhal | year_min: 2024 | 7 | thin |
Counts block:
Searches executed: 10
Unique papers received: 47 (after deduplication)
Papers cited in this guide: 22
Plan tier detected: Free (10/search cap)
Theoretical ceiling: 100 papers; received 47 unique (typical deduplication)Coverage notes:
- Which sub-areas surfaced thin results
- Plan-tier impact on coverage
- Suggested manual supplementation (PubMed, Scholar, etc.)
- Era-gated search yields (terminology shifts detected)
The audit log makes the entire review reproducible and falsifiable. A future reader can rerun the searches and check the work.
DOCX Technical Requirements
Document the key docx library patterns (Node.js):
Page setup
const page = {
size: "LETTER",
margins: { top: 1440, right: 1440, bottom: 1440, left: 1440 }, // 1 inch in twips
};Lists (NEVER unicode bullets)
new Paragraph({
children: [new TextRun(text)],
numbering: { reference: "default-bullet", level: 0 },
});
// Defined in document numbering config with LevelFormat.BULLETHyperlinks (full URL, "Hyperlink" style)
new ExternalHyperlink({
link: "https://consensus.app/full-url-never-truncated/...",
children: [new TextRun({ text: paperTitle, style: "Hyperlink" })],
});Tables (dual widths)
new Table({
columnWidths: [3000, 4000, 2000], // EMU
rows: rows.map(r => new TableRow({
children: r.cells.map(c => new TableCell({
width: { size: c.width, type: WidthType.DXA },
shading: { type: ShadingType.CLEAR, color: "auto", fill: "auto" },
children: [new Paragraph(c.text)],
})),
})),
});Validation
After save:
python3 -c "import zipfile,sys; zipfile.ZipFile(sys.argv[1]).testzip()" output.docxIf validation fails: unpack DOCX (it's a ZIP), fix the offending XML, repack.
Reference the docx skill (docx/SKILL.md in this repo if installed) for full setup patterns.
Anti-Patterns
- Truncating Consensus URLs in hyperlinks — breaks reproducibility
- Phantom bibliography entries — cited paper missing from bib
- Generic "Future Work" section — Section 6 must be specific gaps, not "more research is needed"
- No timeline table in Section 3 — narrative-only loses the milestone structure
- Unicode bullets (• ‣ ▶) instead of
LevelFormat.BULLET— breaks DOCX list rendering in some viewers - Single-width tables (only
columnWidthsor only cellwidth) — renders inconsistently across Word / LibreOffice / Google Docs - Skipping validation step — invalid DOCX silently fails to open or renders broken
- Audit log without theoretical ceiling — user can't calibrate "is this comprehensive?"
Operational Checklist
- [ ] All 8 sections present in DOCX
- [ ] Section 1: 4-6 sentence paragraph
- [ ] Section 2: 5-7 papers in priority order
- [ ] Section 3: narrative + timeline table + terminology note
- [ ] Section 4: one sub-section per sub-area, 4 parts each
- [ ] Section 5: 3-5 groups from cross-search aggregator
- [ ] Section 6: 3 categories with "why it matters" per gap
- [ ] Section 7: alphabetical, hyperlinked, no phantoms / orphans
- [ ] Section 8: search table + counts + tier + coverage notes
- [ ] All Consensus URLs full (no truncation)
- [ ]
LevelFormat.BULLETfor lists (no unicode bullets) - [ ] Tables have both
columnWidthsAND cellwidth - [ ]
python3 -c "import zipfile,sys; zipfile.ZipFile(sys.argv[1]).testzip()" output.docxPASSes
Citations (7 sources)
1. `docx` Node.js library — github.com/dolanmiu/docx (MIT). Authoritative API source. The technical patterns (Paragraph, ExternalHyperlink, Table, LevelFormat.BULLET) come from its documentation.
2. OOXML (Office Open XML) Specification — ECMA-376 (4th ed., 2016). The underlying XML schema for DOCX. Source for the dual-width table pattern (DOCX renderers respect both column widths and cell widths; missing either causes layout inconsistencies).
3. *PRISMA 2020 Statement — Page, M. J. et al., BMJ 372, 2021.* Source for the audit-log section requirements (every reported search must include query, filters, results count, status). PRISMA is the international standard for systematic-review reporting.
4. Cochrane Handbook — Higgins, J. P. T. et al. (Wiley, 2019). Chapter 4 + Chapter 7 on data extraction and synthesis. Source for the sub-area guide structure (synthesis + key papers + search terms + boolean strings) — Cochrane's standard data-extraction template.
5. *Lipsey, M. W. & Wilson, D. B., Practical Meta-Analysis (Sage, 2001).* Source for the bibliography discipline (every inline citation has bib entry; every bib entry is cited). Essential for review integrity.
6. *Tufte, E., Visual Display of Quantitative Information (Graphics Press, 1983, 2001 ed.).* Source for the timeline-table pattern (5-8 milestones, not 20+; "milestones" not "events"). Tufte's "small multiples" + "data-ink ratio" principles inform the audit-log table design.
7. *William Strunk Jr. & E. B. White, The Elements of Style (Macmillan, multiple eds.).* Source for the "Open Questions & Gaps" voice discipline. Gaps must be specific and consequential, not "more research is needed" filler. Strunk's "omit needless words" applies directly: every gap statement should pass the "why it matters" test.
Framework Selection — PICO, SPIDER, Decomposition, Hybrid
This reference answers exactly one decision: which literature-review framework does litreview pick for a given research question, and how does each map sub-areas to search queries?
Pair with scripts/framework_recommender.py for the deterministic heuristic.
The Core Claim
A literature review's framework determines what counts as a sub-area. Pick the wrong framework → sub-areas don't map to actual research → searches return tangential papers → review is shallow.
The three primary frameworks plus hybrid:
| Framework | Best for | Components |
|---|---|---|
| PICO | ~70% of clinical questions; quantitative outcomes | Population / Intervention / Comparison / Outcome |
| SPIDER | Social / qualitative; experiential questions | Sample / Phenomenon / Design / Evaluation / Research-type |
| Decomposition | Technology-focused; design / engineering | Problem / Solution / Evaluation / Limitations |
| Hybrid | Cross-cutting topics (clinical + tech, etc.) | Pick components from multiple frameworks |
PICO (default)
Most clinical and biomedical research questions map cleanly to PICO. Example:
"How do LLMs perform on clinical reasoning tasks compared to physicians?"
| Component | Mapped to topic |
|---|---|
| Population | Clinical reasoning tasks (USMLE, MedQA, NEJM cases) |
| Intervention | LLM-based reasoning (GPT-4, Claude, Med-PaLM) |
| Comparison | Physician baseline (specialists, residents, generalists) |
| Outcome | Diagnostic accuracy, reasoning quality, time-to-decision |
Each component becomes one or more sub-area searches.
PICO weaknesses:
- Maps poorly to qualitative research (no clear comparison)
- Maps poorly to technology evaluation (Population is fuzzy)
- Maps poorly to pure-theory questions (no Intervention)
When PICO doesn't fit cleanly → SPIDER or Decomposition.
SPIDER (social / qualitative)
Designed for qualitative + mixed-methods research where PICO breaks. Example:
"How do clinicians experience burnout in academic medicine?"
| Component | Mapped to topic |
|---|---|
| Sample | Clinicians in academic medical centers |
| Phenomenon | Burnout (specifically: emotional exhaustion, depersonalization, reduced accomplishment) |
| Design | Qualitative interviews, ethnography, phenomenology |
| Evaluation | Lived experience, narrative themes |
| Research-type | Qualitative, mixed-methods |
Strong signal for SPIDER:
- Question contains "experience", "perception", "meaning", "lived"
- Outcome is hard to quantify
- Research methods involve interviews or observation
Decomposition (technology / engineering)
Designed for design / build / evaluate questions. Example:
"How are retrieval-augmented generation systems evaluated for clinical Q&A?"
| Component | Mapped to topic |
|---|---|
| Problem | Clinical Q&A: high recall, factual accuracy, citation traceability |
| Solution | RAG architecture (retriever + generator combinations) |
| Evaluation | Benchmarks (MMLU-clinical, MedMCQA, custom Q&A sets) |
| Limitations | Hallucination rates, latency, retrieval quality |
Strong signal for Decomposition:
- Question is about a system or method, not a population
- Question implicitly has "Problem → proposed Solution → how to test → known issues" structure
- Common in CS / ML / engineering research
Hybrid (cross-cutting)
When no single framework fits, mix components. Example:
"How effective is AI-assisted radiology workflow integration in community hospitals?"
| Component | Source framework | Mapping |
|---|---|---|
| Population | PICO | Community hospital radiology departments |
| Intervention | PICO | AI-assisted workflow integration (tool: vendor X) |
| Phenomenon | SPIDER | Workflow change, radiologist experience |
| Outcome | PICO | Read times, diagnostic accuracy, satisfaction |
| Limitations | Decomposition | Integration friction, false-positive rate |
Hybrid framing is more work but more accurate for questions that genuinely span disciplines.
The Framework Recommender Heuristic
scripts/framework_recommender.py uses keyword signals to suggest a framework:
| Signal in research question | Suggests |
|---|---|
| "compared to", "vs", "versus", "better than" | PICO (Comparison) |
| "intervention", "treatment", "drug", "therapy" | PICO (Intervention) |
| "experience", "perception", "meaning", "narrative" | SPIDER (Phenomenon) |
| "qualitative", "interview", "ethnography" | SPIDER (Design) |
| "system", "model", "algorithm", "architecture" | Decomposition (Solution) |
| "benchmark", "evaluation", "metric" | Decomposition (Evaluation) |
| Multiple signals across frameworks | Hybrid |
| No strong signal | PICO (default) |
The recommender outputs:
- Recommended framework
- Confidence (high / medium / low)
- Rationale (which signals fired)
- 4-5 sub-area starter questions mapped to framework components
The skill then surfaces this in the post-Phase-2 checkpoint for user confirmation/override.
When the User Says "You Pick"
Q2's "you pick" option triggers the recommender. The skill:
1. Runs Phase 1 recon search (using broad terminology from Q1) 2. After recon, runs the recommender heuristic against Q1 text 3. Surfaces in checkpoint: "I'm recommending {framework} because {rationale}. Override if you want."
User can override at checkpoint. Refusing to commit (just saying "go") → use recommender's pick.
Anti-Patterns
Defaulting to PICO without justification
PICO works for 70% but fails the other 30%. Defaulting to PICO for a SPIDER question wastes the search budget. The recommender prevents this; manual override should have justification.
Hybrid for everything
Hybrid framing is more work and produces fuzzier sub-areas. Use only when a single framework genuinely fails. Default to non-hybrid; promote to hybrid only when checkpoint review surfaces real cross-cutting components.
Forcing the framework to fit
If 3 of 5 components don't map naturally, the framework is wrong. Restart with a different framework rather than papering over the misfit.
Picking framework before reading Q1
The recommender requires Q1 text. Asking Q2 before Q1 is answered loses signal.
Ignoring the recommender's recommendation
If the recommender suggests SPIDER with high confidence and the user picks PICO anyway, gently challenge: "I see qualitative signals in your question. Want me to use SPIDER, or do you have a reason to insist on PICO?" Once. Honor user override after one push-back.
Operational Checklist
- [ ] Q1 answered before Q2 (recommender needs Q1 text)
- [ ] Q2 forcing choice with "you pick" default
- [ ]
framework_recommender.pyrun after Q1 (cached for checkpoint) - [ ] Recommendation surfaced in checkpoint with rationale
- [ ] User can override at checkpoint
- [ ] Sub-areas mapped 1-to-1 with framework components
- [ ] Cross-cutting 5th sub-area added regardless of framework
Citations (7 sources)
1. *Sackett, D. L. et al., Evidence-Based Medicine: How to Practice and Teach EBM (Churchill Livingstone, 1997, multiple eds.).* Origin of PICO as a clinical-question framing tool. The "PICO" acronym dates from this text. https://en.wikipedia.org/wiki/Evidence-based_medicine
2. *Cooke, A., Smith, D., & Booth, A., "Beyond PICO: The SPIDER Tool for Qualitative Evidence Synthesis" — Qualitative Health Research 22(10), 2012, pp. 1435-1443.* Origin of SPIDER as a PICO alternative for qualitative research. Documents the systematic failures of PICO on qualitative questions that motivated SPIDER's design.
3. *Booth, A., "Searching for qualitative research for inclusion in systematic reviews: a structured methodological review" — Systematic Reviews 5, 2016.* Comparative analysis of PICO vs SPIDER for qualitative work. Source for the "SPIDER for social/qualitative" guidance.
4. *PRISMA 2020 Statement — Page, M. J. et al., BMJ 372, 2021.* The systematic-review reporting standard. Section on "Eligibility criteria" formalizes the framework-driven approach to defining inclusion/exclusion criteria from sub-areas.
5. Cochrane Handbook for Systematic Reviews of Interventions — Higgins, J. P. T. et al. (Wiley, 2019, online updates). Authoritative source for PICO-driven systematic review methodology. Chapter 4 on "Searching for and selecting studies" formalizes the framework → sub-area → search-string mapping pattern.
6. *Hewitt-Taylor, J., "Use of constant comparative analysis in qualitative research" — Nursing Standard 15(42), 2001.* Source for the cross-cutting-theme pattern that litreview adds as a 5th sub-area regardless of framework. Constant comparative analysis surfaces themes that cross conventional framework boundaries.
7. JBI Evidence Synthesis methodology — Joanna Briggs Institute manual (jbi.global). Comprehensive framework comparison: PICO for quantitative effectiveness, PICo (lowercase 'o' for context) for qualitative, PEO for risk factors, CoCoPop for prevalence. The litreview skill simplifies to PICO/SPIDER/Decomposition + hybrid but the JBI manual catalogs ~12 framework variants for specialty cases.
Search Budget Allocation — Quick / Standard / Deep + Cross-Search Intelligence
This reference answers exactly one decision: how does litreview spend its search budget across the 5/10/20 depth tiers, and what makes the cross-search intelligence layer add value beyond per-query results?
Pair with scripts/cross_search_aggregator.py for the deterministic implementation.
The Core Constraint
Consensus has a 1 query/second rate limit. NEVER parallelize. Sequential execution is the only mode that doesn't break the rate limit. This is the same rule pulse uses for Reddit/HN/Web — research-pack convention.
Plus a plan-tier cap: free tier returns ~10 results per query; Pro returns ~20. Detected at first search response.
The combination produces hard budget ceilings:
| Tier | Plan | Theoretical max papers |
|---|---|---|
| Quick scan (5 q) | Free | 50 |
| Quick scan (5 q) | Pro | 100 |
| Standard (10 q) | Free | 100 |
| Standard (10 q) | Pro | 200 |
| Deep dive (20 q) | Free | 200 |
| Deep dive (20 q) | Pro | 400 |
These are theoretical — deduplication reduces the actual unique paper count by 30-50% in practice.
Why Three Tiers (Not One Adaptive Budget)
Adaptive budgeting (run more searches if early results are thin) sounds smart but:
1. User can't predict run time. A 5-search budget runs in ~5s; a 20-search adaptive could run 10-30s. 2. Sunk-cost bias kicks in. Once 10 searches run, "let's do 5 more" is hard to resist even if results aren't worth it. 3. Cross-search intelligence works best at fixed N. Repeat-hit and recurring-author signals stabilize at known sample sizes.
Fixed tiers with explicit allocations beat adaptive budgets for research-orientation tasks.
Quick Scan (5 searches)
Budget allocation:
- 5 sub-area searches (one per sub-area from Phase 2)
- Skip era-gated searches
- Skip review-specific searches
- Skip follow-ups
Use when:
- User wants a fast orientation (~30s with 1 q/sec)
- Topic is well-known to user; they just need pointers
- Plan tier is free + topic is reasonably narrow
Note in audit: "Quick scan tier — review articles + era-gated comparisons omitted. Bibliography may be thin on foundational older work."
Standard Review (10 searches)
Budget allocation:
- 5 sub-area searches (one per sub-area)
- 2 review article searches (top 2 sub-areas):
"systematic review [topic]"AND"meta-analysis [topic]"- 2 era-gated searches (most important sub-area):
year_max: 2015→ reveals terminology evolutionyear_min: 2021→ captures current frontier- 1 follow-up on highest-cited paper:
- Use its key terms +
year_min: <publication_year + 1> - Surfaces papers that built on this work
Use when (default tier):
- User has some familiarity but wants depth
- Plan tier allows reasonable coverage
- Time budget is 1-2 minutes total
Deep Dive (20 searches)
Budget allocation:
- 5 sub-area searches
- 5 review article searches (one per sub-area)
- 4 era-gated searches (top 2 sub-areas, old + new each):
- Sub-area A:
year_max: 2015+year_min: 2021 - Sub-area B:
year_max: 2015+year_min: 2021 - 3 follow-ups on top 3 highest-cited papers (their terms +
year_min) - 3 spare for emerging threads — surprising findings from earlier searches worth chasing
Use when:
- Topic is genuinely new to user
- Comprehensive orientation is the goal
- Plan tier is Pro (free tier deep-dive is bottlenecked at ~200 papers)
Cross-Search Intelligence
Three trackers across ALL Phase 3 search results. Run after Phase 3 completes via scripts/cross_search_aggregator.py --session NAME.
Tracker 1: Repeat-Hit Papers (foundational signal)
A paper appearing in 3+ sub-area searches is signal that it's foundational — multiple sub-fields cite it, suggesting cross-cutting importance.
Use repeat-hits to populate "Start Here" DOCX section:
- Repeat-hit + high citation → priority foundational paper
- Repeat-hit + recent → likely emerging classic
- Repeat-hit but few citations → niche but cross-cutting
Tracker 2: Recurring Authors (dominant research group signal)
Same author appearing across multiple sub-area searches = research group dominant in this area.
Top 3-5 most-frequent authors → "Key Research Groups" DOCX section.
Pattern:
- 5+ search appearances → dominant group (cite representative paper)
- 3-4 appearances → significant but not dominant
- 1-2 appearances → not a "group" signal; may still be high-impact individual
Note: a single highly-cited paper isn't a "group" signal — the recurrence across multiple sub-areas matters.
Tracker 3: Citation-Per-Year (seminal-work heuristic)
Raw citation count is biased toward older papers (more time to accumulate citations). Citations-per-year normalizes:
- Paper A: 2008, 150 citations → 9.4 cites/year
- Paper B: 2023, 150 citations → 50 cites/year
Paper B is much more seminal in current discourse despite equal absolute citation count.
Citation-per-year ranking → "Start Here" priority ordering.
Why Cross-Search Intelligence Matters
Per-query results show "papers about this sub-area". Cross-search intelligence shows "patterns across the whole field":
- Repeat-hits reveal foundational structure
- Recurring authors reveal who's doing the work
- Citation-per-year reveals what's currently shaping discourse
A literature review WITHOUT cross-search intelligence is just a list of papers. WITH it, the review surfaces the structure of the field.
Sequential Execution Discipline
Each Consensus call must wait for the prior response. NEVER parallelize:
search_1 → wait response → record → 1 second pause → search_2 → ...If parallel: rate limit triggers 429, error counter increments, after 3 consecutive failures → stop.
scripts/citation_tracker.py --action record_search enforces the timestamp gap (rejects calls within 1s of prior).
Plan-Tier Detection
After search 1, parse the response:
| Signal | Tier |
|---|---|
| "Showing top 10" / "upgrade for more" | Free (10/search cap) |
| 20 papers returned | Pro (20/search cap) |
| Auth-failure response | API key missing or invalid |
Surface tier at checkpoint:
Detected free tier (~10 results per search). Calibrating budget:
Quick scan: 5 × 10 = ~50 papers
Standard: 10 × 10 = ~100 papers
Deep dive: 20 × 10 = ~200 papers
If you want deeper coverage, Consensus Pro unlocks 20/search.
User chooses depth after seeing the constraint.
Anti-Patterns
- Parallelizing searches — triggers rate limit; data loss
- Adaptive "just one more" extensions — bias-prone; commit to tier upfront
- Skipping era-gated searches in standard/deep tiers — misses terminology shifts
- Skipping cross-search aggregation — reduces review to a paper list
- Hardcoding plan tier — detect at runtime; don't assume free/Pro
- Reporting raw citation count without per-year — over-weights older papers
- Counting repeat-hits at threshold 2 — too noisy; 3 is the minimum signal
Operational Checklist
- [ ] Plan tier detected from search 1 response
- [ ] Theoretical ceiling reported at checkpoint
- [ ] Search budget allocated per tier (5/10/20)
- [ ] Era-gated searches included in standard/deep
- [ ] Follow-ups on highest-cited papers included
- [ ] 1 second wait between each Consensus call (timestamp-enforced)
- [ ] All search results passed through
cross_search_aggregator.pyafter Phase 3 - [ ] Repeat-hit threshold = 3 sub-areas (not 2)
- [ ] Citation-per-year computed (not raw citation count)
Citations (7 sources)
1. Consensus.app documentation — consensus.app/help. Authoritative source for plan-tier caps (free: 10/search, Pro: 20/search) and 1 q/sec rate limit. The skill detects from response rather than hardcoding because documented values evolve.
2. *Higgins, J. P. T. & Green, S. (eds.), Cochrane Handbook for Systematic Reviews of Interventions (Wiley, 2019).* Chapter 4 on search strategy. Source for the era-gated + review-specific + follow-up search categories. The 5/10/20 tier structure is litreview's compression of Cochrane's exhaustive-search methodology.
3. *Greenhalgh, T. & Peacock, R., "Effectiveness and efficiency of search methods in systematic reviews" — BMJ 331, 2005, pp. 1064-1065.* Empirical analysis of how many searches are "enough" to surface foundational papers. Source for the diminishing-returns curve that justifies fixed-tier budgets vs adaptive.
4. *Page, M. J. et al., PRISMA 2020 Statement — BMJ 372, 2021.* Reporting standard for search audit logs. Source for the audit-log DOCX section's required content (search #, query, filters, results returned).
5. *Sandelowski, M. & Barroso, J., Handbook for Synthesizing Qualitative Research (Springer, 2007).* Source for cross-search intelligence patterns in qualitative reviews — repeat-hits and recurring-authors are documented signals in narrative synthesis literature.
6. *Lawani, S. M., "Bibliometrics: Its theoretical foundations, methods and applications" — Libri 31, 1981.* Foundational bibliometrics paper. Source for the citations-per-year normalization (Lawani's Garfield-style impact normalization). The skill's citation-per-year heuristic is the simplest form of bibliometric normalization.
7. AWS Architecture Blog — Mike Cohen, "Exponential Backoff and Jitter" (2015) + Marc Brooker, "Timeouts, retries, and backoff with jitter" (Builders' Library, 2019). Source for the retry-once-after-3s pattern (research-pack convention). Justifies aggressive failure-detection (3 consecutive → stop) over deep retry loops for research workflows.
#!/usr/bin/env python3
"""citation_tracker.py — JSON-backed three-count audit for litreview runs.
Stdlib-only. Mirrors pulse's citation_tracker.py (research-pack convention)
but adapted for Consensus-based academic search:
- searches executed (Consensus queries issued)
- unique papers received (deduplicated across all searches)
- papers cited (made it into the DOCX guide)
Enforces sequential discipline by rejecting record_search calls within 1
second of the prior (Consensus rate limit).
Session state persists in ~/.litreview_sessions/<session>.json.
Actions:
start Create a new session
record_search Record a search query + enforce 1s gap
record_papers_received Record N papers from this search (with dedup intent)
record_cited Record a paper URL that made it into the DOCX
status Show current counts + audit block
list List all sessions
close Mark session ended
Usage:
python citation_tracker.py --action start --session litreview-20260515 --topic "LLM clinical reasoning"
python citation_tracker.py --action record_search --session ... --query "..." --tier free
python citation_tracker.py --action record_papers_received --session ... --count 10 --unique 8
python citation_tracker.py --action record_cited --session ... --url "https://consensus.app/..."
python citation_tracker.py --action status --session ...
python citation_tracker.py --action list
python citation_tracker.py --action close --session ...
"""
import argparse
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional
SESSIONS_DIR = Path.home() / ".litreview_sessions"
MIN_SEARCH_GAP_SECONDS = 1.0 # Consensus rate limit
def session_path(name: str) -> Path:
return SESSIONS_DIR / f"{name}.json"
def load_session(name: str) -> Dict[str, Any]:
p = session_path(name)
if not p.exists():
raise FileNotFoundError(f"Session not found: {name}")
return json.loads(p.read_text(encoding="utf-8"))
def save_session(name: str, data: Dict[str, Any]) -> None:
SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
session_path(name).write_text(json.dumps(data, indent=2), encoding="utf-8")
def now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def now_ts() -> float:
return datetime.now(timezone.utc).timestamp()
def action_start(name: str, topic: Optional[str]) -> Dict[str, Any]:
if session_path(name).exists():
raise FileExistsError(f"Session already exists: {name}")
data: Dict[str, Any] = {
"session": name,
"topic": topic or "",
"started_at": now_iso(),
"ended_at": None,
"plan_tier": None,
"searches": [],
"papers_received_log": [],
"papers_cited": [],
"counts": {"searches": 0, "papers_received_unique": 0, "papers_cited": 0},
}
save_session(name, data)
return data
def action_record_search(name: str, query: str, tier: Optional[str]) -> Dict[str, Any]:
data = load_session(name)
if data["searches"]:
last_ts = data["searches"][-1].get("ts", 0)
gap = now_ts() - last_ts
if gap < MIN_SEARCH_GAP_SECONDS:
raise RuntimeError(
f"Sequential discipline violation: search submitted {gap:.2f}s after prior "
f"(min gap: {MIN_SEARCH_GAP_SECONDS}s). Wait at least {MIN_SEARCH_GAP_SECONDS - gap:.2f}s more."
)
if tier and not data["plan_tier"]:
data["plan_tier"] = tier
data["searches"].append({"query": query, "tier": tier, "at": now_iso(), "ts": now_ts()})
data["counts"]["searches"] += 1
save_session(name, data)
return data
def action_record_papers_received(name: str, count: int, unique: Optional[int]) -> Dict[str, Any]:
data = load_session(name)
unique_count = unique if unique is not None else count
data["papers_received_log"].append({"raw_count": count, "unique_after_dedup": unique_count, "at": now_iso()})
data["counts"]["papers_received_unique"] += unique_count
save_session(name, data)
return data
def action_record_cited(name: str, url: str, paper_title: Optional[str]) -> Dict[str, Any]:
data = load_session(name)
if any(p["url"] == url for p in data["papers_cited"]):
return data # Already cited; idempotent
data["papers_cited"].append({"url": url, "title": paper_title, "at": now_iso()})
data["counts"]["papers_cited"] += 1
save_session(name, data)
return data
def action_status(name: str) -> Dict[str, Any]:
return load_session(name)
def action_close(name: str) -> Dict[str, Any]:
data = load_session(name)
if data.get("ended_at") is None:
data["ended_at"] = now_iso()
save_session(name, data)
return data
def action_list() -> List[Dict[str, Any]]:
SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
out: List[Dict[str, Any]] = []
for p in sorted(SESSIONS_DIR.glob("*.json")):
try:
d = json.loads(p.read_text(encoding="utf-8"))
out.append({
"session": d.get("session", p.stem),
"topic": d.get("topic", ""),
"started_at": d.get("started_at", ""),
"ended_at": d.get("ended_at"),
"plan_tier": d.get("plan_tier"),
"counts": d.get("counts", {}),
})
except (OSError, json.JSONDecodeError):
continue
return out
def render_status_human(data: Dict[str, Any]) -> str:
out: List[str] = []
out.append(f"Session: {data['session']}")
out.append(f"Topic: {data.get('topic', '(unset)')}")
out.append(f"Plan tier: {data.get('plan_tier') or '(not detected)'}")
out.append(f"Started: {data['started_at']}")
out.append(f"Ended: {data.get('ended_at') or '(active)'}")
out.append("")
c = data["counts"]
out.append("Three-count audit:")
out.append(f" Searches: {c['searches']}")
out.append(f" Unique papers: {c['papers_received_unique']}")
out.append(f" Cited: {c['papers_cited']}")
out.append("")
out.append("Audit block (paste in DOCX Section 8):")
out.append(
f" Searches executed: {c['searches']}. "
f"Unique papers received: {c['papers_received_unique']}. "
f"Papers cited in guide: {c['papers_cited']}. "
f"Plan tier: {data.get('plan_tier') or 'undetected'}."
)
return "\n".join(out)
def render_list_human(rows: List[Dict[str, Any]]) -> str:
if not rows:
return "(no sessions)"
out: List[str] = []
out.append(f"{'session':<40s} {'tier':<6s} {'srch':>4s} {'uniq':>4s} {'cited':>5s} status")
out.append("-" * 78)
for r in rows:
c = r["counts"]
status = "closed" if r["ended_at"] else "active"
tier = r.get("plan_tier") or "—"
out.append(
f"{r['session']:<40s} {tier:<6s} "
f"{c.get('searches', 0):>4d} {c.get('papers_received_unique', 0):>4d} "
f"{c.get('papers_cited', 0):>5d} {status}"
)
return "\n".join(out)
def main(argv: List[str]) -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
parser.add_argument(
"--action",
required=True,
choices=["start", "record_search", "record_papers_received", "record_cited", "status", "list", "close"],
)
parser.add_argument("--session", help="Session name")
parser.add_argument("--topic", help="(start only) topic string")
parser.add_argument("--query", help="(record_search only) Consensus query text")
parser.add_argument("--tier", help="(record_search only) detected tier: free | pro")
parser.add_argument("--count", type=int, help="(record_papers_received only) raw paper count")
parser.add_argument("--unique", type=int, help="(record_papers_received only) unique count after dedup")
parser.add_argument("--url", help="(record_cited only) Consensus URL of cited paper")
parser.add_argument("--title", help="(record_cited only) paper title for the log")
parser.add_argument("--output", choices=["human", "json"], default="human")
args = parser.parse_args(argv)
try:
if args.action == "start":
if not args.session:
print("error: --session required for start", file=sys.stderr); return 2
result = action_start(args.session, args.topic)
elif args.action == "record_search":
if not (args.session and args.query):
print("error: --session, --query required", file=sys.stderr); return 2
result = action_record_search(args.session, args.query, args.tier)
elif args.action == "record_papers_received":
if not (args.session and args.count is not None):
print("error: --session, --count required", file=sys.stderr); return 2
result = action_record_papers_received(args.session, args.count, args.unique)
elif args.action == "record_cited":
if not (args.session and args.url):
print("error: --session, --url required", file=sys.stderr); return 2
result = action_record_cited(args.session, args.url, args.title)
elif args.action == "status":
if not args.session:
print("error: --session required for status", file=sys.stderr); return 2
result = action_status(args.session)
elif args.action == "close":
if not args.session:
print("error: --session required for close", file=sys.stderr); return 2
result = action_close(args.session)
else:
result = action_list()
except (FileNotFoundError, FileExistsError, RuntimeError) as e:
print(f"error: {e}", file=sys.stderr); return 2
if args.output == "json":
print(json.dumps(result, indent=2, default=str))
else:
if args.action == "list":
print(render_list_human(result))
else:
print(render_status_human(result))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
#!/usr/bin/env python3
"""cross_search_aggregator.py — Cross-search intelligence for litreview.
Stdlib-only. Reads all search results recorded across a litreview session
and computes three signals that transform a per-search paper list into
field-level intelligence:
1. Repeat-hit papers: same paper in 3+ sub-area searches (foundational signal)
2. Recurring authors: same author across multiple searches (dominant group)
3. Citation-per-year: normalizes raw citation count by paper age (seminal work)
Reads from a search-results JSON file (one entry per search, each with
papers list including url, title, authors, year, citations).
Outputs feed the DOCX guide's "Start Here" + "Key Research Groups"
sections.
NO LLM CALLS. Pure aggregation + ranking.
Input file format (`--results-file`):
{
"session": "litreview-20260515",
"searches": [
{
"query": "...",
"sub_area": "Intervention",
"papers": [
{"url": "https://...", "title": "...", "authors": ["..."], "year": 2023, "citations": 150}
]
}
]
}
Usage:
python cross_search_aggregator.py --results-file /tmp/results.json
python cross_search_aggregator.py --results-file /tmp/results.json --output json
python cross_search_aggregator.py --sample
"""
import argparse
import json
import sys
from collections import Counter
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List
REPEAT_HIT_THRESHOLD = 3 # paper must appear in 3+ sub-areas
TOP_AUTHORS_N = 5
TOP_REPEAT_HITS_N = 8
SAMPLE_RESULTS = {
"session": "litreview-sample",
"searches": [
{
"query": "LLM clinical reasoning benchmarks",
"sub_area": "Intervention",
"papers": [
{"url": "https://consensus.app/paper/abc1", "title": "Med-PaLM benchmark", "authors": ["Singhal", "Tu", "Gottweis"], "year": 2023, "citations": 250},
{"url": "https://consensus.app/paper/abc2", "title": "LLMs vs physicians on USMLE", "authors": ["Kung", "Cheatham"], "year": 2023, "citations": 800},
{"url": "https://consensus.app/paper/abc3", "title": "Reasoning evaluation framework", "authors": ["Lievin"], "year": 2024, "citations": 120},
],
},
{
"query": "clinical reasoning evaluation methodology",
"sub_area": "Outcome",
"papers": [
{"url": "https://consensus.app/paper/abc1", "title": "Med-PaLM benchmark", "authors": ["Singhal", "Tu", "Gottweis"], "year": 2023, "citations": 250},
{"url": "https://consensus.app/paper/abc4", "title": "Diagnostic accuracy AI", "authors": ["Toma", "Lawler"], "year": 2024, "citations": 90},
{"url": "https://consensus.app/paper/abc5", "title": "AI in medicine review", "authors": ["Singhal", "Azizi"], "year": 2023, "citations": 200},
],
},
{
"query": "GPT-4 medical Q&A",
"sub_area": "Population",
"papers": [
{"url": "https://consensus.app/paper/abc1", "title": "Med-PaLM benchmark", "authors": ["Singhal", "Tu", "Gottweis"], "year": 2023, "citations": 250},
{"url": "https://consensus.app/paper/abc2", "title": "LLMs vs physicians on USMLE", "authors": ["Kung", "Cheatham"], "year": 2023, "citations": 800},
{"url": "https://consensus.app/paper/abc6", "title": "GPT-4 USMLE performance", "authors": ["Nori", "King"], "year": 2023, "citations": 400},
],
},
],
}
def aggregate(results: Dict[str, Any]) -> Dict[str, Any]:
paper_appearances: Dict[str, Dict[str, Any]] = {}
author_appearances: Counter = Counter()
author_paper_sub_areas: Dict[str, set] = {}
for search in results.get("searches", []):
sub_area = search.get("sub_area", "uncategorized")
for paper in search.get("papers", []):
url = paper.get("url", "")
if not url:
continue
if url not in paper_appearances:
paper_appearances[url] = {
"url": url,
"title": paper.get("title", ""),
"authors": paper.get("authors", []),
"year": paper.get("year"),
"citations": paper.get("citations", 0),
"sub_areas": set(),
}
paper_appearances[url]["sub_areas"].add(sub_area)
for author in paper.get("authors", []):
author_appearances[author] += 1
if author not in author_paper_sub_areas:
author_paper_sub_areas[author] = set()
author_paper_sub_areas[author].add(sub_area)
# Tracker 1: Repeat-hit papers
repeat_hits: List[Dict[str, Any]] = []
for url, p in paper_appearances.items():
if len(p["sub_areas"]) >= REPEAT_HIT_THRESHOLD:
entry = {
"url": p["url"],
"title": p["title"],
"authors": p["authors"],
"year": p["year"],
"citations": p["citations"],
"sub_areas": sorted(p["sub_areas"]),
"sub_area_count": len(p["sub_areas"]),
}
repeat_hits.append(entry)
repeat_hits.sort(key=lambda x: (-x["sub_area_count"], -(x["citations"] or 0)))
# Tracker 2: Recurring authors
recurring_authors: List[Dict[str, Any]] = []
for author, count in author_appearances.most_common(TOP_AUTHORS_N):
if count >= 2:
recurring_authors.append({
"author": author,
"appearances": count,
"sub_areas": sorted(author_paper_sub_areas.get(author, set())),
})
# Tracker 3: Citation-per-year
current_year = datetime.now().year
cited_per_year: List[Dict[str, Any]] = []
for url, p in paper_appearances.items():
year = p.get("year")
cites = p.get("citations", 0) or 0
if year and year <= current_year and cites > 0:
age = max(current_year - year, 1)
cpy = cites / age
cited_per_year.append({
"url": p["url"],
"title": p["title"],
"year": year,
"citations": cites,
"age_years": age,
"citations_per_year": round(cpy, 1),
})
cited_per_year.sort(key=lambda x: -x["citations_per_year"])
return {
"session": results.get("session", "(unknown)"),
"total_searches": len(results.get("searches", [])),
"unique_papers": len(paper_appearances),
"repeat_hit_papers": repeat_hits[:TOP_REPEAT_HITS_N],
"repeat_hit_count": len(repeat_hits),
"recurring_authors": recurring_authors,
"citations_per_year_top_5": cited_per_year[:5],
}
def render_human(result: Dict[str, Any]) -> str:
out: List[str] = []
out.append(f"Cross-search intelligence — session {result['session']}")
out.append(f" Total searches: {result['total_searches']}")
out.append(f" Unique papers: {result['unique_papers']}")
out.append(f" Repeat-hit papers (≥{REPEAT_HIT_THRESHOLD} sub-areas): {result['repeat_hit_count']}")
out.append("")
if result["repeat_hit_papers"]:
out.append("Repeat-Hit Papers (foundational signal):")
for p in result["repeat_hit_papers"]:
authors_str = ", ".join(p["authors"][:3]) + (" et al." if len(p["authors"]) > 3 else "")
out.append(f" - {p['title']} ({authors_str}, {p['year']}) — {p['sub_area_count']} sub-areas, {p['citations']} cites")
out.append(f" Sub-areas: {', '.join(p['sub_areas'])}")
out.append(f" URL: {p['url']}")
else:
out.append("Repeat-Hit Papers: (none — increase search budget or check sub-area diversity)")
out.append("")
if result["recurring_authors"]:
out.append(f"Recurring Authors (top {len(result['recurring_authors'])}):")
for a in result["recurring_authors"]:
out.append(f" - {a['author']}: {a['appearances']} appearances across {len(a['sub_areas'])} sub-area(s)")
out.append(f" Sub-areas: {', '.join(a['sub_areas'])}")
else:
out.append("Recurring Authors: (none above threshold)")
out.append("")
if result["citations_per_year_top_5"]:
out.append("Citations-per-Year top 5 (seminal-work heuristic):")
for p in result["citations_per_year_top_5"]:
out.append(f" - {p['title']} ({p['year']}) — {p['citations']} cites / {p['age_years']} yr = {p['citations_per_year']}/yr")
else:
out.append("Citations-per-Year: (insufficient data)")
return "\n".join(out)
def main(argv: List[str]) -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
parser.add_argument("--results-file", help="Path to search-results JSON file")
parser.add_argument("--sample", action="store_true", help="Run on embedded sample results")
parser.add_argument("--output", choices=["human", "json"], default="human")
args = parser.parse_args(argv)
if args.sample:
result = aggregate(SAMPLE_RESULTS)
elif args.results_file:
p = Path(args.results_file)
if not p.exists():
print(f"error: {args.results_file} not found", file=sys.stderr); return 2
try:
data = json.loads(p.read_text(encoding="utf-8"))
except json.JSONDecodeError as e:
print(f"error: invalid JSON in {args.results_file}: {e}", file=sys.stderr); return 2
result = aggregate(data)
else:
parser.print_help(); return 0
if args.output == "json":
print(json.dumps(result, indent=2, default=str))
else:
print(render_human(result))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
#!/usr/bin/env python3
"""framework_recommender.py — Heuristic PICO/SPIDER/Decomposition picker.
Stdlib-only. Given a research question, suggests which literature-review
framework to use, with confidence + rationale + starter sub-area questions.
Heuristic keyword signals:
- "compared to", "vs", "versus", "better than" → PICO (Comparison signal)
- "intervention", "treatment", "drug", "therapy" → PICO (Intervention)
- "experience", "perception", "lived", "meaning" → SPIDER (Phenomenon)
- "qualitative", "interview", "ethnography" → SPIDER (Design)
- "system", "model", "algorithm", "architecture" → Decomposition (Solution)
- "benchmark", "evaluation", "metric" → Decomposition (Evaluation)
- Multiple signals across frameworks → Hybrid
- No strong signal → PICO (default)
NO LLM CALLS. Pure regex + keyword counting.
Usage:
python framework_recommender.py --question "How do LLMs perform on clinical reasoning compared to physicians?"
python framework_recommender.py --question "..." --output json
python framework_recommender.py --sample
"""
import argparse
import json
import re
import sys
from typing import Any, Dict, List
PICO_SIGNALS = {
"comparison": ["compared to", "vs", "versus", "better than", "compared with", "relative to"],
"intervention": ["intervention", "treatment", "drug", "therapy", "drug therapy", "regimen"],
"outcome": ["outcome", "efficacy", "effectiveness", "accuracy", "mortality", "survival"],
"population": ["patients", "subjects", "cohort", "participants"],
}
SPIDER_SIGNALS = {
"phenomenon": ["experience", "perception", "meaning", "lived", "narrative", "perspective"],
"design": ["qualitative", "interview", "ethnography", "phenomenology", "grounded theory"],
"sample": ["women's", "men's", "clinicians", "students", "patients with"], # demographic-context
"evaluation": ["thematic", "narrative analysis", "lived experience"],
}
DECOMPOSITION_SIGNALS = {
"solution": ["system", "model", "algorithm", "architecture", "method", "approach", "framework"],
"evaluation": ["benchmark", "evaluation", "metric", "performance", "accuracy"],
"problem": ["challenge", "problem", "issue with", "limitations of"],
"limitations": ["limitations", "failure mode", "edge case", "robustness"],
}
def count_signals(text: str, signal_map: Dict[str, List[str]]) -> Dict[str, int]:
text_lower = text.lower()
counts: Dict[str, int] = {}
for component, phrases in signal_map.items():
component_count = 0
for phrase in phrases:
# Allow optional plural 's' / 'ed' / 'ing' suffix for single-word phrases (not multi-word)
if " " in phrase:
pattern = re.compile(rf"\b{re.escape(phrase)}\b", re.IGNORECASE)
else:
pattern = re.compile(rf"\b{re.escape(phrase)}(?:s|es|ed|ing)?\b", re.IGNORECASE)
component_count += len(pattern.findall(text_lower))
counts[component] = component_count
return counts
def recommend(question: str) -> Dict[str, Any]:
pico = count_signals(question, PICO_SIGNALS)
spider = count_signals(question, SPIDER_SIGNALS)
decomp = count_signals(question, DECOMPOSITION_SIGNALS)
pico_total = sum(pico.values())
spider_total = sum(spider.values())
decomp_total = sum(decomp.values())
total = pico_total + spider_total + decomp_total
# Confidence: ratio of dominant framework to total
if total == 0:
framework = "PICO"
confidence = "low"
rationale = "No strong framework signals detected — defaulting to PICO (covers ~70% of questions)"
elif pico_total >= 2 and spider_total >= 2:
framework = "Hybrid (PICO + SPIDER)"
confidence = "medium"
rationale = f"Both PICO ({pico_total} signals) and SPIDER ({spider_total}) detected — question spans quantitative + qualitative"
elif pico_total >= 2 and decomp_total >= 2:
framework = "Hybrid (PICO + Decomposition)"
confidence = "medium"
rationale = f"Both PICO ({pico_total}) and Decomposition ({decomp_total}) — clinical + technology evaluation"
elif decomp_total > pico_total and decomp_total > spider_total:
framework = "Decomposition"
confidence = "high" if decomp_total >= 3 else "medium"
active = [k for k, v in decomp.items() if v > 0]
rationale = f"Decomposition signals dominate ({decomp_total} total, components: {', '.join(active)})"
elif spider_total > pico_total and spider_total > decomp_total:
framework = "SPIDER"
confidence = "high" if spider_total >= 3 else "medium"
active = [k for k, v in spider.items() if v > 0]
rationale = f"SPIDER signals dominate ({spider_total} total, components: {', '.join(active)})"
else:
framework = "PICO"
confidence = "high" if pico_total >= 3 else "medium" if pico_total >= 1 else "low"
active = [k for k, v in pico.items() if v > 0]
rationale = f"PICO signals dominate ({pico_total} total, components: {', '.join(active) if active else 'default'})"
# Sub-area starter questions (template — actual generation needs LLM context)
starter_questions = generate_starter_questions(question, framework)
return {
"question": question,
"framework": framework,
"confidence": confidence,
"rationale": rationale,
"signal_counts": {"PICO": pico, "SPIDER": spider, "Decomposition": decomp},
"starter_sub_areas": starter_questions,
}
def generate_starter_questions(question: str, framework: str) -> List[str]:
"""Template-driven sub-area starter questions per framework."""
if framework.startswith("PICO") or "PICO" in framework:
return [
"Population: who is being studied? (define inclusion + exclusion)",
"Intervention: what is being tested? (specify dose / variant / version)",
"Comparison: against what baseline? (placebo / standard / alternative)",
"Outcome: what is being measured? (primary + secondary endpoints)",
"Cross-cutting: methodological quality or population variation",
]
elif framework.startswith("SPIDER") or "SPIDER" in framework:
return [
"Sample: who has the experience? (define context)",
"Phenomenon: what experience or perception? (be specific)",
"Design: what qualitative methods? (interviews / observation / artifacts)",
"Evaluation: what kind of analysis? (thematic / narrative / phenomenological)",
"Cross-cutting: cultural or temporal variation in the phenomenon",
]
elif framework.startswith("Decomposition"):
return [
"Problem: what challenge is being addressed? (constraints + objectives)",
"Solution: what is the proposed approach? (architecture + key innovation)",
"Evaluation: how is it being measured? (benchmarks + metrics + baselines)",
"Limitations: where does it fail? (edge cases + failure modes)",
"Cross-cutting: scalability or deployment considerations",
]
else: # Hybrid
return [
"Primary framework components (from dominant signals)",
"Secondary framework components (from cross-cutting signals)",
"Comparison or evaluation dimension",
"Outcome or impact dimension",
"Cross-cutting: methodological consistency across paradigms",
]
def render_human(result: Dict[str, Any]) -> str:
out: List[str] = []
out.append(f"Question: {result['question']}")
out.append("")
out.append(f"Recommended: {result['framework']}")
out.append(f"Confidence: {result['confidence']}")
out.append(f"Rationale: {result['rationale']}")
out.append("")
out.append("Signal counts:")
for fw, components in result["signal_counts"].items():
total = sum(components.values())
active = ", ".join(f"{k}={v}" for k, v in components.items() if v > 0) or "(none)"
out.append(f" {fw:<18s} total={total} ({active})")
out.append("")
out.append("Starter sub-area questions:")
for q in result["starter_sub_areas"]:
out.append(f" - {q}")
return "\n".join(out)
SAMPLE_QUESTION = "How do large language models perform on clinical reasoning tasks compared to physicians?"
def main(argv: List[str]) -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
parser.add_argument("--question", help="Research question text")
parser.add_argument("--sample", action="store_true", help="Run on embedded sample question")
parser.add_argument("--output", choices=["human", "json"], default="human")
args = parser.parse_args(argv)
if args.sample:
result = recommend(SAMPLE_QUESTION)
elif args.question:
result = recommend(args.question)
else:
parser.print_help(); return 0
if args.output == "json":
print(json.dumps(result, indent=2))
else:
print(render_human(result))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
Related skills
FAQ
What does litreview produce?
A launching-pad orientation guide as a formatted Word .docx, not a finished literature review.
What frameworks does it use?
PICO by default, with SPIDER, Decomposition, or hybrid as fallbacks chosen after a reconnaissance search.