About:Pharmacopedia.ext: Difference between revisions
| [checked revision] | [checked revision] |
MDElliottMD (talk | contribs) Migrate <indication> tags to <problem> (Phase 2 of indications-to-problems rebuild) |
MDElliottMD (talk | contribs) Refresh: reflect current extension state (problems renaming, 0-100 sliders, ±100 valence, ICD ingest, autosave, rich reports, demographics rebuild) |
||
| Line 1: | Line 1: | ||
{{TOC right}} | {{TOC right}} | ||
= Pharmacopedia extension | = Pharmacopedia extension specification = | ||
'''Requires:''' MediaWiki ≥ 1.46.0 · PHP ≥ 8.5 | |||
'''Author:''' MDElliottMD | '''Author:''' MDElliottMD · '''License:''' GPL-2.0-or-later | ||
'''Source:''' <code>/var/www/mediawiki/extensions/Pharmacopedia/</code> | '''Source:''' <code>/var/www/mediawiki/extensions/Pharmacopedia/</code> | ||
The Pharmacopedia extension turns a MediaWiki install into a structured, community-edited | The Pharmacopedia extension turns a MediaWiki install into a structured, community-edited medicine reference with rich user-profile and assessment infrastructure. It adds parser tags, special pages, API modules, a chip-picker / autosave UI framework, and a database schema that together support: | ||
* Structured | * Structured medicine pages via the <code><nowiki>{{MedTemplate}}</nowiki></code> template | ||
* Per-user rating | * Per-user rating on effects, problems, titration strategies, anecdotes, and drug-drug interactions (continuous 0–100 sliders, ±100 valence; no 0–5 likert anywhere) | ||
* Two-perspective data capture (personal vs. provider) wherever clinically meaningful | * Two-perspective data capture (personal vs. provider) wherever clinically meaningful | ||
* | * User profile with dimensional personality / autism assessments (CATI, CAT-Q, MBTI, Enneagram, PID-5-BF, OCEAN/BFI-10) and rich auto-generated reports | ||
* | * Diagnosis autocomplete backed by ~25,500 ICD-10-CM and ~15,800 ICD-11 codes | ||
* Chip-picker / autosave / slider-precise-input UI framework shared across editor surfaces | |||
* Verified-provider role with document-based verification | |||
== Precision doctrine == | |||
A standing design rule (memorialised 2026-05-17) that shapes every storage / UI decision in the extension: | |||
* '''No bucketing where a number or free-text will do.''' Income is numeric + currency, not a 5-band dropdown. Education keeps the bucketed dropdown ''and'' adds numeric years of schooling + free-text field of study. | |||
* '''No single-select where multi-select reflects reality.''' Languages, gender identities, ethnicities, pronouns, religion, marital status, stop-reasons, all use chip-pickers (with optional severity per chip where relevant). | |||
* '''No forced category where a continuous score works.''' All assessments (Enneagram 9 type sliders, MBTI 4 dichotomy sliders, OCEAN 5 trait sliders, CATI/CAT-Q/PID-5-BF items) use continuous 0–100 sliders, never radio buttons or button rows. Valence is ±100, not ±3. | |||
* '''Storage in canonical form, UI converts at display.''' Heights stored cm regardless of user's preferred unit (cm or ft+in); ICD codes stored as ISO; date capture as range / possibility-mix JSON. | |||
* '''Browser auto-fill as suggestion only.''' Country chip pre-fills from <code>navigator.language</code>; languages from <code>navigator.languages</code>; time zone from <code>Intl.DateTimeFormat().resolvedOptions().timeZone</code>. User can always edit or remove. | |||
* '''Always allow custom free-text where the curated list might miss someone.''' Chip-pickers accept Enter-to-add custom chips for all picklists except ISO-coded ones (country, language). | |||
== High-level architecture == | == High-level architecture == | ||
* '''Backend (PHP):''' <code>includes/</code> | * '''Backend (PHP):''' <code>includes/</code>, one class per parser tag, store, special page, or API module. Auto-loaded under <code>MediaWiki\Extension\Pharmacopedia\</code>. Assessment classes under <code>includes/Assessments/</code>. | ||
* '''Frontend (JS):''' <code>resources/ext.pharmacopedia.js</code> | * '''Frontend (JS):''' <code>resources/ext.pharmacopedia.js</code>, single IIFE binding click handlers, modals, chip-pickers, autocomplete, autosave debouncer, slider-precise click-to-type. <code>resources/ext.pharmacopedia.blocksave.js</code> is the autosave infrastructure (debounced AJAX per block, race-safe). | ||
* '''Styles (CSS):''' <code>resources/ext.pharmacopedia.css</code> | * '''Styles (CSS):''' <code>resources/ext.pharmacopedia.css</code>, shared row layout, per-tag chrome, dark-theme palette (black / dark-grey / purple / white primary; red / green / blue / teal sparingly for semantic distinction). | ||
* '''Schema:''' <code>sql/</code> | * '''Datepicker:''' <code>resources/ext.pharmacopedia.datepicker.js</code> + <code>.css</code>, supports single dates, ranges, and possibility-mixed dates (e.g. "1995 or 1996"). | ||
* '''Schema:''' <code>sql/</code>, ~20 core tables plus migration patches. Picked up via <code>LoadExtensionSchemaUpdates</code> hook. | |||
== Parser tags == | == Parser tags == | ||
Registered via <code>Hooks::onParserFirstCallInit</code>: | |||
{| class="wikitable" | {| class="wikitable" | ||
| Line 30: | Line 44: | ||
| <code><vote></code> || Generic up/down binary vote on an arbitrary slug || <code>VoteTag</code> | | <code><vote></code> || Generic up/down binary vote on an arbitrary slug || <code>VoteTag</code> | ||
|- | |- | ||
| <code><effect></code> || Therapeutic or adverse effect | | <code><effect></code> || Therapeutic or adverse effect; patient + provider perspectives; provider freq slider 0–100; shared valence slider ±100 || <code>EffectTag</code> | ||
|- | |- | ||
| <code><discuss></code> || Threaded comment widget || <code>CommentTag</code> | | <code><discuss></code> || Threaded comment widget || <code>CommentTag</code> | ||
| Line 40: | Line 54: | ||
| <code><anecdote></code> || Personal or provider story with up/down vote || <code>AnecdoteTag</code> | | <code><anecdote></code> || Personal or provider story with up/down vote || <code>AnecdoteTag</code> | ||
|- | |- | ||
| <code>< | | <code><problem></code> || A problem (formerly "indication") the medicine addresses; 0–100 efficacy likert slider + "don't know" toggle || <code>ProblemTag</code> | ||
|- | |- | ||
| <code><pharmaInteractions/></code> || Self-closing; renders the Interactions section for the current page || <code>InteractionTag</code> | | <code><pharmaInteractions/></code> || Self-closing; renders the Interactions section for the current page || <code>InteractionTag</code> | ||
|- | |||
| <code><pharmaExperience/></code> || Self-closing; renders the Experience report form (efficacy, burden, dose, route, schedule, stop-reasons) || <code>ExperienceTag</code> | |||
|} | |} | ||
All tags | All non-self-closing tags take a <code>slug</code> argument and (where relevant) a <code>title</code>, <code>label</code>, <code>author</code>, <code>ref</code>, or <code>perspective</code>. | ||
=== Tag wikitext examples === | === Tag wikitext examples === | ||
<pre> | <pre> | ||
<problem slug=" | <problem slug="depression" title="Major depressive disorder" | ||
author="MDElliottMD"> | author="MDElliottMD">First-line for moderate to severe MDD.</problem> | ||
<effect slug="nausea" label="Nausea"/> | <effect slug="nausea" label="Nausea"/> | ||
| Line 64: | Line 80: | ||
<pharmaInteractions/> | <pharmaInteractions/> | ||
<pharmaExperience/> | |||
</pre> | </pre> | ||
== Voting / rating semantics == | == Voting / rating semantics == | ||
| Line 101: | Line 92: | ||
| Titration || +1 / −1 binary || single || <code>pcp_votes</code> | | Titration || +1 / −1 binary || single || <code>pcp_votes</code> | ||
|- | |- | ||
| Anecdote || +1 / −1 binary || single (perspective is | | Anecdote || +1 / −1 binary || single (perspective is metadata) || <code>pcp_votes</code> | ||
|- | |- | ||
| | | Problem (efficacy likert) || 0–100 continuous slider, optional "Don't know" (-1) || single || <code>pcp_likert_reports</code> | ||
|- | |- | ||
| Effect (patient) || experienced ∈ {yes, no, unsure} + valence | | Effect (patient) || experienced ∈ {yes, no, unsure} + valence ±100 slider || patient || <code>pcp_effect_reports</code> (perspective=1) | ||
|- | |- | ||
| Effect (provider) || frequency | | Effect (provider) || frequency 0–100 continuous slider + "Don't know" (-1) + valence ±100 slider || provider || <code>pcp_effect_reports</code> (perspective=2) | ||
|- | |- | ||
| Interaction || experience 1–5 + | | Interaction || experience 1–5 + valence ±100 slider + optional note || user + provider, separate aggregates || <code>pcp_interaction_reports</code> | ||
|} | |} | ||
Server-side aggregates: <code>n</code>, mean of the rating field, and ( | Server-side aggregates: <code>n</code>, mean of the rating field, and (for interactions) <code>severe = (vmean ≤ −83.0)</code> (rescaled from the original ±3-scale −2.5). | ||
Aggregates are recomputed and returned by every report-submit API call so the row re-renders in place without a page reload. | Aggregates are recomputed and returned by every report-submit API call so the row re-renders in place without a page reload. | ||
| Line 127: | Line 118: | ||
| '''Uncommon''' || > 5 and ≤ 20 || collapsed | | '''Uncommon''' || > 5 and ≤ 20 || collapsed | ||
|- | |- | ||
| '''Rare''' || ≤ 5, provider vmean > | | '''Rare''' || ≤ 5, provider vmean > −83 || collapsed | ||
|- | |- | ||
| '''Rare but Severe''' || ≤ 5 and vmean ≤ | | '''Rare but Severe''' || ≤ 5 and vmean ≤ −83 || '''expanded by default''', red highlight | ||
|- | |- | ||
| '''Not yet rated''' || no provider data (n=0) || collapsed, only renders if non-empty | | '''Not yet rated''' || no provider data (n=0) || collapsed, only renders if non-empty | ||
|} | |} | ||
The vmean ≤ | The vmean ≤ −83 threshold is also the trip-wire for the "severe" red treatment on interaction rows. | ||
== | == User profile == | ||
<code>Special:MyProfile</code> is the user-facing editor for everything personal. Every block on it autosaves on a 800 ms debounce (see [[#Autosave infrastructure|Autosave infrastructure]]). | |||
=== Block list === | |||
* '''Identity''' (display alias, default attribution, experience-report visibility) | |||
* '''Demographics''' (full chip-picker rebuild, see below) | |||
* '''Personality''' (Big Five OCEAN sliders + collapsible assessments) | |||
* '''Enneagram''' (9 type sliders + 45-item screening test) | |||
* '''MBTI''' (4 dichotomy sliders + 32-item OEJTS test) | |||
* '''Personality / autism assessments''' (PID-5-BF, CATI, CAT-Q, each as a collapsible inline test) | |||
* '''Diagnoses''' (multi-row with ICD-10-CM + ICD-11 autocomplete, severity slider 0–100, disability slider 0–100, status, origin, dates, notes) | |||
* '''Medicines I have tried''' (multi-row with med-name autocomplete, dose, route 16-option dropdown, schedule with datalist suggestions, efficacy + burden sliders 0–100, periods via date-picker) | |||
=== Demographics (chip-picker / structured-widget rebuild) === | |||
All categorical demographics use the chip-picker widget (single or multi, with optional primary marker, optional custom free-text). All quantitative demographics use numeric inputs or structured composite widgets. | |||
* '''Birthday''' DatePicker (single / range / possibility-mix) | |||
* '''Sex assigned at birth''' single-select (clinical category) | |||
* '''Gender identity''' multi-select chip-picker, 27 common terms + custom | |||
* '''Pronouns''' multi-select chip-picker, 17 common sets + custom | |||
* '''Ethnicity / race''' multi-select chip-picker, 23 broad categories + custom | |||
* '''Country of residence''' chip-picker single-value, ISO 3166 list (~100 entries), auto-suggested from <code>navigator.language</code> | |||
* '''Languages''' chip-picker multi-select with ★ primary marker, ISO 639-1 list (~70 entries with endonyms), auto-suggested from <code>navigator.languages</code> | |||
* '''Height / weight''' unit toggle (Metric cm/kg or US ft+in/lb); stored canonically as cm/kg regardless | |||
* '''Handedness''' single-select (3 options) | |||
* '''Smoking''' structured widget: status + cigs/day + years smoked + quit date; auto-computes pack-years | |||
* '''Alcohol''' structured widget: drinks/week + typical drink type + max one occasion | |||
* '''Education''' bucketed highest-level + numeric years + free-text field of study | |||
* '''Employment''' bucketed status + free-text occupation + numeric hours/week | |||
* '''Income''' numeric amount + currency selector (20 options) + individual/household scope | |||
* '''Marital / relationship status''' chip-picker single + custom | |||
* '''Religion / spirituality''' chip-picker single + custom, 36 traditions + secular stances | |||
* '''Housing''' chip-picker single + custom | |||
* '''Number of children''' numeric | |||
* '''Time zone''' free text, auto-detects IANA TZ on first load if empty | |||
* '''Chronotype / sleep schedule''' two time inputs (typical bedtime, typical wake) | |||
* '''Political orientation''' two-axis compass sliders (economic ±100, social ±100) | |||
=== Diagnosis subsystem === | |||
Diagnoses are stored in <code>pcp_profile_diagnoses</code> with autocomplete backed by <code>pcp_diagnosis_abbreviations</code> (~41,500 rows): | |||
{| class="wikitable" | |||
! System !! Rows !! Notes | |||
|- | |||
| '''ICD-10-CM''' || 25,542 || CMS FY2026 valid-codes file, chapters A B C D E F G H I J K L M N O P Q R U + 553 friendly-alias rows (mdd, adhd, stroke, htn, etc.) | |||
|- | |||
| '''ICD-11''' || 15,823 || WHO MMS Apr 2026 linearization, chapters 01–24 except billing (22) / external causes (23) / extension modifiers (X) / functioning assessment (V) + 361 friendly-alias rows | |||
|- | |||
| '''DSM-5''' || 32 || Legacy hand-seed for codes without ICD equivalents | |||
|- | |||
| Other || 34 || somatic, unofficial, ICD-10 (WHO), instrument | |||
|} | |||
Skipped intentionally: ICD-10-CM S/T (injury body) ~41k codes, V/W/X/Y (external causes) ~7.5k codes; ICD-11 chapter 22 (injury), 23 (external causes), 25 (special purposes), V (functioning scales), X (17.7k extension modifiers). These are billing scaffolding, not diagnoses. | |||
Autocomplete via <code>action=pharmacopediadxsearch</code>, multi-token AND search (e.g. "ADHD inattentive" matches the F90.0 row that contains both substrings); ORDER BY <code>FIELD(da_system, 'ICD-10-CM', 'ICD-11', 'DSM-5', ...)</code> so ICD-10-CM leads, then ICD-11, then everything else. | |||
=== Personality / autism assessments === | |||
Six assessments, all with continuous-slider items, "Not sure" toggle per item, auto-computed subscale + total scores, and a rich auto-generated report at <code>Special:MyAssessment/{key}</code>: | |||
= | {| class="wikitable" | ||
! Assessment !! Items !! Score range !! Cutoffs / threshold !! Report | |||
|- | |||
| '''CATI''' || 42 (6 subscales) || 1–5 per item, sum per subscale || 148 / 139 / 141 / 156 (English 2025 gender-specific) || gender-specific scoring against English 2025 normative tables (12,253-row CatiNorms.php from OSF supplementary) | |||
|- | |||
| '''CAT-Q''' || 25 (3 subscales) || 1–7 per item, sum per subscale || Total ≥ 110 + per-subscale cutoffs (NeurodivUrgent recalibration) || subscale narratives, top-item analysis | |||
|- | |||
| '''PID-5-BF''' || 25 (5 domains) || 0–3 per item, mean per domain || mean ≥ 2.0 per domain || domain narratives, cross-system mapping (DSM-5 AMPD ↔ ICD-11 PD ↔ Big Five) | |||
|- | |||
| '''MBTI''' || 32 OEJTS items + 4 direct dichotomy sliders || ±2 per axis || none (dimensional treatment, no forced categorisation) || 4-axis Position column with letter + strength + bar, cognitive function stack, Big Five mapping, top-item analysis | |||
|- | |||
| '''Enneagram''' || 45 (5 per type × 9 types) || 0–100 per type || none (no clinical cutoffs for typology) || hero banner (primary + wing + tritype), 9-bar profile, primary deep-dive, wing analysis, centers, Hornevian + Harmonic groups, stress / growth lines, cross-system map (Big Five, MBTI) | |||
|- | |||
| '''OCEAN (Big Five)''' || 5 direct sliders + optional BFI-10 (10 items) || 0–100 per trait || none (personality, not pathology) || trait deep-dives (high / mid / low at your score), BFI-10 item table, cross-system mapping (MBTI ↔ Enneagram ↔ PID-5-BF) pulling live data from the profile | |||
|} | |||
All assessment items submit raw responses to <code>pcp_user_profile_fields</code> under namespace <code>{key}_raw</code> (e.g. <code>cati_raw</code>); the computed scores live under namespace <code>{key}</code> with keys like <code>subscale_SOC</code>, <code>total</code>, plus a <code>taken_at</code> timestamp. Re-scoring happens automatically on every save (autosave fires `scoreResponses()` on the full raw set, skipping "unsure" rows). | |||
== Autosave infrastructure == | |||
= | Every block on <code>Special:MyProfile</code> is wrapped in <code><div data-pcp-save-block="block-name"></code>. The blocksave.js library: | ||
<code> | # Listens for <code>input</code> and <code>change</code> events on every input inside any save-block | ||
# 800 ms after the last event, POSTs the block's serialized form data to <code>Special:SaveProfileBlock</code> with <code>block=block-name</code> | |||
# Shows a transient chip (top-right and bottom of the block): pending… → saving… → ✓ saved (fades after 1.2 s) or ✗ error (sticks, clickable to retry) | |||
# Race-safe: if user keeps typing during an in-flight save, the in-flight save records what it sent; newer changes mark the block dirty again and schedule another save when the response returns | |||
# Diagnosis + medicines "Add a row" slots are exempted from autosave (would create duplicates); they require an explicit + Add button | |||
# Programmatic widgets (chip-pickers, units, smoking, alcohol, chronotype) fire <code>change</code> events on their hidden fields so the listener notices | |||
Slider numbers are also clickable: a single delegated handler on every <code><output></code> next to a range slider swaps it for a number input on click, accepts a precise typed value (clamps to the slider's <code>min</code>/<code>max</code>), commits with Enter, cancels with Escape. | |||
<code> | Scroll position is preserved across the rare reloads (delete operations on diagnoses / medicines / experience reports, and the auto-reload after a new diagnosis or medicine is added) via <code>sessionStorage</code>. | ||
== Special pages == | |||
{| class="wikitable" | |||
! Page !! Purpose | |||
|- | |||
| <code>Special:MyProfile</code> || Edit your full profile (identity, demographics, personality, dx, meds). Autosave throughout. | |||
|- | |||
| <code>Special:UserProfile/<name></code> || Public profile view (filtered by per-field visibility) | |||
|- | |||
| <code>Special:MyAssessment</code> || Index of rich assessment reports | |||
|- | |||
| <code>Special:MyAssessment/cati</code>, <code>/catq</code>, <code>/pid5bf</code>, <code>/mbti</code>, <code>/enneagram</code>, <code>/ocean</code> || Rich report per assessment | |||
|- | |||
| <code>Special:SaveProfileBlock</code> || AJAX endpoint for autosave (POST-only, JSON response) | |||
|- | |||
| <code>Special:Problems</code> || Browse the problems repository (165+ entries, 18 categories) | |||
|- | |||
| <code>Special:Problem/<slug></code> || Individual problem page | |||
|- | |||
| <code>Special:SuggestProblem</code> || User-facing form to suggest a new problem | |||
|- | |||
| <code>Special:ManageProblems</code> || Sysop tool for problem-repository moderation | |||
|- | |||
| <code>Special:ReviewExperience</code> || Sysop queue for pending experience reports | |||
|- | |||
| <code>Special:ManageInteractions</code> || Sysop bulk-edit interaction reports | |||
|- | |||
| <code>Special:DeletePharmaElement</code> || Sysop delete tool for any votable element | |||
|} | |||
== API modules == | |||
{| class="wikitable" | |||
! Action !! Purpose | |||
|- | |||
| <code>pharmacopediavote</code> || Submit / clear binary vote on votable element | |||
|- | |||
| <code>pharmacopedialikert</code> || Submit problem-efficacy likert (0–100 + −1 DK) | |||
|- | |||
| <code>pharmacopediaeffect</code> || Submit effect report (patient or provider perspective) | |||
|- | |||
| <code>pharmacopediainteractionreport</code> || Submit interaction report | |||
|- | |||
| <code>pharmacopediainteractionadd</code> || Create a new interaction edge | |||
|- | |||
| <code>pharmacopediacomment</code> || Threaded discussion ops (add / edit / delete / reply) | |||
|- | |||
| <code>pharmacopediaexperiencesubmit</code> || Submit experience report (multi-field form) | |||
|- | |||
| <code>pharmacopediaexperiencereview</code> || Sysop approve / reject experience report | |||
|- | |||
| <code>pharmacopediadxsearch</code> || Diagnosis autocomplete against the 41k-row abbreviation table | |||
|- | |||
| <code>pharmacopediaproblemsearch</code> || Problem-repository autocomplete | |||
|- | |||
| <code>pharmacopediaeffectslookup</code>, <code>indicationslookup</code> || Pickers used by the experience-submit form | |||
|- | |||
| <code>pharmacopedialiteratureadd</code>, <code>literaturedelete</code> || Literature attachment ops | |||
|} | |||
== Interactions feature == | |||
The Interactions section is rendered by placing <code><pharmaInteractions/></code> anywhere in the wikitext of a med article ('''NS_MAIN''') or a Category page ('''NS_CATEGORY'''). | |||
=== Entity model === | |||
An interaction is an undirected edge between two endpoints. Each endpoint has a '''type''' (<code>med</code> or <code>category</code>) and a '''slug''' (DB-key form of the page title). Pairs are stored in canonical order: smaller <code>(type, slug)</code> tuple on the left. | |||
=== Rendering rules === | === Rendering rules === | ||
| Line 180: | Line 301: | ||
* On a '''Category''' page, list direct edges only (no transitive walk). | * On a '''Category''' page, list direct edges only (no transitive walk). | ||
* Sort: pooled <code>valence_mean</code> ascending (most negative on top). Nulls sink. Tiebreakers: <code>n</code> desc, then alphabetic. | * Sort: pooled <code>valence_mean</code> ascending (most negative on top). Nulls sink. Tiebreakers: <code>n</code> desc, then alphabetic. | ||
* Severe (any of pooled / user / provider vmean ≤ | * Severe (any of pooled / user / provider vmean ≤ −83.0): red 4 px left border + red-tinted background + "severe" pill + counterparty title in red. | ||
=== Add-interaction modal === | === Add-interaction modal === | ||
Triggered by the <code>+ Add interaction</code> button at the bottom of the section. Two-stage UX: | Triggered by the <code>+ Add interaction</code> button at the bottom of the section. Two-stage UX: search → click Use → confirm with Add interaction → POST to <code>pharmacopediainteractionadd</code>. Categories appear in the modal only if tagged with the marker category (default <code>Category:MedCategory</code>, configurable via <code>$wgPharmacopediaInteractionCategoryMarker</code>). | ||
== Experience reports == | |||
User-submitted reports of personal or clinical experience with a medicine, via <code><pharmaExperience/></code> on a med page. Stored pending in <code>pcp_experience_reports</code>; visible publicly only after sysop approval through <code>Special:ReviewExperience</code>. | |||
Captured fields: | |||
* Perspective (personal / clinical) | |||
* Currently taking it (yes / no, stopped) | |||
* Duration (value + unit) | |||
* Dose (mg, decimal-precise) | |||
* Route (16-option dropdown: PO, IV, IM, SC, SL, buccal, inhaled, intranasal, topical, transdermal, PR, ophthalmic, otic, vaginal, insufflated, other) | |||
* Schedule (free text with datalist of QD / BID / TID / QID / q4h / q6h / q8h / q12h / qHS / qAM / qPM / PRN) | |||
* Patient count (clinical only): min + optional max for ranges | |||
* Efficacy (0–100 slider) | |||
* Side-effect burden (0–100 slider) | |||
* Stop reasons (personal + stopped only): JSON multi-select with optional severity slider per reason — codes: side_effects / ineffective / cost / no_longer_needed / clinician_advised / other | |||
* Free-text anecdote | |||
* Problems addressed (multi-pick with per-problem efficacy) | |||
* Effects experienced (multi-pick with per-effect valence + frequency) | |||
== Storage tables (selected) == | |||
{| class="wikitable" | |||
! Table !! Purpose | |||
|- | |||
| <code>pcp_votable_elements</code> || Stable per-(page, slug) handle reused by votes / likert / comments | |||
|- | |||
| <code>pcp_votes</code> || Binary +1/−1 votes | |||
|- | |||
| <code>pcp_likert_reports</code> || Problem efficacy (0–100 + −1 DK), TINYINT signed | |||
|- | |||
| <code>pcp_effect_reports</code> || Effect ratings: experienced / frequency / valence (±100); perspective 1/2 | |||
|- | |||
| <code>pcp_interaction_reports</code> || Interaction ratings: experience 1–5 / valence ±100 / note; perspective 1/2 | |||
|- | |||
| <code>pcp_interactions</code> || Interaction edges (canonical-ordered pair) | |||
|- | |||
| <code>pcp_comments</code> || Threaded discussions | |||
|- | |||
| <code>pcp_user_profiles</code> || Per-user profile meta (alias, attribution, voter hash) | |||
|- | |||
| <code>pcp_user_profile_fields</code> || Generic key-value field store: (namespace, key, num, text, visibility) | |||
|- | |||
| <code>pcp_profile_diagnoses</code> || Per-user diagnoses (system, code, description, status, origin, severity 0–100, disability 0–100, dates, notes, visibility) | |||
|- | |||
| <code>pcp_user_meds</code> || Per-user medicines (name, page_id, efficacy 0–100, burden 0–100, dose_mg, route, schedule, duration, periods JSON, current, notes, visibility) | |||
|- | |||
| <code>pcp_diagnosis_abbreviations</code> || ICD-10-CM + ICD-11 + DSM-5 + aliases (~41,500 rows, VARCHAR/utf8mb4 for native case-insensitive search) | |||
|- | |||
| <code>pcp_problem</code>, <code>pcp_problem_alias</code> || Problems repository + alias lookup | |||
|- | |||
| <code>pcp_experience_reports</code> || Experience reports (pending → approved); efficacy + burden 0–100; route + schedule; stop-reasons JSON; patient-count min + max | |||
|- | |||
| <code>pcp_life_events</code>, <code>pcp_life_traits</code>, <code>pcp_life_images</code> || Life Story keyframes (auto-generated from assessments, manually authorable) | |||
|- | |||
| <code>pcp_literature</code> || Citation attachments | |||
|} | |||
== Notable lessons learned == | |||
* '''Cargo string fields cap at ~300 chars.''' <code>structure</code> and <code>mechanism</code> on MedTemplate are short VARCHARs; long prose goes in MEDIUMBLOB sections (pharmacokinetics, pharmacodynamics, intro). Overruns silently lose Cargo data (MySQL Error 1406). | |||
* '''VARBINARY + LOWER() is a no-op.''' MariaDB's LOWER() returns binary types unchanged. <code>pcp_diagnosis_abbreviations</code> was migrated VARBINARY → VARCHAR/utf8mb4 (2026-05-17) so case-insensitive LIKE works natively without CONVERT() wrappers. | |||
* '''FlaggedRevs locks template inclusions by default.''' Config fix: <code>$wgFlaggedRevsHandleIncludes=0</code> + remove NS_TEMPLATE from <code>$wgFlaggedRevsNamespaces</code> via an extension-function callback (MW 1.46 array config concatenation behavior). | |||
* '''CSP must allow Cloudflare Turnstile and any other 3rd-party widget script source.''' Symptom: generic "There are problems with some of your input" on form submit. Audit script-src / frame-src / connect-src / style-src whenever adding any 3rd-party JS widget. | |||
* '''Sidebar cache must be purged after CLI <code>maintenance/edit.php</code> writes''' to <code>MediaWiki:Sidebar</code> or other chrome pages: <code>curl -X POST .../api.php?action=purge&titles=MediaWiki:Sidebar</code>. | |||
* '''CLI E_USER_DEPRECATED suppressed in LocalSettings.php''' (EmbedVideo / FlaggedRevs spam ~6 lines per maintenance/run.php call on MW 1.46). Web behavior unaffected. | |||
== Hooks == | |||
* <code>ParserFirstCallInit</code>: register all parser tags | |||
* <code>LoadExtensionSchemaUpdates</code>: install / migrate schema (the sql/ directory + patches) | |||
* <code>BeforePageDisplay</code>: inject the ext.pharmacopedia.* ResourceLoader modules | |||
* <code>UserGetRights</code> + <code>UserEffectiveGroups</code>: verified-provider role wiring | |||
* Various special-page registrations via <code>SpecialPage_initList</code> | |||
== Configuration globals == | |||
* <code>$wgPharmacopediaInteractionCategoryMarker</code> (default <code>Category:MedCategory</code>): only categories tagged with this marker appear in the add-interaction modal | |||
* (Various permission-grant arrays via standard MW <code>$wgGroupPermissions</code>) | |||
== Source layout == | |||
extensions/Pharmacopedia/ | |||
|-- extension.json | |||
|-- includes/ | |||
| |-- Hooks.php | |||
| |-- *Tag.php (parser tags: VoteTag, EffectTag, ProblemTag, ...) | |||
| |-- *Store.php (data access: EffectStore, ProblemStore, ...) | |||
| |-- UserProfileStore.php (the big one; profile / dx / meds / abbreviations) | |||
| |-- SpecialMyProfile.php (the user profile editor; large) | |||
| |-- SpecialMyAssessment.php (rich reports for all 6 assessments; large) | |||
| |-- SpecialSaveProfileBlock.php (autosave AJAX endpoint) | |||
| |-- Special*.php (other special pages) | |||
| |-- ProfileDatasets.php (countries, languages, genders, religions, etc.) | |||
| |-- DatePicker.php (range / possibility-mix date widget backend) | |||
| |-- Assessments/ | |||
| | |-- Cati.php, CatiNorms.php | |||
| | |-- Catq.php | |||
| | |-- Pid5bf.php | |||
| | |-- Mbti.php | |||
| | |-- Enneagram.php | |||
| | `-- Raadsr.php (deprecated 2026-05-17 in favor of CATI, kept for archival reads) | |||
| `-- Api/ | |||
| `-- (one class per API module) | |||
|-- resources/ | |||
| |-- ext.pharmacopedia.js (single IIFE: chip-picker, dx autocomplete, BFI-10 compute, ...) | |||
| |-- ext.pharmacopedia.blocksave.js (debounced autosave per block) | |||
| |-- ext.pharmacopedia.css | |||
| |-- ext.pharmacopedia.datepicker.js + .css | |||
|-- sql/ | |||
| |-- (one .sql per table; patches as patch-*.sql) | |||
`-- i18n/ | |||
`-- en.json | |||
[[Category:Pharmacopedia documentation]] | |||
Revision as of 11:49, 17 May 2026
Pharmacopedia extension specification
Requires: MediaWiki ≥ 1.46.0 · PHP ≥ 8.5
Author: MDElliottMD · License: GPL-2.0-or-later
Source: /var/www/mediawiki/extensions/Pharmacopedia/
The Pharmacopedia extension turns a MediaWiki install into a structured, community-edited medicine reference with rich user-profile and assessment infrastructure. It adds parser tags, special pages, API modules, a chip-picker / autosave UI framework, and a database schema that together support:
- Structured medicine pages via the
{{MedTemplate}}template - Per-user rating on effects, problems, titration strategies, anecdotes, and drug-drug interactions (continuous 0–100 sliders, ±100 valence; no 0–5 likert anywhere)
- Two-perspective data capture (personal vs. provider) wherever clinically meaningful
- User profile with dimensional personality / autism assessments (CATI, CAT-Q, MBTI, Enneagram, PID-5-BF, OCEAN/BFI-10) and rich auto-generated reports
- Diagnosis autocomplete backed by ~25,500 ICD-10-CM and ~15,800 ICD-11 codes
- Chip-picker / autosave / slider-precise-input UI framework shared across editor surfaces
- Verified-provider role with document-based verification
Precision doctrine
A standing design rule (memorialised 2026-05-17) that shapes every storage / UI decision in the extension:
- No bucketing where a number or free-text will do. Income is numeric + currency, not a 5-band dropdown. Education keeps the bucketed dropdown and adds numeric years of schooling + free-text field of study.
- No single-select where multi-select reflects reality. Languages, gender identities, ethnicities, pronouns, religion, marital status, stop-reasons, all use chip-pickers (with optional severity per chip where relevant).
- No forced category where a continuous score works. All assessments (Enneagram 9 type sliders, MBTI 4 dichotomy sliders, OCEAN 5 trait sliders, CATI/CAT-Q/PID-5-BF items) use continuous 0–100 sliders, never radio buttons or button rows. Valence is ±100, not ±3.
- Storage in canonical form, UI converts at display. Heights stored cm regardless of user's preferred unit (cm or ft+in); ICD codes stored as ISO; date capture as range / possibility-mix JSON.
- Browser auto-fill as suggestion only. Country chip pre-fills from
navigator.language; languages fromnavigator.languages; time zone fromIntl.DateTimeFormat().resolvedOptions().timeZone. User can always edit or remove. - Always allow custom free-text where the curated list might miss someone. Chip-pickers accept Enter-to-add custom chips for all picklists except ISO-coded ones (country, language).
High-level architecture
- Backend (PHP):
includes/, one class per parser tag, store, special page, or API module. Auto-loaded underMediaWiki\Extension\Pharmacopedia\. Assessment classes underincludes/Assessments/. - Frontend (JS):
resources/ext.pharmacopedia.js, single IIFE binding click handlers, modals, chip-pickers, autocomplete, autosave debouncer, slider-precise click-to-type.resources/ext.pharmacopedia.blocksave.jsis the autosave infrastructure (debounced AJAX per block, race-safe). - Styles (CSS):
resources/ext.pharmacopedia.css, shared row layout, per-tag chrome, dark-theme palette (black / dark-grey / purple / white primary; red / green / blue / teal sparingly for semantic distinction). - Datepicker:
resources/ext.pharmacopedia.datepicker.js+.css, supports single dates, ranges, and possibility-mixed dates (e.g. "1995 or 1996"). - Schema:
sql/, ~20 core tables plus migration patches. Picked up viaLoadExtensionSchemaUpdateshook.
Parser tags
Registered via Hooks::onParserFirstCallInit:
| Tag | Purpose | Class |
|---|---|---|
<vote> |
Generic up/down binary vote on an arbitrary slug | VoteTag
|
<effect> |
Therapeutic or adverse effect; patient + provider perspectives; provider freq slider 0–100; shared valence slider ±100 | EffectTag
|
<discuss> |
Threaded comment widget | CommentTag
|
<effectsummary> |
Roll-up aggregate header | EffectSummaryTag
|
<titration> |
Titration strategy card with up/down vote | TitrationTag
|
<anecdote> |
Personal or provider story with up/down vote | AnecdoteTag
|
<problem> |
A problem (formerly "indication") the medicine addresses; 0–100 efficacy likert slider + "don't know" toggle | ProblemTag
|
<pharmaInteractions/> |
Self-closing; renders the Interactions section for the current page | InteractionTag
|
<pharmaExperience/> |
Self-closing; renders the Experience report form (efficacy, burden, dose, route, schedule, stop-reasons) | ExperienceTag
|
All non-self-closing tags take a slug argument and (where relevant) a title, label, author, ref, or perspective.
Tag wikitext examples
<problem slug="depression" title="Major depressive disorder" author="MDElliottMD">First-line for moderate to severe MDD.</problem> <effect slug="nausea" label="Nausea"/> <effect ref="hyperkalemia"/> <!-- ref to global effect library --> <titration slug="slow-start-elderly" title="Slow start (elderly)" author="MDElliottMD">Begin at 10 mg q AM; titrate by 10 mg every 14 days.</titration> <anecdote slug="qi8sg2" perspective="provider" author="MDElliottMD">One patient developed serotonin syndrome at week 3...</anecdote> <pharmaInteractions/> <pharmaExperience/>
Voting / rating semantics
| Element | Scale | Perspectives | Storage |
|---|---|---|---|
| Vote tag | +1 / −1 binary | single | pcp_votes
|
| Titration | +1 / −1 binary | single | pcp_votes
|
| Anecdote | +1 / −1 binary | single (perspective is metadata) | pcp_votes
|
| Problem (efficacy likert) | 0–100 continuous slider, optional "Don't know" (-1) | single | pcp_likert_reports
|
| Effect (patient) | experienced ∈ {yes, no, unsure} + valence ±100 slider | patient | pcp_effect_reports (perspective=1)
|
| Effect (provider) | frequency 0–100 continuous slider + "Don't know" (-1) + valence ±100 slider | provider | pcp_effect_reports (perspective=2)
|
| Interaction | experience 1–5 + valence ±100 slider + optional note | user + provider, separate aggregates | pcp_interaction_reports
|
Server-side aggregates: n, mean of the rating field, and (for interactions) severe = (vmean ≤ −83.0) (rescaled from the original ±3-scale −2.5).
Aggregates are recomputed and returned by every report-submit API call so the row re-renders in place without a page reload.
Effect bucketing
When a wiki <ul> contains only <effect> cards, JavaScript groups them into buckets by the provider frequency mean (data-fmean):
| Bucket | fmean band | Default state |
|---|---|---|
| Common | > 20 | expanded, always visible |
| Uncommon | > 5 and ≤ 20 | collapsed |
| Rare | ≤ 5, provider vmean > −83 | collapsed |
| Rare but Severe | ≤ 5 and vmean ≤ −83 | expanded by default, red highlight |
| Not yet rated | no provider data (n=0) | collapsed, only renders if non-empty |
The vmean ≤ −83 threshold is also the trip-wire for the "severe" red treatment on interaction rows.
User profile
Special:MyProfile is the user-facing editor for everything personal. Every block on it autosaves on a 800 ms debounce (see Autosave infrastructure).
Block list
- Identity (display alias, default attribution, experience-report visibility)
- Demographics (full chip-picker rebuild, see below)
- Personality (Big Five OCEAN sliders + collapsible assessments)
- Enneagram (9 type sliders + 45-item screening test)
- MBTI (4 dichotomy sliders + 32-item OEJTS test)
- Personality / autism assessments (PID-5-BF, CATI, CAT-Q, each as a collapsible inline test)
- Diagnoses (multi-row with ICD-10-CM + ICD-11 autocomplete, severity slider 0–100, disability slider 0–100, status, origin, dates, notes)
- Medicines I have tried (multi-row with med-name autocomplete, dose, route 16-option dropdown, schedule with datalist suggestions, efficacy + burden sliders 0–100, periods via date-picker)
Demographics (chip-picker / structured-widget rebuild)
All categorical demographics use the chip-picker widget (single or multi, with optional primary marker, optional custom free-text). All quantitative demographics use numeric inputs or structured composite widgets.
- Birthday DatePicker (single / range / possibility-mix)
- Sex assigned at birth single-select (clinical category)
- Gender identity multi-select chip-picker, 27 common terms + custom
- Pronouns multi-select chip-picker, 17 common sets + custom
- Ethnicity / race multi-select chip-picker, 23 broad categories + custom
- Country of residence chip-picker single-value, ISO 3166 list (~100 entries), auto-suggested from
navigator.language - Languages chip-picker multi-select with ★ primary marker, ISO 639-1 list (~70 entries with endonyms), auto-suggested from
navigator.languages - Height / weight unit toggle (Metric cm/kg or US ft+in/lb); stored canonically as cm/kg regardless
- Handedness single-select (3 options)
- Smoking structured widget: status + cigs/day + years smoked + quit date; auto-computes pack-years
- Alcohol structured widget: drinks/week + typical drink type + max one occasion
- Education bucketed highest-level + numeric years + free-text field of study
- Employment bucketed status + free-text occupation + numeric hours/week
- Income numeric amount + currency selector (20 options) + individual/household scope
- Marital / relationship status chip-picker single + custom
- Religion / spirituality chip-picker single + custom, 36 traditions + secular stances
- Housing chip-picker single + custom
- Number of children numeric
- Time zone free text, auto-detects IANA TZ on first load if empty
- Chronotype / sleep schedule two time inputs (typical bedtime, typical wake)
- Political orientation two-axis compass sliders (economic ±100, social ±100)
Diagnosis subsystem
Diagnoses are stored in pcp_profile_diagnoses with autocomplete backed by pcp_diagnosis_abbreviations (~41,500 rows):
| System | Rows | Notes |
|---|---|---|
| ICD-10-CM | 25,542 | CMS FY2026 valid-codes file, chapters A B C D E F G H I J K L M N O P Q R U + 553 friendly-alias rows (mdd, adhd, stroke, htn, etc.) |
| ICD-11 | 15,823 | WHO MMS Apr 2026 linearization, chapters 01–24 except billing (22) / external causes (23) / extension modifiers (X) / functioning assessment (V) + 361 friendly-alias rows |
| DSM-5 | 32 | Legacy hand-seed for codes without ICD equivalents |
| Other | 34 | somatic, unofficial, ICD-10 (WHO), instrument |
Skipped intentionally: ICD-10-CM S/T (injury body) ~41k codes, V/W/X/Y (external causes) ~7.5k codes; ICD-11 chapter 22 (injury), 23 (external causes), 25 (special purposes), V (functioning scales), X (17.7k extension modifiers). These are billing scaffolding, not diagnoses.
Autocomplete via action=pharmacopediadxsearch, multi-token AND search (e.g. "ADHD inattentive" matches the F90.0 row that contains both substrings); ORDER BY FIELD(da_system, 'ICD-10-CM', 'ICD-11', 'DSM-5', ...) so ICD-10-CM leads, then ICD-11, then everything else.
Personality / autism assessments
Six assessments, all with continuous-slider items, "Not sure" toggle per item, auto-computed subscale + total scores, and a rich auto-generated report at Special:MyAssessment/{key}:
| Assessment | Items | Score range | Cutoffs / threshold | Report |
|---|---|---|---|---|
| CATI | 42 (6 subscales) | 1–5 per item, sum per subscale | 148 / 139 / 141 / 156 (English 2025 gender-specific) | gender-specific scoring against English 2025 normative tables (12,253-row CatiNorms.php from OSF supplementary) |
| CAT-Q | 25 (3 subscales) | 1–7 per item, sum per subscale | Total ≥ 110 + per-subscale cutoffs (NeurodivUrgent recalibration) | subscale narratives, top-item analysis |
| PID-5-BF | 25 (5 domains) | 0–3 per item, mean per domain | mean ≥ 2.0 per domain | domain narratives, cross-system mapping (DSM-5 AMPD ↔ ICD-11 PD ↔ Big Five) |
| MBTI | 32 OEJTS items + 4 direct dichotomy sliders | ±2 per axis | none (dimensional treatment, no forced categorisation) | 4-axis Position column with letter + strength + bar, cognitive function stack, Big Five mapping, top-item analysis |
| Enneagram | 45 (5 per type × 9 types) | 0–100 per type | none (no clinical cutoffs for typology) | hero banner (primary + wing + tritype), 9-bar profile, primary deep-dive, wing analysis, centers, Hornevian + Harmonic groups, stress / growth lines, cross-system map (Big Five, MBTI) |
| OCEAN (Big Five) | 5 direct sliders + optional BFI-10 (10 items) | 0–100 per trait | none (personality, not pathology) | trait deep-dives (high / mid / low at your score), BFI-10 item table, cross-system mapping (MBTI ↔ Enneagram ↔ PID-5-BF) pulling live data from the profile |
All assessment items submit raw responses to pcp_user_profile_fields under namespace {key}_raw (e.g. cati_raw); the computed scores live under namespace {key} with keys like subscale_SOC, total, plus a taken_at timestamp. Re-scoring happens automatically on every save (autosave fires `scoreResponses()` on the full raw set, skipping "unsure" rows).
Autosave infrastructure
Every block on Special:MyProfile is wrapped in <div data-pcp-save-block="block-name">. The blocksave.js library:
- Listens for
inputandchangeevents on every input inside any save-block - 800 ms after the last event, POSTs the block's serialized form data to
Special:SaveProfileBlockwithblock=block-name - Shows a transient chip (top-right and bottom of the block): pending… → saving… → ✓ saved (fades after 1.2 s) or ✗ error (sticks, clickable to retry)
- Race-safe: if user keeps typing during an in-flight save, the in-flight save records what it sent; newer changes mark the block dirty again and schedule another save when the response returns
- Diagnosis + medicines "Add a row" slots are exempted from autosave (would create duplicates); they require an explicit + Add button
- Programmatic widgets (chip-pickers, units, smoking, alcohol, chronotype) fire
changeevents on their hidden fields so the listener notices
Slider numbers are also clickable: a single delegated handler on every <output> next to a range slider swaps it for a number input on click, accepts a precise typed value (clamps to the slider's min/max), commits with Enter, cancels with Escape.
Scroll position is preserved across the rare reloads (delete operations on diagnoses / medicines / experience reports, and the auto-reload after a new diagnosis or medicine is added) via sessionStorage.
Special pages
| Page | Purpose |
|---|---|
Special:MyProfile |
Edit your full profile (identity, demographics, personality, dx, meds). Autosave throughout. |
Special:UserProfile/<name> |
Public profile view (filtered by per-field visibility) |
Special:MyAssessment |
Index of rich assessment reports |
Special:MyAssessment/cati, /catq, /pid5bf, /mbti, /enneagram, /ocean |
Rich report per assessment |
Special:SaveProfileBlock |
AJAX endpoint for autosave (POST-only, JSON response) |
Special:Problems |
Browse the problems repository (165+ entries, 18 categories) |
Special:Problem/<slug> |
Individual problem page |
Special:SuggestProblem |
User-facing form to suggest a new problem |
Special:ManageProblems |
Sysop tool for problem-repository moderation |
Special:ReviewExperience |
Sysop queue for pending experience reports |
Special:ManageInteractions |
Sysop bulk-edit interaction reports |
Special:DeletePharmaElement |
Sysop delete tool for any votable element |
API modules
| Action | Purpose |
|---|---|
pharmacopediavote |
Submit / clear binary vote on votable element |
pharmacopedialikert |
Submit problem-efficacy likert (0–100 + −1 DK) |
pharmacopediaeffect |
Submit effect report (patient or provider perspective) |
pharmacopediainteractionreport |
Submit interaction report |
pharmacopediainteractionadd |
Create a new interaction edge |
pharmacopediacomment |
Threaded discussion ops (add / edit / delete / reply) |
pharmacopediaexperiencesubmit |
Submit experience report (multi-field form) |
pharmacopediaexperiencereview |
Sysop approve / reject experience report |
pharmacopediadxsearch |
Diagnosis autocomplete against the 41k-row abbreviation table |
pharmacopediaproblemsearch |
Problem-repository autocomplete |
pharmacopediaeffectslookup, indicationslookup |
Pickers used by the experience-submit form |
pharmacopedialiteratureadd, literaturedelete |
Literature attachment ops |
Interactions feature
The Interactions section is rendered by placing <pharmaInteractions/> anywhere in the wikitext of a med article (NS_MAIN) or a Category page (NS_CATEGORY).
Entity model
An interaction is an undirected edge between two endpoints. Each endpoint has a type (med or category) and a slug (DB-key form of the page title). Pairs are stored in canonical order: smaller (type, slug) tuple on the left.
Rendering rules
- On a med page M, list:
- Direct edges: rows where M is one side.
- Transitive edges: rows where one side is a category C that M is itself a member of (via MW's
categorylinks).
- Direct wins: if the same counterparty is reachable both directly and transitively, drop the transitive duplicate.
- On a Category page, list direct edges only (no transitive walk).
- Sort: pooled
valence_meanascending (most negative on top). Nulls sink. Tiebreakers:ndesc, then alphabetic. - Severe (any of pooled / user / provider vmean ≤ −83.0): red 4 px left border + red-tinted background + "severe" pill + counterparty title in red.
Add-interaction modal
Triggered by the + Add interaction button at the bottom of the section. Two-stage UX: search → click Use → confirm with Add interaction → POST to pharmacopediainteractionadd. Categories appear in the modal only if tagged with the marker category (default Category:MedCategory, configurable via $wgPharmacopediaInteractionCategoryMarker).
Experience reports
User-submitted reports of personal or clinical experience with a medicine, via <pharmaExperience/> on a med page. Stored pending in pcp_experience_reports; visible publicly only after sysop approval through Special:ReviewExperience.
Captured fields:
- Perspective (personal / clinical)
- Currently taking it (yes / no, stopped)
- Duration (value + unit)
- Dose (mg, decimal-precise)
- Route (16-option dropdown: PO, IV, IM, SC, SL, buccal, inhaled, intranasal, topical, transdermal, PR, ophthalmic, otic, vaginal, insufflated, other)
- Schedule (free text with datalist of QD / BID / TID / QID / q4h / q6h / q8h / q12h / qHS / qAM / qPM / PRN)
- Patient count (clinical only): min + optional max for ranges
- Efficacy (0–100 slider)
- Side-effect burden (0–100 slider)
- Stop reasons (personal + stopped only): JSON multi-select with optional severity slider per reason — codes: side_effects / ineffective / cost / no_longer_needed / clinician_advised / other
- Free-text anecdote
- Problems addressed (multi-pick with per-problem efficacy)
- Effects experienced (multi-pick with per-effect valence + frequency)
Storage tables (selected)
| Table | Purpose |
|---|---|
pcp_votable_elements |
Stable per-(page, slug) handle reused by votes / likert / comments |
pcp_votes |
Binary +1/−1 votes |
pcp_likert_reports |
Problem efficacy (0–100 + −1 DK), TINYINT signed |
pcp_effect_reports |
Effect ratings: experienced / frequency / valence (±100); perspective 1/2 |
pcp_interaction_reports |
Interaction ratings: experience 1–5 / valence ±100 / note; perspective 1/2 |
pcp_interactions |
Interaction edges (canonical-ordered pair) |
pcp_comments |
Threaded discussions |
pcp_user_profiles |
Per-user profile meta (alias, attribution, voter hash) |
pcp_user_profile_fields |
Generic key-value field store: (namespace, key, num, text, visibility) |
pcp_profile_diagnoses |
Per-user diagnoses (system, code, description, status, origin, severity 0–100, disability 0–100, dates, notes, visibility) |
pcp_user_meds |
Per-user medicines (name, page_id, efficacy 0–100, burden 0–100, dose_mg, route, schedule, duration, periods JSON, current, notes, visibility) |
pcp_diagnosis_abbreviations |
ICD-10-CM + ICD-11 + DSM-5 + aliases (~41,500 rows, VARCHAR/utf8mb4 for native case-insensitive search) |
pcp_problem, pcp_problem_alias |
Problems repository + alias lookup |
pcp_experience_reports |
Experience reports (pending → approved); efficacy + burden 0–100; route + schedule; stop-reasons JSON; patient-count min + max |
pcp_life_events, pcp_life_traits, pcp_life_images |
Life Story keyframes (auto-generated from assessments, manually authorable) |
pcp_literature |
Citation attachments |
Notable lessons learned
- Cargo string fields cap at ~300 chars.
structureandmechanismon MedTemplate are short VARCHARs; long prose goes in MEDIUMBLOB sections (pharmacokinetics, pharmacodynamics, intro). Overruns silently lose Cargo data (MySQL Error 1406). - VARBINARY + LOWER() is a no-op. MariaDB's LOWER() returns binary types unchanged.
pcp_diagnosis_abbreviationswas migrated VARBINARY → VARCHAR/utf8mb4 (2026-05-17) so case-insensitive LIKE works natively without CONVERT() wrappers. - FlaggedRevs locks template inclusions by default. Config fix:
$wgFlaggedRevsHandleIncludes=0+ remove NS_TEMPLATE from$wgFlaggedRevsNamespacesvia an extension-function callback (MW 1.46 array config concatenation behavior). - CSP must allow Cloudflare Turnstile and any other 3rd-party widget script source. Symptom: generic "There are problems with some of your input" on form submit. Audit script-src / frame-src / connect-src / style-src whenever adding any 3rd-party JS widget.
- Sidebar cache must be purged after CLI
maintenance/edit.phpwrites toMediaWiki:Sidebaror other chrome pages:curl -X POST .../api.php?action=purge&titles=MediaWiki:Sidebar. - CLI E_USER_DEPRECATED suppressed in LocalSettings.php (EmbedVideo / FlaggedRevs spam ~6 lines per maintenance/run.php call on MW 1.46). Web behavior unaffected.
Hooks
ParserFirstCallInit: register all parser tagsLoadExtensionSchemaUpdates: install / migrate schema (the sql/ directory + patches)BeforePageDisplay: inject the ext.pharmacopedia.* ResourceLoader modulesUserGetRights+UserEffectiveGroups: verified-provider role wiring- Various special-page registrations via
SpecialPage_initList
Configuration globals
$wgPharmacopediaInteractionCategoryMarker(defaultCategory:MedCategory): only categories tagged with this marker appear in the add-interaction modal- (Various permission-grant arrays via standard MW
$wgGroupPermissions)
Source layout
extensions/Pharmacopedia/
|-- extension.json
|-- includes/
| |-- Hooks.php
| |-- *Tag.php (parser tags: VoteTag, EffectTag, ProblemTag, ...)
| |-- *Store.php (data access: EffectStore, ProblemStore, ...)
| |-- UserProfileStore.php (the big one; profile / dx / meds / abbreviations)
| |-- SpecialMyProfile.php (the user profile editor; large)
| |-- SpecialMyAssessment.php (rich reports for all 6 assessments; large)
| |-- SpecialSaveProfileBlock.php (autosave AJAX endpoint)
| |-- Special*.php (other special pages)
| |-- ProfileDatasets.php (countries, languages, genders, religions, etc.)
| |-- DatePicker.php (range / possibility-mix date widget backend)
| |-- Assessments/
| | |-- Cati.php, CatiNorms.php
| | |-- Catq.php
| | |-- Pid5bf.php
| | |-- Mbti.php
| | |-- Enneagram.php
| | `-- Raadsr.php (deprecated 2026-05-17 in favor of CATI, kept for archival reads)
| `-- Api/
| `-- (one class per API module)
|-- resources/
| |-- ext.pharmacopedia.js (single IIFE: chip-picker, dx autocomplete, BFI-10 compute, ...)
| |-- ext.pharmacopedia.blocksave.js (debounced autosave per block)
| |-- ext.pharmacopedia.css
| |-- ext.pharmacopedia.datepicker.js + .css
|-- sql/
| |-- (one .sql per table; patches as patch-*.sql)
`-- i18n/
`-- en.json