Skip to content
Plate VIIMethod

One rule, enforced
in three places.

Audit last run

26 Sept 2026

Psychedelia will not display a quantity that cannot say where it came from. That is not an editorial aspiration — it is enforced by the type system, by the query layer, and by an audit that runs against the upstream registries and publishes its own failures. This section documents all three, and what each of them misses.

I

The constraint

Every value shown must be traceable to a record a reader can open, or to a computation this codebase performs and names. If a number cannot be sourced, it does not go on the page.

The second sentence is the one that costs something. Removing a feature is the correct outcome when its data cannot be sourced, and two upstream tables have been removed on exactly that basis — they are named, with their numbers, on the sources page.

II

How the site operates

Six upstream sources, one archive, and one direction of travel.

Nothing here is computed on demand from a live API. Data is harvested by the programs in tools/, each measurement is judged as it arrives and stored with the verdict that let it in, and the site reads only what the archive already holds. That is what makes a page reproducible: two readers a week apart see the same numbers, and the date the archive was last refreshed is printed on every plate.

How the site operates: upstream sources are harvested into a PostgreSQL archive, an identity gate filters it inside the SQL, and pages render only sourced values. The application never writes back to the archive.UPSTREAMHARVESTARCHIVEGATEPAGESourcesPubChemChEMBLPDSP Kᵢ databaseHuman Protein AtlasEMBL-EBI AnatomogramClinicalTrials.govtools/*.pyfetch and convertchembl_source.decide()judges every record;one write path, andthe verdict is storedPostgreSQLcompoundsactivitiesassays+ data/*.jsonharvested beside itdisplayableIds()applied INSIDE theSQL, never afterquarantined rowscannot reach aCOUNT or an AVGServer components<Value source=… /><Absent />a value with nosource will nottype-checkthe application never writes to the archiveA correction is a proposal shown on the integrity report, applied at the source by a person.

The crossed arrow is the load-bearing part. The application has no write path to the archive at all — not a disabled one, not a guarded one. A correction found by a reader or by the audit becomes a proposal on the integrity report, with its evidence, and a person applies it at the source. An archive its own front end can edit is one whose history cannot be trusted.

III

Where it is enforced

Three mechanisms, none of which relies on anyone remembering the rule.

01

An identity audit gates the archive

tools/verify_compounds.py → data/verification.json → src/lib/verification.ts

Every compound record is re-checked against the PubChem record it cites and the ChEMBL record it cross-references before anything is displayed. The gate is applied inside the SQL, not after it, so a withheld record cannot contribute to a count, an average or a target’s compound list. Of 545 records examined, 545 are verified, 0 are shown as unconfirmed, and 0 are withheld entirely — taking 0 measurements out of view with them.

The integrity report →
02

Provenance is typography

src/components/Provenance.tsx

Quantities render through a <Value> component that will not compile without a source, and each carries a superscript mark linking to the record it came from. An absent measurement renders as an explicit absence — never a zero, never a dash that could be read as a value, never a carried-forward figure.

03

The failures are a page, not a changelog

src/app/documentation/integrity

The audit publishes what each check does and what each check misses, every withheld record with the collision that caused it, and the corrections that were proposed but deliberately not applied. A reader cannot judge how far to trust a number without knowing what the archive gets wrong and how often.

What failed, and why →

IV

How to read a value

Four marks, the same meaning on every page.

P
PubChem
C
ChEMBL
R
Computed here with RDKit
D
Derived here from the stored measurement

Measured and computed are never mixed. A molecular weight is computed; a binding constant is measured. Both are shown, and the mark tells you which you are looking at.

pX is derived, not stored. Every pX is the negative base-10 logarithm of the value in molar, −log₁₀(M). Values are stored in nanomolar, so it is computed as 9 − log₁₀(value in nM) from the normalized measurement, applied uniformly. It is the quantity ChEMBL publishes as pChEMBL, and a ChEMBL record whose converted pX differs from its own pChEMBL by more than 0.006 is quarantined. The database also holds a partially populated pX column; it is not read, because an axis that is sometimes one quantity and sometimes another is not an axis.

Binding constants and functional readouts are kept apart. Ki and Kd answer a different question from IC₅₀, EC₅₀, AC₅₀ and potency, so they are never pooled in a chart, a ranking or an average.

No dose, route, or safety information appears here, anywhere. The archive holds no sourced data on any of them, so there is nothing this site could say that would not be invented.

V

In this section

VI

Reproducing the audit

The audit is not run on every start — it makes live requests to PubChem and ChEMBL. The site reads whatever the file last said and prints its date on every page, so a stale audit is visible rather than silent. To refresh it:

./run -v

Generated by tools/verify_compounds.py using RDKit 2025.09.2 on Python 3.9.6. Authorities: PubChem, MeSH Pharmacological Classification, ChEMBL.