About:Pharmacopedia.ext: Difference between revisions
| [checked revision] | [checked revision] |
MDElliottMD (talk | contribs) Bump to 0.9.3: life-story observations + episodes + visual timeline + vote choice/multi + ClamAV hardening + TimePicker kit |
MDElliottMD (talk | contribs) Doc update for v0.9.3: life-story timeline, observations, episodes, choice/multi voting, sharing subsystem, ClamAV rule, TimePicker |
||
| Line 1: | Line 1: | ||
= Pharmacopedia extension specification = | = Pharmacopedia extension specification = | ||
| Line 6: | Line 5: | ||
'''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 medicine reference with rich user-profile and | The Pharmacopedia extension turns a MediaWiki install into a structured, community-edited medicine reference with rich user-profile, assessment, life-story, and visibility-sharing infrastructure. It adds parser tags, special pages, API modules, a chip-picker / autosave UI framework, a vis-timeline-based visual life timeline, a granular per-record sharing subsystem, and a database schema that together support: | ||
* Structured medicine pages via the <code><nowiki>{{MedTemplate}}</nowiki></code> template | * Structured medicine pages via the <code><nowiki>{{MedTemplate}}</nowiki></code> 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) | * Per-user rating on effects, problems, titration strategies, anecdotes, and drug-drug interactions (continuous 0–100 sliders, ±100 valence; no 0–5 likert anywhere) | ||
* Binary AND choice/multi voting on arbitrary content (<code>type="single"</code> / <code>type="multi"</code> with 2-5 options, results-visibility policy per element) | |||
* 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 | * 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 ~ | * Diagnosis autocomplete backed by ~41,500 ICD-10-CM, ICD-11, and DSM-5 codes | ||
* '''Life-story timeline''': visual swimlanes (vis-timeline 7.7.3, vendored Apache-2.0) plus a synchronized trait-trajectory overlay (vis.Graph2d); card-list view alongside; quick-add free-text observation parser; range-date episode form with severity slider | |||
* '''Per-record sharing subsystem''': rule types include public, private, users, cohort, link_token, reciprocal; time-bounded; audit log of who-viewed-what; bulk free-text → structured ref upgrader; privacy mode that disables legacy fallback | |||
* Chip-picker / autosave / slider-precise-input UI framework shared across editor surfaces | * Chip-picker / autosave / slider-precise-input UI framework shared across editor surfaces | ||
* Date + time + range kit: PCPDatePicker (point/range/possibility), PCPTimePicker (fuzzy time-only: "4p", "noon", "quarter past 6") | |||
* Verified-provider role with document-based verification | * Verified-provider role with document-based verification | ||
* Fail-closed ClamAV scan on every image / file upload (hard project rule) | |||
== Precision doctrine == | == Precision doctrine == | ||
| Line 29: | Line 33: | ||
== High-level architecture == | == High-level architecture == | ||
* '''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>. | * '''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>. API modules under <code>includes/Api/</code>. | ||
* '''Frontend (JS):''' <code> | * '''Frontend (JS):''' multiple ResourceModules per surface area: | ||
* | ** <code>ext.pharmacopedia</code>: main IIFE (chip-picker, dx autocomplete, BFI-10 compute, vote logic for both binary and choice/multi) | ||
* | ** <code>ext.pharmacopedia.blocksave</code>: debounced autosave per block (race-safe) | ||
* '''Schema:''' <code>sql/</code>, ~ | ** <code>ext.pharmacopedia.datepicker</code>: range / possibility-mix date widget | ||
** <code>ext.pharmacopedia.timepicker</code>: time-only widget (fuzzy parsing, extracted from DatePicker) | |||
** <code>ext.pharmacopedia.share</code>: per-record share dialog (People / Link / Cohorts tabs) | |||
** <code>ext.pharmacopedia.observation</code>: quick-add observation textarea + live preview | |||
** <code>ext.pharmacopedia.refupgrade</code>: bulk linker for free-text → structured refs | |||
** <code>ext.pharmacopedia.vis-timeline-vendor</code>: vis-timeline 7.7.3 (vendored) | |||
** <code>ext.pharmacopedia.lifetimeline</code>: visual life-story timeline + swimlanes + toolbar | |||
** <code>ext.pharmacopedia.lifegraph</code>: trait-trajectory overlay (vis.Graph2d) synced to timeline | |||
** <code>ext.pharmacopedia.kitsync</code>: glue that propagates kit-widget changes into legacy hidden inputs + drives privacy-mode toggle | |||
* '''Styles (CSS):''' shared dark-theme palette (black / dark-grey / purple / white primary; red / green / blue / teal sparingly for semantic distinction). | |||
* '''Schema:''' <code>sql/</code>, ~25 core tables plus migration patches. Picked up via <code>LoadExtensionSchemaUpdates</code> hook. | |||
== Parser tags == | == Parser tags == | ||
| Line 42: | Line 56: | ||
! Tag !! Purpose !! Class | ! Tag !! Purpose !! Class | ||
|- | |- | ||
| <code><vote></code> || | | <code><vote></code> || Binary up/down (default) OR choice/multi when <code>type="single"</code> or <code>type="multi"</code> + <code>options="A; B; C"</code> (2-5 options, semicolon-separated). Optional <code>results="live\|after-vote\|hidden"</code> for tally-visibility policy. || <code>VoteTag</code> | ||
|- | |- | ||
| <code><effect></code> || Therapeutic or adverse effect; patient + provider perspectives; provider freq slider 0–100; shared valence slider ±100 || <code>EffectTag</code> | | <code><effect></code> || Therapeutic or adverse effect; patient + provider perspectives; provider freq slider 0–100; shared valence slider ±100 || <code>EffectTag</code> | ||
| Line 81: | Line 95: | ||
<pharmaInteractions/> | <pharmaInteractions/> | ||
<pharmaExperience/> | <pharmaExperience/> | ||
<vote slug="fav-color" type="single" options="Red; Blue; Green"> | |||
What's your favorite color?</vote> | |||
<vote slug="side-effects" type="multi" results="after-vote" | |||
options="Dry mouth; Insomnia; Anxiety; Headache; None"> | |||
Which side effects did you experience?</vote> | |||
</pre> | </pre> | ||
| Line 88: | Line 109: | ||
! Element !! Scale !! Perspectives !! Storage | ! Element !! Scale !! Perspectives !! Storage | ||
|- | |- | ||
| Vote tag || +1 / −1 binary || single || <code>pcp_votes</code> | | Vote tag (binary) || +1 / −1 binary || single || <code>pcp_votes.v_value</code> | ||
|- | |||
| Vote tag (single-choice) || one of 2-5 options || single || <code>pcp_votes.v_choices</code> (CSV index) | |||
|- | |||
| Vote tag (multi-choice) || any subset of 2-5 options || single || <code>pcp_votes.v_choices</code> (CSV indices) | |||
|- | |- | ||
| Titration || +1 / −1 binary || single || <code>pcp_votes</code> | | Titration || +1 / −1 binary || single || <code>pcp_votes</code> | ||
| Line 103: | Line 128: | ||
|} | |} | ||
Choice / multi vote elements expose per-option tallies via <code>tallyChoices()</code> on demand. Per-option bars render inline in the picker. The <code>results</code> attribute gates tally visibility: | |||
* <code>live</code> (default) — tally always visible | |||
* <code>after-vote</code> — tally hidden until viewer has voted | |||
* <code>hidden</code> — tally never shown (only options + "thanks" on submit) | |||
Server-side options-hash (<code>ve_options_h</code>) detects post-vote option-list edits; the API rejects new votes whose submitted hash doesn't match the live one. Voter identities are stored as HMAC-SHA256 (<code>v_voter_hash</code>) so admins reading the DB cannot map votes to user accounts without the HMAC secret. | |||
Aggregates are recomputed and returned by every report-submit API call so the row re-renders in place without a page reload. | 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. | ||
== Effect bucketing == | == Effect bucketing == | ||
| Line 130: | Line 160: | ||
<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]]). | <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]]). | ||
Visible at top: a Privacy-mode panel toggling whether legacy public fallback applies (privacy-mode ON = only explicit share rules grant access; OFF = the field-level <code>pf_visibility</code> enum applies). | |||
🔗 Share chips appear in each fieldset legend (Demographics, Diagnoses, Medicines) so the owner can scope a share-rule to that namespace via the modal Share dialog. | |||
=== Block list === | === Block list === | ||
| Line 155: | Line 189: | ||
* '''Height / weight''' unit toggle (Metric cm/kg or US ft+in/lb); stored canonically as cm/kg regardless | * '''Height / weight''' unit toggle (Metric cm/kg or US ft+in/lb); stored canonically as cm/kg regardless | ||
* '''Handedness''' single-select (3 options) | * '''Handedness''' single-select (3 options) | ||
* '''Smoking''' structured widget: status + cigs/day + years smoked + quit date; auto-computes pack-years | * '''Smoking''' structured widget: status + cigs/day + years smoked + quit date (PCPDatePicker); auto-computes pack-years | ||
* '''Alcohol''' structured widget: drinks/week + typical drink type + max one occasion | * '''Alcohol''' structured widget: drinks/week + typical drink type + max one occasion | ||
* '''Education''' bucketed highest-level + numeric years + free-text field of study | * '''Education''' bucketed highest-level + numeric years + free-text field of study | ||
| Line 165: | Line 199: | ||
* '''Number of children''' numeric | * '''Number of children''' numeric | ||
* '''Time zone''' free text, auto-detects IANA TZ on first load if empty | * '''Time zone''' free text, auto-detects IANA TZ on first load if empty | ||
* '''Chronotype / sleep schedule''' two | * '''Chronotype / sleep schedule''' two PCPTimePicker widgets (typical bedtime, typical wake; fuzzy parsing accepts "10p", "bedtime", "quarter past 6") | ||
* '''Political orientation''' two-axis compass sliders (economic ±100, social ±100) | * '''Political orientation''' two-axis compass sliders (economic ±100, social ±100) | ||
| Line 208: | Line 242: | ||
|} | |} | ||
All assessment items submit raw responses to <code> | All assessment items submit raw responses to <code>pcp_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 <code>scoreResponses()</code> on the full raw set, skipping "unsure" rows). Each completed assessment also auto-upserts a Life-story keyframe row (see [[#Life-story timeline|Life-story timeline]]) so trajectories plot over time. | ||
== Life-story timeline == | |||
<code>Special:MyLifeStory</code> is the owner-facing editor + viewer for everything time-anchored about the user. Backed by <code>pcp_life_events</code> with extension columns for episode / observation / keyframe metadata. | |||
=== Event types === | |||
{| class="wikitable" | |||
! le_type !! Meaning !! Notes | |||
|- | |||
| 0 (TYPE_STORY) || Plain timeline entry || title, body, optional image | |||
|- | |||
| 1 (TYPE_IMAGE) || Image-primary entry || same shape, image is the main payload | |||
|- | |||
| 2 (TYPE_KEYFRAME) || Auto-created assessment snapshot || populated by <code>upsertAssessmentKeyframe</code> on every assessment save | |||
|- | |||
| 3 (TYPE_OBSERVATION) || Plain-text observation || created via the quick-add textarea; parser extracts date, polarity, refs | |||
|- | |||
| 4 (TYPE_EPISODE) || Time-bounded period || start + end (PCPDatePicker range mode); type / subtype / severity 0-100 | |||
|} | |||
=== Quick-add observation === | |||
A textarea at the top of <code>Special:MyLifeStory</code> (and a 📝 modal trigger on <code>Special:MyProfile</code>) accepts plain text like: | |||
<pre> | |||
anxiety from bupropion in jan 2020 | |||
did not experience anxiety from bupropion in feb 2018 | |||
panic attack 2 months ago | |||
depressed as a freshman | |||
i had a manic episode sep 1 2020 till nov 15 2020 | |||
no insomnia while on melatonin in summer 2023 | |||
felt great on christmas 2020 | |||
happiest on my 30th birthday | |||
</pre> | |||
<code>ObservationParser::parse()</code> extracts: | |||
* '''Date''' (point or range): ISO, MM/DD/YYYY, "Month D YYYY", "Month YYYY", "Season YYYY", "early/mid/late YYYY", bare year, decades ("2010s"); date RANGES via "X to Y", "X till Y", "from X to Y", "X - Y"; relative-to-now ("yesterday", "last week/month/year", "N months ago", "a few weeks ago"); holidays (christmas, halloween, new year's, valentine's, july 4th, thanksgiving with computed 4th-Thursday-of-Nov, MLK / Memorial / Labor day); age-relative ("7y8mo", "51.2yo", "at age 14", "ages 2-10"); life stages ("in childhood", "as a teen", "as a freshman", "junior year"); Nth birthday ("my 30th birthday") | |||
* '''Polarity''' (negation detection): "not", "didn't", "did not experience", "never", "without", "denied" → polarity=0 (negative); else 1 (positive) | |||
* '''Leading verbs stripped''': "I was diagnosed with", "I took", "started taking", "tried", "was on", "experienced", "felt" — so the subject is the noun, not the verb | |||
* '''Adverbs stripped''': "briefly", "occasionally", "frequently", "sometimes", "always" — so the subject is the noun, not the modifier | |||
* '''Role splitting''': "from / caused by / due to / while on" → role='cause'; "with / during / while" → role='context' | |||
* '''Ref resolution''', in priority order: | |||
*# User's meds (<code>pcp_user_meds</code>) | |||
*# Wiki pages in Category:Medicines | |||
*# Effects catalog (<code>pcp_effects</code>) | |||
*# Problems catalog (<code>pcp_problem</code> + <code>pcp_problem_alias</code>) | |||
*# User's diagnoses (<code>pcp_profile_diagnoses</code>) | |||
*# Global ICD diagnosis abbreviations (<code>pcp_diagnosis_abbreviations</code>) | |||
*# Free-text fallback (stored as type='free' for later upgrade) | |||
* '''Episode-shape detection''': "manic episode" → type=mood, subtype=manic; "psychotic break", "panic attack", "anxiety attack", "trauma response" all route to <code>addEpisode</code> instead of <code>addObservation</code>. A date RANGE alone is enough to force <code>is_episode=true</code>. | |||
Live preview chips appear under the textarea as you type. Submit routes to <code>pharmacopediaobservation</code> API which writes the row + refs via <code>setEventRefs()</code>. | |||
=== Episode form === | |||
Click the "🌀 Episode" button (or the quick-add detects an episode shape) for the structured form: | |||
* Type selector: mood / psychotic / anxiety / panic / trauma response / dissociative / substance use / eating / sleep disturbance / pain flare / migraine / medication adjustment / hospitalization / creative surge / spiritual / transcendent / relationship crisis / grief / somatic / other | |||
* Subtype (text + datalist) — for mood: depressive / manic / hypomanic / mixed / dysphoric / euthymic | |||
* Severity slider 0-100 (per precision doctrine) | |||
* Date range via PCPDatePicker locked to range mode | |||
* Title, body, single virus-scanned image, visibility (4-state) | |||
=== Visual timeline === | |||
Top of <code>Special:MyLifeStory</code> has a tabbed view: '''Visual timeline''' (default) / '''Card list'''. | |||
The visual timeline uses '''vis-timeline 7.7.3''' (vendored Apache-2.0 at <code>resources/vendor/vis-timeline/</code>) with these features: | |||
* Swimlanes (toggleable chips): Episodes (range bars), Events, Observations, Keyframes (off by default), Derived | |||
* '''Trait-trajectory overlay''': a synchronized <code>vis.Graph2d</code> draws smooth (centripetal Catmull-Rom interpolated) lines for every keyframe trait series (CATI subscales, PID-5-BF domains, CAT-Q, custom traits like "shyness") DIRECTLY on top of the timeline plot area. Same X-axis; the graph's data + time axes are CSS-hidden so only the lines + points show. Values are normalized to 0-100% within each series so different scales coexist. | |||
* Toolbar: Visual/Card tabs, group toggle chips, magnifier + zoom +/- buttons, Fit visible / Fit everything | |||
* Plain wheel = vertical scroll inside the timeline (520px fixed height); Ctrl+wheel = zoom; Shift+wheel = horizontal pan | |||
* Click empty timeline area: prefills the quick-add textarea with "on YYYY-MM-DD" at that date + scrolls + triggers live preview | |||
* Click an item: routes to its edit form (event / episode / observation, each has its own route) | |||
* Collapsible "Trait series (N)" legend with chip toggles to show/hide individual series | |||
=== Edit / delete / duplicate flows === | |||
Each event type has its own edit route under <code>Special:MyLifeStory</code>: | |||
* <code>Special:MyLifeStory/edit-observation/<id></code> — re-parses raw text on save; supports polarity override + date override | |||
* <code>Special:MyLifeStory/edit-episode/<id></code> — full episode form | |||
* <code>Special:MyLifeStory?edit_event=<id></code> — existing event / image / keyframe form | |||
All three forms have side-by-side "Delete X" + "Duplicate X" buttons. Duplicate copies fields + refs + keyframe traits (NOT images) and redirects to the new row's edit form. | |||
=== Upgrade-link UI === | |||
When the parser stored a free-text ref (because the entity wasn't in the user's data at the time), <code>Special:MyRefLinks</code> finds all such refs and offers one-click "link to {match}" buttons. Matches come from the same catalogs the parser checks at write time. A banner on <code>Special:MyLifeStory</code> ("📎 N free-text references could be linked → Review & link") appears when unmatched refs exist. | |||
== Per-record sharing subsystem == | |||
A granular per-record sharing system layered on top of the legacy <code>pf_visibility</code> enum without removing it. Resolved by <code>VisibilityResolver</code>; the new model is additive (legacy fallback preserved unless Privacy mode is on). | |||
=== Rule types (vr_rule_type) === | |||
* '''<code>private</code>''' — explicit deny (only owner) | |||
* '''<code>public</code>''' — explicit allow (anyone) | |||
* '''<code>users</code>''' — payload <code>{user_ids: [...]}</code>; allow if viewer in list | |||
* '''<code>cohort</code>''' — payload <code>{cohort_id: N}</code>; allow if viewer in <code>pcp_cohort_members</code> for that cohort | |||
* '''<code>link_token</code>''' — payload <code>{token: '...', uses_remaining: null|N}</code>; allow if URL <code>?pcpshare=TOKEN</code> matches | |||
* '''<code>reciprocal</code>''' — allow if viewer has a matching <code>users</code> rule sharing the same shape back to owner | |||
Rules scope at three levels: <code>(profile, namespace, key)</code>, <code>(profile, namespace, NULL)</code>, or <code>(profile, '*', NULL)</code>. Most-specific-first matching. Rules can have <code>vr_expires</code> (time-bounded) and <code>vr_revoked</code> (preserves audit trail). | |||
=== Privacy mode === | |||
When Privacy mode is ON for a profile, a *-wide <code>private</code> rule is created. <code>VisibilityResolver::canView()</code> short-circuits to false instead of falling through to the legacy <code>pf_visibility</code> check, so only explicit share rules grant access. Default: OFF (back-compat). | |||
=== Share dialog === | |||
Triggered by 🔗 chips on assessment reports, profile sections, life-story timeline. Modal with three tabs: | |||
* '''People''': username autocomplete (↓/↑/Enter/Esc keyboard nav), per-shared-user pill with × to remove just that user, optional expires DatePicker, "Auto-share back" reciprocal toggle | |||
* '''Link''': "Generate link" creates a <code>link_token</code> rule; copies the URL to clipboard with toast; optional max-uses + expires | |||
* '''Cohorts''': dropdown of the owner's cohorts (managed at <code>Special:MyCohorts</code>); optional expires | |||
=== Audit log === | |||
Every permitted view through a rule (not legacy) writes a row to <code>pcp_visibility_view_log</code>. <code>Special:MyShareLog</code> shows the last 200 views with timestamp, viewer (or anonymous IP masked to /24), namespace, key, rule id. | |||
== Choice / multi voting == | |||
The <code><vote></code> tag's binary up/down mode is unchanged. With <code>type="single"</code> or <code>type="multi"</code> + <code>options="A; B; C"</code> (2-5 entries, semicolon separator), it renders a compact chart-icon chip that expands inline to a radio (single) or checkbox (multi) picker with per-option count bars. | |||
Server-side storage: | |||
* <code>pcp_votable_elements.ve_options</code> (JSON array of labels) | |||
* <code>pcp_votable_elements.ve_options_h</code> (first 8 hex of sha256; drift hash) | |||
* <code>pcp_votable_elements.ve_results_policy</code> (live / after-vote / hidden) | |||
* <code>pcp_votes.v_choices</code> (CSV indices) | |||
* <code>pcp_votes.v_options_h</code> (drift hash at vote time) | |||
API route: same <code>pharmacopediavote</code> endpoint; presence of <code>choices</code> or <code>options_h</code> params routes to <code>castChoice()</code>. Response includes <code>tally</code> + <code>user_choices</code> (null for binary). Tally hidden per <code>results</code> policy. | |||
Drift behavior: if the page editor changes the options list after votes exist, <code>ve_options_h</code> updates and new votes' <code>v_options_h</code> reflects the new value. Existing votes stay but their hash no longer matches (marked stale). New votes whose submitted hash mismatches the live one are rejected — protects against browser cache races. Tallies still aggregate by raw index, so RENAMING an option in place silently turns old votes into new-label votes; reordering is the dangerous case. Appending new options is safe. | |||
== ClamAV scan rule (project standard) == | |||
Hard rule, set 2026-05-17: every server-accepted file upload (image, PDF, document, anything) MUST go through <code>VirusScanner::scanFile($path)</code> (<code>includes/VirusScanner.php</code>) BEFORE being moved to permanent storage. Fail-closed: if <code>/usr/bin/clamdscan</code> is unavailable or returns error, the upload is rejected. | |||
Wired into: | |||
* <code>LifeStoryStore::addImage()</code> — life events / episodes / observations | |||
* <code>LiteratureStore::storeUploadedPdf()</code> — literature PDFs | |||
* <code>ProviderAppStore::saveUploadedFile()</code> — provider verification documents | |||
<code>AttachmentScanner</code> (used by feature-request attachments) is left alone — its status-return model is intentional for the queued-moderation flow there. <code>AntivirusHelper</code> (the old silent-no-op variant) was deleted; all callers consolidated onto <code>VirusScanner</code>. | |||
== Autosave infrastructure == | == Autosave infrastructure == | ||
| Line 230: | Line 413: | ||
! Page !! Purpose | ! Page !! Purpose | ||
|- | |- | ||
| <code>Special:MyProfile</code> || Edit your full profile (identity, demographics, personality, dx, meds). Autosave throughout. | | <code>Special:MyProfile</code> || Edit your full profile (identity, demographics, personality, dx, meds). Autosave throughout. Privacy mode toggle + share chips per fieldset. | ||
|- | |- | ||
| <code>Special:UserProfile/<name></code> || Public profile view (filtered by per-field visibility) | | <code>Special:UserProfile/<name></code> || Public profile view (filtered by per-field visibility + rule-based access). 🔗 Share chip in subtitle for self-view. | ||
|- | |- | ||
| <code>Special:MyAssessment</code> || Index of rich assessment reports | | <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:MyAssessment/cati</code>, <code>/catq</code>, <code>/pid5bf</code>, <code>/mbti</code>, <code>/enneagram</code>, <code>/ocean</code> || Rich report per assessment; 🔗 Share chip in subtitle for owner | ||
|- | |||
| <code>Special:MyLifeStory</code> || Owner editor + visual timeline + card list. Quick-add observation textarea + Event/Episode buttons. 🔗 Share chip in subtitle. Privacy-mode + free-text-refs banners. | |||
|- | |||
| <code>Special:MyLifeStory/add-episode</code>, <code>/edit-episode/<id></code> || Episode form (create / edit) | |||
|- | |||
| <code>Special:MyLifeStory/edit-observation/<id></code> || Observation edit form (re-parses raw text) | |||
|- | |||
| <code>Special:LifeStory/<name></code> || Public life-story view (read-only, visibility-filtered) | |||
|- | |||
| <code>Special:MyCohorts</code> || Owner-managed groups for share-with-cohort flows; create / rename / delete / add+remove members | |||
|- | |||
| <code>Special:MyShareLog</code> || Who has viewed your shared content (timestamp, viewer, namespace, rule id; anonymous IPs masked) | |||
|- | |||
| <code>Special:MyRefLinks</code> || Bulk linker: find free-text refs in observations that now match a structured entity; one-click upgrade or dismiss | |||
|- | |- | ||
| <code>Special:SaveProfileBlock</code> || AJAX endpoint for autosave (POST-only, JSON response) | | <code>Special:SaveProfileBlock</code> || AJAX endpoint for autosave (POST-only, JSON response) | ||
| Line 253: | Line 450: | ||
|- | |- | ||
| <code>Special:DeletePharmaElement</code> || Sysop delete tool for any votable element | | <code>Special:DeletePharmaElement</code> || Sysop delete tool for any votable element | ||
|- | |||
| <code>Special:PCPCtrls</code> || Sysop ctrls dashboard (gated by <code>$wgSpecialPageLockdown</code>) | |||
|} | |} | ||
| Line 260: | Line 459: | ||
! Action !! Purpose | ! Action !! Purpose | ||
|- | |- | ||
| <code>pharmacopediavote</code> || | | <code>pharmacopediavote</code> || Binary OR choice/multi vote (routes by presence of <code>choices</code> param). Returns tally + user_choices for choice modes; gated by results-policy. | ||
|- | |- | ||
| <code>pharmacopedialikert</code> || Submit problem-efficacy likert (0–100 + −1 DK) | | <code>pharmacopedialikert</code> || Submit problem-efficacy likert (0–100 + −1 DK) | ||
| Line 283: | Line 482: | ||
|- | |- | ||
| <code>pharmacopedialiteratureadd</code>, <code>literaturedelete</code> || Literature attachment ops | | <code>pharmacopedialiteratureadd</code>, <code>literaturedelete</code> || Literature attachment ops | ||
|- | |||
| <code>pharmacopediaobservation</code> || op=preview / op=submit for plain-text observations (routes to addObservation OR addEpisode) | |||
|- | |||
| <code>pharmacopediavisrules</code> || Visibility rule CRUD (list / create / update / revoke / newtoken) | |||
|- | |||
| <code>pharmacopediausersearch</code> || Username autocomplete for share-with-people picker | |||
|- | |||
| <code>pharmacopediacohorts</code> || Cohort CRUD + membership | |||
|- | |||
| <code>pharmacopediarefupgrade</code> || Free-text ref linker (op=candidates / apply / dismiss) | |||
|} | |} | ||
| Line 331: | Line 540: | ||
! Table !! Purpose | ! Table !! Purpose | ||
|- | |- | ||
| <code>pcp_votable_elements</code> || Stable per-(page, slug) handle reused by votes / likert / comments | | <code>pcp_votable_elements</code> || Stable per-(page, slug) handle reused by votes / likert / comments. Now also <code>ve_options</code> + <code>ve_options_h</code> + <code>ve_results_policy</code> for choice votes. | ||
|- | |- | ||
| <code>pcp_votes</code> || Binary +1/−1 votes | | <code>pcp_votes</code> || Binary +1/−1 votes (v_value) AND choice/multi votes (v_choices CSV + v_options_h drift hash) | ||
|- | |- | ||
| <code>pcp_likert_reports</code> || Problem efficacy (0–100 + −1 DK), TINYINT signed | | <code>pcp_likert_reports</code> || Problem efficacy (0–100 + −1 DK), TINYINT signed | ||
| Line 347: | Line 556: | ||
| <code>pcp_user_profiles</code> || Per-user profile meta (alias, attribution, voter hash) | | <code>pcp_user_profiles</code> || Per-user profile meta (alias, attribution, voter hash) | ||
|- | |- | ||
| <code> | | <code>pcp_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_profile_diagnoses</code> || Per-user diagnoses (system, code, description, status, origin, severity 0–100, disability 0–100, dates, notes, visibility) | ||
| Line 359: | Line 568: | ||
| <code>pcp_experience_reports</code> || Experience reports (pending → approved); efficacy + burden 0–100; route + schedule; stop-reasons JSON; patient-count min + max | | <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_events</code> || Timeline events. Columns: le_type (0=story, 1=image, 2=keyframe, 3=observation, 4=episode), le_polarity, le_raw_text (parser input), le_episode_type, le_episode_subtype, le_severity, le_date_struct (PCPDatePicker JSON; supports range) | ||
|- | |||
| <code>pcp_life_event_refs</code> || Join table linking events to entities (med / effect / problem / diagnosis / med_page / diagnosis_code / free); role (subject / cause / context / symptom / trigger / treatment) | |||
|- | |||
| <code>pcp_life_traits</code> || Keyframe trait values (assessment subscale snapshots → trajectory graph) | |||
|- | |||
| <code>pcp_life_images</code> || Image attachments (ClamAV-scanned) | |||
|- | |||
| <code>pcp_visibility_rules</code> || Per-(profile, namespace, key) sharing rules. Types: public / private / users / cohort / link_token / reciprocal. Optional vr_expires, vr_revoked, vr_attribution, vr_label. | |||
|- | |||
| <code>pcp_cohorts</code>, <code>pcp_cohort_members</code> || Owner-managed user groups for share-with-cohort flows | |||
|- | |||
| <code>pcp_visibility_view_log</code> || Audit trail of rule-permitted views (Special:MyShareLog) | |||
|- | |- | ||
| <code>pcp_literature</code> || Citation attachments | | <code>pcp_literature</code> || Citation attachments | ||
| Line 366: | Line 587: | ||
== Notable lessons learned == | == 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 | * '''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. 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 | * '''VARBINARY + LOWER() is a no-op.''' MariaDB's LOWER() returns binary types unchanged. <code>pcp_diagnosis_abbreviations</code> was migrated VARBINARY → VARCHAR/utf8mb4 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 | * '''FlaggedRevs locks template inclusions by default.''' Config fix: <code>$wgFlaggedRevsHandleIncludes=0</code> + remove NS_TEMPLATE from <code>$wgFlaggedRevsNamespaces</code> via an extension-function callback. | ||
* '''CSP must allow Cloudflare Turnstile and any other 3rd-party widget script source.''' | * '''CSP must allow Cloudflare Turnstile and any other 3rd-party widget script source.''' 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> | * '''Sidebar cache must be purged after CLI <code>maintenance/edit.php</code> writes''' to <code>MediaWiki:Sidebar</code> or other chrome pages. | ||
* ''' | * '''CLI E_USER_DEPRECATED suppressed in LocalSettings.php''' (EmbedVideo / FlaggedRevs spam on MW 1.46). Web behavior unaffected. | ||
* '''MW ApiResult drops keys starting with <code>_</code>.''' Any field whose key starts with underscore in an ApiResult payload is treated as internal metadata and stripped. Rename to plain identifier. Bit Phase 2-4 of visibility-rules subsystem. | |||
* '''MW API serializes int-keyed assoc arrays unreliably.''' <code>[23 => 'Alice']</code> may arrive as <code>{"23": "Alice"}</code> or worse. Always use list-of-objects (<code>[{"id": 23, "name": "Alice"}]</code>) for id-to-value maps across the API boundary. | |||
* '''PHP single-quoted strings don't interpret <code>\xNN</code> byte escapes.''' <code>'\xF0\x9F\x94\x97'</code> is 16 literal chars, NOT the 4-byte UTF-8 for 🔗. Use literal Unicode char or double-quoted <code>\u{1F517}</code>. Bit three times in one session. | |||
* '''PHP "0" is FALSY.''' <code>!"0"</code> evaluates to TRUE. So <code>if ( !$x )</code> silently treats <code>"0"</code> (string zero) as missing. Bit choice-vote tallying when voter picked option index 0; vote was IN the DB but invisible to readers. Use <code>=== null || === ''</code> for "missing or blank" intent. <code>empty()</code> has the same problem. | |||
* '''<code><span></code> can't contain <code><p></code>.''' MediaWiki auto-wraps tag content in <code><p></code> when there are newlines; browsers auto-close any <code><span></code> before the <code><p></code>, scattering child elements into wrong DOM positions. Use <code><div></code> for parser-tag wrappers whose content can span paragraphs (choice votes, life-story cards). | |||
* '''vis.Graph2d group <code>style</code> belongs at TOP LEVEL''' not nested in <code>options</code>. Nesting silently fails (no error, just invisible lines). Bit the trait-trajectory graph on first build. | |||
== Hooks == | == Hooks == | ||
| Line 384: | Line 611: | ||
* <code>$wgPharmacopediaInteractionCategoryMarker</code> (default <code>Category:MedCategory</code>): only categories tagged with this marker appear in the add-interaction modal | * <code>$wgPharmacopediaInteractionCategoryMarker</code> (default <code>Category:MedCategory</code>): only categories tagged with this marker appear in the add-interaction modal | ||
* <code>$wgPharmacopediaVoteHashSecret</code> (required): HMAC secret for <code>v_voter_hash</code> so vote rows can't be mapped back to user accounts by anyone reading the DB without the secret | |||
* (Various permission-grant arrays via standard MW <code>$wgGroupPermissions</code>) | * (Various permission-grant arrays via standard MW <code>$wgGroupPermissions</code>) | ||
| Line 393: | Line 621: | ||
| |-- Hooks.php | | |-- Hooks.php | ||
| |-- *Tag.php (parser tags: VoteTag, EffectTag, ProblemTag, ...) | | |-- *Tag.php (parser tags: VoteTag, EffectTag, ProblemTag, ...) | ||
| |-- *Store.php (data access: EffectStore, ProblemStore, ...) | | |-- *Store.php (data access: EffectStore, ProblemStore, ElementStore, LifeStoryStore, ...) | ||
| |-- UserProfileStore.php (the big one; profile / dx / meds / abbreviations) | | |-- UserProfileStore.php (the big one; profile / dx / meds / abbreviations) | ||
| |-- VisibilityResolver.php (per-record sharing decision engine) | |||
| |-- VirusScanner.php (ClamAV gate; fail-closed) | |||
| |-- ObservationParser.php (plain-text -> structured observation) | |||
| |-- SpecialMyProfile.php (the user profile editor; large) | | |-- SpecialMyProfile.php (the user profile editor; large) | ||
| |-- SpecialMyAssessment.php (rich reports for all 6 assessments; large) | | |-- SpecialMyAssessment.php (rich reports for all 6 assessments; large) | ||
| |-- SpecialMyLifeStory.php (life-story editor + visual timeline; large) | |||
| |-- SpecialMyCohorts.php (cohort CRUD UI) | |||
| |-- SpecialMyShareLog.php (audit log viewer) | |||
| |-- SpecialMyRefLinks.php (bulk free-text -> structured upgrader) | |||
| |-- SpecialSaveProfileBlock.php (autosave AJAX endpoint) | | |-- SpecialSaveProfileBlock.php (autosave AJAX endpoint) | ||
| |-- Special*.php (other special pages) | | |-- Special*.php (other special pages) | ||
| |-- ProfileDatasets.php (countries, languages, genders, religions, etc.) | | |-- ProfileDatasets.php (countries, languages, genders, religions, etc.) | ||
| |-- DatePicker.php (range / possibility-mix date widget backend) | | |-- DatePicker.php (range / possibility-mix date widget backend + injectBirthdayContextOnce) | ||
| |-- Assessments/ | | |-- Assessments/ | ||
| | |-- Cati.php, CatiNorms.php | | | |-- Cati.php, CatiNorms.php | ||
| Line 407: | Line 642: | ||
| | |-- Mbti.php | | | |-- Mbti.php | ||
| | |-- Enneagram.php | | | |-- Enneagram.php | ||
| | |-- OceanNorms.php | |||
| | `-- Raadsr.php (deprecated 2026-05-17 in favor of CATI, kept for archival reads) | | | `-- Raadsr.php (deprecated 2026-05-17 in favor of CATI, kept for archival reads) | ||
| `-- Api/ | | `-- Api/ | ||
| `-- | | |-- VoteApi.php, EffectApi.php, ... | ||
| |-- ObservationApi.php | |||
| |-- VisibilityRulesApi.php, UserSearchApi.php, CohortsApi.php | |||
| `-- RefUpgradeApi.php | |||
|-- resources/ | |-- resources/ | ||
| |-- ext.pharmacopedia.js | | |-- ext.pharmacopedia.js (single IIFE: chip-picker, dx autocomplete, vote logic, ...) | ||
| |-- ext.pharmacopedia.blocksave.js (debounced autosave per block) | | |-- ext.pharmacopedia.blocksave.js (debounced autosave per block) | ||
| |-- ext.pharmacopedia.css | | |-- ext.pharmacopedia.css | ||
| |-- ext.pharmacopedia.datepicker.js + .css | | |-- ext.pharmacopedia.datepicker.js + .css | ||
| |-- ext.pharmacopedia.timepicker.js + .css | |||
| |-- ext.pharmacopedia.share.js + .css | |||
| |-- ext.pharmacopedia.observation.js + .css | |||
| |-- ext.pharmacopedia.refupgrade.js + .css | |||
| |-- ext.pharmacopedia.lifetimeline.js + .css | |||
| |-- ext.pharmacopedia.lifegraph.js + .css | |||
| |-- ext.pharmacopedia.kitsync.js | |||
| `-- vendor/vis-timeline/ (vis-timeline 7.7.3, Apache-2.0 + MIT) | |||
| |-- vis-timeline-graph2d.min.js | |||
| |-- vis-timeline-graph2d.min.css | |||
| `-- LICENSE | |||
|-- sql/ | |-- sql/ | ||
| |-- (one .sql per table; patches as patch-*.sql) | | |-- (one .sql per table; patches as patch-*.sql) | ||
Revision as of 04:05, 18 May 2026
Pharmacopedia extension specification
Version: 0.9.3 · 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, assessment, life-story, and visibility-sharing infrastructure. It adds parser tags, special pages, API modules, a chip-picker / autosave UI framework, a vis-timeline-based visual life timeline, a granular per-record sharing subsystem, 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)
- Binary AND choice/multi voting on arbitrary content (
type="single"/type="multi"with 2-5 options, results-visibility policy per element) - 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 ~41,500 ICD-10-CM, ICD-11, and DSM-5 codes
- Life-story timeline: visual swimlanes (vis-timeline 7.7.3, vendored Apache-2.0) plus a synchronized trait-trajectory overlay (vis.Graph2d); card-list view alongside; quick-add free-text observation parser; range-date episode form with severity slider
- Per-record sharing subsystem: rule types include public, private, users, cohort, link_token, reciprocal; time-bounded; audit log of who-viewed-what; bulk free-text → structured ref upgrader; privacy mode that disables legacy fallback
- Chip-picker / autosave / slider-precise-input UI framework shared across editor surfaces
- Date + time + range kit: PCPDatePicker (point/range/possibility), PCPTimePicker (fuzzy time-only: "4p", "noon", "quarter past 6")
- Verified-provider role with document-based verification
- Fail-closed ClamAV scan on every image / file upload (hard project rule)
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/. API modules underincludes/Api/. - Frontend (JS): multiple ResourceModules per surface area:
ext.pharmacopedia: main IIFE (chip-picker, dx autocomplete, BFI-10 compute, vote logic for both binary and choice/multi)ext.pharmacopedia.blocksave: debounced autosave per block (race-safe)ext.pharmacopedia.datepicker: range / possibility-mix date widgetext.pharmacopedia.timepicker: time-only widget (fuzzy parsing, extracted from DatePicker)ext.pharmacopedia.share: per-record share dialog (People / Link / Cohorts tabs)ext.pharmacopedia.observation: quick-add observation textarea + live previewext.pharmacopedia.refupgrade: bulk linker for free-text → structured refsext.pharmacopedia.vis-timeline-vendor: vis-timeline 7.7.3 (vendored)ext.pharmacopedia.lifetimeline: visual life-story timeline + swimlanes + toolbarext.pharmacopedia.lifegraph: trait-trajectory overlay (vis.Graph2d) synced to timelineext.pharmacopedia.kitsync: glue that propagates kit-widget changes into legacy hidden inputs + drives privacy-mode toggle
- Styles (CSS): shared dark-theme palette (black / dark-grey / purple / white primary; red / green / blue / teal sparingly for semantic distinction).
- Schema:
sql/, ~25 core tables plus migration patches. Picked up viaLoadExtensionSchemaUpdateshook.
Parser tags
Registered via Hooks::onParserFirstCallInit:
| Tag | Purpose | Class |
|---|---|---|
<vote> |
after-vote\|hidden" for tally-visibility policy. | 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/> <vote slug="fav-color" type="single" options="Red; Blue; Green"> What's your favorite color?</vote> <vote slug="side-effects" type="multi" results="after-vote" options="Dry mouth; Insomnia; Anxiety; Headache; None"> Which side effects did you experience?</vote>
Voting / rating semantics
| Element | Scale | Perspectives | Storage |
|---|---|---|---|
| Vote tag (binary) | +1 / −1 binary | single | pcp_votes.v_value
|
| Vote tag (single-choice) | one of 2-5 options | single | pcp_votes.v_choices (CSV index)
|
| Vote tag (multi-choice) | any subset of 2-5 options | single | pcp_votes.v_choices (CSV indices)
|
| 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
|
Choice / multi vote elements expose per-option tallies via tallyChoices() on demand. Per-option bars render inline in the picker. The results attribute gates tally visibility:
live(default) — tally always visibleafter-vote— tally hidden until viewer has votedhidden— tally never shown (only options + "thanks" on submit)
Server-side options-hash (ve_options_h) detects post-vote option-list edits; the API rejects new votes whose submitted hash doesn't match the live one. Voter identities are stored as HMAC-SHA256 (v_voter_hash) so admins reading the DB cannot map votes to user accounts without the HMAC secret.
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).
Visible at top: a Privacy-mode panel toggling whether legacy public fallback applies (privacy-mode ON = only explicit share rules grant access; OFF = the field-level pf_visibility enum applies).
🔗 Share chips appear in each fieldset legend (Demographics, Diagnoses, Medicines) so the owner can scope a share-rule to that namespace via the modal Share dialog.
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 (PCPDatePicker); 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 PCPTimePicker widgets (typical bedtime, typical wake; fuzzy parsing accepts "10p", "bedtime", "quarter past 6")
- 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_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). Each completed assessment also auto-upserts a Life-story keyframe row (see Life-story timeline) so trajectories plot over time.
Life-story timeline
Special:MyLifeStory is the owner-facing editor + viewer for everything time-anchored about the user. Backed by pcp_life_events with extension columns for episode / observation / keyframe metadata.
Event types
| le_type | Meaning | Notes |
|---|---|---|
| 0 (TYPE_STORY) | Plain timeline entry | title, body, optional image |
| 1 (TYPE_IMAGE) | Image-primary entry | same shape, image is the main payload |
| 2 (TYPE_KEYFRAME) | Auto-created assessment snapshot | populated by upsertAssessmentKeyframe on every assessment save
|
| 3 (TYPE_OBSERVATION) | Plain-text observation | created via the quick-add textarea; parser extracts date, polarity, refs |
| 4 (TYPE_EPISODE) | Time-bounded period | start + end (PCPDatePicker range mode); type / subtype / severity 0-100 |
Quick-add observation
A textarea at the top of Special:MyLifeStory (and a 📝 modal trigger on Special:MyProfile) accepts plain text like:
anxiety from bupropion in jan 2020 did not experience anxiety from bupropion in feb 2018 panic attack 2 months ago depressed as a freshman i had a manic episode sep 1 2020 till nov 15 2020 no insomnia while on melatonin in summer 2023 felt great on christmas 2020 happiest on my 30th birthday
ObservationParser::parse() extracts:
- Date (point or range): ISO, MM/DD/YYYY, "Month D YYYY", "Month YYYY", "Season YYYY", "early/mid/late YYYY", bare year, decades ("2010s"); date RANGES via "X to Y", "X till Y", "from X to Y", "X - Y"; relative-to-now ("yesterday", "last week/month/year", "N months ago", "a few weeks ago"); holidays (christmas, halloween, new year's, valentine's, july 4th, thanksgiving with computed 4th-Thursday-of-Nov, MLK / Memorial / Labor day); age-relative ("7y8mo", "51.2yo", "at age 14", "ages 2-10"); life stages ("in childhood", "as a teen", "as a freshman", "junior year"); Nth birthday ("my 30th birthday")
- Polarity (negation detection): "not", "didn't", "did not experience", "never", "without", "denied" → polarity=0 (negative); else 1 (positive)
- Leading verbs stripped: "I was diagnosed with", "I took", "started taking", "tried", "was on", "experienced", "felt" — so the subject is the noun, not the verb
- Adverbs stripped: "briefly", "occasionally", "frequently", "sometimes", "always" — so the subject is the noun, not the modifier
- Role splitting: "from / caused by / due to / while on" → role='cause'; "with / during / while" → role='context'
- Ref resolution, in priority order:
- User's meds (
pcp_user_meds) - Wiki pages in Category:Medicines
- Effects catalog (
pcp_effects) - Problems catalog (
pcp_problem+pcp_problem_alias) - User's diagnoses (
pcp_profile_diagnoses) - Global ICD diagnosis abbreviations (
pcp_diagnosis_abbreviations) - Free-text fallback (stored as type='free' for later upgrade)
- User's meds (
- Episode-shape detection: "manic episode" → type=mood, subtype=manic; "psychotic break", "panic attack", "anxiety attack", "trauma response" all route to
addEpisodeinstead ofaddObservation. A date RANGE alone is enough to forceis_episode=true.
Live preview chips appear under the textarea as you type. Submit routes to pharmacopediaobservation API which writes the row + refs via setEventRefs().
Episode form
Click the "🌀 Episode" button (or the quick-add detects an episode shape) for the structured form:
- Type selector: mood / psychotic / anxiety / panic / trauma response / dissociative / substance use / eating / sleep disturbance / pain flare / migraine / medication adjustment / hospitalization / creative surge / spiritual / transcendent / relationship crisis / grief / somatic / other
- Subtype (text + datalist) — for mood: depressive / manic / hypomanic / mixed / dysphoric / euthymic
- Severity slider 0-100 (per precision doctrine)
- Date range via PCPDatePicker locked to range mode
- Title, body, single virus-scanned image, visibility (4-state)
Visual timeline
Top of Special:MyLifeStory has a tabbed view: Visual timeline (default) / Card list.
The visual timeline uses vis-timeline 7.7.3 (vendored Apache-2.0 at resources/vendor/vis-timeline/) with these features:
- Swimlanes (toggleable chips): Episodes (range bars), Events, Observations, Keyframes (off by default), Derived
- Trait-trajectory overlay: a synchronized
vis.Graph2ddraws smooth (centripetal Catmull-Rom interpolated) lines for every keyframe trait series (CATI subscales, PID-5-BF domains, CAT-Q, custom traits like "shyness") DIRECTLY on top of the timeline plot area. Same X-axis; the graph's data + time axes are CSS-hidden so only the lines + points show. Values are normalized to 0-100% within each series so different scales coexist. - Toolbar: Visual/Card tabs, group toggle chips, magnifier + zoom +/- buttons, Fit visible / Fit everything
- Plain wheel = vertical scroll inside the timeline (520px fixed height); Ctrl+wheel = zoom; Shift+wheel = horizontal pan
- Click empty timeline area: prefills the quick-add textarea with "on YYYY-MM-DD" at that date + scrolls + triggers live preview
- Click an item: routes to its edit form (event / episode / observation, each has its own route)
- Collapsible "Trait series (N)" legend with chip toggles to show/hide individual series
Edit / delete / duplicate flows
Each event type has its own edit route under Special:MyLifeStory:
Special:MyLifeStory/edit-observation/<id>— re-parses raw text on save; supports polarity override + date overrideSpecial:MyLifeStory/edit-episode/<id>— full episode formSpecial:MyLifeStory?edit_event=<id>— existing event / image / keyframe form
All three forms have side-by-side "Delete X" + "Duplicate X" buttons. Duplicate copies fields + refs + keyframe traits (NOT images) and redirects to the new row's edit form.
Upgrade-link UI
When the parser stored a free-text ref (because the entity wasn't in the user's data at the time), Special:MyRefLinks finds all such refs and offers one-click "link to {match}" buttons. Matches come from the same catalogs the parser checks at write time. A banner on Special:MyLifeStory ("📎 N free-text references could be linked → Review & link") appears when unmatched refs exist.
Per-record sharing subsystem
A granular per-record sharing system layered on top of the legacy pf_visibility enum without removing it. Resolved by VisibilityResolver; the new model is additive (legacy fallback preserved unless Privacy mode is on).
Rule types (vr_rule_type)
private— explicit deny (only owner)public— explicit allow (anyone)users— payload{user_ids: [...]}; allow if viewer in listcohort— payload{cohort_id: N}; allow if viewer inpcp_cohort_membersfor that cohortlink_token— payload{token: '...', uses_remaining: null|N}; allow if URL?pcpshare=TOKENmatchesreciprocal— allow if viewer has a matchingusersrule sharing the same shape back to owner
Rules scope at three levels: (profile, namespace, key), (profile, namespace, NULL), or (profile, '*', NULL). Most-specific-first matching. Rules can have vr_expires (time-bounded) and vr_revoked (preserves audit trail).
Privacy mode
When Privacy mode is ON for a profile, a *-wide private rule is created. VisibilityResolver::canView() short-circuits to false instead of falling through to the legacy pf_visibility check, so only explicit share rules grant access. Default: OFF (back-compat).
Share dialog
Triggered by 🔗 chips on assessment reports, profile sections, life-story timeline. Modal with three tabs:
- People: username autocomplete (↓/↑/Enter/Esc keyboard nav), per-shared-user pill with × to remove just that user, optional expires DatePicker, "Auto-share back" reciprocal toggle
- Link: "Generate link" creates a
link_tokenrule; copies the URL to clipboard with toast; optional max-uses + expires - Cohorts: dropdown of the owner's cohorts (managed at
Special:MyCohorts); optional expires
Audit log
Every permitted view through a rule (not legacy) writes a row to pcp_visibility_view_log. Special:MyShareLog shows the last 200 views with timestamp, viewer (or anonymous IP masked to /24), namespace, key, rule id.
Choice / multi voting
The <vote> tag's binary up/down mode is unchanged. With type="single" or type="multi" + options="A; B; C" (2-5 entries, semicolon separator), it renders a compact chart-icon chip that expands inline to a radio (single) or checkbox (multi) picker with per-option count bars.
Server-side storage:
pcp_votable_elements.ve_options(JSON array of labels)pcp_votable_elements.ve_options_h(first 8 hex of sha256; drift hash)pcp_votable_elements.ve_results_policy(live / after-vote / hidden)pcp_votes.v_choices(CSV indices)pcp_votes.v_options_h(drift hash at vote time)
API route: same pharmacopediavote endpoint; presence of choices or options_h params routes to castChoice(). Response includes tally + user_choices (null for binary). Tally hidden per results policy.
Drift behavior: if the page editor changes the options list after votes exist, ve_options_h updates and new votes' v_options_h reflects the new value. Existing votes stay but their hash no longer matches (marked stale). New votes whose submitted hash mismatches the live one are rejected — protects against browser cache races. Tallies still aggregate by raw index, so RENAMING an option in place silently turns old votes into new-label votes; reordering is the dangerous case. Appending new options is safe.
ClamAV scan rule (project standard)
Hard rule, set 2026-05-17: every server-accepted file upload (image, PDF, document, anything) MUST go through VirusScanner::scanFile($path) (includes/VirusScanner.php) BEFORE being moved to permanent storage. Fail-closed: if /usr/bin/clamdscan is unavailable or returns error, the upload is rejected.
Wired into:
LifeStoryStore::addImage()— life events / episodes / observationsLiteratureStore::storeUploadedPdf()— literature PDFsProviderAppStore::saveUploadedFile()— provider verification documents
AttachmentScanner (used by feature-request attachments) is left alone — its status-return model is intentional for the queued-moderation flow there. AntivirusHelper (the old silent-no-op variant) was deleted; all callers consolidated onto VirusScanner.
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. Privacy mode toggle + share chips per fieldset. |
Special:UserProfile/<name> |
Public profile view (filtered by per-field visibility + rule-based access). 🔗 Share chip in subtitle for self-view. |
Special:MyAssessment |
Index of rich assessment reports |
Special:MyAssessment/cati, /catq, /pid5bf, /mbti, /enneagram, /ocean |
Rich report per assessment; 🔗 Share chip in subtitle for owner |
Special:MyLifeStory |
Owner editor + visual timeline + card list. Quick-add observation textarea + Event/Episode buttons. 🔗 Share chip in subtitle. Privacy-mode + free-text-refs banners. |
Special:MyLifeStory/add-episode, /edit-episode/<id> |
Episode form (create / edit) |
Special:MyLifeStory/edit-observation/<id> |
Observation edit form (re-parses raw text) |
Special:LifeStory/<name> |
Public life-story view (read-only, visibility-filtered) |
Special:MyCohorts |
Owner-managed groups for share-with-cohort flows; create / rename / delete / add+remove members |
Special:MyShareLog |
Who has viewed your shared content (timestamp, viewer, namespace, rule id; anonymous IPs masked) |
Special:MyRefLinks |
Bulk linker: find free-text refs in observations that now match a structured entity; one-click upgrade or dismiss |
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 |
Special:PCPCtrls |
Sysop ctrls dashboard (gated by $wgSpecialPageLockdown)
|
API modules
| Action | Purpose |
|---|---|
pharmacopediavote |
Binary OR choice/multi vote (routes by presence of choices param). Returns tally + user_choices for choice modes; gated by results-policy.
|
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 |
pharmacopediaobservation |
op=preview / op=submit for plain-text observations (routes to addObservation OR addEpisode) |
pharmacopediavisrules |
Visibility rule CRUD (list / create / update / revoke / newtoken) |
pharmacopediausersearch |
Username autocomplete for share-with-people picker |
pharmacopediacohorts |
Cohort CRUD + membership |
pharmacopediarefupgrade |
Free-text ref linker (op=candidates / apply / dismiss) |
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. Now also ve_options + ve_options_h + ve_results_policy for choice votes.
|
pcp_votes |
Binary +1/−1 votes (v_value) AND choice/multi votes (v_choices CSV + v_options_h drift hash) |
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_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 |
Timeline events. Columns: le_type (0=story, 1=image, 2=keyframe, 3=observation, 4=episode), le_polarity, le_raw_text (parser input), le_episode_type, le_episode_subtype, le_severity, le_date_struct (PCPDatePicker JSON; supports range) |
pcp_life_event_refs |
Join table linking events to entities (med / effect / problem / diagnosis / med_page / diagnosis_code / free); role (subject / cause / context / symptom / trigger / treatment) |
pcp_life_traits |
Keyframe trait values (assessment subscale snapshots → trajectory graph) |
pcp_life_images |
Image attachments (ClamAV-scanned) |
pcp_visibility_rules |
Per-(profile, namespace, key) sharing rules. Types: public / private / users / cohort / link_token / reciprocal. Optional vr_expires, vr_revoked, vr_attribution, vr_label. |
pcp_cohorts, pcp_cohort_members |
Owner-managed user groups for share-with-cohort flows |
pcp_visibility_view_log |
Audit trail of rule-permitted views (Special:MyShareLog) |
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. 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 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. - CSP must allow Cloudflare Turnstile and any other 3rd-party widget script source. 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. - CLI E_USER_DEPRECATED suppressed in LocalSettings.php (EmbedVideo / FlaggedRevs spam on MW 1.46). Web behavior unaffected.
- MW ApiResult drops keys starting with
_. Any field whose key starts with underscore in an ApiResult payload is treated as internal metadata and stripped. Rename to plain identifier. Bit Phase 2-4 of visibility-rules subsystem. - MW API serializes int-keyed assoc arrays unreliably.
[23 => 'Alice']may arrive as{"23": "Alice"}or worse. Always use list-of-objects ([{"id": 23, "name": "Alice"}]) for id-to-value maps across the API boundary. - PHP single-quoted strings don't interpret
\xNNbyte escapes.'\xF0\x9F\x94\x97'is 16 literal chars, NOT the 4-byte UTF-8 for 🔗. Use literal Unicode char or double-quoted\u{1F517}. Bit three times in one session. - PHP "0" is FALSY.
!"0"evaluates to TRUE. Soif ( !$x )silently treats"0"(string zero) as missing. Bit choice-vote tallying when voter picked option index 0; vote was IN the DB but invisible to readers. Use=== null || ===for "missing or blank" intent.empty()has the same problem. <span>can't contain<p>. MediaWiki auto-wraps tag content in<p>when there are newlines; browsers auto-close any<span>before the<p>, scattering child elements into wrong DOM positions. Use<div>for parser-tag wrappers whose content can span paragraphs (choice votes, life-story cards).- vis.Graph2d group
stylebelongs at TOP LEVEL not nested inoptions. Nesting silently fails (no error, just invisible lines). Bit the trait-trajectory graph on first build.
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$wgPharmacopediaVoteHashSecret(required): HMAC secret forv_voter_hashso vote rows can't be mapped back to user accounts by anyone reading the DB without the secret- (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, ElementStore, LifeStoryStore, ...)
| |-- UserProfileStore.php (the big one; profile / dx / meds / abbreviations)
| |-- VisibilityResolver.php (per-record sharing decision engine)
| |-- VirusScanner.php (ClamAV gate; fail-closed)
| |-- ObservationParser.php (plain-text -> structured observation)
| |-- SpecialMyProfile.php (the user profile editor; large)
| |-- SpecialMyAssessment.php (rich reports for all 6 assessments; large)
| |-- SpecialMyLifeStory.php (life-story editor + visual timeline; large)
| |-- SpecialMyCohorts.php (cohort CRUD UI)
| |-- SpecialMyShareLog.php (audit log viewer)
| |-- SpecialMyRefLinks.php (bulk free-text -> structured upgrader)
| |-- 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 + injectBirthdayContextOnce)
| |-- Assessments/
| | |-- Cati.php, CatiNorms.php
| | |-- Catq.php
| | |-- Pid5bf.php
| | |-- Mbti.php
| | |-- Enneagram.php
| | |-- OceanNorms.php
| | `-- Raadsr.php (deprecated 2026-05-17 in favor of CATI, kept for archival reads)
| `-- Api/
| |-- VoteApi.php, EffectApi.php, ...
| |-- ObservationApi.php
| |-- VisibilityRulesApi.php, UserSearchApi.php, CohortsApi.php
| `-- RefUpgradeApi.php
|-- resources/
| |-- ext.pharmacopedia.js (single IIFE: chip-picker, dx autocomplete, vote logic, ...)
| |-- ext.pharmacopedia.blocksave.js (debounced autosave per block)
| |-- ext.pharmacopedia.css
| |-- ext.pharmacopedia.datepicker.js + .css
| |-- ext.pharmacopedia.timepicker.js + .css
| |-- ext.pharmacopedia.share.js + .css
| |-- ext.pharmacopedia.observation.js + .css
| |-- ext.pharmacopedia.refupgrade.js + .css
| |-- ext.pharmacopedia.lifetimeline.js + .css
| |-- ext.pharmacopedia.lifegraph.js + .css
| |-- ext.pharmacopedia.kitsync.js
| `-- vendor/vis-timeline/ (vis-timeline 7.7.3, Apache-2.0 + MIT)
| |-- vis-timeline-graph2d.min.js
| |-- vis-timeline-graph2d.min.css
| `-- LICENSE
|-- sql/
| |-- (one .sql per table; patches as patch-*.sql)
`-- i18n/
`-- en.json