Runbooks for editing the dataset and refreshing external assets, plus the reference
for what each tool is (Tool reference, below) and the emitted data contract
(Data contract, below). Both moved here from ../CLAUDE.md to keep that
file terse; CLAUDE.md keeps the viewer/runtime map and the non-obvious rules. Author-side
corpus locations are in CLAUDE.local.md.
- Changing the data
- Writing release notes
- Refreshing external data (author-side)
- Tool reference
- Shared libraries
- Generator and data modules
- Code and data checks
- Meshes, dev server, screenshots, icons, demos
- Stahl quote sourcing (bindings, NbN, classes)
- Expression and function (GtoPdb #7 / #23, Allen AHBA #8)
- Receptor effects, release modulation and demographics (BETA)
- Quote recheck and citation sites
- Pharmacogenomics (PharmFreq #13, CPIC #15, ClinPGx #20)
- Book heading resolution
- Drug metabolism (CYP roles)
- Binding Ki (PDSP #5, GtoPdb #11)
- Wikipedia articles and brand names (#9, #10)
- Half-life and time to peak (Stahl, DailyMed #14, BDPM #16, Europe PMC #17)
- Drug history, origin and approvals (Drugs@FDA #18)
- Physicochemistry, workup and TDM (#19, #21, #22, #24)
- Prodrugs and metabolite bindings
- Binding directions and UNKNOWN searches
- Binding gaps
- Book corpora (PDF to pages)
- README stats, images, git hooks
- Data contract (emitted
public/data/)
- Edit the relevant list in
generate_data.py(ortools/data/drugs_data.jsonlfor drugs).- Structures: edit
PAIRED/MIDLINE; search-only acronyms ("VTA", "LC") go inregions/__init__.pySTRUCTURE_ALIASESkeyed by base id (emitted asaliases, never rendered nor graded; an unknown key fails the build, likeDRUG_ALIASES). Paired entries are auto-mirrored (define on the right, x > 0; the generator emits one right-side shape file and the_Lmember references it withmirror:true). A region is a noise-deformed ellipsoid by default; blob/curve/composite shape knobs are in the Geometry section (medial=Truederives the right medial clip;clip_planesis auto via_bisecting_clip_planes,JIGSAW_CLIP.enabled; cortex pattern is shader-drawn viainjectCortexSwirl/CORTEX_SWIRL). Layout: theposfield positions regions to assemble at explode 0; lobes overlap +medialso the hemispheres meet atMIDLINE_GAP(temporal is the lateral exception); deep nuclei sit small + central. Re-render to check (only=frontal_R,parietal_R,temporal_R,occipital_R&explode=0&view=right). - Structure links + grades: add a
base -> URLentry to theWIKIPEDIAregistry for a Wikipedia link (both hemispheres share it; a non-base key raises). Anatomy source grade isclassification_provenance, defaultllm, overridable inSTRUCTURE_PROVENANCE(theRECEPTOR_PROVENANCE/TARGET_PROVENANCE/STRUCTURE_PROVENANCEtrio via_lookup_provenance). - Projections: edit
PROJECTIONS.from/toare structure ids; the arrow pointsfrom -> to. Carrylabel/neurotransmitter/description. A pathway is graded by a verified quote inKANDEL_QUOTES(keyed by the right-side(from, to)pair); an unsourced pathway showsNOSOURCE(no fabricated citations).bidirectional: True(both cones; use withsymmetric: False+ explicit_L/_Rfor commissures).tentative: True(dotted, Hypothetical section). Projections are bilateral by default (define once on the right): such a pathway is emitted once withmirror: trueand the consumer reflects it to the other hemisphere (no duplicate row per pathway; seemirrorunder the data contract below).symmetric: Falsekeeps a one-sided pathway (emits no flag).kindmust be aPROJECTION_COLORSkey (excitatory / inhibitory / dopaminergic / cholinergic / neuroendocrine / serotonergic / noradrenergic); a new kind also needsKIND_TO_SIGN(->SIGN_COLORS/SIGN_LABELS) andBURSTincircuit-anim.js. - Circuits: append to
CIRCUITS:id,name,structuresas base ids (arrows derived). Optionaldescription+description_fr+wikipedia+sources(a list of quote-level{corpus,page,quote,provenance}dicts, validated by_expand_sources). - Projection groups: edit
PROJECTION_GROUPS: one entry per group in both modes,{mode, key, name, description, description_fr, wikipedia, sources}(modekind|sign,keyvalidated;sourcesquote-level dicts). Normally you only edit descriptions/wikipedia (all 7 kinds + 3 signs exist); a new entry is needed only when adding a new projectionkind. - Receptors: append to
RECEPTORS:id,name,family,neurotransmitter,receptor_class,sign,synaptic,locations(base ids or"ALL"). Optionaldescription+description_fr(inline) +wikipedia. A stub = emptylocations+ no description._receptor_recordvalidates keys + bases. A new family/class/synaptic value needs its label map entry (+ FR). The four classification attributes (family/receptor_class/sign/synaptic) are each independently graded: the base grade isRECEPTOR_PROVENANCE(defaultllm), lifted per-attribute only whereRECEPTOR_CLASSIFICATION_COVERAGEsays the receptor's Stahl-Essential quote (STAHL_ESSENTIAL_RECEPTOR_QUOTES) actually backs that attribute (conservative: never list an attribute the quote or the record disagrees on;RECEPTOR_ATTR_QUOTESgives an attribute a different quote than the main one, or several to back a compound value likesynaptic="both"). An individual expression region is sourced (abovellm) by adding a{receptor_id: {base: [quote-source]}}entry toRECEPTOR_LOCATION_SOURCES. - Drugs: edit
tools/data/drugs_data.jsonl. Each:id,name,categories, optionalnbn+description(inline{en,fr}),wikipedia,bindings. A binding is{target, action}(+ optionaleffect/note/tentative);targetis a merged map key (aDRUG_TARGETSkey or a receptor id),actionaDRUG_ACTIONSkey (agonist / partial_agonist / antagonist / inverse_agonist / reuptake_inhibitor / releaser / enzyme_inhibitor / pam / nam / blocker / modulator)."bindings": []is allowed: the drug still opens its panel (lithium), it just lights nothing. A new coarse target/category/action needs aDRUG_TARGETS/DRUG_CATEGORY_LABELS/DRUG_ACTIONSentry (with{en,fr}labels; aDRUG_TARGETSentry needs atype+ optionalwikipedia). Target classification grade overridable inTARGET_PROVENANCE; an individual "Regional distribution" region is sourced (abovellm) by adding a{target_id: {base: [quote-source]}}entry toTARGET_LOCATION_SOURCES(mirror ofRECEPTOR_LOCATION_SOURCES). The drug's class classification grade is overridable inDRUG_CATEGORY_PROVENANCE(or upgraded by a quote-levelcategory_sourceson the authored drug). Keep Stahl extraction strictly dump-sourced (a Wikipedia binding only adds a target Stahl does not list). - Panel annotations (addons): append to
ADDONSintools/data_generators/addons.py:id,owner_kind+owner(the node it annotates; a structure anchors its base, andsectionanchors a whole browse view via anADDON_SECTIONSkey instead of a node),slot(anADDON_SLOTSkey, which is the panel hook the viewer offers),display(admonition),tone(caution/info), optionaltitle,text(both inline{en,fr}) andsources(graded like any node). Use it for a claim no existing kind has a home for. A new slot is two edits: theADDON_SLOTSentry here and the one matchingappendAddons(...)call in the viewer (js/panel/views/*.js,js/legend/anatomy.js); a slot with no call site would swallow the node silently. A new display is anADDON_DISPLAYSentry plus oneADDON_RENDERERSfunction. - Translations: every display string is wrapped with
_t(); add the French to theFRtable or the build raises listing every miss. For a feminine/plural paired name setfr_gender(f/mp/fp).
- Structures: edit
- Run
python tools/generate_data.pyto regeneratepublic/data/. - If you touched a
data/shapes/*.jsonsdf spec (or added/removed a structure), re-bake the geometry:node tools/bake_meshes.mjs. Skipping it is caught, not silent (check_data.pyfamily 11 and--checkboth re-hash the shape files), and the site still renders correctly meanwhile by meshing in the browser, just slower. Seedocs/BAKED_MESHES.md. - Optionally run
python tools/check_data.py. - For new drugs/structures with links, run the fetch tools (network, idempotent, touch
only the new ones):
fetch_molecules.py,fetch_structure_images.py. To refresh binding affinities, runfetch_ki.py --apply(reads the local PDSP CSV; idempotent), which rewritesdrugs_data.jsonl'skiannotations +affinity_onlybindings. - Commit the generator change + the regenerated artifacts together.
The legend is generated at runtime from the data, so it updates automatically.
Every version bump needs a docs/changelog/<major>.<minor>.<patch>/changelog.md, or
check_data.py family 9 fails (the gate exists so a bump can never ship a "What's new"
popup that announces an update it cannot describe). The format:
# 3.39.0 (2026-07-28)
## Added
- What a visitor can now do, in their words, no jargon (2e7c22f, 211e89f)
fr: La même chose en francais- The
# <version> (YYYY-MM-DD)title is required: it carries the release date (nothing else does, the emit being a plain file read) and must match its directory, which catches the copy-paste that made the file. - Headings are the five categories
Added/Improved/Fixed/Data/Docs(CATEGORIESindata_generators/changelog.py), used in whatever order the release wants. - Every bullet needs an indented
fr:line right under it, like every other display string. - A trailing
(sha, sha)list is stripped intocommitsand rendered as links to the commit on the source host; other trailing parentheses stay as prose. Write the shas after committing the work, so they are real (family 9 checks the shape, the host resolves the rest). - Write for a casual reader: what changed for them, not which module changed.
Then python tools/generate_data.py compiles the whole tree into data/changelog.json
(newest first) and the French into translations.fr.json.
Every fetcher sends the one User-Agent of tools/lib/polite_http.py, and most go through its polite GET (backoff on 429/503). Set NEURARIUM_FETCH_CONTACT to a real e-mail or URL before a large run (Wikimedia asks for a contact); unset, it sends "contact via repository".
To re-pull every third-party asset the dataset hot-links or vendors, run these (all
network, idempotent, polite; each touches only what changed). Always finish with
generate_data.py so the emitted public/data/ picks up the new urls/files:
python tools/fetch/fetch_molecules.py— new per-drug molecule SVGs intopublic/data/molecules/(only drugs missing one); writestools/generated_cache/molecules_sources.json.python tools/fetch/fetch_structure_images.py— re-resolve each structure's and wiki-linked circuit's Wikipedia hero + gallery image urls intotools/generated_cache/structure_images_sources.json+tools/generated_cache/circuit_images_sources.json(no bytes downloaded; the gif/svg is hot-linked at runtime).--target structures|circuitsscopes to one.- PDSP Ki:
python tools/fetch/fetch_ki.py --downloadre-downloads the whole-DB CSV fromhttps://pdspdb.unc.edu/databases/kiDownload/download.phpoverdata_sources/books/pdsp_ki/KiDatabase.csv(author-side; see that dir'sREADME.md), paging with thefromDate=<kiId>cursor because the whole-DB export is silently capped. TLS is fully verified: the host chains to an emSign root missing from the Mozilla store, so the download also trusts the pinned cross-certificatetools/fetch/certs/pdsp_chain.pem(its header gives the fingerprint and why); a rotated chain fails loudly pointing there. Thenpython tools/fetch/stamp_fetch_dates.pyandpython tools/fetch/fetch_ki.py --applyto rewritedrugs_data.jsonl'ski+affinity_only. - GtoPdb ligand interactions (the
gtopdb_kiKi fallback + the curated directions foraffinity_onlybindings):python tools/fetch/fetch_gtopdb_ki.pyre-downloads the bulkinteractions.csvand rebuildsdata_sources/gtopdb/pages_ki/(author-side) +tools/generated_cache/gtopdb_ki.json, thenpython tools/sourcing/apply_gtopdb_ki.pyquote-gates + merges intodrugs_data.jsonl. Run it afterfetch_ki.py, since it only fills what PDSP left empty. See docs/PROVENANCE.md (corpus #11). - GtoPdb receptor expression (the
receptor_locationssources):python tools/fetch/fetch_gtopdb.pyre-pulls each receptor's tissue comments (caches todata_sources/gtopdb/, author-side), then run the confirm-only judge overdata_sources/gtopdb/worklist.jsonandpython tools/sourcing/apply_location_sources.py --judged <f>to merge newverifiedsources intotools/generated_cache/location_sources.json. See docs/PROVENANCE.md (corpora #7/#8). - Allen AHBA expression (the
target_locations+ residualreceptor_locationssources):python tools/fetch/fetch_allen.pyre-pulls each donor's microarray PACall (caches todata_sources/allen/, author-side; ~2GB across donors), thenpython tools/sourcing/apply_location_sources.py --corpus allenmerges the deterministicverifiedsources (no judge) intotools/generated_cache/location_sources.json. See docs/PROVENANCE.md (corpora #7/#8). - Allen AHBA expression density (the
receptor_density+target_densityprofiles): the samefetch_allen.pyrun also z-scores each gene's microarray intensity per region and writesdata_sources/allen/density.json(pass--skip-densityto opt out of the heavy MicroarrayExpression read), thenpython tools/sourcing/apply_expression_density.pyre-gates each profile quote and writestools/generated_cache/expression_density.json. See docs/VIEWER.md Expression density. - GtoPdb target classification (the
receptor_class+sign+ non-receptortargetssources):python tools/fetch/fetch_gtopdb_class.pypulls each target'stype+ transduction table (caches todata_sources/gtopdb/pages_class/, author-side, +tools/generated_cache/gtopdb_class.json), thenpython tools/sourcing/apply_classification_sources.pymaps + quote-gates + merges the confirm-onlyverifiedsources (no judge) intotools/generated_cache/classification_sources.json. See docs/PROVENANCE.md (corpus #12). Gotchas:/services/families/{id}404s (the family name rides the target record instead), and a transducer cell can list several mechanisms, so it is split on commas with "G protein independent mechanism" ignored (SIGN_NEUTRAL_MECHANISMS). - Drug metabolism (the
drug_enzymesrows):python tools/fetch/fetch_cyp_worklist.pyre-reads Stahl's per-drugPharmacokineticsblock and the stored English Wikipedia articles (data_sources/wikipedia/pages/, corpus #9) intotools/generated_cache/cyp_worklist.json, offering candidate sentences with no verdict. An LLM pass writes each drug's picks (a candidate index plus enzyme/role/strength) intotools/generated_cache/cyp_judged.json, thenpython tools/sourcing/apply_cyp_sources.pygates them and writes bothtools/generated_cache/drug_enzymes.jsonanddrug_enzymes_wikipedia.json;generate_data.pymerges them Stahl-first. A drug whose article is not stored yet needsuv run tools/fetch/fetch_wikipedia_pharmacology.py --drug <id>first. See docs/DRUGS.md Drug metabolism. - Freshness refetch (every editable online corpus, see docs/PROVENANCE.md "Freshness";
check_data.pyfails once a cited page is over 12 months old, andpre-pushenforces it):uv run tools/fetch/fetch_wikipedia_pharmacology.py --refresh(every stored EN article plus any roster drug lacking one),uv run tools/fetch/fetch_brand_names.py(every FR article; prints the drugs with none),python tools/fetch/fetch_gtopdb_ki.py --refresh,python tools/fetch/fetch_dailymed.py --redownload(the chosen labels in place;--refreshwould re-pick labels and orphan the cited ones),python tools/fetch/fetch_bdpm.py --refresh,python tools/fetch/fetch_bdpm_rcp.py(after refreshing the RCP copies it reads),python tools/fetch/fetch_drugsfda.py --refresh(thenapply_drugsfda.py),python tools/fetch/fetch_cpic.py,python tools/fetch/fetch_clinpgx.py --refresh,python tools/fetch/fetch_physchem.py --refresh, the PDSP CSV (item 3), andfetch_gtopdb.py --refresh/fetch_gtopdb_class.py --refresh/fetch_gtopdb_function.py --refresh(all three call the GtoPdb web services, which need a per-user API key: put it in envGTOPDB_API_KEYor the gitignored filedata_sources/gtopdb/api_key.txt; it is sent as theGTP-API-Keyheader and never written to any page, cache or log). Allen (~2GB) and the hand-downloaded PharmFreq export are dated by their download; refetch them only when their date nears the limit. Then dry-run the appliers (apply_gtopdb_ki.py --dry-run, ...) and runcheck_data.pyto list quotes no longer on their refreshed page, and finallypython tools/fetch/stamp_fetch_dates.pyto freeze the new dates intotools/generated_cache/fetch_dates.json. python tools/generate_data.py— regeneratepublic/data/from all of the above.python tools/update_readme_stats.py— refresh the README sourcing table (CI runs it--check).
Panel descriptions need no refresh script: each fetches the current Wikipedia lead
at runtime (js/wiki.js), so they stay current on their own.
What each authoring / fetch / check script in tools/ is (one line each). The dev/runtime
tooling (serve.py, shot.py, demos/) is also summarized in ../CLAUDE.md (Running,
Screenshots).
- Shared helpers (reuse, never copy):
lib/polite_http.py(the one GET + User-Agent),pipeline/judged.py(qid-checked index picks) +pipeline/gate.py(the applier's page gate),lib/salt_words.py(EN salt words for name joins),lib/chirality.py(enantiomer tags),data_generators/caches.py(load,labels,attestsfor committed caches),data_generators/node_id.py(the id shapecheck_dataandrender_apienforce). tools/lib/: shared libraries (nomain, never run): the root helpers above,quote_match.py(normalize_for_match,emitted_page_dirs),gene_maps.py(owner -> HGNC gene),page_file.py(page_file/page_path/read_page/read_page_file, the one citation page -> file rule and the one page reader, lint-enforced bytests/test_page_file_lint.py),structure_ids.py(base_id, the one hemisphere-suffix rule),corpora/wikipedia.py+corpora/bdpm.py(stored corpus pages + their joins),parse/durations.py(EN/FR duration parsing). A script imports these asfrom lib import Xafter insertingtools/onsys.path; it never imports another script for a helper.tools/lib/drugs_io.py— shared JSONL load/save fordrugs_data.jsonl(load_drugs/save_drugs); used bygenerate_data.py,fetch_ki.py, and the threeapply_*_sources.pywriters.tools/lib/text_fold.py: shared name folding (strip_accents,alnum_key) for every author-side matcher; reuse it rather than re-spelling an NFKD fold or an INDEX-name key.
generate_data.py— single source of truth for the anatomy (stdlib-only, offline): defines every region/projection/receptor once, emits the artifacts below. Drugs are the exception (authored intools/data/drugs_data.jsonl, read by_load_drugs). Display strings are{en,fr}via_t()(see CLAUDE.md I18n).tools/data/drugs_data.jsonl— the authored drug dataset (from Stahl 8th ed.), one compact JSON object per line, read by_load_drugs, emitted todata/drugs.jsonl. Edit to add/change a drug.tools/data_generators/— pure-data modules imported bygenerate_data.py:drugs.py,provenance.py(grades,_GRADE_RANK, source validators),corpora.py(SOURCE_CORPORA),provenance_stats.py(the sourcing tally_strongest_grade/_binding_grade+_provenance_stats, which reduce the emitted nodes tometa.provenance_stats),integrity.py(authored-table typo guards),meta.py(themeta.jsonbuilder),i18n.py,quote_table.py(the serialization-time source-quote externalize pass ->quotes.jsonl, mirroring the i18n externalize), and thereceptors/subpackage (one module per neurotransmitter family, e.g.serotonergic.py, each exposingENTRIES;__init__.pyconcatenates them intoRECEPTORSin the original order), theregions/subpackage (one module per anatomicalgroup, e.g.cortex.py/basal_ganglia.py, each exposingPAIRED+MIDLINE;__init__.pyconcatenates them intoPAIRED/MIDLINEin the original order), andgeometry.py(the shared cortical-dome SDF helpers, e.g._cortex_lobe_entry+MIDLINE_GAP, plus the pure geometry math helpers_scale_sdf/_scale_triple/_directional_extent/_bisecting_clip_planes, imported byregions/cortex.pyandgenerate_data.py),genes.py(the HGNC symbol per receptor / non-receptor target, re-exported from the fetchers that own the maps rather than restated; an identifier for the gene-keyed lookup links, not a graded node), andconnectivity.py(the three connectivity node literalsPROJECTIONS/CIRCUITS/PROJECTION_GROUPSplus the_KQ_*/_SG_*pathway quote-source constants they cite; the shared_kandel/_nieuwenhuys/_stahl_essquote constructors live inprovenance.py),addons.py(the addon-node kind: theADDON_SLOTS/ADDON_DISPLAYS/ADDON_TONESvocabularies emitted intometa.jsonplus the authoredADDONSlist), andpresentation.py(the presentation maps emitted intometa.json: the colour/flow mapsPROJECTION_COLORS/KIND_TO_SIGN/SIGN_COLORS/SYSTEM_FLOW_KINDS, the label mapsSIGN_LABELS/GROUP_LABELS/RECEPTOR_FAMILY_LABELS/RECEPTOR_CLASS_LABELS/SYNAPTIC_LABELS, plus the per-structureWIKIPEDIAlink table; a dependency-free leaf), and thequotes/subpackage (verified quote registries by corpus:kandel.py=PROJECTION_QUOTES+STRUCTURE_QUOTES(Kandel/Nieuwenhuys anatomy, cites the connectivity_KQ_*);stahl_essential.py=STAHL_ESSENTIAL_RECEPTOR_QUOTES/STAHL_ESSENTIAL_TARGET_QUOTES/RECEPTOR_ATTR_QUOTES/RECEPTOR_CLASSIFICATION_COVERAGE/CLASSIFICATION_ATTRS/TARGET_POLARITY_QUOTES;metabolism.py=METABOLITE_ENZYME_QUOTES;contradictions.py=CONTRADICTIONS, the hand-curated table of claims another corpus DENIES, keyed by citation site, any node kind (thecontradictedbadge, mostly the in-vitro-vs-in-vivo split; applied byapply_contradictions);uncertainty.py= the mirror image of the others (UNCERTAINTY_REASONS+apply_binding_uncertainty: why a quote-checked claim still deserves doubt. Alone among these, it authors nothing: both flags and bullets are derived from the emitted data, reading each quote's heading trail throughquote_table.heading_of); chain stays acyclic provenance <- connectivity <- quotes <- generate_data).tools/data_generators/changelog.py— parses the authoreddocs/changelog/<version>/changelog.mdfiles into thechangelog.jsonshape (stdlib, no deps). Fails loud withfile:lineon an unknown category, a bullet with nofr:line, or stray prose, so a typo cannot silently drop a bullet.
tools/check_data.py: stdlib integrity checker over emittedpublic/data/(see CLAUDE.md Data checks); the CLI only, one module per family intools/checks/(family 5's walks intools/checks/sources/).tools/check_js.py— stdlib scope walk overpublic/**/*.js, reporting every identifier reference no enclosing scope binds (theReferenceErrora valid-syntax file still ships). Takes paths, else checks the whole viewer; exit 1 on a finding. Run bypre-pushand bytools/tests/test_check_js.py.
tools/bake_meshes.mjs— Node (no deps): meshes every distinctsdfshape author-side intopublic/data/meshes/*.bin+index.json, so a visitor downloads the geometry instead of rebuilding it (~0.7s instead of ~3.0s, more on a phone). Imports the browser's ownjs/sdf-core.js+js/noise.jsso the bake cannot drift from the runtime fallback, and round-trips every mesh through the real decoder before writing. Run aftergenerate_data.py(it reads the emittedstructures.jsonl);--checkverifies the bake is current and writes nothing. Seedocs/BAKED_MESHES.md.tools/serve.py— stdlib dev server,Cache-Control: no-store, roots atpublic/(see CLAUDE.md Running).tools/shot.py— Playwright screenshot helper (see CLAUDE.md Screenshots).tools/icons/brain_icon.py: stdlib generator of every app icon: the README / PWA brain (docs/images/neurarium-icon.svg,public/icon-*.png,public/apple-touch-icon.png) and the simplified favicon (public/favicon.svg); PNG export viarsvg-convert. Edit it, never the generated files.tools/demos/— Playwright demo-video recorder:recorder.py(aDemoAPI) +neurarium.py(the showcase tour, writes the README herodocs/images/preview.gif). Needs ffmpeg+gifski+GPU; seetools/demos/README.md.
tools/sourcing/build_source_worklist.py— lists not-yet-sourced drug bindings with Stahl page ranges (input to the source-extraction workflow; resumable).tools/sourcing/apply_source_quotes.py— applies the extraction workflow's accepted quotes onto bindings (re-finds the quote in the page range; idempotent).tools/sourcing/apply_nbn_sources.py— sources each drug's NbN line (greps Stahl's verbatim line, substring-confirms, no judge); falls back to the drug Class line (nbn_nonstandard) for a newer drug with no NbN line. Idempotent.tools/sourcing/apply_category_sources.py— sources each drug's class classification (drug_categories) from an extract/judge results file (a judge is needed: our coarsecategoriesre-map Stahl's free-text class line, unlike the fixed NbN field). Idempotent.tools/sourcing/apply_binding_sources.py— writes the small hand-curatedBINDING_SOURCEStable onto the bindings no mechanical pass can reach (PDSP/GtoPdb have no assay at the subtype granularity we model, and the page prose is too irregular to grep). Re-gates every quote withcheck_data's own normalizer, never overwrites an existing source, and prints known cross-corpusCONFLICTSrather than resolving them. Idempotent;--dry-run.
tools/fetch/fetch_gtopdb.py— fetches receptor tissue-distribution comments from the Guide to Pharmacology API (corpus #7gtopdb), the source for receptor expression regions;RECEPTOR_GENESmaps receptor->gene->targetId. Cachesdata_sources/gtopdb/+worklist.json(each quote carries assay species). See docs/PROVENANCE.md (corpora #7/#8).tools/fetch/fetch_gtopdb_function.py: stdlib, network (GtoPdb web services, API key as for #7), author-side. Corpus #23gtopdb_function: per receptor / target, the/function,/pathophysiology,/alteredExpressionand/commentsendpoints intodata_sources/gtopdb/raw_function/<owner>.json+ one quote-gate pagepages_function/<targetId>.md(one statement per line, section + species named).--only <ids>,--refresh,--no-fetch(re-flatten offline). Feeds the receptor-effect pipeline: thenpython tools/fetch/fetch_receptor_effects.py(offers the human or species-less lines as candidates) -> LLM picks by index intoreceptor_effects_judged.json->python tools/sourcing/apply_receptor_effects.py->recheck_quotes.py build --kinds receptor_effect --unstamped+apply --llm <judge>->stamp_fetch_dates.py->generate_data.py. For a receptor no stored corpus gives an effect, runpython tools/fetch/fetch_europepmc.py --effect-searchfirst (writeseuropepmc_effect_search.json, whose abstracts then join the candidates), and after the recheckapply_receptor_effects.py --unknown-log --model <model>logs each still effect-less owner as UNKNOWN. See docs/DRUGS.md (Receptor effects).tools/fetch/fetch_allen.py— fetches the Allen Human Brain Atlas microarray (corpus #8allen_ahba), the source for target expression regions + the receptor regions GtoPdb misses; a PACall detection-boolean vote per (gene, region), no judge.TARGET_GENES+lib.gene_maps.RECEPTOR_GENESmap owners to genes. Cachesdata_sources/allen/+confirmed.json. The same run also emits the density profiles intodata_sources/allen/density.json(--skip-densityopts out of the heavy MicroarrayExpression read;DENSITY_MIN_Ris the cross-donor reliability floor). See docs/PROVENANCE.md (corpora #7/#8) + docs/VIEWER.md Expression density.tools/sourcing/apply_expression_density.py— re-gates each density profile's quote againstdata_sources/allen/pages/<gene>.md, trims it to the regions the owner claims, and writestools/generated_cache/expression_density.json(loaded intoRECEPTOR_DENSITY/TARGET_DENSITY). Idempotent. See docs/VIEWER.md Expression density.tools/sourcing/apply_location_sources.py— merges accepted expression quotes intotools/generated_cache/location_sources.json,--corpus {gtopdb,allen}(gtopdb needs a judged file; allen is deterministic). Idempotent. See docs/PROVENANCE.md (corpora #7/#8).tools/generated_cache/location_sources.json— machine-written bulk location sources, loaded bygenerate_data.pyintoRECEPTOR_LOCATION_SOURCES/TARGET_LOCATION_SOURCES. Not served.
Four-step passes (fetcher offers candidates, one LLM pass picks by index, applier gates, Opus recheck); concepts and gates in docs/DRUGS.md.
Receptor effects (receptor_effect):
python tools/fetch/fetch_receptor_effects.py # -> receptor_effects_worklist.json (keyword-offered candidates, no verdict)
<one LLM pass> # -> receptor_effects_judged.json ({owner: [{effect, mode, picks}]})
python tools/sourcing/apply_receptor_effects.py --dry-run # then without; sole writer of receptor_effects.json
python tools/sourcing/recheck_quotes.py build --out <dir> --kinds receptor_effect # independent Opus judge, then `apply --llm opus`
Release modulation (receptor_release_modulation):
python tools/fetch/fetch_release_modulation.py # -> release_modulation_worklist.json (mechanism + transmitter cues, no verdict)
<one LLM pass> # -> release_modulation_judged.json ({owner: [{kind, structure, cell, modulates, effect, picks}]})
python tools/sourcing/apply_release_modulation.py --dry-run # then without; sole writer of release_modulation.json
python tools/sourcing/recheck_quotes.py build --out <dir> --unstamped # independent Opus judge, then `apply --llm opus`
Demographics (receptor_demographic, after adding the paper to fetch_europepmc.PAPERS):
python tools/fetch/fetch_europepmc.py # store any new paper (network)
python tools/fetch/fetch_demographics.py # -> demographics_worklist.json (age / sex cues, no verdict)
<one LLM pass> # -> demographics_judged.json ({owner: [{factor, direction, structure, magnitude?, picks}]})
python tools/sourcing/apply_demographics.py --dry-run # then without; sole writer of receptor_demographics.json
python tools/sourcing/recheck_quotes.py build --out <dir> --unstamped # independent Opus judge, then `apply --llm opus`
tools/sourcing/recheck_quotes.py— re-verifies every emitted verified quote with a stronger model (Sonnet) and stamps the sourcing LLM.build --out <dir>writes per-page batches (page text loaded once per batch to minimize tokens; Allen AHBA excluded as deterministic); an LLM judges each batch (present + supports claim);apply --batches <dir> --verdicts <f> [--llm sonnet]writestools/generated_cache/quote_llm.json({quote_id: llm}, applied uniformly byquote_table) +quote_recheck_flagged.json(quotes the recheck could not fully confirm, for review) +quote_recheck_disputed.json(quotes two models disagree about). Each batch item carries the quote'sheading(below), which is the context the "supports" half of the verdict turns on. Corpus -> page dir is read frommeta.source_corpora, never restated. See docs/PROVENANCE.md ("The sourcing model"). Scope a pass with--kinds/--unstamped/--stamped-by/--flagged/--disputed, and--max-page-charstrims a page too long to be worth embedding whole (a stored Wikipedia article) to the neighbourhoods of the quotes cited on it;applymerges into the three caches, so a scoped pass keeps the stamps, flags and disputes it did not re-judge. A stamp names the strongest model that has confirmed the quote, so a second, weaker read (--stamped-by opusre-read by Sonnet) can only corroborate it; the mirror of that rule is that a weaker model doubting a quote is a disagreement, not a verdict, so it parks inquote_recheck_disputed.jsonwith the stamp intact until the stamping model is asked again (build --disputed,apply --llm <it>).tools/sourcing/demote_quotes.py— the other half of a recheck: applies what the flags mean. A flagged quote is still stored, so the node still shows a green pill for a claim its sentence does not back; this removes that source (never the claim: a drug that loses its class quote is still classified, it just no longer says a book said so) or, with--replace <proposals.json>from a re-extraction pass, rewrites the quote in place after re-gating it against the cited page, falling back to removal when the proposal is not verbatim there. Walkstools/data/drugs_data.jsonlplus the applier-written caches, drops an emptied*sourceskey rather than leaving it empty, and clears the ids it handled fromquote_llm.json/quote_recheck_flagged.json. Quotes authored in hand-written Python are reported with corpus/page/quote, never rewritten, so nothing is silently skipped. Matching is byquote_table.quote_id, so an id from any of the three caches resolves. A verdict that names the claim it failed (a target'ssites, or--ids X --sites S) demotes only those citations: the quote's other claims keep their source and the quote keeps its judging stamp, and a site matching nothing is reported instead of passing for a run that changed nothing. Idempotent;--flagged/--ids/--sites/--dry-run. Stdlib, author-side.tools/sourcing/citation_site.py— the address of one citation, shared by the two passes above. A quote id is a content hash, so one sentence reaching two claims is one id, and a verdict keyed by it can only condemn or clear the sentence everywhere at once. A site (drug:clozapine/bindings[5ht2a],receptor:5ht1a/locations[amygdala]) names one place the quote is cited from, so a judge can reject the class and keep the binding.recheck_quotes.pyspells it out of the emitted data anddemote_quotes.pyrecognizes it walking the authoring files, whose shapes differ;tools/tests/test_citation_site.pycross-checks the two ends over the real dataset, because a divergence does not crash, it just quietly demotes nothing. Stdlib, author-side.
tools/fetch/fetch_pharmfreq.py— stdlib, offline, author-side. Reshapes PharmFreq's hand-downloaded "Metabolizer status tool" export (corpus #13) into the committedtools/generated_cache/enzyme_variability.json, whichbuild_enzymes()merges ontometa.enzymes. The export itself is committed, one TSV per gene undertools/data/pharmfreq/(small and freely redistributable, unlike the books), and the cache pins each file's sha256, so this corpus declarestsv_dirinstead ofpages_dir: there is no page, because the quote is composed out of the table and finding it on a page we also wrote would prove nothing.check_data.pyfamily 12 rebuilds every profile and quote from the pinned files instead, which is the same guarantee and, unlike every other corpus's gate, one that runs on a plain clone. The reader both ends share lives indata_generators/pharmfreq.py. One node per isoform (kindenzyme_variability) whose quote carries the whole profile, like an Allen density profile, so the verbatim gate covers the numbers the panel shows. Deterministic, no judge, no network. Reports the isoforms it could not map (UGT1A1, which names no isoform we model) and the two the source omits (CYP3A4, CYP1A2), so a gap never reads as "does not vary".--zippoints at another export,--dry-runwrites nothing.tools/fetch/fetch_cpic.py— stdlib, network (one-shot), author-side. Pulls CPIC's PostgREST API (api.cpicpgx.org/v1, no key) for the level A/B gene-drug pairs on our roster and writes the committed exporttools/data/cpic/cpic_recommendations.json(corpus #15) plus the pinnedtools/generated_cache/cpic_guidance.json, which_drug_pgxingenerate_data.pyreads into each drug'spgxblock. Same committed-export shape asfetch_pharmfreq.pyabove and the same family-12 re-derivation gate, through the shared readerdata_generators/cpic.py. It imports only genes that corpus #13 has a frequency profile for, keeps a two-gene row only where the other gene is a normal metabolizer (recorded asassumes), drops CPIC's uncertainty phenotypes, and picks one population per pair out ofgeneral/adults/PHT naive. Every exclusion is printed on each run (out-of-roster pairs, genes with no profile, pairs CPIC has levelled but written no guideline for, pairs whose only populations this panel cannot read), so no gap passes as covered. Deterministic after the fetch, no judge: the API's own strings are the quotes (extraction: "code"). Re-running it is how you refresh the corpus; the pin makes generation fail loudly if the export moves without it.tools/fetch/fetch_clinpgx.py(stdlib, network once, author-side): ClinPGx's clinical-annotations zip (corpus #20) filtered to our receptor/target genes at levels 1A-3, written as the committed exporttools/data/clinpgx/clinical_annotations.json+ its pintools/generated_cache/clinpgx_variants.json; read bydata_generators/clinpgx.py(receptor_variantnodes) and re-derived by check_data family 12.--refreshre-downloads,--dry-runreports.
tools/fetch/fetch_quote_headers.py— stdlib, offline, author-side. Resolves, for every emitted quote from a book corpus (a paged corpus whose page tree has anINDEX.md), the trail of headings it sits under, into the committedtools/generated_cache/quote_headers.json({quote_id: [outermost, ..., innermost]}), whichquote_tablemerges onto the quote nodes by id. Reads the emittedquotes.jsonl, so the order is generate -> fetch -> generate (likerecheck_quotes.py). Two resolvers:OutlineIndextakes the ancestor chain of theINDEX.mdoutline entry covering the page (Kandel, Stahl Essential, Carlat, Nieuwenhuys),StahlPagesreads Stahl's page text (which has no usable outline) for the monograph title, the positional subsection, and the section from a subsection -> section majority vote over all 158 monographs, because the extraction drops ~30 section headings and the nearest surviving one then leaks in from the previous section or drug. Capped toMAX_TRAILlevels. A book missing from the checkout keeps its cached trails rather than dropping them. No LLM.--dry-runreports without writing. See docs/PROVENANCE.md ("Where a quote sits").
tools/fetch/fetch_cyp_worklist.py: stdlib, offline. Thedrug_enzymesworklist builder, and the only pass that reads a corpus. Pulls Stahl's per-drugPharmacokineticsbullets out of the author-side dump and the drugboxMetabolismrow plus every isoform-naming sentence out of the stored English Wikipedia article, confirms each verbatim on a real page (Stahl's inside that drug's ownINDEX.mdrange), and writestools/generated_cache/cyp_worklist.json: per drug, a numbered candidate list plus the enzyme/role/strength vocabularies the judge must answer in. It applies no veto and states no role: a negation or a sentence about another molecule is the judge's to reject.--only <ids>scopes. See docs/DRUGS.md Drug metabolism.tools/fetch/fetch_cyp.pyandtools/lib/corpora/wikipedia.py: libraries, not scripts, imported by the worklist builder. Each knows how one corpus is shaped (Stahl: monograph page spans, the dump'sPharmacokineticsanswers, the isoform vocabulary, the verbatim gate that shortens a bullet once when a running header or a hyphenated line break defeats it; Wikipedia: which file an article lives in including redirectPAGE_ALIASES, the sentence split, the reference-list skip, the drugbox row). Neither decides whether a sentence supports a role.tools/sourcing/apply_cyp_sources.py— quote-gates the judged CYP roles (cyp_judged.json, a candidate index per row, never a quote string) and is the sole writer of bothtools/generated_cache/drug_enzymes.json(Stahl) anddrug_enzymes_wikipedia.json(corpus #9). Five gates: the drug and the index resolve; the (enzyme, role, strength) triple is in the vocabulary; the quote names the isoform the row claims; the quote is verbatim on the cited page underlib.quote_match.normalize_for_match; one (enzyme, role) pair per drug per corpus, keeping the reading that carries a strength tier. Stdlib, author-side (the gate needs the gitignored corpora).--dry-runreports,--verboselists every rejected row. See docs/DRUGS.md Drug metabolism. Every source it writes is stampedllmwith the model that judged the file (--llm, defaultopus).
tools/fetch/fetch_ki.py— parses the PDSP Ki CSV (data_sources/books/pdsp_ki/, author-side) into per-drug binding affinities;--applywrites eachki+ adds median-strongeraffinity_onlybindings. A curatedALIASmap recovers drugs PDSP lists under a related compound. See docs/DRUGS.md Drugs. When adding one drug, scope every writer with its--only(fetch_ki.py,fetch_gtopdb_ki.py,fetch_molecules.py,fetch_brand_names.py): an unscoped refresh legitimately rewrites every drug whose upstream rows moved, burying the one-drug change. A scoped run merges into the committed cache.tools/fetch/fetch_gtopdb_ki.py— stdlib. Downloads GtoPdb's bulk ligand-interaction CSV (data_sources/gtopdb/interactions.csv, author-side), joins it to our drugs by name (an HTML-strippingnorm+ a curatedLIGAND_ALIASES) and to our targets by case-folded gene symbol (reusinglib.gene_maps.RECEPTOR_GENES+TARGET_GENES, plusEXTRA_TARGET_GENESfor the ionotropic subunit families), and writes one quotable row line per interaction intodata_sources/gtopdb/pages_ki/<slug>.md(corpus #11gtopdb_ki) + the proposals intotools/generated_cache/gtopdb_ki.json. Only a pKi/pKd becomes a Ki (10^(9-pKi)nM); a functional potency rides along as the direction's citation. No judge: the CSV's owntypecolumn is the direction. See docs/PROVENANCE.md (corpus #11).tools/sourcing/apply_gtopdb_ki.py— merges those proposals intodrugs_data.jsonl(a drug's bindings and each non-modeled active metabolite's): confirm-only (never adds a target), PDSP-first (akionly where there is none), and aprovisional_actiononly on anaffinity_onlybinding, so a stated direction always wins. A disagreement is reported, split into compatible refinements (COMPATIBLE) and genuine conflicts, never auto-resolved. Re-gates every quote throughlib.quote_match.normalize_for_match; idempotent (strip_previousrebuilds its own prior write).
tools/fetch/fetch_wikipedia_pharmacology.py—uv run(deps: beautifulsoup4). Fetches a drug's English Wikipedia article pinned to its revision id, stores the whole page author-side (data_sources/wikipedia/raw/<slug>.html+pages/<slug>.md, corpus #9wikipedia_pharm), and mines the pharmacodynamics binding table for a per-target Ki (adaptive multi-row-header grid + fuzzy target resolution reusingfetch_ki'snorm/parse_ki/NAME_PATTERNS). A Ki source'squoteis the verbatim table row, so the normal quote gate applies. Preview by default;--json,--no-fetch. Fills the Ki gap where PDSP has none (e.g. alpha1, non-psychiatric agents). See docs/DRUGS.md Drugs.tools/fetch/fetch_brand_names.py—uv run(deps: beautifulsoup4). Resolves each drug's French Wikipedia article via the EN article's langlinks (reusingfetch_wikipedia_pharmacology.py), stores it author-side (data_sources/wikipedia/pages_fr/<slug>.md, corpus #10wikipedia_fr), and writes a candidate trade-name-sentence worklist (tools/generated_cache/brand_worklist.json) for the LLM that extracts each drug's ordered European/French brands. EN is used only for the langlink (its pages are not refetched, so no corpus #9 Ki gate can drift). See docs/PROVENANCE.md (corpus #10).tools/sourcing/apply_brand_names.py— merges the LLM-extracted eu/fr brands (brand_judged.json) intodrugs_data.jsonl, quote-gating each name verbatim on its FR page and tagging the firstfr, the resteu(na from Stahl is kept). Idempotent. See docs/PROVENANCE.md (corpus #10).
tools/fetch/fetch_pharmacokinetics.py— stdlib. Scans each drug's Stahl page span (fromINDEX.md) for elimination half-life + active-metabolite lines, pre-parsing durations to hours, into a candidate worklist (tools/generated_cache/pk_worklist.json) for the LLM that extracts each drug's T½ + metabolites. See docs/DRUGS.md Drugs (Half-life + active metabolites).tools/sourcing/apply_pharmacokinetics.py— merges the LLM-extracted T½ + metabolites (pk_judged.json) intodrugs_data.jsonl, quote-gating each value verbatim on its Stahl page and resolving a metabolite'sdrug_idby norm-name match (never self-link). Sole writer ofhalf_life/metabolites; merges by metabolite name, acceptswikipedia_pharmquotes,--only <drug ids>scopes and re-applies those drugs as judged (a full run only fills what is absent, so re-running it is a no-op). See docs/DRUGS.md Drugs (Half-life + active metabolites).tools/fetch/fetch_dailymed.py— stdlib, network, resumable. Corpus #14 fetcher: searches DailyMed for each drug's US prescribing label, picks ONE (plain-oral forms preferred, since a modified-release product states a real Tmax for a different product; the first whose pharmacokinetics section actually names a peak wins), and storesdata_sources/dailymed/raw/<setid>.xml+data_sources/dailymed/pages/<setid>.md(the 12.3 / Clinical Pharmacology text only) plus the chosen label per drug ingenerated_cache/dailymed_labels.json. States no claim: it only puts pages on disk forfetch_tmax.pyto read. See docs/PROVENANCE.md (corpus #14).tools/fetch/fetch_bdpm.py— stdlib, network. Corpus #16 fetcher: downloads the BDPM (French public drug database) tables, joins each drug to its single-active marketed products by exact INN, picks ONE plain systemic RCP and stores its section 5.2 asdata_sources/bdpm/pages/<cis>.md, plus the chosen RCP and the French brand names per drug ingenerated_cache/bdpm_labels.json. States no claim:fetch_tmax.py/fetch_half_life.pyread the pages,apply_brand_names.py --bdpmwrites the brands (code read, no judge). See docs/PROVENANCE.md (corpus #16).tools/fetch/fetch_europepmc.py: stdlib, network (www.ebi.ac.uk). Corpus #17 fetcher: stores the Europe PMC abstract of each paper in its hand-listedPAPERSregistry (PMID -> the claim it was added for) asdata_sources/europepmc/pages/<pmid>.md. States no claim.--ki-prosefollows a drug's (or an active metabolite's OWN) article citations instead: for every binding with no Ki, it stores the abstracts of the papers each affinity-stating paragraph of that EN article cites, and writestools/generated_cache/ki_prose_worklist.json(each line stating a Ki / Kd value, or a row of a Ki table, no verdict). Every stored paper with an open-access copy also gets its full text (raw/<pmid>.xml+ the flattened body and tables after a## Full textmarker, licence in the header). The LLM picks by index intotools/generated_cache/ki_prose_judged.json;tools/sourcing/apply_ki_prose.py(--dry-run) gates + writes the Ki (confirm-only, Ki-less bindings only, Ki / Kd only, value written in the quote, a μM / pM one viaunit+stated_value; ametabolitessection writes the metabolite store), then runrecheck_quotes.py build --unjudged+ an independent judge +apply(remaining gap: docs/SOURCING_GAPS.md "Measured Ki"). See docs/PROVENANCE.md (corpus #17).tools/fetch/fetch_tmax.py— stdlib. Offers, per drug, every line of its own stored Wikipedia article (#9), Stahl monograph (#1) and DailyMed label (#14) that names a PEAK and carries a duration, pre-parsed into every{hours, hours_max?}it states, intotools/generated_cache/tmax_worklist.json. States no verdict of its own: the LLM pass answers only which candidate index is the drug's own oral time-to-peak. The drugbox is deliberately not read (no Tmax row; itsOnset of actionis clinical onset). Importable as a library, which is how the applier re-derives what was offered. See docs/DRUGS.md Drugs (Time-to-peak).tools/sourcing/apply_tmax.py— stdlib. Mergestmax_judged.jsonintodrugs_data.jsonlbehind six gates (drug in the dataset, drug offered by a freshfetch_tmax.build(), index resolves, the judged hours equal a duration the quote states (re-parsed here, so the model supplies no number),MAX_TMAX_HOURS= 24 h oral plausibility, quote verbatim on its page). Idempotent, sole writer oftmax/tmax_sources; writes nollmstamp, sorecheck_quotes.pymust run after it.
tools/fetch/fetch_drug_history.py— stdlib, offline. Offers, per drug, every sentence of the lead and history-like sections (EN History / Society and culture / Legal status, FR Histoire / Historique / ...) of its stored EN and FR Wikipedia articles that carries a year and an approval or withdrawal word (plus the sentence before it ascontext), intogenerated_cache/drug_history_worklist.json, with no verdict and withinstructions(a withdrawal counts only for a marketed medicine). See docs/DRUGS.md Drugs (Regulatory history).tools/sourcing/apply_drug_history.py— stdlib. Mergesdrug_history_judged.json(per drug{approval: {index, year, country?, agency?} | null, withdrawals: [{index, year, country, reason}]}) intodrugs_data.jsonlbehind gates (drug offered, index resolves, year in the quote, approval / withdrawal wording, no food / salt-substitute context on a withdrawal, every vocabulary value attested by the quote, quote verbatim on its page). Idempotent, sole writer ofwithdrawalsand of a Wikipediafirst_approval/first_approval_sources(a Drugs@FDA one is kept unless a judged approval replaces it); writes nollmstamp, so runrecheck_quotes.py build --kinds drug_first_approval,drug_withdrawalsafter it.tools/sourcing/apply_drug_holders.py— stdlib, offline, author-side. Code-read (no judge, no recheck): rebuilds every drug'sholders[](kinddrug_holders, one node per (drug, market)) from the Drugs@FDA page title of the first-approval NDA (us) and theholder:header line of the BDPM brand product pages (fr). Run afterfetch_drugsfda.py/fetch_bdpm.py. See docs/DRUGS.md Drugs (Regulatory history).tools/fetch/fetch_originator.py— stdlib, offline. Offers, per drug, the lead / history sentences of its stored EN article naming who discovered / synthesized / developed it, and, per originator company article listed inCOMPANY_ARTICLES(fetched once withfetch_wikipedia_pharmacology.py --title), its Headquarters / Country rows and its country / successor sentences, intogenerated_cache/originator_worklist.json, no verdict.tools/sourcing/apply_originator.py— stdlib. Mergesoriginator_judged.json(drugs: {id: {index, name, company}},companies: {slug: {country: {index, country}, now: {index, name}}}) behind gates (index resolves, name verbatim in an originator sentence, company lists that name, country attested,nowin a successor sentence, quote verbatim on its page); sole writer oforiginator*; nollmstamp, so runrecheck_quotes.py build --kinds drug_originator,drug_originator_countryafter it.tools/fetch/fetch_drugsfda.py— stdlib, network once (download.open.fda.gov;--refreshre-downloads). Corpus #18: reads openFDA's bulk Drugs@FDA export and picks, per drug, the earliest original approval among the NDAs whose single active ingredient is that drug (salt words dropped,FDA_NAMESfor US names, stereo-aware), only when that NDA is classed a new molecular entity and no matching NDA is undated. Writesdata_sources/drugsfda/pages/<appl>.mdandgenerated_cache/drugsfda_approvals.json. No judge (code extraction).tools/sourcing/apply_drugsfda.py— stdlib. Fallback writer of a first US approval (national: true) onto a drug with none (or its own earlier one), gated (year in the quote, quote verbatim on its page); replaces a Wikipedia approval only when the FDA date is strictly earlier (earliest dated approval wins,history.earliest_wins, whichapply_drug_history.pyhonours too, so run order does not matter); norecheck_quotes.pyneeded.
tools/fetch/fetch_physchem.pystdlib, network (pubchem.ncbi.nlm.nih.gov, the B3DB TSV once from GitHub), author-side. Corpora #21 PubChem + #22 B3DB: reads each drug's CID off its stored Wikipedia drugbox, writesdata_sources/pubchem/pages/<cid>.md(XLogP3, TPSA) anddata_sources/b3db/pages/<cid>.md(BBB label + logBB rows, joined by CID else exact name, InChI-guarded), thengenerated_cache/physchem.json.--only <ids>scopes,--refreshre-downloads. No judge (code extraction); follow withpython tools/fetch/stamp_fetch_dates.pyandpython tools/generate_data.py. See docs/DRUGS.md (Physicochemistry and brain entry).tools/fetch/fetch_bdpm_rcp.py: stdlib, offline for drugs with a French product, author-side. Corpus #24bdpm_rcp: per drug, picks the princeps (else #16's product) with an RCP 4.4 and writesdata_sources/bdpm/pages_rcp4/<cis>.md(sections 4.2 / 4.3 / 4.4) from the freshest copy on disk (#16'sraw/or../justelesRCP/data/rcp/, else EMA's SmPC in../justelesRCP/data/eu/for a centrally authorised drug, read only;--justelesrcppoints elsewhere), plusgenerated_cache/bdpm_rcp_labels.json; prints the drugs with no page. A drug with no CIS product at all falls back to its EPAR found by name (tools/fetch/fetch_ema_epar.py: EMA's two EPAR JSON reports, cached indata_sources/ema/raw/; the SmPC PDF downloaded once and converted with justelesRCP'ssrc/ema_pdf.py, so runuv run --with pymupdf python tools/fetch/fetch_bdpm_rcp.pywhen a new one is needed;--offlineuses only stored SmPCs,--refresh-emare-pulls the reports and SmPCs) intopages_rcp4/EMEA-H-C-<n>.md, the chosen EPAR recorded underemain the cache; the run prints which drugs it reached that way. Thenpython tools/fetch/fetch_workup.py(worklist of the 4.2 / 4.3 / 4.4 lines naming a test, no verdict; excipient notices dropped) -> one LLM pass ->generated_cache/workup_judged.json({drug: [{index, test, timing, condition?}]}, every offered drug keyed,[]= nothing, soapply_workup.undecidedandtest_workup.JudgedCoverageTestcatch a drug nobody read) ->python tools/sourcing/apply_workup.py --dry-run, then real (seven gates, sole writer ofworkup.json) ->python tools/fetch/stamp_fetch_dates.py->python tools/generate_data.py->python tools/sourcing/recheck_quotes.py build --kinds drug_workup --unjudged, judge,apply --llm opus->generate_data.pyagain. See docs/DRUGS.md (Pre-prescription workup).tools/fetch/fetch_agnp_tdm.py: uv script (pymupdf), offline, author-side. Corpus #19: reads the AGNP TDM guideline's Table 5 (therapeutic range, alert level, TDM level) and Table 1 (substrate isoforms) by cell rectangle offdata_sources/books/agnp_tdm_2026/a-2860-7861.pdf, gates each row quote onpages/<n>.md, writesgenerated_cache/agnp_tdm.jsonand the drug -> pages table of that book'sINDEX.md. No judge (code extraction);generate_data.pyreads the cache viadata_generators/tdm.py.
tools/fetch/fetch_prodrug_worklist.py— stdlib, offline. Offers every prodrug / inactive sentence of each drug's stored EN Wikipedia article intogenerated_cache/prodrug_worklist.json, with no verdict (--onlymerges). See docs/DRUGS.md Drugs (Prodrugs).tools/sourcing/apply_prodrug.py— stdlib. Mergesprodrug_judged.json(per drug{index, metabolite, drug_id|null}) intodrugs_data.jsonlbehind gates (drug offered, index resolves, prodrug wording, metabolite named, quote verbatim on its page); sole writer ofprodrug/prodrug_sources, adds the metabolite when missing and drops the drug's own bindings (printed). Writes nollmstamp, so runrecheck_quotes.py build --kinds drug_prodrugafter it.tools/fetch/fetch_metabolite_bindings.py—uv run(deps: beautifulsoup4). Reusesfetch_wikipedia_pharmacologyas a library to mine each non-modeled active metabolite's own English Wikipedia article (an allow-list of confirmed own-articles; the rest fall back to the parent article for prose only, filtered to sentences naming the metabolite) intometabolite_bindings_worklist.json(Ki-table rows + candidate action sentences) for the LLM pass. See docs/DRUGS.md Drugs.tools/sourcing/apply_metabolite_bindings.py—uv run(deps: beautifulsoup4). Merges the LLM-extracted metabolite bindings (metabolite_bindings_judged.json) intodrugs_data.jsonl: quote-gates each action sentence, resolves the target, attaches a Ki (PDSP measured preferred, else the Wikipedia table value), and adds every remaining Ki-table target asaffinity_only. Sole writer of a metabolite'sbindings;--only <parent drug ids>scopes (use it: a full rerun clobbers later direction / Ki writes). See docs/DRUGS.md Drugs.
tools/fetch/fetch_binding_directions.py:uv run(deps: beautifulsoup4, only because it importsfetch_metabolite_bindings; no HTML is parsed). Offline. The binding-direction worklist builder: for every drug with anaffinity_onlybinding it reads the stored English Wikipedia article (lib.corpora.wikipedia.page_for, which owns the redirect aliases) and offers the prose lines naming one of that drug's affinity-only targets, with no verdict of its own, intotools/generated_cache/binding_directions_worklist.json.--only <ids>scopes. See docs/DRUGS.md Drugs (Binding directions).tools/fetch/fetch_binding_direction_tables.py:uv run(deps: beautifulsoup4). Offline once the articles are stored. The other direction input, a code extraction: it reusesfetch_wikipedia_pharmacology's grid, header detection, target resolver and row text to read theActioncolumn beside the Ki one, and proposes a direction only where the cell maps exactly ontoDRUG_ACTIONS(Inhibitor/Blockertyped by the target, aND/ glyph / percentage / slash reading reported and left alone, two rows that disagree skipped), for a target the drug currently bindsaffinity_only, intotools/generated_cache/binding_directions_tables.json.--only <ids>scopes. See docs/DRUGS.md Drugs (Binding directions).tools/sourcing/apply_binding_directions.py: stdlib. Quote-gates the judged directions (binding_directions_judged.json, a candidate index per row, never a quote string) and is the sole writer of anactiononto anaffinity_onlybinding indrugs_data.jsonl. Six gates: the drug was offered; the index resolves; the target is a currently affinity-only binding of that drug (confirm-only, so nothing is added or revised); the action is inDRUG_ACTIONS; the quote names the target (tools/lib/target_aliases.py, the matcher the fetcher offered it with); the quote is verbatim ondata_sources/wikipedia/pages/<slug>.md. Writes nollmstamp (recheck_quotes.py's job). Idempotent,--dry-run/--verbose.--tablesapplies the table proposals instead (binding_directions_tables.json): gates 3, 4 and 6 are the same sharedBindingGate, there is no index to resolve, and gate 5 becomes "the row opens with its target cell and that cell resolves to the target claimed" under the fetcher's own resolver; the source is stampedextraction: "code", so this input needs norecheck_quotes.pypass. Needs beautifulsoup4 on the path for that resolver.--searchapplies the UNKNOWN search's picks (binding_directions_search_judged.json, index per site intobinding_directions_search_worklist.json) through the sameBindingGate, the citation carrying the candidate's own corpus (EN/FR Wikipedia or a Europe PMC abstract) and page; a metabolite pick is written once, on itsmetabolites_data.jsonlrecord.--unknown-log(run after the recheck) writestools/generated_cache/unknown_searches.json: every searched binding stillaffinity_onlywhose search found no direction anywhere, with its per-corpus search record (see docs/PROVENANCE.md, UNKNOWN). See docs/DRUGS.md Drugs (Binding directions).- The UNKNOWN search, end to end (binding directions; the recipe for any kind in
unknown.UNKNOWN_REQUIRED_SOURCES):python tools/fetch/fetch_europepmc.py --binding-search(network, resumable, ledgertools/generated_cache/europepmc_binding_search.json, top 5 abstracts per binding stored as corpus #17 pages) ->uv run --with beautifulsoup4 tools/fetch/fetch_binding_directions.py --search(offline, builds the worklist over PDSP, GtoPdb, EN + FR Wikipedia and those abstracts, recording what each corpus was asked and returned) -> one LLM pick by index ->apply_binding_directions.py --search->recheck_quotes.py build --kinds drug_bindings,drug_metabolite_bindings --unjudged+ an independent judge +apply --llm <model>->apply_binding_directions.py --unknown-log->generate_data.py. tools/lib/target_aliases.py: stdlib library, imported by both halves above: every name a modeled drug target goes by in prose (Greek folded to Latin, separator-tolerant, boundary-strict, sod2never fires onCYP2D6andalpha2never absorbsalpha2A). One definition, because a gate that disagreed with the offer would accept what the worklist could not have shown.
- The binding-gap pass (a new binding a drug's EN Wikipedia article states, corpus #9), end to end:
uv run tools/fetch/fetch_binding_gaps.py(offline,--only <ids>; writestools/generated_cache/binding_gaps_worklist.json, drugbox target cells + direction-stating prose sentences naming an uncovered target) -> one LLM pass deciding every candidate by index intobinding_gaps_judged.json({drug: [{index, target, pick, action | reason}]},_-prefixed keys are metadata) ->python tools/sourcing/apply_binding_gaps.py --dry-run --verbosethen without--dry-run(stdlib, sole writer,--verboselists the additions on Stahl drugs, whose own bindings stay untouched) ->generate_data.py->recheck_quotes.py build --kinds drug_bindings --unjudged+ judge +apply --llm <model>->generate_data.py.tools/lib/binding_gaps.py(stdlib library) holds the predicates both halves run (negation per clause, stereo relatives, covered targets, Stahl detection). See docs/DRUGS.md Drugs (Binding gaps).
tools/fetch/pdf_to_pages.py— splits a PDF into one<page>.mdper page (the quote-gate text);uv run,--layoutfor OCR.tools/fetch/build_toc_index.py—INDEX.mdfrom a PDF's embedded TOC (generic).uv run.tools/fetch/build_index.py— Stahl-specific page index (byTHERAPEUTICSheading).uv run.
tools/update_readme_stats.py— rewrites the READMESOURCING_STATS+SOURCES_TABLEblocks (and the headline %) frommeta;--checkexits 1 if stale (CI). Idempotent.tools/fetch/fetch_molecules.py— downloads each drug's molecule SVG intopublic/data/molecules/; writestools/generated_cache/molecules_sources.json. See docs/VIEWER.md Images.tools/fetch/fetch_structure_images.py— resolves the url of each structure's (and wiki-linked circuit's) Wikipedia hero + gallery images intotools/{structure,circuit}_images_sources.json(--target structures|circuits|all); downloads no bytes. See docs/VIEWER.md Images.tools/generated_cache/{molecules,structure_images,circuit_images}_sources.json— provenance/attribution for the fetch tools (the image ones are read bygenerate_data.pyoffline; not served).tools/git-hooks/— repo-tracked git hooks (see CLAUDE.md Git hooks).
The field list of each emitted file. The viewer reads exactly these shapes; the generator
emits them. Every claim carries its own quote-level source (see docs/PROVENANCE.md);
there is no node-level catch-all sources block.
Source quotes are externalized. A quote-bearing source is emitted on the node as a bare
{quote_id, provenance}reference; its immutable excerpt{id, corpus, page, quote, species?}lives once in the deduplicatedquotes.jsonlside table (keyed by a content hash, so identical excerpts share one entry). The viewer (js/data.js) andcheck_data.pyboth rehydrate each reference in memory (merging the excerpt back onto the source) at load, so every shape below that showssources[{corpus,page,quote,...}]is the rehydrated view; on disk it is{quote_id, provenance}. Ki sources (noquote) and bare wikipedia provenance are not externalized. See the serialization pass indata_generators/quote_table.py(mirrors the i18n externalize pass).
Emitted data is English-only. Display strings marked
{en,fr}below are authored bilingual but serialized as the plain English string; the French is deduplicated into one side table,translations.fr.json({english: french}, sorted keys), which the viewer fetches only in French and looks each English string up in (see docs/I18N.md, the externalize pass). So on disk everyname{en,fr}etc. is just a string.
meta.json— presentation maps + tallies, so the dataset is self-describing (a port needs no hardcoded palette):projection_colors,kind_labels,group_labels,kind_signs,sign_colors,sign_labels,system_flow_kinds(drug target system -> projection kind), the receptor maps (receptor_family_labelskey order = legend family order,receptor_family_colors= the same families' swatch colours, reusingprojection_colorswhere the family has a modeled pathway, so a view colouring by transmitter (the simulation's receptor bars) matches the 3D scene;receptor_class_labels,synaptic_labels), the drug maps (drug_category_labelskey order = Drugs legend order,drug_actionsaction->{label,effect},drug_effect_colors,drug_effect_labels,drug_targets= every non-receptor target + every receptor id; a target with a direction-flippingvesicular/sign/synapticflag also carriespolarity_provenance(+ optionalpolarity_sources), its own graded node kindtarget_polarity; areceptor_grouptarget also carriessubtypes(its modeled subtype receptor ids, a sourceless taxonomy the viewer lists as per-subtype drug dropdowns); a target with an Allen profile carries the samedensityobject as a receptor; a non-receptor target also carriesgene, the representative HGNC symbol, like a receptor's),enzymes(metabolic isoform id -> {label, wikipedia}),enzyme_roles(role -> {label,direction: what it does to a co-prescribed substrate's level}) andenzyme_strengths+enzyme_reactions(the chemical step by which an enzyme makes an active metabolite; see docs/DRUGS.md Drug metabolism),target_type_labels/target_type_colors,source_corpora,uncertainty_reasons(the closed vocabulary a node'suncertainty[]bullets draw from, each{source, absence, args}),addon_slots(the addon-node hook registry, slot -> the node kind whose panel it is in) +addon_sections(the browse views an addon may anchor instead of a node, key -> label) +addon_displays+addon_tones(tone -> glyph),density_min_reliability(the cross-donor r floor every published profile clears),provenance_stats(the sourcing tally; see docs/PROVENANCE.md).translations.fr.json— the deduplicated French side table,{english: french}with sorted keys, covering every emitted display string whose French differs from its English. Fetched by the viewer only in French (English users skip it); a missing key falls back to the English string. Written last bywrite_artifactsfrom the externalize pass (see docs/I18N.md).quotes.jsonl— the deduplicated source-quote side table, one quote node per line sorted byid(aq_<12 hex>content hash):id,corpus,page,quote, optionalspecies, optionalllm(the model that extracted+judged the quote,haiku/sonnet/opus; absent = unknown), optionalheading(the trail of book headings the passage sits under, outermost first, derived byfetch/fetch_quote_headers.py; only the levels it could resolve are present). Every node's quote-bearing source references one byquote_id; the viewer +check_data.pyrehydrate it at load (see the externalize note above).changelog.json— the release notes,{versions: [{version, date, entries[{category, text{en,fr}, commits[sha]}]}]}, newest version first (the orderjs/changelog.jswalks to show every release since a visitor's last one). Not a node kind: it is editorial prose about the app, carries no provenance grade, and is absent from the sourcing tally. Compiled from the authoreddocs/changelog/<version>/changelog.mdfiles (see Writing release notes below).structures.jsonl—id,name{en,fr},base_name{en,fr}(hemisphere-stripped, legend row),group,position,color,shape_file,classification_provenance, optionalwikipedia(+_provenance), optionalstructure_image(hot-linked Wikimedia url, shared by both hemispheres) +structure_image_gallery.projections.jsonl—from,to,kind,label{en,fr},neurotransmitter{en,fr},description{en,fr}, optionalsources[{corpus,page,quote,provenance}](fromKANDEL_QUOTES),bidirectional,tentative(dotted, off-by-default section).mirror: truemarks a symmetric pathway stored once (the right-hemisphere record): the consumer reflects it by flipping_R<->_Lon both endpoints (js/data.jsat load,check_data.pybefore its checks), so the file carries no per-side duplicate. Set from thesymmetricauthoring hint (see Projections above).circuits.jsonl—id,name{en,fr},structures[ids](arrows derived in the viewer), optionaldescription{en,fr}+sources+wikipedia(+prov) +structure_image(+ gallery); same shape + rendering as a structure's.projection_groups.jsonl— a legend pathway row promoted to a sourced structure so it opens a panel:id(<mode>_<key>),mode(kind|sign),key,name{en,fr},description{en,fr},classification_provenance, optionalwikipedia(+prov) +sources. One record per group in BOTH colour modes (7 per-transmitter + 3 per-sign); member pathways derived in the viewer.receptors.jsonl—id,name,family,neurotransmitter{en,fr},receptor_class(ionotropic/metabotropic/chaperone),sign(excit/inhib/modulatory),synaptic(pre/post/both),locations(structure base ids, both hemispheres), optionalubiquitous:true,classification({family,receptor_class,sign,synaptic}-> each a{grade, sources?}sub-claim, so a quote grades only the attributes it substantiates), optionallocation_sources({base:[quote-source]}, sparse per-region upgrade abovellm;"ALL"= the ubiquitous claim), optionaldensity({reliability, donors, profile:{base:z} sorted strongest first, grade, sources}, ONE graded node for the whole Allen z-score profile, see docs/VIEWER.md Expression density), optionaldescription{en,fr}+wikipedia(+prov), optionalgene(the HGNC symbol, an identifier for the gene-keyed lookup links: NOT a graded node; absent only for a stub with no human gene). Empty locations + no description = a deliberate stub (listed, not focusable).drugs.jsonl—id,name,categories,category_provenance(+ optionalcategory_sources), optionalnbn{en,fr}(+nbn_sources, +nbn_nonstandard:truewhen the value is Stahl's class descriptor not a formal NbN),bindings[](each:target,action, optionaleffect/note{en,fr}/tentative/sources[{corpus,page,quote,provenance}]/ki(measured PDSP affinity)/affinity_only:true(Ki but no known direction, panel-only)/uncertainty[](each:kind(ameta.uncertainty_reasonskey), optionalargs(the i18n sentence's slots), and eithersources[]orabsence:true; non-empty = the orange ⚠ badge takes the pill over, see docs/PROVENANCE.md)), optionalhalf_life({hours, hours_max?}, elimination T½) +half_life_sources[], optionaltmax(same shape, time to peak plasma concentration, kinddrug_tmax) +tmax_sources[], optionalformulations[]({release, release_sources, brands?, tmax?, half_life?}, kinddrug_formulations, written only bytools/sourcing/apply_formulations.pyaftertools/fetch/fetch_formulation_worklist.py+ one index-pick pass; re-run it after any brand / Tmax / T½ applier, see docs/DRUGS.md "Formulations"), optionalfirst_approval({year, country?, agency?}, kinddrug_first_approval) +first_approval_sources[], optionalwithdrawals[]({year, country, reason, sources[]}, kinddrug_withdrawals; ids fromdata_generators/history.py, written byapply_drug_history.py), optionalholders[]({market, names[], sources[]}, kinddrug_holders, written byapply_drug_holders.py), optionaloriginator({name, now?}, kinddrug_originator) +originator_sources[]+originator_now_sources[]andoriginator_country+originator_country_sources[](kinddrug_originator_country; written byapply_originator.py), optionaltherapeutic_range+alert_level(+ their*_sources[], kindsdrug_therapeutic_range/drug_alert_level, emitted fromgenerated_cache/agnp_tdm.json, never authored), optionalprodrug: true+prodrug_sources[](kinddrug_prodrug, written byapply_prodrug.py; a prodrug carries nobindings, its metabolite does), optionalconstituents[](names) +constituents_sources[]for a salt with an INN of its own (kinddrug_constituents, hand-authored; nobindings, the viewer borrows its modeled constituents'), optionalenzymes[](each:enzyme(ameta.enzymeskey),role, optionalstrength,sources[]; one node per (enzyme, role) pair, kinddrug_enzymes, merged in from the committedgenerated_cache/drug_enzymes.json+drug_enzymes_wikipedia.json, never authored),metabolites[](each a reference{id, sources[]}intometabolites.jsonl, the sources being " is an active metabolite of this drug"; optionalformed_by[](each:enzyme, optionalreaction(ameta.enzyme_reactionskey),sources[]), one node per (parent, metabolite, enzyme), kinddrug_metabolite_enzyme, hand-curated indata_generators/quotes/metabolism.py), optionalwikipedia(+prov), optionalstructure_image(vendoreddata/molecules/<id>.svg, only when the file exists),focusable. No drug-level source: provenance is per-claim (see docs/PROVENANCE.md).metabolites.jsonl: one record per active-metabolite molecule (authored intools/data/metabolites_data.jsonl, written by the PK / prodrug / metabolite-bindings appliers viadrugs_io.attach_metabolite):id,name, optionaldrug_idlinking a modeled drug, optionalhalf_life(+half_life_sources), optionalbindings[]for a non-modeled metabolite (same shape as a drug binding, kinddrug_metabolite_bindings, counted once however many drugs reference the record).addons.jsonl— addon nodes: a sourced annotation that carries its own insertion point.id,owner_kind(drug/receptor/target/structure/section),owner(that node's id; a structure anchors its hemisphere-less base, asectionanchors ameta.addon_sectionskey rather than a node),slot(ameta.addon_slotskey),display(ameta.addon_displaysentry),tone(ameta.addon_toneskey), optionaltitle{en,fr},text{en,fr}(the claim),sources[{corpus,page,quote,provenance}]. Tally kindaddons. Authored indata_generators/addons.py; the vocabularies are closed, and both the generator andcheck_data.pyreject an unknown slot / display / tone or an anchor that names no real node (an addon in a slot nothing draws would ship silently invisible).molecules/<id>.svg— vendored per-drug structure diagrams (fetch_molecules.py). Structure illustrations are NOT vendored (hot-linked, see docs/VIEWER.md Images).
Geometry (data/shapes/<name>.json): one file per distinct form. L/R pairs share a single
right-side file; the left member sets mirror:true on its structure record and the viewer
reflects it across x. Three types:
blob{radii, seed, detail, noise, + optional octaves/ridged/frequency/aniso/ clip/clip_planes}— a gradient-noise-deformed ellipsoid.curve{points, profile, seed, noise, radial/tubular_segments}— a round-capped tapered tube swept along a spline (caudate; brainstem levels midbrain/pons/medulla).composite{parts:[...]}— sub-shapes (each optional offset/scale/rotate) merged into one mesh (cerebellum = 2 hemispheres + vermis). Thesdftype (SDF atlas, undergeometry_refinements/) is documented there; see CLAUDE.md's geometry note.