Sola dosis facit venenum · the dose makes the poison
Risk assessment for a data catalog. Most data-quality tools rank problems by how broken something is. This one ranks them by how much harm can actually reach someone — and treats missing evidence as a reason to worry more, not less.
The problem
Point a modern data-quality tool at a catalog and it will find hundreds of things wrong. It ranks them by severity multiplied by the number of things downstream — often called blast radius. Everything important-looking floats to the top, all of it marked critical, all of it equally loud.
Teams stop reading. That is not a side effect of the design; it is the design. A ranking where the top forty items are indistinguishable has not prioritised anything.
The missing distinction is old and well understood outside software. Hazard is not risk. A hazard is what a thing can do in principle. Risk is what it can do to whoever is actually exposed to it, in the amount they are actually exposed to. Toxicology has separated these two since the sixteenth century, which is where this project takes its name and its method.
The method
Paracelsus scores each finding the way a regulatory risk assessment scores an exposure. The score is computed in plain Python, deterministically. No number in the output comes from a language model.
Risk=Hazard×Exposure×Receptor×Uncertainty
how far below the action threshold = threshold ÷ risk
The argument, in one row pair
Two assets called addresses on two platforms. Both had two of nine columns change. Identical hazard, identical dose, identical receptor. Only exposure and uncertainty differ.
ADDRESSES
snowflake
44.47
Monitornaive rank #15 → real rank #1
1.5× below the action threshold
Watch, no ticket needed yet: a null spike affecting 2 of 9 columns, with measurable but moderate real exposure (5.34). This verdict would change if column-level lineage into this asset appears.
addresses
postgres
14.83
No Actionnaive rank #2 → real rank #4
4.6× below the action threshold
No action needed today: a null spike affecting 2 of 9 columns, but real exposure (0.89) is too low to justify it. This verdict would change if column-level lineage into this asset appears, or usage data shows this asset is actually being queried.
The postgres table has twenty-four downstream assets. A blast-radius ranker puts it second in the entire catalog on that count alone. Almost none of those successors are queried, so its real exposure is 0.89 against the Snowflake table's 5.34.
And note which way uncertainty ran. The postgres table scored double the uncertainty surcharge, because we have neither column-level lineage nor any usage data for it. We raised its score twice for missing evidence — and it still does not need action, because nothing is drinking from it. That is the dose making the poison.
The action threshold is not a tuned number. It is derived from a written reference case — a confirmed defect of at least moderate class, touching a non-trivial share of an asset, reaching a sensitive receptor, with one uncertainty factor triggered — which computes to 67.5. Critical is twice that.
The whole catalog
Run against DataHub's showcase-ecommerce catalog, the naive detector raises alerts on twenty-six assets. Paracelsus scores twenty-seven and returns this:
| Asset | Platform | Naive | Real | Risk | Band |
|---|---|---|---|---|---|
| ADDRESSES | snowflake | #15 | #1 | 44.47 | Monitor |
| CUSTOMERS | snowflake | #17 | #2 | 36.92 | Monitor |
| customers | dbt | #1 | #3 | 23.70 | Monitor |
| addresses | postgres | #2 | #4 | 14.83 | No Action |
| ORDER_DETAILS | snowflake | #14 | #5 | 7.35 | No Action |
| 22 further assets | — | — | #6–27 | 0.00 | No Action |
The two assets a naive ranker puts first and second land fourth and third. The asset it ranks fifteenth is the one that matters most.
Those twenty-two zeros are the point, not a gap. Nothing has actually changed on them — they carry static schema properties that a hazard ranker reports as defects. The correct answer to a healthy catalog is silence, and silence is exactly what a severity ranker cannot produce.
What it does with the answer
An assessment that lives in a dashboard dies there. Paracelsus writes each result back into DataHub through its MCP server, so the next person and the next agent inherit the reasoning rather than the alarm:
paracelsus.risk, .hazard, .exposure, .receptor, .uncertaintyEvery write is read back and verified, because the API can report success on a write that silently did nothing.
The agent orchestrates the tool calls against DataHub. It is deliberately forbidden from producing any number — every figure comes from the deterministic engine, and the model only puts them into sentences.
Running it
The repository brings up its own DataHub, loads a real catalog, and runs end to end. There is no hosted instance to depend on and nothing to sign up for.
make preflightChecks Docker, Python 3.11, the DataHub CLI and uv. Names anything missing instead of failing later with an unrelated error.make venvCreates the project's own environment.make upStarts DataHub with token auth on, loads the showcase catalog, imports usage statistics, mints an access token.make statusVerifies the catalog actually finished indexing and the metadata the model needs is readable.make riskDetect, expose, assess. Produces the assessment for every finding.make writeWrites properties, tags and assessments back into DataHub, then reads them back to confirm.make boardServes the triage board: naive ranking against real ranking, with the factor breakdown behind every score.make risk twice produces byte-identical output. This was not free — an earlier version consumed its own baseline on each run, so a second run reported an empty catalog. The baseline now ships as a fixed artifact and is never overwritten.Honest boundaries
Real: the catalog, its lineage, its column-level lineage with confidence scores, its usage statistics, its tags and domains. Every exposure figure, receptor classification and uncertainty surcharge is computed from metadata that shipped with the catalog.
Staged: the schema changes themselves. A static demonstration catalog contains no incidents — nothing has broken, nothing has drifted. Five changes were planted across it so there is something to triage, on assets chosen by a rule written down before any score was computed, and disclosed in the repository, on the board and in the write-up.
The distinction that matters: what is staged is the defect. What separates the scores is not. The seed decides that something broke. The catalog decides whether it matters.
Why it is worth having
A data platform team opens their quality tool and sees forty red items. They read the first three, recognise two as known noise, and close the tab. Nothing about that improves by detecting more defects.
What changes it is a ranking that can say no with a reason. Paracelsus produces, for every downgraded finding, a sentence a person can check: this touches two of nine columns, its consumers have not been queried in thirty days, and we have no column-level lineage into it so we have tripled the surcharge anyway. That sentence is worth more than the alert it replaces, and it is why the ranking that says nothing needs attention today is a result rather than a failure.