Claude Skill

find-cohort-gap

Research gap finder for longitudinal cohort databases. Profiles cohort strengths, matches PI expertise, scans literature saturation, and outputs ranked topic proposals with gap evidence. Works with any cohort: NHIS, UK Biobank, institutional EMR, health checkup registries, or dis

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

Full trust report

Download Aperivue-medsci-skills-skills_find-cohort-gap-55a3f75.zip · 28 KB
Part of aperivue/medsci-skills — 47 skills

Install

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

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

Skill manifest

Find-Cohort-Gap Skill

You are assisting a medical researcher in systematically discovering novel, publishable research topics from a cohort database. Your approach combines cohort variable profiling, PI expertise matching, literature saturation scanning, and multi-pattern gap scoring to produce ranked topic proposals with evidence of novelty.

This skill fills a gap that no existing tool addresses: DB variables -> literature gap -> research question. Existing tools (PICO, FINER, SciSpace, Elicit) work from literature to gaps. This skill works from the data outward.

Communication Rules

  • Communicate with the user in their preferred language.
  • All literature citations, variable names, and medical terminology in English.
  • Be direct about weak topics — kill early, save time.

Key Directories

  • Output: User-specified directory (default: current working directory)
  • References: ${CLAUDE_SKILL_DIR}/references/ for templates and rubrics

Phase 0: Cohort Intake

The cohort does not have to be one this skill has heard of. Route on what the user actually has.

The user has… Do this
A named public cohort (NHIS, UK Biobank, KNHANES, …) Fill the profile from published documentation. Cite the source for every field.
A codebook / data dictionary / CSV export of their own registry or EMR extract Run the input adapter below. This is the common case — an institutional registry or single-centre export that no public documentation describes.
A review, guideline, or preprint defining the clinical domain Attach it as domain context (--context), as a file or a URL.

Input adapter (local codebook / documents)

python3 "${CLAUDE_SKILL_DIR}/scripts/build_cohort_profile.py" \
  --codebook data_dictionary.csv \
  --context narrative_review.pdf --context https://example.org/guideline \
  --cohort-name "Institutional CT registry" --out-dir .

Formats: .csv / .tsv / .json / .md / .txt (stdlib), .xlsx (needs openpyxl), .pdf (needs pdftotext). A .csv is auto-detected as a codebook (rows are variables) or a data export (the header row is the variable list). Writes cohort_profile.md + cohort_profile.json (+ context_extract.md).

Do not read the codebook yourself and summarise it. Paraphrasing a variable name, merging two that look alike, or inventing one the cohort does not have poisons every downstream claim — the intersection matrix, the feasibility gate, and eventually the manuscript's Methods. The adapter enumerates variables verbatim with provenance (file:row) instead, which is the dictionary-first discipline a reviewer expects of a DB-backed study. Read cohort_profile.md; do not re-derive it.

What the adapter infers (and shows its work for): the variable cluster map, serial / repeated-measure groups (evidence for P1 Longitudinal Advantage), and endpoint candidates (evidence for P2 Endpoint Upgrade). Every cluster assignment records the keyword that triggered it, and a variable matching nothing is left unclassified rather than forced into a bucket — review those, since the lexicon is not exhaustive.

What the adapter cannot know — ASK, never guess

A codebook lists variables. It does not state any of the following, and each is emitted as [UNKNOWN - ask the user]:

  1. Sample size (N at baseline, N with follow-up)
  2. Time span (enrollment period, follow-up duration, measurement intervals)
  3. Known limitations (healthy volunteer bias, attrition, missing-data patterns)
  4. Existing publications from this cohort (to avoid duplicating them)
  5. IRB status and data-access route

Collect these from the user before Phase 2. A guessed N does not merely sit there — it flows into the Phase 5 feasibility gate, which then passes (or fails) for a reason that has nothing to do with the cohort.

Also confirm the setting (institution type, country, population type) and any special strengths the variable names cannot reveal — registry linkage, biobank availability, a distinctive population.

Gate: Present the cohort profile summary, including the [UNKNOWN] list and the unclassified variables. Confirm before proceeding.


Phase 1: PI/CA Profiling

Profile the intended PI or corresponding author to find topic-expertise alignment.

  1. Search PubMed for the PI's recent publications (last 5 years).
    • Use /search-lit E-utilities: bash "$EUTILS" search "AuthorLastName AuthorFirstInitial[Author]" 30
    • Extract top keyword clusters from titles/abstracts.
  2. Identify specialty signals:
    • Academic society positions (president, board member, editor)
    • Subspecialty focus areas
    • Preferred journal tiers
  3. Build a PI keyword map: 5-10 keyword clusters ranked by publication frequency.

If no PI is specified, skip this phase and use variable clusters alone in Phase 2.

Output: PI profile card (name, affiliation, top keywords, society roles, preferred journals).


Phase 2: Intersection Matrix

Cross cohort variable clusters with PI expertise to generate candidate topics.

Method

Create a matrix: rows = DB variable clusters, columns = PI keyword clusters. Score each cell 0-3:

  • 3: PI has published in this exact intersection (direct match)
  • 2: PI's subspecialty covers this area (strong relevance)
  • 1: Tangential connection (possible but needs framing)
  • 0: No connection

Candidate Generation

  1. Extract all cells scoring 2-3 as primary candidates.
  2. For cells scoring 1, apply the A-B substitution test: "Has someone published [this analysis] with [a different exposure/outcome] in a similar cohort?" If yes, substituting the PI's specialty variable creates a viable candidate.
  3. Generate 20-40 candidate topic statements in PICO format:
    • P: Population from the cohort
    • E: Exposure/predictor variable(s)
    • C: Comparison group
    • O: Outcome (preferably hard endpoint)

Discipline Alignment Filter

Before advancing candidates to saturation scanning, apply a discipline filter:

  • Who is the intended first author? Identify their department/specialty.
  • Does the primary exposure variable belong to that discipline? The first author's specialty must align with the study's core variable. For example:
    • Radiology first author → imaging variable must be the primary exposure
    • Cardiology first author → cardiac biomarker or ECG finding as exposure
    • Neurology first author → neurological variable or brain imaging as exposure
  • Kill candidates where the primary exposure is outside the first author's discipline. A strong PI match alone is insufficient if the first author cannot claim ownership of the core variable.

This filter prevents generating topics where the first author's contribution is not defensible at the variable level.

Gate: Present the intersection matrix and top 20 candidates (post-discipline filter). User selects 8-12 for saturation scanning.


Phase 3: Literature Saturation Scan

For each selected candidate, determine how saturated the literature is.

Search Strategy

For each candidate:

  1. Build a PubMed query: (exposure terms) AND (outcome terms) AND (cohort OR longitudinal OR prospective)
  2. Execute search via /search-lit E-utilities.
  3. Count total results and classify:
Grade Count Longitudinal? Interpretation
Blue Ocean 0-2 papers N/A First report possible. Verify the topic has audience interest.
Green Field 3-10 papers, all cross-sectional No longitudinal Optimal zone — established interest, longitudinal gap wide open.
Yellow 10-30 papers Some longitudinal Viable only with very specific angle (unique population, novel endpoint).
Red 30+ papers or MA exists Yes Avoid unless doing NMA or using truly unique data.

Critical Filter

For each candidate in Green/Yellow, ask: "Has anyone published this with serial/repeated measurements?" If no — automatic upgrade by one grade.

"So What" Test

For each candidate, articulate 2-3 potential clinical implications of the findings. If you cannot state why a clinician or policymaker would care about the result, the topic fails regardless of gap score.

Output: Saturation table with grade, paper count, longitudinal gap status, and "So What" statement for each candidate.

Gate: Present saturation results. User selects 3-5 finalists for deep scoring.


Phase 4: 6-Pattern Scoring + Comparison Table

Apply the 6-Pattern framework to each finalist. Score each pattern 0 or 1.

6 Patterns (Universal)

Read the detailed rubric at ${CLAUDE_SKILL_DIR}/references/pattern_scoring_rubric.md.

# Pattern Question Score 1 if...
P1 Longitudinal Advantage Does the cohort's serial/repeated measurement structure create a clear edge over existing cross-sectional studies? Cohort has 3+ timepoints for key variables AND no prior study used serial data for this topic.
P2 Endpoint Upgrade Can we escalate to a harder endpoint than existing studies? Cohort links to mortality/cancer/CVD registries AND existing studies stop at surrogate endpoints.
P3 Cohort Uniqueness Is the cohort's population, scale, or setting distinctive? Largest in this population, unique ethnic group, screening-based (no referral bias), or novel linkage.
P4 PI-Topic Alignment Does the PI's expertise and reputation strengthen this topic? PI has society role or 5+ papers directly in this domain. Skip if no PI specified.
P5 Comparison Table Gaps Does the THIS STUDY column show 3+ differences vs existing papers? Build comparison table (see below). 3+ checkmarks in THIS STUDY that are absent in all prior papers.
P6 Complementary Design Can this topic pair with another study from the same cohort? Two studies using the same DB but different populations or complementary variables (e.g., viral vs non-viral).

Comparison Table Construction

For each finalist, build a table comparing the top 3-5 existing papers against THIS STUDY:

| Feature | Author1 (Year) | Author2 (Year) | Author3 (Year) | THIS STUDY |
|---------|----------------|----------------|----------------|------------|
| Design | Cross-sectional | Cohort (5yr) | Cross-sectional | Cohort (20yr) |
| N | 3,200 | 8,500 | 12,000 | ~200,000 |
| Serial data | No | No | No | Yes (avg 5 visits) |
| Hard endpoint | Surrogate | Surrogate | All-cause mortality | CVD + all-cause mortality |
| Population | Referral | General | Screening | Health checkup (no referral bias) |
| Ethnicity | Western | Western | Asian (Japan) | Asian (Korea) |
| Subgroup analysis | No | Age only | No | Age + sex + comorbidity |

Score Interpretation

Total Score Recommendation
5-6 Top-tier journal target (Lancet sub, JACC, J Hepatol level)
3-4 Specialty journal target (solid publication)
1-2 Restructure or kill — find a stronger angle before proceeding

Gate: Present scoring results and comparison tables. User approves final ranking.


Phase 5: Feasibility Gate

For each scored finalist, verify practical feasibility.

Checks

  1. Sample size adequacy:

    • Cox regression: minimum 10 events per predictor variable (EPV rule)
    • Logistic regression: same EPV rule
    • For large cohorts (N>100K): warn about p-value inflation — statistically significant results are nearly guaranteed, so focus on effect size thresholds (e.g., HR >1.2 or <0.8 for clinical relevance)
    • Consider negative control strategy (EPCV) for very large samples
  2. Missing data:

    • Key exposure variable: <20% missing acceptable
    • Key outcome: <5% missing
    • If serial data: assess attrition pattern (MCAR/MAR/MNAR)
  3. Follow-up adequacy:

    • Outcome must have plausible latency within available follow-up
    • Cancer outcomes: minimum 5 years
    • CVD events: minimum 3 years
    • Mortality: minimum 5 years
  4. Operational definition:

    • Can the exposure be defined from available variables?
    • For claims data: ICD codes alone = 40-60% accuracy. Require combination strategy (diagnosis + prescription + visit frequency + special codes)
    • Cross-check expected prevalence against known epidemiological data
  5. IRB/ethics:

    • Is the data already IRB-approved for this type of analysis?
    • Any additional approvals needed for data linkage?
  6. Disease Novelty Bonus (informational, not Go/No-Go):

    • Idiopathic etiology or debated mechanism → higher journal interest
    • Established mechanism → needs stronger methodological novelty

Decision

  • Go: All checks pass.
  • Conditional Go: Minor issues solvable (e.g., missing data manageable with imputation).
  • No-Go: Fatal flaw (insufficient events, no valid endpoint, key variable unavailable).

Output: Feasibility report for each finalist with Go/Conditional/No-Go status.


Phase 6: Output — Ranked Proposals + One-Pagers

Generate the final deliverables.

Ranked Summary Table

| Rank | Topic (PICO) | Saturation | 6-Pattern Score | Feasibility | Target Journal | Timeline |
|------|--------------|------------|-----------------|-------------|----------------|----------|
| 1 | ... | Green (0 longitudinal) | 5/6 | Go | JACC | 6 months |
| 2 | ... | Green (1 longitudinal) | 4/6 | Go | Eur Heart J | 6 months |
| 3 | ... | Blue (0 papers) | 3/6 | Conditional | Radiology | 8 months |

One-Pager for Each Finalist

Use the template at ${CLAUDE_SKILL_DIR}/references/onepager_template.md.

Each one-pager includes:

  1. Title: Working title for the study
  2. Background: 3-4 sentences establishing the gap (with "Zero Papers" claim if applicable)
  3. Comparison Table: THIS STUDY vs existing papers
  4. Objective: Primary research question in PICO format
  5. Methods Summary: Study design, key variables, statistical approach
  6. PI Role: Why this PI is the right corresponding author
  7. Target Journal: With rationale (PI alignment, scope match, gap fit)
  8. Timeline: Realistic estimate (data preparation → analysis → drafting → submission)
  9. 6-Pattern Score Card: Visual breakdown of each pattern

Save one-pagers as markdown files: {output_dir}/gap_proposal_{rank}_{short_topic}.md


Skill Integration

Phase Calls to other skills
Phase 1 (PI profiling) /search-lit E-utilities for PubMed author search
Phase 3 (Saturation scan) /search-lit E-utilities for topic searches
Phase 4 (Comparison table) /search-lit for retrieving paper metadata
Downstream Output feeds into /design-study → /write-paper pipeline

What This Skill Does NOT Do

  • Does not perform the actual statistical analysis (use /analyze-stats)
  • Does not write the full manuscript (use /write-paper)
  • Does not validate study design (use /design-study)
  • Does not generate references (use /search-lit)
  • Does not make publication-ready figures (use /make-figures)

Anti-Hallucination

  • Never fabricate references. All citations must be verified via /search-lit with confirmed DOI or PMID. Mark unverified references as [UNVERIFIED - NEEDS MANUAL CHECK].
  • Never invent clinical definitions, diagnostic criteria, or guideline recommendations. If uncertain, flag with [VERIFY] and ask the user.
Files (medsci-skills)
  • references
    • cohort_profile_template.md 4.1 KB
      # Cohort Profile Template
      
      Fill in the sections below to describe the cohort database. This profile drives
      the intersection matrix and feasibility checks.
      
      ---
      
      ## Basic Information
      
      - **Cohort name:**
      - **Institution/Organization:**
      - **Country:**
      - **Population type:** (general population / health checkup / disease registry / claims data / hospital EMR)
      - **Enrollment period:** (e.g., 2002-2019)
      - **Total N at baseline:**
      - **N with follow-up data:**
      - **Mean/median follow-up duration:**
      - **Measurement intervals:** (e.g., annual, biennial, at-event)
      
      ## Variable Categories
      
      Check all that apply and list key variables in each category:
      
      - [ ] **Demographics**: (age, sex, BMI, smoking, alcohol, exercise, income, education)
      - [ ] **Laboratory**: (CBC, metabolic panel, lipid panel, liver function, kidney function, tumor markers, HbA1c, ...)
      - [ ] **Imaging**: (chest X-ray, CT, ultrasound, DEXA, mammography, ...)
      - [ ] **Questionnaires**: (PHQ-9, IPAQ, diet, sleep, quality of life, ...)
      - [ ] **Vital signs**: (BP, heart rate, ...)
      - [ ] **Anthropometry**: (height, weight, waist circumference, body composition, ...)
      - [ ] **Medications**: (prescription records, drug categories, ...)
      - [ ] **Procedures**: (surgery codes, intervention records, ...)
      - [ ] **Diagnoses**: (ICD codes, physician diagnosis, ...)
      
      ## Endpoints Available
      
      Check all that apply:
      
      - [ ] **All-cause mortality** (linkage to: ___)
      - [ ] **Cause-specific mortality** (categories: ___)
      - [ ] **Cancer incidence** (linkage to: ___)
      - [ ] **Cardiovascular events** (definition: ___)
      - [ ] **Hospitalization** (source: ___)
      - [ ] **Disease incidence** (ICD-based / physician-confirmed / registry)
      - [ ] **Other**: ___
      
      ## Special Strengths
      
      What makes this cohort unique? (check all that apply)
      
      - [ ] **Serial measurements** (same variables measured repeatedly over time)
      - [ ] **Large scale** (>100K participants)
      - [ ] **Long follow-up** (>10 years)
      - [ ] **National registry linkage** (mortality, cancer, insurance claims)
      - [ ] **Screening-based** (no referral bias — general population health checkups)
      - [ ] **Unique population** (ethnicity, occupation, geography not well-studied)
      - [ ] **Rich phenotyping** (imaging + labs + questionnaires)
      - [ ] **Biobank/genetic data available**
      - [ ] **Other**: ___
      
      ## Known Limitations
      
      - [ ] **Healthy volunteer bias** (participants may be healthier than general population)
      - [ ] **Attrition** (estimated dropout rate: ___%)
      - [ ] **Missing data** (key variables with >20% missing: ___)
      - [ ] **Limited demographics** (e.g., single sex, narrow age range, single institution)
      - [ ] **Claims-only diagnoses** (no clinical validation of ICD codes)
      - [ ] **No imaging data**
      - [ ] **No medication data**
      - [ ] **Other**: ___
      
      ## Existing Publications
      
      List known papers already published from this cohort (to avoid topic duplication):
      
      1. (Author, Year, Topic, Journal)
      2. ...
      
      ## Data Access
      
      - **IRB status:** (approved / needs application)
      - **Access method:** (on-site analysis center / remote access / direct download)
      - **Estimated turnaround:** (application to data receipt)
      - **Cost:** (if applicable)
      
      ---
      
      ## Variable Cluster Map (Auto-generated)
      
      If a codebook / data dictionary / CSV export is available, do not fill this in by hand
      and do not summarise the file yourself — run the input adapter:
      
      ```bash
      python3 "${CLAUDE_SKILL_DIR}/scripts/build_cohort_profile.py" --codebook <file> --out-dir .
      ```
      
      It writes `cohort_profile.md` + `cohort_profile.json` with:
      
      | Section | Content |
      |---------|---------|
      | Variable cluster map | every variable copied **verbatim**, with its source (`file:row`) and the keyword that placed it in its cluster |
      | Serial / repeated measures | measurement groups that genuinely repeat (evidence for **P1**) |
      | Endpoint candidates | mortality / cancer / CVD / hospitalisation variables (evidence for **P2**) |
      | `[UNKNOWN]` list | sample size, follow-up, IRB, prior publications — **ask the user; never guess** |
      
      Variables that match no cluster keyword are reported as `unclassified` rather than forced
      into a bucket. Review them: the lexicon is not exhaustive, and a mis-clustered exposure
      variable will distort the intersection matrix.
      
    • onepager_template.md 2.7 KB
      # Research Topic Proposal — One-Pager
      
      ## [Working Title]
      
      **Rank:** #X of Y | **6-Pattern Score:** X/6 | **Saturation Grade:** Green Field
      **Target Journal:** [Journal Name] | **Estimated Timeline:** X months
      
      ---
      
      ### Background
      
      [3-4 sentences establishing the clinical problem, current evidence gaps, and why this
      topic matters now. End with the "Zero Papers" claim if applicable: "To our knowledge,
      no study has examined [specific gap] using serial measurements in a [population type]."]
      
      ### Comparison Table
      
      | Feature | Author1 (Year) | Author2 (Year) | Author3 (Year) | **THIS STUDY** |
      |---------|----------------|----------------|----------------|----------------|
      | Design | | | | |
      | N | | | | |
      | Serial data | | | | |
      | Hard endpoint | | | | |
      | Population | | | | |
      | Ethnicity | | | | |
      | Follow-up | | | | |
      | Key gap | | | | |
      
      **Unique differentiators: X** (minimum 3 required)
      
      ### Objective
      
      **Primary:** [PICO format research question]
      
      **Secondary (optional):** [1-2 secondary questions]
      
      ### Methods Summary
      
      - **Study design:** Retrospective cohort study
      - **Population:** [Inclusion/exclusion criteria]
      - **Exposure:** [Key variable(s) and operational definition]
      - **Outcome:** [Primary endpoint and ascertainment method]
      - **Statistical approach:** [Key methods — Cox regression, trajectory analysis, etc.]
      - **Sample size justification:** [N eligible, expected events, EPV ratio]
      
      ### 6-Pattern Score Card
      
      | Pattern | Status | Evidence |
      |---------|--------|----------|
      | P1 Longitudinal Advantage | [+/-] | [one-line justification] |
      | P2 Endpoint Upgrade | [+/-] | [one-line justification] |
      | P3 Cohort Uniqueness | [+/-] | [one-line justification] |
      | P4 PI-Topic Alignment | [+/-] | [one-line justification] |
      | P5 Comparison Table 3+ | [+/-] | [one-line justification] |
      | P6 Complementary Design | [+/-] | [one-line justification] |
      
      ### PI Role
      
      **Corresponding Author:** [Name, Title, Affiliation]
      **Relevance:** [Why this PI is the right CA — society role, expertise, journal connections]
      
      ### Feasibility
      
      - **Go / Conditional Go / No-Go**
      - **Sample size:** N = X, expected events = Y, EPV = Z
      - **Key variables:** Available / needs derivation / missing
      - **IRB:** Covered / needs new application
      - **Data access:** Ready / X weeks to obtain
      
      ### Timeline
      
      | Phase | Duration | Milestone |
      |-------|----------|-----------|
      | Data preparation | X weeks | Clean dataset, operational definitions |
      | Analysis | X weeks | Primary + sensitivity analyses |
      | Drafting | X weeks | Full manuscript |
      | Internal review | X weeks | Co-author feedback |
      | Submission | Target date | [Journal] |
      
      ### Clinical Implications ("So What")
      
      1. [Implication for clinical practice]
      2. [Implication for screening/prevention policy]
      3. [Implication for future research directions]
      
    • pattern_scoring_rubric.md 6.4 KB
      # 6-Pattern Scoring Rubric
      
      ## Overview
      
      Score each pattern 0 (absent) or 1 (present). Total: 0-6 points.
      Interpretation: 5-6 = top-tier, 3-4 = specialty journal, 1-2 = restructure or kill.
      
      ---
      
      ## P1: Longitudinal Advantage
      
      **Question:** Does the cohort's serial/repeated measurement structure create a clear
      edge over existing cross-sectional studies?
      
      **Score 1 if ALL of:**
      - The cohort has 3+ measurement timepoints for the key exposure variable
      - No prior study on this topic used serial/trajectory data
      - The research question benefits from temporal modeling (change over time, trajectory
        clusters, time-to-event with time-varying exposure)
      
      **Score 0 if ANY of:**
      - The exposure is a one-time measurement (e.g., genetic variant, birth weight)
      - Prior longitudinal studies already exist for this topic
      - Serial data adds no interpretive value (e.g., stable demographic variable)
      
      **Examples:**
      - Score 1: Serial body composition → sarcopenia trajectory → mortality (no prior serial study)
      - Score 0: Blood type → cancer risk (blood type doesn't change over time)
      
      **Theoretical basis:** Repeated measures increase statistical efficiency by reducing
      within-subject variance and enabling trajectory-based phenotyping that cross-sectional
      designs cannot achieve (Lee et al., 2014, PMID 25464127).
      
      ---
      
      ## P2: Endpoint Upgrade
      
      **Question:** Can we escalate to a harder endpoint than existing studies?
      
      **Score 1 if BOTH of:**
      - The cohort links to mortality, cancer, or major cardiovascular event registries
      - Existing studies on this topic used only surrogate endpoints (biomarkers, imaging
        findings, composite scores) without hard clinical outcomes
      
      **Score 0 if ANY of:**
      - The cohort lacks hard endpoint linkage
      - Prior studies already reported hard endpoints for this topic
      - The research question is inherently about a surrogate (e.g., mechanism study)
      
      **Examples:**
      - Score 1: Existing studies link fatty liver to liver enzymes only; our cohort links to
        liver-related mortality and HCC incidence
      - Score 0: Existing studies already report all-cause mortality for this exposure
      
      **Endpoint hierarchy** (strongest to weakest):
      1. All-cause mortality
      2. Cause-specific mortality
      3. Major adverse events (MACE, cancer diagnosis)
      4. Hospitalization
      5. Disease incidence (physician diagnosis)
      6. Surrogate markers (lab values, imaging scores)
      
      ---
      
      ## P3: Cohort Uniqueness
      
      **Question:** Is the cohort's population, scale, or setting distinctive?
      
      **Score 1 if ANY of:**
      - Largest cohort for this topic (>5x larger than existing studies)
      - First study in this ethnic/geographic population
      - Screening-based population (no referral bias) when prior studies used hospital cohorts
      - Unique data linkage not available elsewhere (e.g., national registry + health checkup)
      - Community-dwelling general population when prior studies used disease-specific cohorts
      
      **Score 0 if:**
      - Similar-sized cohorts with the same population type have published on this topic
      
      **Examples:**
      - Score 1: 486K health checkup participants vs existing studies of 3-9K referral patients
      - Score 0: Another 500K cohort from the same country already published on this topic
      
      ---
      
      ## P4: PI-Topic Alignment
      
      **Question:** Does the PI's expertise and reputation strengthen this topic?
      
      **Score 1 if ANY of:**
      - PI holds a society leadership role directly relevant to the topic
      - PI has 5+ first/corresponding author papers in this specific domain
      - PI is an editorial board member of a target journal in this field
      
      **Score 0 if:**
      - PI's expertise is only tangentially related
      - No specific PI identified (skip this pattern; score out of 5 instead)
      
      **Why this matters:** A PI with society standing in the topic area signals that the
      study has expert oversight. Editors recognize this. The PI's name also guides target
      journal selection (e.g., hepatology society president -> J Hepatol).
      
      **When no PI is specified:** Remove this pattern from scoring. Interpret:
      4-5/5 = top-tier, 2-3/5 = specialty, 0-1/5 = restructure.
      
      ---
      
      ## P5: Comparison Table Gaps (3+)
      
      **Question:** Does the THIS STUDY column show 3+ unique features vs all existing papers?
      
      **Score 1 if:**
      - The comparison table has at least 3 rows where THIS STUDY has a checkmark/advantage
        that NO prior paper has
      
      **Score 0 if:**
      - Fewer than 3 unique differentiators
      
      **Common differentiator categories:**
      1. Study design (longitudinal vs cross-sectional)
      2. Sample size (order of magnitude larger)
      3. Serial measurements (multiple timepoints vs single)
      4. Hard endpoints (mortality vs surrogate)
      5. Population type (screening vs referral)
      6. Ethnicity/geography (first in this population)
      7. Subgroup analyses (age/sex/comorbidity stratification)
      8. Adjustment for key confounders (missing in prior studies)
      9. Exposure definition (validated operational definition vs ICD-only)
      10. Follow-up duration (significantly longer)
      
      **Construction method:**
      1. Identify 3-5 most relevant existing papers from saturation scan
      2. Create table with Feature rows and Paper columns + THIS STUDY column
      3. For each feature, check whether each paper and THIS STUDY address it
      4. Count features unique to THIS STUDY
      
      ---
      
      ## P6: Complementary Design
      
      **Question:** Can this topic pair with another study from the same cohort?
      
      **Score 1 if ANY of:**
      - A complementary analysis using the same DB but different population subset is
        feasible (e.g., diabetic vs non-diabetic; viral vs non-viral liver disease)
      - The same exposure can be studied against a different outcome in a companion paper
      - The topic creates a "series" with a previously published paper from the same cohort
      
      **Score 0 if:**
      - The topic is standalone with no natural complement
      - The complementary analysis would be trivially similar (not publishable separately)
      
      **Why this matters:** Paired papers from the same cohort strengthen both: the second
      paper can reference the first as "in this cohort, we previously showed..." and reviewers
      see a programmatic research line, not a one-off analysis.
      
      ---
      
      ## Quick Reference Card
      
      ```
      Pattern         | Key Signal
      ----------------|------------------------------------------
      P1 Longitudinal | "No prior study used serial data for this"
      P2 Endpoint     | "We add mortality/cancer to surrogate-only literature"
      P3 Uniqueness   | "Largest / first in this population / no referral bias"
      P4 PI Alignment | "PI is society president in this exact field"
      P5 Comparison   | "3+ checkmarks unique to THIS STUDY"
      P6 Complement   | "Natural pair study exists in same DB"
      ```
      
    • saturation_query_templates.md 4.3 KB
      # Literature Saturation Query Templates
      
      ## Purpose
      
      These templates help construct PubMed queries for the Phase 3 saturation scan.
      Adapt the bracketed terms to the specific topic.
      
      ---
      
      ## Basic Saturation Query
      
      ```
      ([exposure MeSH] OR [exposure free text]) AND ([outcome MeSH] OR [outcome free text])
      AND (cohort OR longitudinal OR prospective OR "follow-up")
      ```
      
      **Filters:** English, Humans, last 20 years (to capture the full landscape)
      
      ## Longitudinal-Specific Query
      
      To check if anyone has used serial/repeated measurements for this topic:
      
      ```
      ([exposure] OR [exposure synonym]) AND ([outcome] OR [outcome synonym])
      AND ("repeated measure*" OR "serial" OR "trajectory" OR "longitudinal change"
      OR "time-varying" OR "growth curve" OR "latent class trajectory")
      ```
      
      ## Meta-Analysis Check Query
      
      To verify if a meta-analysis already exists:
      
      ```
      ([exposure] OR [exposure synonym]) AND ([outcome] OR [outcome synonym])
      AND ("meta-analysis"[Publication Type] OR "systematic review"[Publication Type])
      ```
      
      If a meta-analysis exists → Red grade (avoid unless doing NMA).
      
      ## Population-Specific Queries
      
      ### Korean/Asian population filter
      ```
      AND (Korea* OR Korean OR "Republic of Korea" OR Asia* OR Japan* OR China OR Chinese
      OR Taiwan*)
      ```
      
      ### Health checkup / screening population filter
      ```
      AND ("health checkup" OR "health screening" OR "health examination" OR "medical checkup"
      OR "periodic health exam*" OR "annual exam*")
      ```
      
      ### Large cohort filter (to find comparator studies)
      ```
      AND ("national health insurance" OR "claims data" OR "administrative data"
      OR "population-based" OR "nationwide" OR "registry")
      ```
      
      ---
      
      ## Saturation Grading Protocol
      
      After running the basic saturation query:
      
      ### Step 1: Count total results
      - 0-2: Blue Ocean
      - 3-10: Possible Green Field (proceed to Step 2)
      - 10-30: Possible Yellow (proceed to Step 2)
      - 30+: Likely Red (check for MA in Step 3)
      
      ### Step 2: Check longitudinal gap
      Run the longitudinal-specific query.
      - 0 results with serial/trajectory data → upgrade one grade
      - 1-2 results → maintain current grade
      - 3+ results → no upgrade
      
      ### Step 3: Check meta-analysis existence
      Run the MA check query.
      - MA exists and is comprehensive → Red (firm)
      - MA exists but outdated (>5 years) or limited scope → Yellow (update MA possible)
      - No MA → maintain current grade
      
      ### Step 4: Final grade assignment
      
      | Base Count | Longitudinal Papers | MA Exists? | Final Grade |
      |------------|-------------------|------------|-------------|
      | 0-2 | 0 | No | Blue Ocean |
      | 3-10 | 0 | No | **Green Field** |
      | 3-10 | 1-2 | No | Yellow |
      | 10-30 | 0 | No | Green Field (upgraded) |
      | 10-30 | 1-2 | No | Yellow |
      | 10-30 | 3+ | No | Yellow |
      | 30+ | Any | No | Yellow (borderline Red) |
      | Any | Any | Yes (recent) | Red |
      | Any | Any | Yes (outdated) | Yellow |
      
      ---
      
      ## Example: Fatty Liver and Cardiovascular Mortality
      
      ### Basic query
      ```
      ("fatty liver" OR "hepatic steatosis" OR NAFLD OR MASLD) AND
      ("cardiovascular mortality" OR "cardiac death" OR "MACE")
      AND (cohort OR longitudinal OR prospective)
      ```
      Result: ~45 papers → base grade Red
      
      ### Longitudinal check
      ```
      ("fatty liver" OR "hepatic steatosis") AND ("cardiovascular mortality")
      AND ("trajectory" OR "serial" OR "repeated measure*" OR "longitudinal change")
      ```
      Result: 2 papers → no upgrade
      
      ### MA check
      ```
      ("fatty liver" OR NAFLD) AND ("cardiovascular mortality")
      AND ("meta-analysis"[PT] OR "systematic review"[PT])
      ```
      Result: 3 MAs → confirmed Red
      
      **Conclusion:** Avoid this topic unless using truly unique data angle.
      
      ---
      
      ## Tips for Effective Saturation Scanning
      
      1. **Start broad, then narrow.** If the broad query returns >30, add population or
         design filters to find the exact niche.
      
      2. **Check the "last 3 years" subset.** A topic with 20 total papers but 15 in the
         last 3 years is trending (good for timeliness, bad for novelty).
      
      3. **Read the most recent review article.** It maps the field faster than scanning
         individual papers. Look for "future research directions" sections.
      
      4. **Check for registered protocols.** Search PROSPERO or ClinicalTrials.gov for
         ongoing studies that haven't published yet — these are invisible competitors.
      
      5. **Use Semantic Scholar** for citation network analysis. A paper with 200+ citations
         on this exact topic means the field is well-established.
      
  • scripts
    • build_cohort_profile.py 23.2 KB
      #!/usr/bin/env python3
      """Build a cohort profile from a local codebook (and optional domain context).
      
      `/find-cohort-gap` used to start from a *named* database — NHIS, UK Biobank, and the
      handful of registries the skill knows about. Most researchers do not have one of those.
      They have an institutional registry, a single-centre EMR export, or a specialty cohort,
      described by a data dictionary nobody else has ever seen (issue #69).
      
      This is the input layer that lets them in. It reads their codebook and emits the same
      cohort profile the skill already consumes, so everything downstream — the intersection
      matrix, saturation scan, 6-pattern scoring — is unchanged.
      
      WHY A SCRIPT AND NOT JUST READING THE FILE. A language model asked to "summarise this
      codebook" will paraphrase a variable name, merge two variables that look alike, or
      quietly invent one that the cohort does not have — and every downstream claim inherits
      it. So the variable inventory is *enumerated*, never generated: each variable is copied
      verbatim from the file and carries its provenance (`file:row`), which is exactly the
      dictionary-first discipline that a reviewer expects of a DB-backed study.
      
      WHAT IT REFUSES TO DO. A codebook lists variables; it does not state the sample size,
      the enrollment window, the follow-up duration, or the IRB status. Those are emitted as
      `[UNKNOWN - ask the user]`, never guessed — a fabricated N is worse than a missing one,
      because it survives all the way to a feasibility gate that then passes for the wrong
      reason.
      
      WHAT IT INFERS (and shows its work). Two structural facts *are* derivable from variable
      names alone, and both feed patterns the skill already scores:
      
        * serial / repeated measures (`bp_v1`, `bp_v2`, `visit2_hba1c`, ...) -> P1 Longitudinal
          Advantage. Reported as the actual variable groups, so the claim is auditable.
        * endpoint-like variables (`death_date`, `cvd_event`, `cancer_incidence`) -> P2
          Endpoint Upgrade.
      
      Every cluster assignment records the keyword that triggered it, and anything that matches
      nothing is left `unclassified` rather than forced into a bucket.
      
      Usage:
          build_cohort_profile.py --codebook dict.csv [--codebook more.xlsx ...] \\
              [--context review.pdf --context https://example.org/guideline] \\
              [--cohort-name "Institutional CT registry"] [--out-dir .]
      
      Formats: .csv / .tsv / .json / .md / .txt (stdlib), .xlsx (needs openpyxl),
      .pdf (needs `pdftotext` from poppler). Context URLs are fetched with stdlib urllib;
      a paywalled or JavaScript-rendered page will not extract, and that is reported rather
      than papered over.
      """
      
      from __future__ import annotations
      
      import argparse
      import csv
      import html.parser
      import json
      import re
      import shutil
      import subprocess
      import sys
      import urllib.error
      import urllib.request
      from pathlib import Path
      
      UNKNOWN = "[UNKNOWN - ask the user]"
      
      # Variable-name lexicons. Each cluster maps to the template's category names, and the
      # matched keyword is recorded so a wrong assignment is visible instead of silent.
      #
      # Short keys are matched as WHOLE TOKENS, not substrings (see `classify`). Substring
      # matching on a two-letter abbreviation is a false-positive machine: `us` (ultrasound)
      # fires on `statin_use`, and `age` fires on `storage_temp`. Long keys keep substring
      # matching so that `smok` still catches `smoking_status`.
      CLUSTERS: dict[str, tuple[str, ...]] = {
          "demographics": (
              "age", "sex", "gender", "birth", "race", "ethnic", "income", "educat",
              "marital", "occupation", "smok", "alcohol", "drink", "exercise", "activity",
          ),
          "anthropometry": ("height", "weight", "bmi", "waist", "hip", "circumf", "body_fat", "muscle"),
          "vital_signs": ("sbp", "dbp", "bp", "blood_pressure", "pulse", "heart_rate", "hr", "resp_rate", "temp"),
          "laboratory": (
              "glucose", "glu", "hba1c", "chol", "lipid", "ldl", "hdl", "triglyc", "tg", "ast", "alt",
              "ggt", "bilirubin", "albumin", "creatinin", "egfr", "bun", "uric", "crp", "esr",
              "hb", "hgb", "hct", "wbc", "rbc", "platelet", "psa", "cea", "afp", "ca19", "tsh",
              "insulin", "lab",
          ),
          "imaging": (
              "ct", "mri", "xray", "x_ray", "cxr", "ultrasound", "us", "sono", "dexa",
              "dxa", "mammo", "echo", "angio", "pet", "cac", "calcium_score", "imaging", "radiol",
          ),
          "questionnaire": (
              "phq", "gad", "psqi", "ipaq", "sf36", "sf_36", "eq5d", "eq_5d", "qol", "questionn",
              "survey", "scale", "sleep", "diet", "food_freq", "ffq", "stress", "depress",
          ),
          "medication": ("med", "drug", "rx", "prescri", "statin", "antihyp", "therapy", "treat"),
          "procedure": ("surg", "operat", "procedur", "biopsy", "resect", "ablat", "stent", "pci", "cabg", "intervention"),
          "diagnosis": ("icd", "diag", "dx", "disease", "hypertension", "diabetes", "history_of", "hx", "comorbid"),
          "identifier_admin": ("id", "key", "index", "seq", "code", "visit_date", "site", "center"),
      }
      
      # Below this length a keyword is matched as a whole token, never as a substring.
      TOKEN_ONLY = 4
      
      # Endpoint-like variables. A cohort's value proposition is usually its hard endpoints, and
      # the skill scores that (P2), so they get both a cluster of their own (`outcome_endpoint`,
      # assigned only when no other cluster claims the variable) and an explicit candidate list.
      ENDPOINT_HINTS = (
          "death", "mortal", "expire", "died", "survival", "cancer", "malign", "incid",
          "cvd", "chd", "mace", "stroke", "myocard", "mi", "infarct", "event", "outcome",
          "hospital", "admission", "readmit", "recurrence", "progression", "relapse",
      )
      
      # Serial / repeated measures: <stem><separator><index>, or an explicit visit/wave marker.
      SERIAL_SUFFIX = re.compile(r"^(?P<stem>.+?)[ _\-.]?(?:v|t|w|wave|visit|yr|y|round|r|time)?(?P<idx>\d{1,2})$", re.I)
      SERIAL_PREFIX = re.compile(r"^(?:v|t|w|wave|visit|yr|y|round|time)(?P<idx>\d{1,2})[ _\-.](?P<stem>.+)$", re.I)
      
      # A column in a codebook that holds the variable NAME (as opposed to its description).
      NAME_COL = re.compile(r"^\s*(variable|var|var_?name|name|field|field_?name|column|col|item|code)\s*$", re.I)
      DESC_COL = re.compile(r"^\s*(desc|description|label|definition|meaning|explanation|comment|note|한글|설명)", re.I)
      
      
      class _Strip(html.parser.HTMLParser):
          """Minimal HTML -> text. Not a browser: a JS-rendered page yields nothing, and the
          caller is told so rather than being handed an empty 'context'."""
      
          def __init__(self) -> None:
              super().__init__()
              self.chunks: list[str] = []
              self._skip = 0
      
          def handle_starttag(self, tag, attrs):
              if tag in ("script", "style", "nav", "footer"):
                  self._skip += 1
      
          def handle_endtag(self, tag):
              if tag in ("script", "style", "nav", "footer") and self._skip:
                  self._skip -= 1
      
          def handle_data(self, data):
              if not self._skip and data.strip():
                  self.chunks.append(data.strip())
      
      
      # --------------------------------------------------------------------------------------
      # Variable extraction — one function per format, all returning (name, description, where)
      # --------------------------------------------------------------------------------------
      
      Var = tuple[str, str, str]
      
      
      def _from_delimited(path: Path, delim: str) -> list[Var]:
          """A .csv/.tsv is either a CODEBOOK (rows are variables) or a DATA export (columns are
          variables). Decide by whether a header column names a variable column."""
          with path.open(newline="", encoding="utf-8-sig", errors="replace") as fh:
              rows = list(csv.reader(fh, delimiter=delim))
          if not rows:
              return []
          header = rows[0]
          name_idx = next((i for i, h in enumerate(header) if NAME_COL.match(h or "")), None)
      
          if name_idx is None:  # data export: the header row IS the variable list
              return [
                  (h.strip(), "", f"{path.name}:1 (column {i + 1})")
                  for i, h in enumerate(header)
                  if h and h.strip()
              ]
      
          desc_idx = next((i for i, h in enumerate(header) if DESC_COL.match(h or "")), None)
          out: list[Var] = []
          for r, row in enumerate(rows[1:], start=2):
              if name_idx >= len(row) or not (row[name_idx] or "").strip():
                  continue
              desc = row[desc_idx].strip() if desc_idx is not None and desc_idx < len(row) else ""
              out.append((row[name_idx].strip(), desc, f"{path.name}:{r}"))
          return out
      
      
      def _from_json(path: Path) -> list[Var]:
          data = json.loads(path.read_text(encoding="utf-8"))
          out: list[Var] = []
          if isinstance(data, dict):
              for i, (k, v) in enumerate(data.items(), start=1):
                  desc = v if isinstance(v, str) else (v.get("description", "") if isinstance(v, dict) else "")
                  out.append((str(k), str(desc), f"{path.name}:key {i}"))
          elif isinstance(data, list):
              for i, item in enumerate(data, start=1):
                  if isinstance(item, str):
                      out.append((item, "", f"{path.name}:item {i}"))
                  elif isinstance(item, dict):
                      name = next((item[k] for k in ("variable", "var", "name", "field", "column") if k in item), None)
                      if name:
                          desc = next((item[k] for k in ("description", "desc", "label", "definition") if k in item), "")
                          out.append((str(name), str(desc), f"{path.name}:item {i}"))
          return out
      
      
      def _from_markdown(text: str, origin: str) -> list[Var]:
          """A markdown/plain codebook: a pipe table, or `var` — description lines."""
          out: list[Var] = []
          for i, line in enumerate(text.splitlines(), start=1):
              s = line.strip()
              if not s or set(s) <= set("|-: "):  # separator row
                  continue
              if s.startswith("|"):
                  cells = [c.strip().strip("`*") for c in s.strip("|").split("|")]
                  if len(cells) >= 1 and cells[0] and not NAME_COL.match(cells[0]):
                      out.append((cells[0], cells[1] if len(cells) > 1 else "", f"{origin}:{i}"))
                  continue
              m = re.match(r"^[-*+]?\s*`([^`]+)`\s*[-–—:]?\s*(.*)$", s)
              if m:
                  out.append((m.group(1).strip(), m.group(2).strip(), f"{origin}:{i}"))
          return out
      
      
      def _from_xlsx(path: Path) -> list[Var]:
          try:
              from openpyxl import load_workbook  # type: ignore
          except ImportError:
              raise SystemExit(
                  f"reading {path.name} needs openpyxl (`pip install openpyxl`). "
                  "Or export the sheet to .csv and pass that — the CSV path is stdlib-only."
              )
          ws = load_workbook(path, read_only=True, data_only=True).active
          rows = [[("" if c is None else str(c)) for c in row] for row in ws.iter_rows(values_only=True)]
          if not rows:
              return []
          header = rows[0]
          name_idx = next((i for i, h in enumerate(header) if NAME_COL.match(h or "")), None)
          if name_idx is None:
              return [(h.strip(), "", f"{path.name}:1 (column {i + 1})") for i, h in enumerate(header) if h.strip()]
          desc_idx = next((i for i, h in enumerate(header) if DESC_COL.match(h or "")), None)
          out: list[Var] = []
          for r, row in enumerate(rows[1:], start=2):
              if name_idx >= len(row) or not row[name_idx].strip():
                  continue
              desc = row[desc_idx].strip() if desc_idx is not None and desc_idx < len(row) else ""
              out.append((row[name_idx].strip(), desc, f"{path.name}:{r}"))
          return out
      
      
      def _pdf_text(path: Path) -> str:
          if not shutil.which("pdftotext"):
              raise SystemExit(
                  f"reading {path.name} needs `pdftotext` (poppler: `brew install poppler` / "
                  "`apt install poppler-utils`). Or convert the PDF to .md / .txt and pass that."
              )
          p = subprocess.run(["pdftotext", "-layout", str(path), "-"], capture_output=True, text=True)
          if p.returncode != 0:
              raise SystemExit(f"pdftotext failed on {path.name}: {p.stderr.strip()}")
          return p.stdout
      
      
      def read_codebook(path: Path) -> list[Var]:
          if not path.is_file():
              raise SystemExit(f"not found: {path}")
          suf = path.suffix.lower()
          if suf == ".csv":
              return _from_delimited(path, ",")
          if suf in (".tsv", ".tab"):
              return _from_delimited(path, "\t")
          if suf == ".json":
              return _from_json(path)
          if suf == ".xlsx":
              return _from_xlsx(path)
          if suf == ".pdf":
              return _from_markdown(_pdf_text(path), path.name)
          if suf in (".md", ".markdown", ".txt"):
              return _from_markdown(path.read_text(encoding="utf-8", errors="replace"), path.name)
          raise SystemExit(f"unsupported codebook format: {path.suffix} ({path.name})")
      
      
      # --------------------------------------------------------------------------------------
      # Structure inference — clusters, serial groups, endpoints
      # --------------------------------------------------------------------------------------
      
      
      def _tokens(s: str) -> set[str]:
          return set(re.split(r"[^a-z0-9]+", s.lower())) - {""}
      
      
      def _hits(key: str, hay: str, toks: set[str]) -> bool:
          """A short key must BE a token; a long key may appear anywhere.
      
          This is the whole defence against the abbreviation false positive: `us` matching
          `statin_use`, `age` matching `storage_temp`, `id` matching `lipid`.
          """
          if len(key) >= TOKEN_ONLY or "_" in key:
              return key in hay
          return key in toks
      
      
      def classify(name: str, desc: str) -> tuple[str, str]:
          """Return (cluster, matched_keyword). Unmatched stays 'unclassified' — a variable is
          never forced into a bucket to make the map look complete.
      
          Endpoints are resolved LAST, and only against variables no other cluster claimed, so a
          cohort's hard endpoint lands in `outcome_endpoint` instead of `unclassified`. Otherwise
          the profile contradicts itself: `death_date` would be listed as "matched nothing —
          review it" in the cluster map and as the P2 evidence two sections below.
          """
          hay = f"{name} {desc}".lower()
          toks = _tokens(hay)
          for cluster, keys in CLUSTERS.items():
              for k in keys:
                  if _hits(k, hay, toks):
                      return cluster, k
          kw = is_endpoint(name, desc)
          if kw:
              return "outcome_endpoint", kw
          return "unclassified", ""
      
      
      def is_endpoint(name: str, desc: str) -> str:
          hay = f"{name} {desc}".lower()
          toks = _tokens(hay)
          return next((k for k in ENDPOINT_HINTS if _hits(k, hay, toks)), "")
      
      
      def serial_groups(names: list[str]) -> dict[str, list[str]]:
          """Group variables that look like the same measurement repeated over time.
      
          Only a stem seen with >= 2 distinct indices counts: a lone `visit1_bp` is not evidence
          of serial data, and claiming otherwise would hand P1 a free point it has not earned.
          """
          stems: dict[str, dict[str, str]] = {}
          for n in names:
              for rx in (SERIAL_PREFIX, SERIAL_SUFFIX):
                  m = rx.match(n)
                  if m:
                      stem = m.group("stem").strip("_- .").lower()
                      if stem:
                          stems.setdefault(stem, {})[m.group("idx")] = n
                      break
          return {
              stem: [v for _, v in sorted(idx.items(), key=lambda kv: int(kv[0]))]
              for stem, idx in stems.items()
              if len(idx) >= 2
          }
      
      
      # --------------------------------------------------------------------------------------
      # Domain context (a review, a guideline page) — extracted, never summarised here
      # --------------------------------------------------------------------------------------
      
      
      def read_context(src: str) -> tuple[str, str]:
          """Return (text, provenance). The LLM reads this; the script only fetches it."""
          if src.startswith(("http://", "https://")):
              try:
                  req = urllib.request.Request(src, headers={"User-Agent": "medsci-skills/find-cohort-gap"})
                  with urllib.request.urlopen(req, timeout=30) as r:  # noqa: S310 - user-supplied context URL
                      raw = r.read().decode("utf-8", errors="replace")
              except (urllib.error.URLError, TimeoutError, ValueError) as exc:
                  raise SystemExit(
                      f"could not fetch {src}: {exc}\n"
                      "If the page is paywalled or JavaScript-rendered (UpToDate and many guideline "
                      "portals are), save it as PDF/markdown and pass the file instead."
                  )
              p = _Strip()
              p.feed(raw)
              text = "\n".join(p.chunks)
              if len(text.split()) < 50:
                  raise SystemExit(
                      f"{src} yielded almost no text ({len(text.split())} words) — it is probably "
                      "JavaScript-rendered or paywalled. Save it as PDF/markdown and pass the file."
                  )
              return text, src
          path = Path(src)
          if not path.is_file():
              raise SystemExit(f"not found: {src}")
          if path.suffix.lower() == ".pdf":
              return _pdf_text(path), path.name
          return path.read_text(encoding="utf-8", errors="replace"), path.name
      
      
      # --------------------------------------------------------------------------------------
      
      
      def build(codebooks: list[Path], contexts: list[str], cohort_name: str | None) -> tuple[dict, str]:
          variables: list[dict] = []
          seen: set[str] = set()
          for cb in codebooks:
              for name, desc, where in read_codebook(cb):
                  if name.lower() in seen:
                      continue
                  seen.add(name.lower())
                  cluster, kw = classify(name, desc)
                  variables.append(
                      {
                          "name": name,
                          "description": desc,
                          "source": where,
                          "cluster": cluster,
                          "matched_keyword": kw,
                          "endpoint_hint": is_endpoint(name, desc),
                      }
                  )
      
          names = [v["name"] for v in variables]
          serial = serial_groups(names)
          endpoints = [v for v in variables if v["endpoint_hint"]]
      
          by_cluster: dict[str, list[str]] = {}
          for v in variables:
              by_cluster.setdefault(v["cluster"], []).append(v["name"])
      
          ctx: list[dict] = []
          for c in contexts:
              text, prov = read_context(c)
              ctx.append({"source": prov, "words": len(text.split()), "text": text})
      
          profile = {
              "cohort_name": cohort_name or UNKNOWN,
              "codebooks": [str(c) for c in codebooks],
              "n_variables": len(variables),
              "variables": variables,
              "clusters": {k: sorted(v) for k, v in sorted(by_cluster.items())},
              "serial_groups": serial,
              "endpoint_candidates": [v["name"] for v in endpoints],
              "context_documents": [{"source": c["source"], "words": c["words"]} for c in ctx],
              # Not derivable from a codebook. Guessing any of these corrupts the feasibility gate.
              "must_ask_user": {
                  "n_baseline": UNKNOWN,
                  "n_with_followup": UNKNOWN,
                  "enrollment_period": UNKNOWN,
                  "followup_duration": UNKNOWN,
                  "measurement_intervals": UNKNOWN,
                  "irb_status": UNKNOWN,
                  "existing_publications": UNKNOWN,
                  "known_limitations": UNKNOWN,
              },
          }
          return profile, "\n\n".join(f"# Context: {c['source']}\n\n{c['text']}" for c in ctx)
      
      
      def render_markdown(p: dict) -> str:
          L = [
              "# Cohort Profile (auto-generated)",
              "",
              f"**Cohort name:** {p['cohort_name']}",
              f"**Codebook(s):** {', '.join(p['codebooks']) or UNKNOWN}",
              f"**Variables enumerated:** {p['n_variables']}",
              "",
              "> Every variable below is copied verbatim from the codebook and carries its source",
              "> location. Nothing here is inferred except the cluster assignment and the serial /",
              "> endpoint flags, each of which shows the keyword that triggered it.",
              "",
              "## Variable Cluster Map",
              "",
              "| Cluster | N | Variables |",
              "|---------|--:|-----------|",
          ]
          for cluster, vs in p["clusters"].items():
              shown = ", ".join(f"`{v}`" for v in vs[:12])
              if len(vs) > 12:
                  shown += f", … (+{len(vs) - 12})"
              L.append(f"| {cluster} | {len(vs)} | {shown} |")
      
          L += ["", "## Serial / repeated measures (evidence for P1 Longitudinal Advantage)", ""]
          if p["serial_groups"]:
              L += ["| Measurement | Timepoints | Variables |", "|-------------|-----------:|-----------|"]
              for stem, vs in sorted(p["serial_groups"].items()):
                  L.append(f"| {stem} | {len(vs)} | {', '.join(f'`{v}`' for v in vs)} |")
          else:
              L.append("None detected from variable names. If the cohort *does* have repeated measures,")
              L.append("say so — the naming convention may simply not encode the timepoint.")
      
          L += ["", "## Endpoint candidates (evidence for P2 Endpoint Upgrade)", ""]
          if p["endpoint_candidates"]:
              L += [", ".join(f"`{v}`" for v in p["endpoint_candidates"])]
          else:
              L.append("None detected. Without a hard endpoint, P2 cannot score and the cohort is")
              L.append("limited to cross-sectional questions — confirm before proceeding.")
      
          if p["context_documents"]:
              L += ["", "## Domain context supplied", ""]
              for c in p["context_documents"]:
                  L.append(f"- {c['source']} ({c['words']:,} words) — see `context_extract.md`")
      
          L += [
              "",
              "## Not derivable from a codebook — ASK THE USER before Phase 2",
              "",
              "A data dictionary lists variables. It does not state any of the following, and a",
              "guessed value here would silently pass the Phase 5 feasibility gate for the wrong reason.",
              "",
          ]
          L += [f"- **{k.replace('_', ' ')}:** {v}" for k, v in p["must_ask_user"].items()]
          return "\n".join(L) + "\n"
      
      
      def main() -> int:
          ap = argparse.ArgumentParser(
              description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
          )
          ap.add_argument("--codebook", action="append", type=Path, required=True,
                          help="data dictionary / codebook / CSV header (repeatable)")
          ap.add_argument("--context", action="append", default=[],
                          help="domain context: a review/guideline file (.md/.txt/.pdf) or a URL (repeatable)")
          ap.add_argument("--cohort-name", help="what the cohort is called")
          ap.add_argument("--out-dir", type=Path, default=Path("."))
          a = ap.parse_args()
      
          profile, context_text = build(a.codebook, a.context, a.cohort_name)
          if not profile["n_variables"]:
              raise SystemExit(
                  "no variables found. If this is a data export, the first row must be the header; "
                  "if it is a codebook, one column must be named variable / var / name / field / column."
              )
      
          a.out_dir.mkdir(parents=True, exist_ok=True)
          (a.out_dir / "cohort_profile.json").write_text(json.dumps(profile, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
          (a.out_dir / "cohort_profile.md").write_text(render_markdown(profile), encoding="utf-8")
          if context_text:
              (a.out_dir / "context_extract.md").write_text(context_text + "\n", encoding="utf-8")
      
          print(f"{profile['n_variables']} variables enumerated from {len(a.codebook)} codebook(s)")
          for cluster, vs in profile["clusters"].items():
              print(f"  {cluster:<18} {len(vs)}")
          print(f"  serial groups      {len(profile['serial_groups'])}")
          print(f"  endpoint candidates {len(profile['endpoint_candidates'])}")
          unclassified = len(profile["clusters"].get("unclassified", []))
          if unclassified:
              print(f"\n{unclassified} variable(s) matched no cluster — review them; the lexicon is not exhaustive.")
          print(f"\nwrote {a.out_dir / 'cohort_profile.md'} + cohort_profile.json")
          print("ASK THE USER for: " + ", ".join(k.replace("_", " ") for k in profile["must_ask_user"]))
          return 0
      
      
      if __name__ == "__main__":
          sys.exit(main())
      
  • tests
    • test_cohort_profile.sh 8.9 KB
      #!/usr/bin/env bash
      # Regression test for skills/find-cohort-gap/scripts/build_cohort_profile.py — the
      # local-codebook / document input layer (issue #69).
      #
      # The contract under test is as much about what the adapter REFUSES to do as what it
      # extracts: variables are enumerated verbatim with provenance (never paraphrased or
      # invented), a sample size that the codebook does not state stays [UNKNOWN], and a serial
      # structure is only claimed when a measurement really does repeat.
      set -u
      
      REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
      B="$REPO_ROOT/skills/find-cohort-gap/scripts/build_cohort_profile.py"
      TMP="$(mktemp -d)"
      trap 'rm -rf "$TMP"' EXIT
      
      pass=0
      fail=0
      ck() {
        local label="$1" expected="$2" actual="$3"
        if [ "$expected" = "$actual" ]; then
          printf '  PASS  %-54s exit=%s\n' "$label" "$actual"
          pass=$((pass + 1))
        else
          printf '  FAIL  %-54s expected=%s actual=%s\n' "$label" "$expected" "$actual"
          fail=$((fail + 1))
        fi
      }
      
      # --- a CODEBOOK (rows are variables, one column names them) ---
      cat > "$TMP/codebook.csv" <<'CSV'
      variable,description,type
      subject_id,Study identifier,char
      age,Age at baseline in years,num
      sex,Biological sex,char
      sbp_v1,Systolic blood pressure at visit 1,num
      sbp_v2,Systolic blood pressure at visit 2,num
      sbp_v3,Systolic blood pressure at visit 3,num
      hba1c,Glycated haemoglobin,num
      ct_lung_nodule,Lung nodule seen on chest CT,char
      phq9_total,PHQ-9 depression score,num
      statin_use,Statin prescription at baseline,char
      death_date,Date of death from national registry,date
      cvd_event,Incident cardiovascular event,char
      weird_unmatchable_token,,char
      CSV
      
      # --- a DATA EXPORT (columns are the variables; no codebook column) ---
      printf 'age,sex,ldl_chol,mri_brain,death_date\n45,M,130,normal,\n' > "$TMP/export.csv"
      
      # --- a markdown codebook (pipe table AND bullet/backtick lines) ---
      cat > "$TMP/codebook.md" <<'MD'
      | variable | description |
      |----------|-------------|
      | waist_circumference | Waist circumference (cm) |
      
      Additional variables:
      
      - `alt` — Alanine aminotransferase
      MD
      
      # --- a JSON codebook ---
      cat > "$TMP/codebook.json" <<'JSON'
      {"egfr": "Estimated glomerular filtration rate", "cancer_incidence": "Incident cancer from registry"}
      JSON
      
      # 1) a codebook is read and every variable is enumerated
      python3 "$B" --codebook "$TMP/codebook.csv" --cohort-name "Test registry" --out-dir "$TMP/o1" > /dev/null 2>&1
      ck "codebook (rows = variables) is read" 0 "$?"
      
      python3 - "$TMP/o1/cohort_profile.json" <<'PY'
      import json, sys
      p = json.load(open(sys.argv[1]))
      names = [v["name"] for v in p["variables"]]
      assert len(names) == 13, f"expected 13 variables, got {len(names)}: {names}"
      # verbatim, not paraphrased
      for must in ("sbp_v1", "ct_lung_nodule", "phq9_total", "weird_unmatchable_token"):
          assert must in names, f"{must} missing — variables must be copied verbatim"
      # provenance points at the real row
      src = {v["name"]: v["source"] for v in p["variables"]}
      assert src["age"].endswith(":3"), f"bad provenance for age: {src['age']}"
      PY
      ck "variables verbatim + provenance (file:row)" 0 "$?"
      
      # 2) clusters are assigned, and an unmatchable variable is NOT forced into a bucket
      python3 - "$TMP/o1/cohort_profile.json" <<'PY'
      import json, sys
      p = json.load(open(sys.argv[1]))
      c = {v["name"]: v["cluster"] for v in p["variables"]}
      assert c["age"] == "demographics", c["age"]
      assert c["hba1c"] == "laboratory", c["hba1c"]
      assert c["ct_lung_nodule"] == "imaging", c["ct_lung_nodule"]
      assert c["phq9_total"] == "questionnaire", c["phq9_total"]
      assert c["statin_use"] == "medication", c["statin_use"]
      assert c["weird_unmatchable_token"] == "unclassified", c["weird_unmatchable_token"]
      # a hard endpoint gets its own cluster — it must never read as "matched nothing, review it"
      # in the cluster map while being cited as P2 evidence two sections below.
      assert c["death_date"] == "outcome_endpoint", c["death_date"]
      assert c["cvd_event"] == "outcome_endpoint", c["cvd_event"]
      assert "death_date" not in p["clusters"].get("unclassified", []), "endpoint landed in unclassified"
      # every assignment shows the keyword that caused it
      assert all(v["matched_keyword"] for v in p["variables"] if v["cluster"] != "unclassified")
      PY
      ck "clusters assigned; endpoints get outcome_endpoint" 0 "$?"
      
      # 2b) the abbreviation false positive: a short keyword must match a whole TOKEN, never a
      # substring. `us` (ultrasound) inside `statin_use` once made a statin an imaging variable.
      # -B: importing the module must not leave a __pycache__ artifact in the skill directory.
      python3 -B - "$B" <<'PY'
      import importlib.util, sys
      spec = importlib.util.spec_from_file_location("bcp", sys.argv[1])
      m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
      cases = [
          ("statin_use", "Statin prescription", "medication"),   # not imaging via "us"
          ("lipid_panel", "Lipid panel", "laboratory"),          # not identifier_admin via "id"
          ("ct_lung_nodule", "Chest CT nodule", "imaging"),      # token "ct" still works
          ("sbp_v1", "Systolic BP visit 1", "vital_signs"),
      ]
      for name, desc, want in cases:
          got, kw = m.classify(name, desc)
          assert got == want, f"{name!r} -> {got} (via {kw!r}), expected {want}"
      PY
      ck "short keywords match whole tokens, not substrings" 0 "$?"
      
      # 3) serial structure detected — the P1 evidence — and only when it really repeats
      python3 - "$TMP/o1/cohort_profile.json" <<'PY'
      import json, sys
      p = json.load(open(sys.argv[1]))
      sg = p["serial_groups"]
      assert "sbp" in sg, f"sbp_v1/v2/v3 not detected as serial: {sg}"
      assert sg["sbp"] == ["sbp_v1", "sbp_v2", "sbp_v3"], sg["sbp"]
      # phq9_total ends in a digit but never repeats -> must NOT be claimed as serial
      assert "phq" not in sg and "phq9_total" not in sg, f"false serial group: {sg}"
      PY
      ck "serial group found; a lone digit-suffixed var is not one" 0 "$?"
      
      # 4) endpoints surfaced (P2 evidence)
      python3 - "$TMP/o1/cohort_profile.json" <<'PY'
      import json, sys
      p = json.load(open(sys.argv[1]))
      e = set(p["endpoint_candidates"])
      assert {"death_date", "cvd_event"} <= e, e
      assert "age" not in e
      PY
      ck "endpoint candidates surfaced (death, cvd event)" 0 "$?"
      
      # 5) THE ANTI-HALLUCINATION CONTRACT: what a codebook cannot state stays UNKNOWN
      python3 - "$TMP/o1/cohort_profile.json" "$TMP/o1/cohort_profile.md" <<'PY'
      import json, re, sys
      p = json.load(open(sys.argv[1]))
      must = p["must_ask_user"]
      for k in ("n_baseline", "enrollment_period", "followup_duration", "irb_status"):
          assert must[k] == "[UNKNOWN - ask the user]", f"{k} was invented: {must[k]!r}"
      md = open(sys.argv[2]).read()
      assert "ASK THE USER" in md.upper()
      # no fabricated sample size anywhere in the rendered profile
      assert not re.search(r"\bN\s*=\s*[\d,]+", md), "a sample size appeared from nowhere"
      PY
      ck "un-stated facts stay [UNKNOWN], never guessed" 0 "$?"
      
      # 6) a DATA EXPORT (header row = variables) is routed correctly
      python3 "$B" --codebook "$TMP/export.csv" --out-dir "$TMP/o2" > /dev/null 2>&1
      python3 - "$TMP/o2/cohort_profile.json" <<'PY'
      import json, sys
      p = json.load(open(sys.argv[1]))
      names = [v["name"] for v in p["variables"]]
      assert names == ["age", "sex", "ldl_chol", "mri_brain", "death_date"], names
      assert not any(v["name"] == "45" for v in p["variables"]), "read a data row as a variable"
      PY
      ck "data export (header = variables) routed correctly" 0 "$?"
      
      # 7) markdown + json codebooks, and multiple codebooks merge without duplicates
      python3 "$B" --codebook "$TMP/codebook.md" --codebook "$TMP/codebook.json" \
        --codebook "$TMP/codebook.csv" --out-dir "$TMP/o3" > /dev/null 2>&1
      python3 - "$TMP/o3/cohort_profile.json" <<'PY'
      import json, sys
      p = json.load(open(sys.argv[1]))
      names = [v["name"] for v in p["variables"]]
      for must in ("waist_circumference", "alt", "egfr", "cancer_incidence", "sbp_v1"):
          assert must in names, f"{must} missing from merged profile"
      assert len(names) == len(set(n.lower() for n in names)), "duplicate variables across codebooks"
      PY
      ck "markdown + json + csv codebooks merge, de-duplicated" 0 "$?"
      
      # 8) a local document is attached as domain context (no URL needed)
      cat > "$TMP/review.md" <<'MD'
      # Narrative review: coronary calcium and mortality
      Coronary artery calcium scoring predicts cardiovascular events across populations.
      Repeated scanning and its incremental value remain debated.
      MD
      python3 "$B" --codebook "$TMP/codebook.csv" --context "$TMP/review.md" --out-dir "$TMP/o4" > /dev/null 2>&1
      ck "local document accepted as domain context" 0 "$?"
      grep -q "coronary calcium" "$TMP/o4/context_extract.md"
      ck "context text extracted to context_extract.md" 0 "$?"
      
      # 9) a missing file fails loudly rather than proceeding with an empty profile
      python3 "$B" --codebook "$TMP/does_not_exist.csv" --out-dir "$TMP/o5" > /dev/null 2>&1
      ck "missing codebook fails loudly" 1 "$?"
      
      # 10) an unparseable codebook fails rather than emitting an empty profile
      printf 'just some prose with no table and no variables at all\n' > "$TMP/prose.txt"
      python3 "$B" --codebook "$TMP/prose.txt" --out-dir "$TMP/o6" > /dev/null 2>&1
      ck "codebook with no variables fails (no empty profile)" 1 "$?"
      
      echo "----"
      echo "test_cohort_profile: $pass passed, $fail failed"
      [ "$fail" -eq 0 ]
      
  • SKILL.md 15.8 KB
    ---
    name: find-cohort-gap
    description: >
      Research gap finder for longitudinal cohort databases. Profiles cohort strengths,
      matches PI expertise, scans literature saturation, and outputs ranked topic proposals
      with gap evidence. Works with any cohort: NHIS, UK Biobank, institutional EMR, health
      checkup registries, or disease-specific registries.
    triggers: cohort gap, research topic, DB 주제, 코호트 갭, gap analysis, 연구주제 찾기, find research gap, 주제 발굴
    tools: Read, Write, Edit, Bash, Grep, Glob
    model: opus
    ---
    
    # Find-Cohort-Gap Skill
    
    You are assisting a medical researcher in systematically discovering novel, publishable
    research topics from a cohort database. Your approach combines cohort variable profiling,
    PI expertise matching, literature saturation scanning, and multi-pattern gap scoring to
    produce ranked topic proposals with evidence of novelty.
    
    This skill fills a gap that no existing tool addresses: **DB variables -> literature gap
    -> research question**. Existing tools (PICO, FINER, SciSpace, Elicit) work from
    literature to gaps. This skill works from the data outward.
    
    ## Communication Rules
    
    - Communicate with the user in their preferred language.
    - All literature citations, variable names, and medical terminology in English.
    - Be direct about weak topics — kill early, save time.
    
    ## Key Directories
    
    - **Output**: User-specified directory (default: current working directory)
    - **References**: `${CLAUDE_SKILL_DIR}/references/` for templates and rubrics
    
    ---
    
    ## Phase 0: Cohort Intake
    
    The cohort does not have to be one this skill has heard of. Route on what the user
    actually has.
    
    | The user has… | Do this |
    |---------------|---------|
    | A **named public cohort** (NHIS, UK Biobank, KNHANES, …) | Fill the profile from published documentation. Cite the source for every field. |
    | A **codebook / data dictionary / CSV export** of their own registry or EMR extract | Run the input adapter below. This is the common case — an institutional registry or single-centre export that no public documentation describes. |
    | A **review, guideline, or preprint** defining the clinical domain | Attach it as domain context (`--context`), as a file or a URL. |
    
    ### Input adapter (local codebook / documents)
    
    ```bash
    python3 "${CLAUDE_SKILL_DIR}/scripts/build_cohort_profile.py" \
      --codebook data_dictionary.csv \
      --context narrative_review.pdf --context https://example.org/guideline \
      --cohort-name "Institutional CT registry" --out-dir .
    ```
    
    Formats: `.csv` / `.tsv` / `.json` / `.md` / `.txt` (stdlib), `.xlsx` (needs `openpyxl`),
    `.pdf` (needs `pdftotext`). A `.csv` is auto-detected as a **codebook** (rows are
    variables) or a **data export** (the header row is the variable list). Writes
    `cohort_profile.md` + `cohort_profile.json` (+ `context_extract.md`).
    
    **Do not read the codebook yourself and summarise it.** Paraphrasing a variable name,
    merging two that look alike, or inventing one the cohort does not have poisons every
    downstream claim — the intersection matrix, the feasibility gate, and eventually the
    manuscript's Methods. The adapter *enumerates* variables verbatim with provenance
    (`file:row`) instead, which is the dictionary-first discipline a reviewer expects of a
    DB-backed study. Read `cohort_profile.md`; do not re-derive it.
    
    What the adapter infers (and shows its work for): the **variable cluster map**, **serial
    / repeated-measure groups** (evidence for P1 Longitudinal Advantage), and **endpoint
    candidates** (evidence for P2 Endpoint Upgrade). Every cluster assignment records the
    keyword that triggered it, and a variable matching nothing is left `unclassified` rather
    than forced into a bucket — review those, since the lexicon is not exhaustive.
    
    ### What the adapter cannot know — ASK, never guess
    
    A codebook lists variables. It does not state any of the following, and each is emitted
    as `[UNKNOWN - ask the user]`:
    
    1. **Sample size** (N at baseline, N with follow-up)
    2. **Time span** (enrollment period, follow-up duration, measurement intervals)
    3. **Known limitations** (healthy volunteer bias, attrition, missing-data patterns)
    4. **Existing publications** from this cohort (to avoid duplicating them)
    5. **IRB status and data-access route**
    
    Collect these from the user before Phase 2. A guessed N does not merely sit there — it
    flows into the Phase 5 feasibility gate, which then passes (or fails) for a reason that
    has nothing to do with the cohort.
    
    Also confirm the **setting** (institution type, country, population type) and any
    **special strengths** the variable names cannot reveal — registry linkage, biobank
    availability, a distinctive population.
    
    **Gate:** Present the cohort profile summary, including the `[UNKNOWN]` list and the
    unclassified variables. Confirm before proceeding.
    
    ---
    
    ## Phase 1: PI/CA Profiling
    
    Profile the intended PI or corresponding author to find topic-expertise alignment.
    
    1. **Search PubMed** for the PI's recent publications (last 5 years).
       - Use `/search-lit` E-utilities: `bash "$EUTILS" search "AuthorLastName AuthorFirstInitial[Author]" 30`
       - Extract top keyword clusters from titles/abstracts.
    2. **Identify specialty signals**:
       - Academic society positions (president, board member, editor)
       - Subspecialty focus areas
       - Preferred journal tiers
    3. **Build a PI keyword map**: 5-10 keyword clusters ranked by publication frequency.
    
    If no PI is specified, skip this phase and use variable clusters alone in Phase 2.
    
    **Output:** PI profile card (name, affiliation, top keywords, society roles, preferred journals).
    
    ---
    
    ## Phase 2: Intersection Matrix
    
    Cross cohort variable clusters with PI expertise to generate candidate topics.
    
    ### Method
    
    Create a matrix: rows = DB variable clusters, columns = PI keyword clusters.
    Score each cell 0-3:
    - **3**: PI has published in this exact intersection (direct match)
    - **2**: PI's subspecialty covers this area (strong relevance)
    - **1**: Tangential connection (possible but needs framing)
    - **0**: No connection
    
    ### Candidate Generation
    
    1. Extract all cells scoring 2-3 as primary candidates.
    2. For cells scoring 1, apply the **A-B substitution test**: "Has someone published
       [this analysis] with [a different exposure/outcome] in a similar cohort?" If yes,
       substituting the PI's specialty variable creates a viable candidate.
    3. Generate 20-40 candidate topic statements in PICO format:
       - **P**: Population from the cohort
       - **E**: Exposure/predictor variable(s)
       - **C**: Comparison group
       - **O**: Outcome (preferably hard endpoint)
    
    ### Discipline Alignment Filter
    
    Before advancing candidates to saturation scanning, apply a discipline filter:
    
    - **Who is the intended first author?** Identify their department/specialty.
    - **Does the primary exposure variable belong to that discipline?** The first
      author's specialty must align with the study's core variable. For example:
      - Radiology first author → imaging variable must be the primary exposure
      - Cardiology first author → cardiac biomarker or ECG finding as exposure
      - Neurology first author → neurological variable or brain imaging as exposure
    - **Kill candidates where the primary exposure is outside the first author's
      discipline.** A strong PI match alone is insufficient if the first author
      cannot claim ownership of the core variable.
    
    This filter prevents generating topics where the first author's contribution
    is not defensible at the variable level.
    
    **Gate:** Present the intersection matrix and top 20 candidates (post-discipline
    filter). User selects 8-12 for saturation scanning.
    
    ---
    
    ## Phase 3: Literature Saturation Scan
    
    For each selected candidate, determine how saturated the literature is.
    
    ### Search Strategy
    
    For each candidate:
    1. Build a PubMed query: `(exposure terms) AND (outcome terms) AND (cohort OR longitudinal OR prospective)`
    2. Execute search via `/search-lit` E-utilities.
    3. Count total results and classify:
    
    | Grade | Count | Longitudinal? | Interpretation |
    |-------|-------|---------------|----------------|
    | **Blue Ocean** | 0-2 papers | N/A | First report possible. Verify the topic has audience interest. |
    | **Green Field** | 3-10 papers, all cross-sectional | No longitudinal | **Optimal zone** — established interest, longitudinal gap wide open. |
    | **Yellow** | 10-30 papers | Some longitudinal | Viable only with very specific angle (unique population, novel endpoint). |
    | **Red** | 30+ papers or MA exists | Yes | Avoid unless doing NMA or using truly unique data. |
    
    ### Critical Filter
    
    For each candidate in Green/Yellow, ask: **"Has anyone published this with serial/repeated
    measurements?"** If no — automatic upgrade by one grade.
    
    ### "So What" Test
    
    For each candidate, articulate 2-3 potential clinical implications of the findings.
    If you cannot state why a clinician or policymaker would care about the result,
    the topic fails regardless of gap score.
    
    **Output:** Saturation table with grade, paper count, longitudinal gap status, and
    "So What" statement for each candidate.
    
    **Gate:** Present saturation results. User selects 3-5 finalists for deep scoring.
    
    ---
    
    ## Phase 4: 6-Pattern Scoring + Comparison Table
    
    Apply the 6-Pattern framework to each finalist. Score each pattern 0 or 1.
    
    ### 6 Patterns (Universal)
    
    Read the detailed rubric at `${CLAUDE_SKILL_DIR}/references/pattern_scoring_rubric.md`.
    
    | # | Pattern | Question | Score 1 if... |
    |---|---------|----------|---------------|
    | P1 | **Longitudinal Advantage** | Does the cohort's serial/repeated measurement structure create a clear edge over existing cross-sectional studies? | Cohort has 3+ timepoints for key variables AND no prior study used serial data for this topic. |
    | P2 | **Endpoint Upgrade** | Can we escalate to a harder endpoint than existing studies? | Cohort links to mortality/cancer/CVD registries AND existing studies stop at surrogate endpoints. |
    | P3 | **Cohort Uniqueness** | Is the cohort's population, scale, or setting distinctive? | Largest in this population, unique ethnic group, screening-based (no referral bias), or novel linkage. |
    | P4 | **PI-Topic Alignment** | Does the PI's expertise and reputation strengthen this topic? | PI has society role or 5+ papers directly in this domain. Skip if no PI specified. |
    | P5 | **Comparison Table Gaps** | Does the THIS STUDY column show 3+ differences vs existing papers? | Build comparison table (see below). 3+ checkmarks in THIS STUDY that are absent in all prior papers. |
    | P6 | **Complementary Design** | Can this topic pair with another study from the same cohort? | Two studies using the same DB but different populations or complementary variables (e.g., viral vs non-viral). |
    
    ### Comparison Table Construction
    
    For each finalist, build a table comparing the top 3-5 existing papers against THIS STUDY:
    
    ```
    | Feature | Author1 (Year) | Author2 (Year) | Author3 (Year) | THIS STUDY |
    |---------|----------------|----------------|----------------|------------|
    | Design | Cross-sectional | Cohort (5yr) | Cross-sectional | Cohort (20yr) |
    | N | 3,200 | 8,500 | 12,000 | ~200,000 |
    | Serial data | No | No | No | Yes (avg 5 visits) |
    | Hard endpoint | Surrogate | Surrogate | All-cause mortality | CVD + all-cause mortality |
    | Population | Referral | General | Screening | Health checkup (no referral bias) |
    | Ethnicity | Western | Western | Asian (Japan) | Asian (Korea) |
    | Subgroup analysis | No | Age only | No | Age + sex + comorbidity |
    ```
    
    ### Score Interpretation
    
    | Total Score | Recommendation |
    |-------------|----------------|
    | 5-6 | Top-tier journal target (Lancet sub, JACC, J Hepatol level) |
    | 3-4 | Specialty journal target (solid publication) |
    | 1-2 | Restructure or kill — find a stronger angle before proceeding |
    
    **Gate:** Present scoring results and comparison tables. User approves final ranking.
    
    ---
    
    ## Phase 5: Feasibility Gate
    
    For each scored finalist, verify practical feasibility.
    
    ### Checks
    
    1. **Sample size adequacy**:
       - Cox regression: minimum 10 events per predictor variable (EPV rule)
       - Logistic regression: same EPV rule
       - For large cohorts (N>100K): warn about p-value inflation — statistically
         significant results are nearly guaranteed, so focus on **effect size thresholds**
         (e.g., HR >1.2 or <0.8 for clinical relevance)
       - Consider negative control strategy (EPCV) for very large samples
    
    2. **Missing data**:
       - Key exposure variable: <20% missing acceptable
       - Key outcome: <5% missing
       - If serial data: assess attrition pattern (MCAR/MAR/MNAR)
    
    3. **Follow-up adequacy**:
       - Outcome must have plausible latency within available follow-up
       - Cancer outcomes: minimum 5 years
       - CVD events: minimum 3 years
       - Mortality: minimum 5 years
    
    4. **Operational definition**:
       - Can the exposure be defined from available variables?
       - For claims data: ICD codes alone = 40-60% accuracy. Require combination
         strategy (diagnosis + prescription + visit frequency + special codes)
       - Cross-check expected prevalence against known epidemiological data
    
    5. **IRB/ethics**:
       - Is the data already IRB-approved for this type of analysis?
       - Any additional approvals needed for data linkage?
    
    6. **Disease Novelty Bonus** (informational, not Go/No-Go):
       - Idiopathic etiology or debated mechanism → higher journal interest
       - Established mechanism → needs stronger methodological novelty
    
    ### Decision
    
    - **Go**: All checks pass.
    - **Conditional Go**: Minor issues solvable (e.g., missing data manageable with imputation).
    - **No-Go**: Fatal flaw (insufficient events, no valid endpoint, key variable unavailable).
    
    **Output:** Feasibility report for each finalist with Go/Conditional/No-Go status.
    
    ---
    
    ## Phase 6: Output — Ranked Proposals + One-Pagers
    
    Generate the final deliverables.
    
    ### Ranked Summary Table
    
    ```
    | Rank | Topic (PICO) | Saturation | 6-Pattern Score | Feasibility | Target Journal | Timeline |
    |------|--------------|------------|-----------------|-------------|----------------|----------|
    | 1 | ... | Green (0 longitudinal) | 5/6 | Go | JACC | 6 months |
    | 2 | ... | Green (1 longitudinal) | 4/6 | Go | Eur Heart J | 6 months |
    | 3 | ... | Blue (0 papers) | 3/6 | Conditional | Radiology | 8 months |
    ```
    
    ### One-Pager for Each Finalist
    
    Use the template at `${CLAUDE_SKILL_DIR}/references/onepager_template.md`.
    
    Each one-pager includes:
    1. **Title**: Working title for the study
    2. **Background**: 3-4 sentences establishing the gap (with "Zero Papers" claim if applicable)
    3. **Comparison Table**: THIS STUDY vs existing papers
    4. **Objective**: Primary research question in PICO format
    5. **Methods Summary**: Study design, key variables, statistical approach
    6. **PI Role**: Why this PI is the right corresponding author
    7. **Target Journal**: With rationale (PI alignment, scope match, gap fit)
    8. **Timeline**: Realistic estimate (data preparation → analysis → drafting → submission)
    9. **6-Pattern Score Card**: Visual breakdown of each pattern
    
    Save one-pagers as markdown files: `{output_dir}/gap_proposal_{rank}_{short_topic}.md`
    
    ---
    
    ## Skill Integration
    
    | Phase | Calls to other skills |
    |-------|----------------------|
    | Phase 1 (PI profiling) | `/search-lit` E-utilities for PubMed author search |
    | Phase 3 (Saturation scan) | `/search-lit` E-utilities for topic searches |
    | Phase 4 (Comparison table) | `/search-lit` for retrieving paper metadata |
    | **Downstream** | Output feeds into `/design-study` → `/write-paper` pipeline |
    
    ## What This Skill Does NOT Do
    
    - Does not perform the actual statistical analysis (use `/analyze-stats`)
    - Does not write the full manuscript (use `/write-paper`)
    - Does not validate study design (use `/design-study`)
    - Does not generate references (use `/search-lit`)
    - Does not make publication-ready figures (use `/make-figures`)
    
    ## Anti-Hallucination
    
    - **Never fabricate references.** All citations must be verified via `/search-lit` with confirmed DOI or PMID. Mark unverified references as `[UNVERIFIED - NEEDS MANUAL CHECK]`.
    - **Never invent clinical definitions, diagnostic criteria, or guideline recommendations.** If uncertain, flag with `[VERIFY]` and ask the user.
    
  • skill.yml 2.1 KB
    schema_version: 2
    name: find-cohort-gap
    layer: D
    owner_domain: research_gap_analysis
    maturity: official
    
    when_to_use: "Find research gaps in a longitudinal cohort DB by profiling its strengths, matching PI expertise, and scanning literature saturation."
    when_NOT_to_use: "Finding meta-analysis topics (use ma-scout); designing a chosen study (use design-study)."
    
    inputs:
      - "named cohort, OR a local codebook / data dictionary / CSV export (.csv/.tsv/.json/.md/.xlsx/.pdf)"
      - "optional domain context: a review / guideline / preprint (file or URL)"
      - "PI expertise"
      - "literature landscape"
    outputs:
      - "cohort_profile.md + cohort_profile.json (variables enumerated verbatim, with provenance)"
      - "ranked topic proposals with gap evidence"
    deterministic_scripts:
      - scripts/build_cohort_profile.py
    side_effects:
      - writes_report_artifacts
      - network_access_literature
    downstream_consumers:
      - design-study
      - define-variables
    forbidden_actions:
      - claim_a_gap_without_literature_evidence
      - fabricate_saturation_counts
      - invent_a_variable_the_codebook_does_not_contain
      - guess_a_sample_size_or_follow_up_the_codebook_does_not_state
    
    # v2.1 quality card
    purpose: "Rank under-studied topics for a specific cohort, each backed by literature-saturation evidence and feasibility."
    safety_boundaries:
      - "Each proposed gap cites the literature scan that supports it; saturation counts come from real searches."
      - "Advisory report only; does not design or execute studies."
    known_limitations:
      - "Literature scans are point-in-time; a gap can close between scan and submission."
      - "No standalone demo; proposals require domain judgement."
      - "The codebook adapter clusters variables by a keyword lexicon that is not exhaustive; unmatched variables are surfaced as 'unclassified' for the user to review rather than force-assigned."
      - "A codebook cannot state sample size, follow-up, or IRB status; those are emitted as [UNKNOWN] and must be collected from the user."
    validation_commands:
      - "bash tests/test_cohort_profile.sh"
      - "re-run the saturation search before committing to a topic"
    evidence_surface: bundled_script
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related