From 3d4ae63e1aad6f73000481f944106d624872ff81 Mon Sep 17 00:00:00 2001 From: Andrew Su Date: Thu, 3 Sep 2026 15:56:42 -0700 Subject: [PATCH] site: enable GitHub Pages deploy and serve the live curated corpus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pages is now configured with build_type=workflow, so wire up the deploy and switch the site off the bundled sample data. deploy-site.yml: lift the two dormancy safeties — add the `push` trigger on main for web-revamp-mockup/** (plus the workflow itself), and widen the deploy guard so a merge publishes automatically while a manual dispatch stays a build-only dry run unless deploy=true. Stale DORMANT/maintainer-checklist header replaced with what the workflow now does. Added a sanity check for a non-empty data/index.json, since the site runs in live mode and would render an empty browse page without it. dmdb-data.js: flip SOURCE to "live". record.html reads ?id= so browse results deep-link to a specific record; browse.html gains a "Curation run" facet and shows per-record sourced/flagged edge counts. scripts/build_site_data.py + web-revamp-mockup/data/: the generator and its output (index.json + 135 records across the 4 agentic curation runs, 59 distinct path ids). Data is committed rather than rebuilt in CI so the deploy stays dependency-free and the published site is exactly the reviewed diff; re-run the script after a curation and commit the result. Verified: workflow YAML parses; a dry-run dispatch on main packaged the artifact successfully; the generator reproduces the committed data byte for byte; all 135 index rows resolve to record files with no orphans; every page and JSON endpoint returns 200 over local HTTP; no root-absolute asset paths, so the site works under the /DrugMechDB-agentic/ subpath. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/deploy-site.yml | 64 +- scripts/build_site_data.py | 291 ++ web-revamp-mockup/browse.html | 23 +- web-revamp-mockup/data/index.json | 2614 +++++++++++++++++ web-revamp-mockup/data/records/opus__P01.json | 215 ++ web-revamp-mockup/data/records/opus__P02.json | 210 ++ web-revamp-mockup/data/records/opus__P03.json | 272 ++ web-revamp-mockup/data/records/opus__P04.json | 176 ++ web-revamp-mockup/data/records/opus__P05.json | 212 ++ web-revamp-mockup/data/records/opus__P06.json | 202 ++ web-revamp-mockup/data/records/opus__P07.json | 151 + web-revamp-mockup/data/records/opus__P08.json | 253 ++ web-revamp-mockup/data/records/opus__P09.json | 282 ++ web-revamp-mockup/data/records/opus__P10.json | 246 ++ web-revamp-mockup/data/records/opus__P11.json | 204 ++ web-revamp-mockup/data/records/opus__P12.json | 203 ++ web-revamp-mockup/data/records/opus__P13.json | 212 ++ web-revamp-mockup/data/records/opus__P14.json | 186 ++ web-revamp-mockup/data/records/opus__P15.json | 236 ++ web-revamp-mockup/data/records/opus__P16.json | 203 ++ web-revamp-mockup/data/records/opus__P17.json | 158 + web-revamp-mockup/data/records/opus__P18.json | 223 ++ web-revamp-mockup/data/records/opus__P19.json | 232 ++ web-revamp-mockup/data/records/opus__P20.json | 174 ++ web-revamp-mockup/data/records/opus__P21.json | 253 ++ web-revamp-mockup/data/records/opus__P22.json | 180 ++ web-revamp-mockup/data/records/opus__P23.json | 136 + web-revamp-mockup/data/records/opus__P24.json | 227 ++ web-revamp-mockup/data/records/opus__P25.json | 140 + web-revamp-mockup/data/records/opus__P26.json | 251 ++ web-revamp-mockup/data/records/opus__P27.json | 209 ++ web-revamp-mockup/data/records/opus__P28.json | 191 ++ web-revamp-mockup/data/records/opus__P29.json | 179 ++ web-revamp-mockup/data/records/opus__P30.json | 221 ++ web-revamp-mockup/data/records/opus__P31.json | 271 ++ web-revamp-mockup/data/records/opus__P32.json | 175 ++ web-revamp-mockup/data/records/opus__P33.json | 248 ++ web-revamp-mockup/data/records/opus__P34.json | 211 ++ web-revamp-mockup/data/records/opus__P35.json | 282 ++ web-revamp-mockup/data/records/opus__P36.json | 178 ++ web-revamp-mockup/data/records/opus__P37.json | 179 ++ web-revamp-mockup/data/records/opus__P38.json | 245 ++ web-revamp-mockup/data/records/opus__P39.json | 219 ++ web-revamp-mockup/data/records/opus__P40.json | 248 ++ .../data/records/phase3__P01.json | 318 ++ .../data/records/phase3__P01_no_provider.json | 312 ++ .../data/records/phase3__P02.json | 259 ++ .../data/records/phase3__P03.json | 201 ++ .../data/records/phase3__P04.json | 249 ++ .../data/records/phase3__P05.json | 222 ++ .../data/records/phase3__P06.json | 258 ++ .../data/records/phase3__P07.json | 230 ++ .../data/records/phase3__P08.json | 186 ++ .../data/records/phase3__P09.json | 370 +++ .../data/records/phase3__P10.json | 257 ++ .../data/records/phase3__P11.json | 260 ++ .../data/records/phase3__P12.json | 253 ++ .../data/records/phase3__P13.json | 290 ++ .../data/records/phase3__P14.json | 347 +++ .../data/records/phase3__P15.json | 311 ++ .../data/records/phase3__P16.json | 251 ++ .../data/records/phase3__P17.json | 356 +++ .../data/records/phase3__P18.json | 196 ++ .../data/records/phase3__P19.json | 271 ++ .../data/records/phase3__P20.json | 279 ++ .../data/records/phase3__P21.json | 232 ++ .../data/records/phase3__P22.json | 221 ++ .../data/records/phase3__P23.json | 269 ++ .../data/records/phase3__P24.json | 324 ++ .../data/records/phase3__P25.json | 301 ++ .../data/records/phase3__P26.json | 265 ++ .../data/records/phase3__P27.json | 232 ++ .../data/records/phase3__P28.json | 274 ++ .../data/records/phase3__P29.json | 297 ++ .../data/records/phase3__P30.json | 256 ++ .../records/pilot__B02362_MESH_D003882_1.json | 112 + .../pilot__B02362_MESH_D003882_1__r2.json | 112 + .../pilot__B02362_MESH_D003882_1__r3.json | 102 + .../pilot__DB00002_MESH_D003110_1.json | 100 + .../pilot__DB00002_MESH_D003110_1__r2.json | 135 + .../pilot__DB00002_MESH_D003110_1__r3.json | 142 + .../pilot__DB00005_MESH_D015535_1.json | 99 + .../pilot__DB00009_MESH_D011655_1.json | 100 + .../pilot__DB00009_MESH_D011655_1__r2.json | 149 + .../pilot__DB00009_MESH_D011655_1__r3.json | 116 + .../pilot__DB00010_MESH_D004393_1.json | 127 + .../pilot__DB00013_MESH_D011655_1.json | 120 + .../pilot__DB00026_MESH_D056587_1.json | 118 + .../pilot__DB00030_MESH_D003922_1.json | 144 + .../pilot__DB00035_MESH_D006467_1.json | 179 ++ .../pilot__DB00038_MESH_D013921_1.json | 138 + .../pilot__DB00152_MESH_D001361_1.json | 58 + .../pilot__DB00152_MESH_D001361_1__r2.json | 126 + .../pilot__DB00152_MESH_D001361_1__r3.json | 100 + .../pilot__DB00258_MESH_D006996_1.json | 84 + .../pilot__DB00290_MESH_D002294_1.json | 121 + .../pilot__DB00619_MESH_D015464_1.json | 100 + .../pilot__DB00619_MESH_D015464_1__r2.json | 84 + .../pilot__DB00619_MESH_D015464_1__r3.json | 107 + .../pilot__DB00843_MESH_D000544_1.json | 108 + .../pilot__DB00993_MESH_D001172_1.json | 160 + .../pilot__DB01076_MESH_D006937_1.json | 171 ++ .../pilot__DB01527_MESH_D000856_1.json | 87 + .../pilot__DB09038_MESH_D003924_1.json | 158 + .../data/records/sonnet__P01.json | 252 ++ .../data/records/sonnet__P02.json | 202 ++ .../data/records/sonnet__P03.json | 270 ++ .../data/records/sonnet__P04.json | 268 ++ .../data/records/sonnet__P05.json | 246 ++ .../data/records/sonnet__P06.json | 227 ++ .../data/records/sonnet__P07.json | 323 ++ .../data/records/sonnet__P08.json | 233 ++ .../data/records/sonnet__P09.json | 278 ++ .../data/records/sonnet__P10.json | 186 ++ .../data/records/sonnet__P11.json | 300 ++ .../data/records/sonnet__P12.json | 234 ++ .../data/records/sonnet__P13.json | 169 ++ .../data/records/sonnet__P14.json | 285 ++ .../data/records/sonnet__P15.json | 219 ++ .../data/records/sonnet__P16.json | 252 ++ .../data/records/sonnet__P17.json | 194 ++ .../data/records/sonnet__P18.json | 243 ++ .../data/records/sonnet__P19.json | 222 ++ .../data/records/sonnet__P20.json | 182 ++ .../data/records/sonnet__P21.json | 267 ++ .../data/records/sonnet__P23.json | 384 +++ .../data/records/sonnet__P24.json | 244 ++ .../data/records/sonnet__P25.json | 182 ++ .../data/records/sonnet__P26.json | 285 ++ .../data/records/sonnet__P29.json | 159 + .../data/records/sonnet__P30.json | 247 ++ .../data/records/sonnet__P31.json | 266 ++ .../data/records/sonnet__P32.json | 211 ++ .../data/records/sonnet__P34.json | 212 ++ .../data/records/sonnet__P35.json | 319 ++ .../data/records/sonnet__P36.json | 258 ++ .../data/records/sonnet__P37.json | 167 ++ .../data/records/sonnet__P38.json | 240 ++ .../data/records/sonnet__P40.json | 210 ++ web-revamp-mockup/dmdb-data.js | 13 +- web-revamp-mockup/record.html | 9 +- 141 files changed, 31699 insertions(+), 48 deletions(-) create mode 100644 scripts/build_site_data.py create mode 100644 web-revamp-mockup/data/index.json create mode 100644 web-revamp-mockup/data/records/opus__P01.json create mode 100644 web-revamp-mockup/data/records/opus__P02.json create mode 100644 web-revamp-mockup/data/records/opus__P03.json create mode 100644 web-revamp-mockup/data/records/opus__P04.json create mode 100644 web-revamp-mockup/data/records/opus__P05.json create mode 100644 web-revamp-mockup/data/records/opus__P06.json create mode 100644 web-revamp-mockup/data/records/opus__P07.json create mode 100644 web-revamp-mockup/data/records/opus__P08.json create mode 100644 web-revamp-mockup/data/records/opus__P09.json create mode 100644 web-revamp-mockup/data/records/opus__P10.json create mode 100644 web-revamp-mockup/data/records/opus__P11.json create mode 100644 web-revamp-mockup/data/records/opus__P12.json create mode 100644 web-revamp-mockup/data/records/opus__P13.json create mode 100644 web-revamp-mockup/data/records/opus__P14.json create mode 100644 web-revamp-mockup/data/records/opus__P15.json create mode 100644 web-revamp-mockup/data/records/opus__P16.json create mode 100644 web-revamp-mockup/data/records/opus__P17.json create mode 100644 web-revamp-mockup/data/records/opus__P18.json create mode 100644 web-revamp-mockup/data/records/opus__P19.json create mode 100644 web-revamp-mockup/data/records/opus__P20.json create mode 100644 web-revamp-mockup/data/records/opus__P21.json create mode 100644 web-revamp-mockup/data/records/opus__P22.json create mode 100644 web-revamp-mockup/data/records/opus__P23.json create mode 100644 web-revamp-mockup/data/records/opus__P24.json create mode 100644 web-revamp-mockup/data/records/opus__P25.json create mode 100644 web-revamp-mockup/data/records/opus__P26.json create mode 100644 web-revamp-mockup/data/records/opus__P27.json create mode 100644 web-revamp-mockup/data/records/opus__P28.json create mode 100644 web-revamp-mockup/data/records/opus__P29.json create mode 100644 web-revamp-mockup/data/records/opus__P30.json create mode 100644 web-revamp-mockup/data/records/opus__P31.json create mode 100644 web-revamp-mockup/data/records/opus__P32.json create mode 100644 web-revamp-mockup/data/records/opus__P33.json create mode 100644 web-revamp-mockup/data/records/opus__P34.json create mode 100644 web-revamp-mockup/data/records/opus__P35.json create mode 100644 web-revamp-mockup/data/records/opus__P36.json create mode 100644 web-revamp-mockup/data/records/opus__P37.json create mode 100644 web-revamp-mockup/data/records/opus__P38.json create mode 100644 web-revamp-mockup/data/records/opus__P39.json create mode 100644 web-revamp-mockup/data/records/opus__P40.json create mode 100644 web-revamp-mockup/data/records/phase3__P01.json create mode 100644 web-revamp-mockup/data/records/phase3__P01_no_provider.json create mode 100644 web-revamp-mockup/data/records/phase3__P02.json create mode 100644 web-revamp-mockup/data/records/phase3__P03.json create mode 100644 web-revamp-mockup/data/records/phase3__P04.json create mode 100644 web-revamp-mockup/data/records/phase3__P05.json create mode 100644 web-revamp-mockup/data/records/phase3__P06.json create mode 100644 web-revamp-mockup/data/records/phase3__P07.json create mode 100644 web-revamp-mockup/data/records/phase3__P08.json create mode 100644 web-revamp-mockup/data/records/phase3__P09.json create mode 100644 web-revamp-mockup/data/records/phase3__P10.json create mode 100644 web-revamp-mockup/data/records/phase3__P11.json create mode 100644 web-revamp-mockup/data/records/phase3__P12.json create mode 100644 web-revamp-mockup/data/records/phase3__P13.json create mode 100644 web-revamp-mockup/data/records/phase3__P14.json create mode 100644 web-revamp-mockup/data/records/phase3__P15.json create mode 100644 web-revamp-mockup/data/records/phase3__P16.json create mode 100644 web-revamp-mockup/data/records/phase3__P17.json create mode 100644 web-revamp-mockup/data/records/phase3__P18.json create mode 100644 web-revamp-mockup/data/records/phase3__P19.json create mode 100644 web-revamp-mockup/data/records/phase3__P20.json create mode 100644 web-revamp-mockup/data/records/phase3__P21.json create mode 100644 web-revamp-mockup/data/records/phase3__P22.json create mode 100644 web-revamp-mockup/data/records/phase3__P23.json create mode 100644 web-revamp-mockup/data/records/phase3__P24.json create mode 100644 web-revamp-mockup/data/records/phase3__P25.json create mode 100644 web-revamp-mockup/data/records/phase3__P26.json create mode 100644 web-revamp-mockup/data/records/phase3__P27.json create mode 100644 web-revamp-mockup/data/records/phase3__P28.json create mode 100644 web-revamp-mockup/data/records/phase3__P29.json create mode 100644 web-revamp-mockup/data/records/phase3__P30.json create mode 100644 web-revamp-mockup/data/records/pilot__B02362_MESH_D003882_1.json create mode 100644 web-revamp-mockup/data/records/pilot__B02362_MESH_D003882_1__r2.json create mode 100644 web-revamp-mockup/data/records/pilot__B02362_MESH_D003882_1__r3.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00002_MESH_D003110_1.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00002_MESH_D003110_1__r2.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00002_MESH_D003110_1__r3.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00005_MESH_D015535_1.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00009_MESH_D011655_1.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00009_MESH_D011655_1__r2.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00009_MESH_D011655_1__r3.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00010_MESH_D004393_1.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00013_MESH_D011655_1.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00026_MESH_D056587_1.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00030_MESH_D003922_1.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00035_MESH_D006467_1.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00038_MESH_D013921_1.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00152_MESH_D001361_1.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00152_MESH_D001361_1__r2.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00152_MESH_D001361_1__r3.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00258_MESH_D006996_1.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00290_MESH_D002294_1.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00619_MESH_D015464_1.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00619_MESH_D015464_1__r2.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00619_MESH_D015464_1__r3.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00843_MESH_D000544_1.json create mode 100644 web-revamp-mockup/data/records/pilot__DB00993_MESH_D001172_1.json create mode 100644 web-revamp-mockup/data/records/pilot__DB01076_MESH_D006937_1.json create mode 100644 web-revamp-mockup/data/records/pilot__DB01527_MESH_D000856_1.json create mode 100644 web-revamp-mockup/data/records/pilot__DB09038_MESH_D003924_1.json create mode 100644 web-revamp-mockup/data/records/sonnet__P01.json create mode 100644 web-revamp-mockup/data/records/sonnet__P02.json create mode 100644 web-revamp-mockup/data/records/sonnet__P03.json create mode 100644 web-revamp-mockup/data/records/sonnet__P04.json create mode 100644 web-revamp-mockup/data/records/sonnet__P05.json create mode 100644 web-revamp-mockup/data/records/sonnet__P06.json create mode 100644 web-revamp-mockup/data/records/sonnet__P07.json create mode 100644 web-revamp-mockup/data/records/sonnet__P08.json create mode 100644 web-revamp-mockup/data/records/sonnet__P09.json create mode 100644 web-revamp-mockup/data/records/sonnet__P10.json create mode 100644 web-revamp-mockup/data/records/sonnet__P11.json create mode 100644 web-revamp-mockup/data/records/sonnet__P12.json create mode 100644 web-revamp-mockup/data/records/sonnet__P13.json create mode 100644 web-revamp-mockup/data/records/sonnet__P14.json create mode 100644 web-revamp-mockup/data/records/sonnet__P15.json create mode 100644 web-revamp-mockup/data/records/sonnet__P16.json create mode 100644 web-revamp-mockup/data/records/sonnet__P17.json create mode 100644 web-revamp-mockup/data/records/sonnet__P18.json create mode 100644 web-revamp-mockup/data/records/sonnet__P19.json create mode 100644 web-revamp-mockup/data/records/sonnet__P20.json create mode 100644 web-revamp-mockup/data/records/sonnet__P21.json create mode 100644 web-revamp-mockup/data/records/sonnet__P23.json create mode 100644 web-revamp-mockup/data/records/sonnet__P24.json create mode 100644 web-revamp-mockup/data/records/sonnet__P25.json create mode 100644 web-revamp-mockup/data/records/sonnet__P26.json create mode 100644 web-revamp-mockup/data/records/sonnet__P29.json create mode 100644 web-revamp-mockup/data/records/sonnet__P30.json create mode 100644 web-revamp-mockup/data/records/sonnet__P31.json create mode 100644 web-revamp-mockup/data/records/sonnet__P32.json create mode 100644 web-revamp-mockup/data/records/sonnet__P34.json create mode 100644 web-revamp-mockup/data/records/sonnet__P35.json create mode 100644 web-revamp-mockup/data/records/sonnet__P36.json create mode 100644 web-revamp-mockup/data/records/sonnet__P37.json create mode 100644 web-revamp-mockup/data/records/sonnet__P38.json create mode 100644 web-revamp-mockup/data/records/sonnet__P40.json diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index 6cf1586..9036c0d 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -5,39 +5,30 @@ name: Deploy site # WHY THIS APPROACH: the legacy DrugMechDB site was a heavyweight Jekyll build # (~30 min local build, pushed to a gh-pages-html branch) — the exact bloat the # revamp removes. web-revamp-mockup/ is a zero-dependency, client-rendered static -# site (plain HTML/CSS/JS), so there is NO build step: we just publish the directory -# via the modern GitHub Pages Actions flow (upload-pages-artifact + deploy-pages), -# the same Pages-via-Actions model comparable documentation sites use. Serving the -# Pages artifact does not run Jekyll, so files are published as-authored (no .nojekyll -# needed). +# site (plain HTML/CSS/JS + prebuilt JSON), so there is NO build step: we just +# publish the directory via the modern GitHub Pages Actions flow +# (upload-pages-artifact + deploy-pages). Serving the Pages artifact does not run +# Jekyll, so files are published as-authored (no .nojekyll needed). # -# DONE-WHEN (future live behavior): a merge to main that touches web-revamp-mockup/** -# rebuilds and redeploys the site automatically. +# DATA: web-revamp-mockup/data/ (index.json + records/*.json) is committed, and +# regenerated by `python scripts/build_site_data.py` after a curation run. It is +# deliberately NOT rebuilt here — keeping the deploy dependency-free means the +# published site is exactly the reviewed diff, and a data refresh goes through +# normal review like any other change. # -# ───────────────────────────────────────────────────────────────────────────── -# DORMANT. Two independent safeties keep it from ever deploying until enabled: -# (a) trigger is workflow_dispatch ONLY (no push), and -# (b) the `deploy` job is guarded by `if:` and defaults OFF — a manual dispatch only -# builds+uploads the artifact (a safe dry run); it publishes solely when a -# maintainer sets deploy=true. +# LIVE. A push to main touching web-revamp-mockup/** (or this workflow) rebuilds +# and redeploys automatically. A manual run still defaults to a DRY RUN: it builds +# and uploads the artifact but publishes only when dispatched with deploy=true. # -# INTENDED LIVE TRIGGER (uncomment to enable — publish on merge to main): -# on: -# push: -# branches: [main] -# paths: -# - 'web-revamp-mockup/**' -# workflow_dispatch: -# -# TO ENABLE (maintainer checklist — one-time GitHub Pages setup): -# * Settings -> Pages -> Build and deployment -> Source = "GitHub Actions". -# * Uncomment the live `push` trigger above. -# * Change the deploy-job guard below to `if: github.event_name == 'push' || -# (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy == 'true')`. -# * (Optional) custom domain: add web-revamp-mockup/CNAME and set it under Settings -# -> Pages. The site otherwise serves at https://.github.io//. -# ───────────────────────────────────────────────────────────────────────────── +# Site: https://sulab.github.io/DrugMechDB-agentic/ +# (Optional custom domain: add web-revamp-mockup/CNAME and set it under +# Settings -> Pages.) on: + push: + branches: [main] + paths: + - 'web-revamp-mockup/**' + - '.github/workflows/deploy-site.yml' workflow_dispatch: inputs: deploy: @@ -67,7 +58,9 @@ jobs: set -euo pipefail test -f web-revamp-mockup/index.html \ || { echo "::error::web-revamp-mockup/index.html not found — nothing to deploy."; exit 1; } - echo "Publishing $(find web-revamp-mockup -type f | wc -l) files from web-revamp-mockup/." + test -s web-revamp-mockup/data/index.json \ + || { echo "::error::web-revamp-mockup/data/index.json missing or empty — the site runs in live mode and would render nothing. Run scripts/build_site_data.py and commit data/."; exit 1; } + echo "Publishing $(find web-revamp-mockup -type f | wc -l) files from web-revamp-mockup/ ($(find web-revamp-mockup/data/records -name '*.json' | wc -l) records)." - name: Configure Pages uses: actions/configure-pages@v5 @@ -77,14 +70,15 @@ jobs: with: path: web-revamp-mockup - # ── 2. Deploy to GitHub Pages (GUARDED — off by default) ─────────────────── + # ── 2. Deploy to GitHub Pages ───────────────────────────────────────────── deploy: name: Deploy to GitHub Pages needs: build - # GUARD: publish only when a maintainer explicitly opts in on a manual dispatch. - # When the live `push` trigger is enabled, widen this per the maintainer checklist - # above so a merge to main deploys automatically. - if: ${{ github.event.inputs.deploy == 'true' }} + # Auto-publish on a merge to main; on a manual dispatch, publish only when the + # maintainer explicitly opts in (otherwise the run is a build-only dry run). + if: >- + github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy == 'true') runs-on: ubuntu-latest permissions: contents: read diff --git a/scripts/build_site_data.py b/scripts/build_site_data.py new file mode 100644 index 0000000..99a4866 --- /dev/null +++ b/scripts/build_site_data.py @@ -0,0 +1,291 @@ +""" +Build the static-site JSON (index + per-record) that web-revamp-mockup/ consumes +in "live" mode, from the **agentically curated** path records. + +WHY THIS EXISTS +The site's data layer (web-revamp-mockup/dmdb-data.js) has one documented swap +point: flip SOURCE to "live" and the pages fetch the JSON files named in its +ENDPOINTS map. This script emits exactly those shapes. + +WHAT IT READS +Not kb/paths/ — those 4,846 records are the legacy corpus and carry no per-edge +evidence. The AI-curated records produced by the harness live in the run output +directories listed in RUNS below. The same (drug, disease) pair is often curated +by several runs (model comparison, pilot repeats), so a record's site id is +namespaced by run: "__". graph._id is kept as `record_id`. + +WHAT IT ADDS (join, not invention) + * `paper` — title/authors/journal/year/doi for each cited PMID, read from + reference's PMID_*.md frontmatter in any references_cache/. + * `source_type` — derived from the reference CURIE prefix (PMID -> pubmed, ...). + * `source_tier` — from the cache entry's content_type (abstract vs full text). + * `graph.summary` — the node chain along the record's longest path, joined with + arrows. Generated for display; flagged as such in `site`. + * `area` — a COARSE display-only bucket keyword-matched from the disease + name. Not an ontology assertion; it exists to drive the + browse facet and nothing else. + +Usage: + python scripts/build_site_data.py # write web-revamp-mockup/data/ + python scripts/build_site_data.py --out DIR # write elsewhere +""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path + +import yaml + +HERE = Path(__file__).resolve().parent +REPO = HERE.parent +# Every reference cache in the repo: the shared one plus the per-run caches the +# model-eval runs wrote alongside their outputs. +CACHE_DIRS = [ + REPO / "references_cache", + REPO / "experiments" / "opus_vs_sonnet" / "opus" / "references_cache", + REPO / "experiments" / "opus_vs_sonnet" / "sonnet" / "references_cache", +] +DEFAULT_OUT = REPO / "web-revamp-mockup" / "data" + +# The agentic curation runs, in display order: (run key, label, glob). +RUNS = [ + ("pilot", "Pilot run", "experiments/pilot/outputs/*.yaml"), + ("opus", "Opus (model eval)", "experiments/opus_vs_sonnet/opus/outputs/*.yaml"), + ("sonnet", "Sonnet (model eval)", "experiments/opus_vs_sonnet/sonnet/outputs/*.yaml"), + ("phase3", "Phase-3 eval", "tests/phase3_eval_outputs/*.yaml"), +] + +# Coarse display buckets for the browse facet. First match wins, so the more +# specific patterns are listed first. +AREA_RULES = [ + ("Neoplasms", r"leukemia|leukaemia|lymphoma|neoplasm|carcinoma|tumor|tumour|myeloma|myelogenous|cancer"), + ("Immune", r"psoriasis|arthritis|arthropath|lupus|crohn|sclerosis|dermatomyositis|asthma|cryopyrin|periodic syndrome|colitis"), + ("Infectious disease", r"tuberculosis|influenza|herpes|hepatitis|conjunctivitis|staphylococc|infection|malaria|hiv"), + ("Cardiovascular", r"heart failure|hypertension|myocardial|stroke|thromb|embolism|angina|arrhythmia"), + ("Blood", r"hemophilia|haemophilia|factor viii|thrombocytopen|anemia|anaemia"), + ("Metabolic", r"cholesterol|diabetes|gout|avitaminosis|deficiency|hypocalcemia|obesity|anorexia|lipid"), + ("Endocrine", r"thyroid|dwarfism|acromegal|adrenal"), + ("Nervous system", r"alzheimer|parkinson|epilep|migraine|neuropath|pain"), + ("Mental health", r"depress|anxiety|bipolar|schizophren|psychos"), + ("Respiratory", r"pulmonary fibrosis|copd|bronch"), + ("Musculoskeletal", r"osteoarthritis|osteoporosis|myopath"), + ("Kidney", r"nephritis|nephropath|renal"), +] + +TIER = {"abstract": "ABSTRACT", "full_text": "FULL_TEXT", "fulltext": "FULL_TEXT"} + + +def area_for(disease: str) -> str: + d = (disease or "").lower() + for area, pattern in AREA_RULES: + if re.search(pattern, d): + return area + return "Other" + + +def source_type_for(reference: str) -> str: + ref = (reference or "").upper() + if ref.startswith("PMID"): + return "pubmed" + if ref.startswith("PMC"): + return "pmc" + if ref.startswith("DB") or ref.startswith("DRUGBANK"): + return "drugbank" + if ref.startswith("DOI") or ref.startswith("10."): + return "doi" + return "other" + + +def load_cache() -> dict[str, dict]: + """Map reference id -> paper credential block, from references_cache frontmatter.""" + papers: dict[str, dict] = {} + for md in sorted(m for d in CACHE_DIRS if d.is_dir() for m in d.glob("*.md")): + text = md.read_text(encoding="utf-8", errors="replace") + if not text.startswith("---"): + continue + end = text.find("\n---", 3) + if end == -1: + continue + try: + fm = yaml.safe_load(text[3:end]) or {} + except yaml.YAMLError: + continue + ref = fm.get("reference_id") + if not ref: + continue + papers[str(ref)] = { + "title": fm.get("title"), + "authors": fm.get("authors") or [], + "journal": fm.get("journal"), + "year": str(fm.get("year")) if fm.get("year") is not None else None, + "doi": fm.get("doi"), + "pmcid": fm.get("pmcid"), + "license": fm.get("license"), + "_tier": TIER.get(str(fm.get("content_type") or "").lower()), + } + return papers + + +def longest_chain(nodes: list[dict], links: list[dict]) -> list[str]: + """Node ids along a longest source->target chain (the record's 'spine').""" + incoming: dict[str, list[str]] = {n["id"]: [] for n in nodes} + for l in links: + if l.get("target") in incoming: + incoming[l["target"]].append(l.get("source")) + + best: dict[str, list[str]] = {} + visiting: set[str] = set() + + def chain(nid: str) -> list[str]: + if nid in best: + return best[nid] + if nid in visiting: # cycle guard + return [nid] + visiting.add(nid) + parents = [p for p in incoming.get(nid, []) if p in incoming] + longest: list[str] = [] + for p in parents: + c = chain(p) + if len(c) > len(longest): + longest = c + visiting.discard(nid) + best[nid] = longest + [nid] + return best[nid] + + return max((chain(n["id"]) for n in nodes), key=len, default=[]) + + +def build_record(path: Path, run: str, run_label: str, papers: dict[str, dict]) -> dict: + raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {} + graph = dict(raw.get("graph") or {}) + nodes = raw.get("nodes") or [] + links = [dict(l) for l in (raw.get("links") or [])] + by_id = {n["id"]: n for n in nodes} + + # enrich each EvidenceItem with its joined paper credential + derived fields + for link in links: + items = link.get("evidence") + if not isinstance(items, list): + continue + enriched = [] + for item in items: + it = dict(item) + ref = it.get("reference") + it["source_type"] = source_type_for(ref) + paper = papers.get(str(ref)) + if paper: + p = {k: v for k, v in paper.items() if k != "_tier"} + it["paper"] = p + it.setdefault("source_tier", paper.get("_tier") or "ABSTRACT") + else: + it["paper"] = None + enriched.append(it) + link["evidence"] = enriched + + chain = longest_chain(nodes, links) + graph["summary"] = " → ".join(by_id[n]["name"] for n in chain if n in by_id) + + # references ship as bare URL strings in the record files; the site wants + # {url, type, label} rows. + refs = [] + for r in raw.get("references") or []: + if isinstance(r, dict): + refs.append(r) + continue + url = str(r) + host = re.sub(r"^https?://(www\.)?", "", url).split("/")[0] + kind = "drugbank" if "drugbank" in host else "wiki" if "wikipedia" in host else "other" + refs.append({"url": url, "type": kind, "label": f"{host} — record source"}) + + site_id = f"{run}__{path.stem}" + return { + "directed": raw.get("directed", True), + "multigraph": raw.get("multigraph", True), + "graph": graph, + "nodes": nodes, + "links": links, + "references": refs, + "site": { + "id": site_id, + "record_id": graph.get("_id"), + "run": run, + "run_label": run_label, + "source_file": str(path.relative_to(REPO)), + "summary_generated": True, + }, + } + + +def index_row(rec: dict) -> dict: + nodes, links = rec["nodes"], rec["links"] + types, seen = [], set() + for nid in longest_chain(nodes, links): + label = next((n["label"] for n in nodes if n["id"] == nid), None) + if label and label not in seen: + seen.add(label) + types.append(label) + for n in nodes: # types off the spine still matter to the facet + if n.get("label") and n["label"] not in seen: + seen.add(n["label"]) + types.append(n["label"]) + + target = next((n["name"] for n in nodes if n.get("label") == "Protein"), None) + if not target: + target = next((n["name"] for n in nodes[1:]), "") + + sourced = sum(1 for l in links if l.get("evidence")) + flagged = sum( + 1 for l in links + if any((it.get("supports") or "SUPPORT") != "SUPPORT" for it in (l.get("evidence") or [])) + ) + return { + "id": rec["site"]["id"], + "record_id": rec["site"]["record_id"], + "run": rec["site"]["run_label"], + "drug": rec["graph"].get("drug") or "", + "disease": rec["graph"].get("disease") or "", + "area": area_for(rec["graph"].get("disease")), + "target": target or "", + "types": types, + "steps": max(len(longest_chain(nodes, links)) - 1, 0), + "edges": len(links), + "sourced": sourced, + "flagged": flagged, + } + + +def main() -> int: + ap = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--out", default=str(DEFAULT_OUT), help="output data directory") + args = ap.parse_args() + + out = Path(args.out) + records_dir = out / "records" + records_dir.mkdir(parents=True, exist_ok=True) + + papers = load_cache() + rows, written = [], 0 + for run, run_label, pattern in RUNS: + for path in sorted(REPO.glob(pattern)): + rec = build_record(path, run, run_label, papers) + (records_dir / f"{rec['site']['id']}.json").write_text( + json.dumps(rec, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + rows.append(index_row(rec)) + written += 1 + + rows.sort(key=lambda r: (r["drug"].lower(), r["disease"].lower(), r["id"])) + (out / "index.json").write_text( + json.dumps(rows, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + + runs = {r["run"] for r in rows} + print(f"Wrote {written} records + index.json to {out} " + f"({len(runs)} runs, {len({r['record_id'] for r in rows})} distinct path ids).") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/web-revamp-mockup/browse.html b/web-revamp-mockup/browse.html index 990fb38..70b9940 100644 --- a/web-revamp-mockup/browse.html +++ b/web-revamp-mockup/browse.html @@ -7,7 +7,7 @@ -
MOCKUP · DrugMechDB website revamp · sample data, not final — navigation is search-driven (no giant sidebar)
+
LIVE · 135 AI-curated records from 4 agentic curation runs (built by scripts/build_site_data.py) · site design still a mockup