Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

About:Pharmacopedia.ext

From Pharmacopedia

Template:TOC right

Pharmacopedia extension specification

Version: 0.9.0 · 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 from navigator.languages; time zone from Intl.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 under MediaWiki\Extension\Pharmacopedia\. Assessment classes under includes/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.js is 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 via LoadExtensionSchemaUpdates hook.

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:

  1. Listens for input and change events on every input inside any save-block
  2. 800 ms after the last event, POSTs the block's serialized form data to Special:SaveProfileBlock with block=block-name
  3. 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)
  4. 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
  5. Diagnosis + medicines "Add a row" slots are exempted from autosave (would create duplicates); they require an explicit + Add button
  6. Programmatic widgets (chip-pickers, units, smoking, alcohol, chronotype) fire change events 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_mean ascending (most negative on top). Nulls sink. Tiebreakers: n desc, 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. structure and mechanism 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. pcp_diagnosis_abbreviations 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: $wgFlaggedRevsHandleIncludes=0 + remove NS_TEMPLATE from $wgFlaggedRevsNamespaces 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 maintenance/edit.php writes to MediaWiki:Sidebar or 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 tags
  • LoadExtensionSchemaUpdates: install / migrate schema (the sql/ directory + patches)
  • BeforePageDisplay: inject the ext.pharmacopedia.* ResourceLoader modules
  • UserGetRights + UserEffectiveGroups: verified-provider role wiring
  • Various special-page registrations via SpecialPage_initList

Configuration globals

  • $wgPharmacopediaInteractionCategoryMarker (default Category: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