799 lines
28 KiB
Python
799 lines
28 KiB
Python
"""PRISMA project scaffolding + criteria access.
|
|
|
|
A "project" is three Zotero items living in a dedicated collection:
|
|
|
|
- ``prisma:criteria`` — PICO inclusion/exclusion criteria (markdown note)
|
|
- ``prisma:extraction-template`` — structured form the LLM fills for
|
|
included studies (JSON-schema-ish markdown note)
|
|
- ``prisma:reasons`` — the exclusion-reason codebook
|
|
|
|
Storing criteria this way (vs. git-tracked docs) keeps them with the
|
|
library that's being screened and lets non-engineer reviewers edit in
|
|
Zotero. Downside: edits aren't automatically versioned; if you care,
|
|
commit the markdown into docs/ as a mirror.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING
|
|
|
|
from zot.db import TYPE_MAP, generate_key, now_iso
|
|
|
|
if TYPE_CHECKING:
|
|
from zot.db import Db
|
|
|
|
# Tag markers for the three per-project anchor items.
|
|
TAG_CRITERIA = "prisma:criteria"
|
|
TAG_EXTRACTION = "prisma:extraction-template"
|
|
TAG_REASONS = "prisma:reasons"
|
|
|
|
COLLECTION_ROOT = "PRISMA Projects"
|
|
|
|
|
|
@dataclass
|
|
class Project:
|
|
name: str # e.g. "skin-subs" — matches project:<name> tag
|
|
criteria: str # markdown
|
|
extraction_template: str # markdown / JSON-schema-in-markdown
|
|
reasons: str # markdown codebook
|
|
|
|
@property
|
|
def project_tag(self) -> str:
|
|
return f"project:{self.name}"
|
|
|
|
|
|
# ── Bootstrap ────────────────────────────────────────────────────
|
|
|
|
|
|
_STARTER_CRITERIA_SKIN_SUBS = """\
|
|
# Skin Substitutes — Inclusion / Exclusion Criteria
|
|
|
|
## Review question
|
|
|
|
Do cellular/tissue-based or biologically-derived skin substitute
|
|
products improve wound healing outcomes compared to standard wound
|
|
care in adult patients with chronic lower-extremity wounds
|
|
(DFU, VLU, pressure ulcers, burns)?
|
|
|
|
## PICO
|
|
|
|
- **Population** — adults (≥18y) with chronic non-healing wounds:
|
|
diabetic foot ulcers (DFU), venous leg ulcers (VLU), pressure
|
|
ulcers, or burns. Exclude purely cosmetic dermatology and acute
|
|
surgical wound closure without chronicity context.
|
|
- **Intervention** — any cellular or tissue-based product (CTP),
|
|
tissue-engineered, bioengineered, xenograft, or allograft skin
|
|
substitute. Named products include Apligraf, Dermagraft, EpiFix,
|
|
MatriStem, PriMatrix, Oasis, AmnioBand, Affinity, Integra, and
|
|
similar. Topical wound dressings alone do **not** qualify.
|
|
- **Comparator** — standard wound care (debridement + offloading /
|
|
compression + moist wound dressing), head-to-head against another
|
|
skin substitute, or placebo.
|
|
- **Outcomes** — primary: complete wound closure at 12 weeks
|
|
(or nearest reported). Secondary: time to closure, percent area
|
|
reduction (PAR), infection rate, recurrence, quality of life,
|
|
cost per healed wound, QALYs.
|
|
|
|
## Study designs
|
|
|
|
Include: RCT, quasi-RCT, prospective cohort with comparator,
|
|
systematic review, meta-analysis, economic evaluation alongside any
|
|
of the above.
|
|
|
|
Exclude: case reports, case series without comparator, narrative
|
|
reviews, editorials, letters, commentaries, conference abstracts
|
|
without full data.
|
|
|
|
## Time / language
|
|
|
|
Any publication year. English, or non-English with available English
|
|
abstract.
|
|
|
|
## Policy / fraud sub-domain
|
|
|
|
Docs tagged with any of ``source:oig``, ``source:gao``, ``source:medpac``,
|
|
``source:court``, ``source:cms-rule``, ``source:mac-lcd`` are reference
|
|
material and bypass clinical screening. Flag with
|
|
``screen:reason:off-topic`` only if unrelated to skin substitutes.
|
|
"""
|
|
|
|
|
|
_STARTER_EXTRACTION = """\
|
|
# Data extraction template
|
|
|
|
The LLM will fill this form for every included study. Fields
|
|
absent from the paper should be returned as empty strings rather
|
|
than omitted.
|
|
|
|
## Study metadata
|
|
- **authors** — lead + et al.
|
|
- **year** — publication year (YYYY)
|
|
- **country** — setting of the study
|
|
- **funding** — industry-funded (name sponsor) / public / mixed /
|
|
unreported
|
|
- **coi_declared** — yes / no / partial
|
|
|
|
## Population
|
|
- **n_total** — total enrolled
|
|
- **n_analyzed** — included in primary analysis
|
|
- **wound_type** — DFU / VLU / pressure / burn / mixed
|
|
- **baseline_wound_area_cm2** — mean (SD) or median (IQR)
|
|
- **inclusion_notes** — key eligibility criteria
|
|
|
|
## Intervention
|
|
- **product_name** — commercial / generic name
|
|
- **manufacturer** — sponsor
|
|
- **product_category** — CTP / synthetic / xenograft / amniotic /
|
|
other
|
|
- **application_frequency** — e.g. weekly, until closure or 12 wk
|
|
- **concomitant_care** — offloading, compression, debridement
|
|
|
|
## Comparator
|
|
- **comparator_type** — standard care / active / placebo
|
|
- **comparator_detail**
|
|
|
|
## Outcomes
|
|
- **primary_outcome** — definition
|
|
- **primary_result_treatment** — e.g. 62% healed at 12 wk
|
|
- **primary_result_comparator** — e.g. 34% healed at 12 wk
|
|
- **effect_size** — OR / RR / HR / mean diff with 95% CI
|
|
- **p_value**
|
|
- **time_to_closure_days** — median (IQR)
|
|
- **adverse_events** — brief text
|
|
- **cost_per_healed** — USD if reported
|
|
|
|
## Risk of bias (Cochrane RoB-2 domains, if applicable)
|
|
- **randomization** — low / some concerns / high
|
|
- **deviations_from_intervention**
|
|
- **missing_outcome_data**
|
|
- **measurement_of_outcome**
|
|
- **selection_of_reported_result**
|
|
- **overall** — low / some concerns / high
|
|
"""
|
|
|
|
|
|
_STARTER_REASONS = """\
|
|
# Exclusion reason codebook
|
|
|
|
Each code applies to a single PRISMA stage. When the LLM decides to
|
|
exclude a record it must return one of these codes plus a one-sentence
|
|
rationale.
|
|
|
|
## Stage 2 — title / abstract screening
|
|
|
|
- `not-skin-substitute` — intervention is not a cellular or
|
|
tissue-based skin substitute.
|
|
- `wrong-population` — not chronic lower-extremity wounds or burns.
|
|
- `case-report` — case report / case series without comparator.
|
|
- `editorial` — editorial, commentary, narrative review, letter.
|
|
- `conference-abstract` — abstract only, no full data.
|
|
- `not-english` — not English and no English abstract available.
|
|
- `duplicate` — duplicate publication (same data, different venue).
|
|
- `retracted` — paper has been retracted.
|
|
- `off-topic` — unrelated to skin substitutes altogether.
|
|
|
|
## Stage 3 — full-text eligibility
|
|
|
|
Everything in Stage 2, plus:
|
|
|
|
- `no-comparator` — no control group.
|
|
- `wrong-outcome` — does not report any of the primary or secondary
|
|
outcomes in the review's PICO.
|
|
- `unavailable-fulltext` — full text cannot be retrieved.
|
|
|
|
## Policy / fraud sub-domain (OIG, GAO, MedPAC, court, CMS rules)
|
|
|
|
Included by default; excluded only with code:
|
|
|
|
- `off-topic` — unrelated to skin substitutes.
|
|
"""
|
|
|
|
|
|
_STARTER_CRITERIA_PALLIATIVE_RFI = """\
|
|
# Community-Based Palliative Care RFI — Inclusion / Exclusion Criteria
|
|
|
|
## Review question
|
|
|
|
What evidence can inform CMS's Request for Information on
|
|
Community-Based Palliative Care in the CY2027 PFS NPRM (CMS-1848-P,
|
|
91 FR 43949-43950)? The RFI asks five things, and a record is
|
|
included when it provides usable evidence for at least one:
|
|
|
|
- **(a)** where fraud, waste, and abuse concentrate in
|
|
community-based palliative care and adjacent hospice programs;
|
|
- **(b)** whether eligibility for a serious-illness service should be
|
|
gated on certified life expectancy, and what prognostic interval
|
|
the evidence supports;
|
|
- **(c)** whether eligibility is better defined by impact on daily
|
|
function (ADLs) and/or caregiver strain, and with what instruments;
|
|
- **(d)** which service elements of community-based palliative care
|
|
management (interdisciplinary teams, home visits, 24/7 access,
|
|
care plans, post-discharge follow-up) drive outcomes;
|
|
- **(e)** how quality of palliative care management should be
|
|
measured (including CBE 3665, "feeling heard and understood");
|
|
- **(f)** informs the design of a proposed palliative care management
|
|
code family (palliative care screening/initial assessment;
|
|
palliative care planning; palliative care follow-up) — program
|
|
standards, interdisciplinary team composition and staffing, service
|
|
elements, assessment instruments, and documentation practices
|
|
(CAPC / NASW / Joint Commission anchored).
|
|
|
|
## Population
|
|
|
|
Adults with serious illness — palliative care, hospice, or
|
|
supportive-care populations. U.S. Medicare-relevant settings
|
|
preferred; non-U.S. evidence is includable when the finding
|
|
transfers (instruments, service elements, prognostic accuracy).
|
|
Exclude pediatric-only populations.
|
|
|
|
## Topic scope
|
|
|
|
Include: service-delivery models (community-based, home-based,
|
|
outpatient/ambulatory palliative care), eligibility/identification
|
|
criteria and instruments, prognostication accuracy, caregiver strain
|
|
measurement, care-management service elements, quality measures and
|
|
patient-reported outcomes, utilization/cost/hospitalization outcomes,
|
|
billing-integrity and program-integrity evidence.
|
|
|
|
Exclude: pure symptom-pharmacology efficacy trials (drug/dose
|
|
comparisons) with no service-delivery, eligibility, or quality
|
|
measurement angle; inpatient-only intervention studies with no
|
|
community/outpatient transferability; disease-specific prognostic
|
|
biomarker papers with no service-eligibility framing.
|
|
|
|
## Study designs
|
|
|
|
Include: RCT, quasi-experimental, prospective/retrospective cohort
|
|
with comparator, validation studies of instruments or prognostic
|
|
criteria, systematic review, meta-analysis, economic evaluation,
|
|
health-services/claims analyses, mixed-methods evaluations of
|
|
delivery models.
|
|
|
|
Exclude: case reports, case series without comparator, narrative
|
|
reviews, editorials, letters, commentaries, conference abstracts
|
|
without full data.
|
|
|
|
## Time / language
|
|
|
|
Any publication year (CMMI's synthesis window is 2012-2021; older
|
|
foundational instrument papers still qualify). English, or
|
|
non-English with available English abstract.
|
|
|
|
## Policy / grey sub-domain
|
|
|
|
Docs tagged with any of ``source:oig``, ``source:gao``, ``source:medpac``,
|
|
``source:court``, ``source:cms-rule``, ``source:mac-lcd`` are reference
|
|
material and bypass clinical screening. Flag with
|
|
``screen:reason:off-topic`` only if unrelated to palliative or
|
|
hospice care.
|
|
"""
|
|
|
|
|
|
_STARTER_EXTRACTION_PALLIATIVE_RFI = """\
|
|
# Data extraction template — palliative-rfi
|
|
|
|
The LLM will fill this form for every included study. Fields absent
|
|
from the paper should be returned as empty strings rather than
|
|
omitted.
|
|
|
|
## Study metadata
|
|
- **authors** — lead + et al.
|
|
- **year** — publication year (YYYY)
|
|
- **country** — setting of the study
|
|
- **funding** — industry / public / mixed / unreported
|
|
- **design** — RCT / quasi-experimental / cohort / validation /
|
|
systematic review / meta-analysis / economic / claims analysis
|
|
|
|
## RFI mapping
|
|
- **rfi_parts** — which RFI questions the study informs (a-e,
|
|
comma-separated)
|
|
- **medicare_relevance** — direct (Medicare data/population) /
|
|
transferable / weak
|
|
|
|
## Population & setting
|
|
- **n_total** — total enrolled or analyzed
|
|
- **population** — serious-illness definition used
|
|
- **setting** — home / outpatient clinic / community / mixed /
|
|
inpatient
|
|
|
|
## Eligibility evidence (parts b-c)
|
|
- **eligibility_basis** — prognosis / function-ADL / caregiver-strain /
|
|
diagnosis-count / combination
|
|
- **instrument** — named instrument or criteria set
|
|
- **prognostic_interval** — life-expectancy interval studied
|
|
- **instrument_performance** — accuracy / discrimination as reported
|
|
|
|
## Delivery-model evidence (part d)
|
|
- **service_elements** — team composition, home visits, 24/7 access,
|
|
care planning, follow-up cadence as reported
|
|
- **outcomes** — utilization, hospitalization, cost, place of death,
|
|
patient/caregiver-reported outcomes
|
|
- **effect_size** — OR / RR / HR / mean diff with 95% CI
|
|
- **p_value**
|
|
|
|
## Quality-measure evidence (part e)
|
|
- **measures_used** — named quality measures / PROMs (note CBE 3665)
|
|
- **measure_findings**
|
|
|
|
## Integrity evidence (part a)
|
|
- **fwa_findings** — billing-pattern, enforcement, or integrity
|
|
findings
|
|
|
|
## Risk of bias
|
|
- **overall** — low / some concerns / high
|
|
- **rob_notes**
|
|
"""
|
|
|
|
|
|
_STARTER_REASONS_PALLIATIVE_RFI = """\
|
|
# Exclusion reason codebook — palliative-rfi
|
|
|
|
Each code applies to a single PRISMA stage. When the LLM decides to
|
|
exclude a record it must return one of these codes plus a one-sentence
|
|
rationale.
|
|
|
|
## Stage 2 — title / abstract screening
|
|
|
|
- `off-topic` — unrelated to palliative, hospice, or serious-illness
|
|
care.
|
|
- `wrong-population` — pediatric-only, or not a serious-illness /
|
|
palliative / hospice population.
|
|
- `symptom-pharmacology` — drug/dose efficacy study with no
|
|
service-delivery, eligibility, or quality-measurement angle.
|
|
- `inpatient-only` — exclusively inpatient/ICU intervention with no
|
|
community, home, or outpatient transferability.
|
|
- `no-rfi-question` — on-topic but informs none of RFI parts a-e.
|
|
- `case-report` — case report / case series without comparator.
|
|
- `editorial` — editorial, commentary, narrative review, letter.
|
|
- `conference-abstract` — abstract only, no full data.
|
|
- `not-english` — not English and no English abstract available.
|
|
- `duplicate` — duplicate publication (same data, different venue).
|
|
- `retracted` — paper has been retracted.
|
|
|
|
## Stage 3 — full-text eligibility
|
|
|
|
Everything in Stage 2, plus:
|
|
|
|
- `no-usable-evidence` — full text reports no findings usable for any
|
|
RFI question.
|
|
- `wrong-outcome` — outcomes unrelated to eligibility, delivery,
|
|
quality, cost/utilization, or integrity.
|
|
- `unavailable-fulltext` — full text cannot be retrieved.
|
|
|
|
## Policy / grey sub-domain (OIG, GAO, MedPAC, court, CMS rules)
|
|
|
|
Included by default; excluded only with code:
|
|
|
|
- `off-topic` — unrelated to palliative or hospice care.
|
|
"""
|
|
|
|
|
|
_LEAD_SUSPECTING = """\
|
|
Suspecting is in scope as both a code uplift and a care change. Each is a
|
|
hypothesized status transition, such as not High Needs to High Needs, an
|
|
absent HCC becoming present, or a quality denominator without a numerator
|
|
gaining one. A code uplift makes an existing status visible to the math.
|
|
Care changes the underlying state. Record how far each can move the score,
|
|
including when demographic reweighting shrinks the coding payoff.
|
|
"""
|
|
|
|
_LEAD_CRITERIA_HISTORY = """\
|
|
# LEAD history — inclusion criteria
|
|
|
|
Strand 1. For each target in the LEAD registry, find where the component
|
|
came from, what assumption it rests on, what evidence supported it when it
|
|
was adopted and what has appeared since, and how it changed through MA,
|
|
MSSP, Pioneer, NGACO, and REACH on the way to LEAD.
|
|
|
|
Include primary CMS methodology, the papers those methods cite, and
|
|
critiques of the same component. Exclude a record that names no registry
|
|
target and no predecessor of one.
|
|
"""
|
|
|
|
_LEAD_EXTRACTION_HISTORY = """\
|
|
# Extraction — lead-history
|
|
|
|
- **target_id** — registry id this row is about
|
|
- **origin** — where the component came from
|
|
- **assumptions** — the assumption it rests on
|
|
- **supporting_evidence** — evidence at adoption and since
|
|
- **critiques** — published critiques of the component
|
|
- **changes_to_lead** — how it changed on the way to LEAD
|
|
- **status_in_lead** — adopted / modified / dropped
|
|
- **page_refs** — page or section locators
|
|
"""
|
|
|
|
_LEAD_REASONS_HISTORY = """\
|
|
# Exclusion reasons — lead-history
|
|
|
|
- `off-topic` — names no registry target and no predecessor of one.
|
|
- `editorial` — editorial, commentary, or letter with no historical claim.
|
|
- `not-english` — no English text.
|
|
- `unavailable-fulltext` — full text cannot be retrieved.
|
|
"""
|
|
|
|
_LEAD_CRITERIA_LIMITS = f"""\
|
|
# LEAD limits — inclusion criteria
|
|
|
|
Strand 2. For each registry target, the mathematical and empirical limits
|
|
on predictive accuracy, how far an ACO's actions can move the scored
|
|
result, bias across subgroups, and incentive distortion.
|
|
|
|
{_LEAD_SUSPECTING}
|
|
Include a limit when it states a bound, a formula, or a number that can
|
|
be checked. Exclude commentary that names no limit.
|
|
"""
|
|
|
|
_LEAD_EXTRACTION_LIMITS = """\
|
|
# Extraction — lead-limits
|
|
|
|
- **target_id** — registry id the limit applies to
|
|
- **limit_type** — accuracy / movability / fairness / incentive
|
|
- **statement** — the limit in one or two sentences
|
|
- **formula** — formula or bound, when the source gives one
|
|
- **empirical_value** — number the source reports, with its unit
|
|
- **computable_expression** — expression that can run on public data
|
|
- **page_refs** — page or section locators
|
|
"""
|
|
|
|
_LEAD_REASONS_LIMITS = """\
|
|
# Exclusion reasons — lead-limits
|
|
|
|
- `off-topic` — not a limit on a registry target.
|
|
- `no-limit` — on topic but states no bound, formula, or checkable number.
|
|
- `editorial` — editorial, commentary, or letter.
|
|
- `not-english` — no English text.
|
|
- `unavailable-fulltext` — full text cannot be retrieved.
|
|
"""
|
|
|
|
_LEAD_CRITERIA_SCAN = """\
|
|
# LEAD environment scan — inclusion criteria
|
|
|
|
Strand 3, from 2023 onward. What surrounds LEAD: adjacent policy, how the
|
|
industry identifies High Needs patients, public data sources and their
|
|
grain, current methods for risk and prognosis, and open-model candidates
|
|
with their licenses.
|
|
|
|
Include a dated item the chassis would have to notice. Exclude undated
|
|
background with no consequence for the model.
|
|
"""
|
|
|
|
_LEAD_EXTRACTION_SCAN = """\
|
|
# Extraction — lead-scan
|
|
|
|
- **item_type** — policy / method / data / model / industry
|
|
- **item_date** — date or year of the item
|
|
- **summary** — what the item says, in a few sentences
|
|
- **target_ids** — registry ids it bears on, if any
|
|
- **aacods** — AACODS appraisal notes
|
|
- **chassis_meaning** — what the chassis should do because of this item
|
|
"""
|
|
|
|
_LEAD_REASONS_SCAN = """\
|
|
# Exclusion reasons — lead-scan
|
|
|
|
- `off-topic` — not about the environment LEAD is built in.
|
|
- `undated` — no date, and no way to place it in 2023 or later.
|
|
- `editorial` — color with no fact the chassis can use.
|
|
- `not-english` — no English text.
|
|
- `unavailable-fulltext` — full text cannot be retrieved.
|
|
"""
|
|
|
|
_LEAD_CRITERIA_PRECURSORS = f"""\
|
|
# LEAD precursors — inclusion criteria
|
|
|
|
Strand 4, a prognostic-factor review. The population is Medicare
|
|
beneficiaries, or comparable older, disabled, or ESRD adults, in claims
|
|
or EHR cohorts. An index factor is measured before the outcome. The
|
|
outcome is a registry target or a named proxy, and the lead time is
|
|
recorded. Eligible designs are cohort, case-control, prediction-model
|
|
development or validation, and quasi-experimental. Grain may be the
|
|
beneficiary, the provider, the ACO, or an area.
|
|
|
|
A cross-sectional study with no temporal order is excluded.
|
|
|
|
{_LEAD_SUSPECTING}
|
|
"""
|
|
|
|
_LEAD_EXTRACTION_PRECURSORS = """\
|
|
# Extraction — lead-precursors
|
|
|
|
- **design** — study design as reported
|
|
- **data_source** — claims, EHR, registry, or survey named by the paper
|
|
- **population** — who was studied
|
|
- **n** — sample size
|
|
- **setting** — care setting
|
|
- **country** — country of the data
|
|
- **years** — years the data cover
|
|
- **factor_definition** — how the precursor was defined
|
|
- **outcome_target_id** — registry id of the outcome, or of its proxy
|
|
- **proxy_closeness** — how close a proxy is to the registry target
|
|
- **lead_time** — time from factor to outcome
|
|
- **grain** — beneficiary / provider / ACO / area
|
|
- **effect_measure** — measure the paper reports
|
|
- **effect_value** — point estimate
|
|
- **effect_ci** — confidence interval or other uncertainty
|
|
- **adjusted_for** — covariates the estimate adjusts for
|
|
- **c_statistic** — discrimination, when reported
|
|
- **calibration** — calibration, when reported
|
|
- **risk_of_bias** — bias appraisal, with the tool named
|
|
- **observable_public** — whether public data can supply the factor
|
|
- **observable_claims** — whether claims can supply the factor
|
|
"""
|
|
|
|
_LEAD_REASONS_PRECURSORS = """\
|
|
# Exclusion reasons — lead-precursors
|
|
|
|
- `off-topic` — not a precursor of a registry target.
|
|
- `wrong-population` — not Medicare or a comparable older, disabled, or ESRD cohort.
|
|
- `cross-sectional-no-time-order` — cross-sectional study with no temporal order.
|
|
- `editorial` — editorial, commentary, or letter with no study result.
|
|
- `not-english` — no English text.
|
|
- `unavailable-fulltext` — full text cannot be retrieved.
|
|
- `wrong-outcome` — outcome is not a registry target and not a named proxy.
|
|
"""
|
|
|
|
_LEAD_CRITERIA_EXTERNAL = """\
|
|
# LEAD external forces — inclusion criteria
|
|
|
|
Strand 5. How forces outside the ACO move LEAD's inputs and scores: labor
|
|
markets and prices, legislation and rulemaking, tax and coverage policy,
|
|
and market structure.
|
|
|
|
Include a series or a dated event when the record states a mechanism that
|
|
reaches a registry target. Exclude a series with no stated path into the
|
|
score.
|
|
"""
|
|
|
|
_LEAD_EXTRACTION_EXTERNAL = """\
|
|
# Extraction — lead-external
|
|
|
|
- **series_or_event** — name of the series or the dated event
|
|
- **dates** — period or date the record covers
|
|
- **geography** — geography the figure applies to
|
|
- **source** — agency or publication the figure comes from
|
|
- **mechanism** — how this force reaches a LEAD input or score
|
|
- **magnitude** — size of the move, with its unit
|
|
"""
|
|
|
|
_LEAD_REASONS_EXTERNAL = """\
|
|
# Exclusion reasons — lead-external
|
|
|
|
- `off-topic` — no path into a LEAD input or score.
|
|
- `no-mechanism` — a series or event with no stated mechanism.
|
|
- `editorial` — commentary with no series and no dated event.
|
|
- `not-english` — no English text.
|
|
- `unavailable-fulltext` — full text cannot be retrieved.
|
|
"""
|
|
|
|
|
|
STARTERS = {
|
|
"skin-subs": (
|
|
_STARTER_CRITERIA_SKIN_SUBS,
|
|
_STARTER_EXTRACTION,
|
|
_STARTER_REASONS,
|
|
),
|
|
"palliative-rfi": (
|
|
_STARTER_CRITERIA_PALLIATIVE_RFI,
|
|
_STARTER_EXTRACTION_PALLIATIVE_RFI,
|
|
_STARTER_REASONS_PALLIATIVE_RFI,
|
|
),
|
|
"lead-history": (
|
|
_LEAD_CRITERIA_HISTORY,
|
|
_LEAD_EXTRACTION_HISTORY,
|
|
_LEAD_REASONS_HISTORY,
|
|
),
|
|
"lead-limits": (
|
|
_LEAD_CRITERIA_LIMITS,
|
|
_LEAD_EXTRACTION_LIMITS,
|
|
_LEAD_REASONS_LIMITS,
|
|
),
|
|
"lead-scan": (
|
|
_LEAD_CRITERIA_SCAN,
|
|
_LEAD_EXTRACTION_SCAN,
|
|
_LEAD_REASONS_SCAN,
|
|
),
|
|
"lead-precursors": (
|
|
_LEAD_CRITERIA_PRECURSORS,
|
|
_LEAD_EXTRACTION_PRECURSORS,
|
|
_LEAD_REASONS_PRECURSORS,
|
|
),
|
|
"lead-external": (
|
|
_LEAD_CRITERIA_EXTERNAL,
|
|
_LEAD_EXTRACTION_EXTERNAL,
|
|
_LEAD_REASONS_EXTERNAL,
|
|
),
|
|
}
|
|
|
|
|
|
# Legacy cohort tag → canonical project tag mapping. Items tagged
|
|
# ``module:skin-subs`` pre-date PRISMA; init() backfills ``project:<name>``
|
|
# onto them so the screening queue has something to chew on.
|
|
_LEGACY_COHORT_TAGS: dict[str, str] = {
|
|
"skin-subs": "module:skin-subs",
|
|
"palliative-rfi": "module:palliative-rfi",
|
|
}
|
|
|
|
|
|
def init(db: Db, name: str) -> Project:
|
|
"""Create or find the three anchor items for a PRISMA project.
|
|
|
|
Idempotent: re-running returns the existing records (matched by
|
|
``project:<name> + prisma:*`` tags). Edits to criteria happen in
|
|
Zotero; we don't overwrite user changes.
|
|
"""
|
|
if name not in STARTERS:
|
|
raise ValueError(
|
|
f"no starter criteria defined for project {name!r}. "
|
|
f"Known projects: {sorted(STARTERS)}"
|
|
)
|
|
criteria_md, extraction_md, reasons_md = STARTERS[name]
|
|
|
|
project_tag = f"project:{name}"
|
|
root_key = db.ensure_collection(COLLECTION_ROOT)
|
|
_ = db.ensure_collection(name, parent_key=root_key)
|
|
|
|
_ensure_anchor(
|
|
db,
|
|
project_tag,
|
|
TAG_CRITERIA,
|
|
root_key,
|
|
title=f"Screening criteria — {name}",
|
|
note_md=criteria_md,
|
|
)
|
|
_ensure_anchor(
|
|
db,
|
|
project_tag,
|
|
TAG_EXTRACTION,
|
|
root_key,
|
|
title=f"Data extraction template — {name}",
|
|
note_md=extraction_md,
|
|
)
|
|
_ensure_anchor(
|
|
db,
|
|
project_tag,
|
|
TAG_REASONS,
|
|
root_key,
|
|
title=f"Exclusion reason codebook — {name}",
|
|
note_md=reasons_md,
|
|
)
|
|
|
|
if name in _LEGACY_COHORT_TAGS:
|
|
_backfill_cohort(db, legacy=_LEGACY_COHORT_TAGS[name], project_tag=project_tag)
|
|
|
|
return Project(
|
|
name=name,
|
|
criteria=_load_note(db, project_tag, TAG_CRITERIA),
|
|
extraction_template=_load_note(db, project_tag, TAG_EXTRACTION),
|
|
reasons=_load_note(db, project_tag, TAG_REASONS),
|
|
)
|
|
|
|
|
|
def _backfill_cohort(db: Db, *, legacy: str, project_tag: str) -> int:
|
|
"""Tag every item carrying *legacy* with *project_tag* (idempotent)."""
|
|
# Resolve or create the target tag.
|
|
row = db.con.execute(
|
|
"SELECT tagID FROM tags WHERE name = ?", (project_tag,)
|
|
).fetchone()
|
|
if row:
|
|
target_id = row[0]
|
|
else:
|
|
cur = db.con.execute(
|
|
"INSERT INTO tags (name) VALUES (?)",
|
|
(project_tag,),
|
|
)
|
|
target_id = cur.lastrowid
|
|
|
|
# Items with legacy cohort tag that don't already carry project_tag.
|
|
affected = db.con.execute(
|
|
"""
|
|
INSERT OR IGNORE INTO itemTags (itemID, tagID, type)
|
|
SELECT i.itemID, ?, 1
|
|
FROM items i
|
|
JOIN itemTags it ON i.itemID = it.itemID
|
|
JOIN tags t ON it.tagID = t.tagID
|
|
WHERE t.name = ?
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM itemTags ex
|
|
WHERE ex.itemID = i.itemID AND ex.tagID = ?
|
|
)
|
|
""",
|
|
(target_id, legacy, target_id),
|
|
).rowcount
|
|
db.commit()
|
|
return affected
|
|
|
|
|
|
def load(db: Db, name: str) -> Project:
|
|
"""Load an existing project's three documents from Zotero."""
|
|
project_tag = f"project:{name}"
|
|
return Project(
|
|
name=name,
|
|
criteria=_load_note(db, project_tag, TAG_CRITERIA),
|
|
extraction_template=_load_note(db, project_tag, TAG_EXTRACTION),
|
|
reasons=_load_note(db, project_tag, TAG_REASONS),
|
|
)
|
|
|
|
|
|
# ── Internals ────────────────────────────────────────────────────
|
|
|
|
|
|
def _ensure_anchor(
|
|
db: Db,
|
|
project_tag: str,
|
|
role_tag: str,
|
|
collection_key: str,
|
|
*,
|
|
title: str,
|
|
note_md: str,
|
|
) -> int:
|
|
"""Return the anchor item's Zotero itemID, creating if absent."""
|
|
existing = _find_anchor_id(db, project_tag, role_tag)
|
|
if existing is not None:
|
|
return existing
|
|
|
|
item_key = generate_key()
|
|
item_id = db.create_item(TYPE_MAP["document"], key=item_key, now=now_iso())
|
|
db.set_fields(item_id, {"title": title})
|
|
db.sync_tags(item_id, [project_tag, role_tag, "module:prisma"])
|
|
db.add_to_collection(item_id, collection_key=collection_key)
|
|
|
|
# Zotero notes store HTML. Wrap the markdown verbatim inside a
|
|
# <pre> block so it survives a round-trip and stays legible in the
|
|
# UI — reviewers can still edit it, and our loader strips the
|
|
# wrapper when reading it back.
|
|
db.add_note(
|
|
item_id,
|
|
f"<pre>{_html_escape(note_md)}</pre>",
|
|
title=title,
|
|
)
|
|
db.commit()
|
|
return item_id
|
|
|
|
|
|
def _find_anchor_id(db: Db, project_tag: str, role_tag: str) -> int | None:
|
|
row = db.con.execute(
|
|
"""
|
|
SELECT it1.itemID
|
|
FROM itemTags it1
|
|
JOIN tags t1 ON it1.tagID = t1.tagID AND t1.name = ?
|
|
JOIN itemTags it2 ON it1.itemID = it2.itemID
|
|
JOIN tags t2 ON it2.tagID = t2.tagID AND t2.name = ?
|
|
LIMIT 1
|
|
""",
|
|
(project_tag, role_tag),
|
|
).fetchone()
|
|
return row[0] if row else None
|
|
|
|
|
|
def _load_note(db: Db, project_tag: str, role_tag: str) -> str:
|
|
item_id = _find_anchor_id(db, project_tag, role_tag)
|
|
if item_id is None:
|
|
raise LookupError(
|
|
f"project anchor not found: {project_tag} + {role_tag}. "
|
|
f"Run `stack prisma init <project>` first."
|
|
)
|
|
row = db.con.execute(
|
|
"SELECT note FROM itemNotes WHERE parentItemID = ? LIMIT 1",
|
|
(item_id,),
|
|
).fetchone()
|
|
if not row:
|
|
raise LookupError(f"anchor item {item_id} has no note attached")
|
|
raw = row[0] or ""
|
|
# Strip the <pre>…</pre> wrapper we put in on create (best-effort;
|
|
# if a reviewer pasted over it with plain HTML, we surface whatever
|
|
# they wrote).
|
|
stripped = raw.strip()
|
|
if stripped.startswith("<pre>") and stripped.endswith("</pre>"):
|
|
stripped = stripped[5:-6]
|
|
return _html_unescape(stripped)
|
|
|
|
|
|
def _html_escape(s: str) -> str:
|
|
return s.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
|
|
|
|
def _html_unescape(s: str) -> str:
|
|
return s.replace("<", "<").replace(">", ">").replace("&", "&")
|