Sola dosis facit venenum · the dose makes the poison

Paracelsus

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

A wall of red is the same as no alerts at all

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

Four factors, multiplied, every one of them sourced

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

H
Hazard. How bad this class of break is in principle, scaled by dose — the share of the asset's columns the defect actually touches. A change to two columns of nine is a different dose from seven of twenty-two, even when the defect class is identical. source · schema comparison against a recorded baseline
E
Exposure. Not how many things sit downstream, but how much they are actually used. Each successor contributes according to its own query volume over the last thirty days, decaying with each additional hop. A successor nobody queries contributes almost nothing, no matter how many further things hang off it. source · lineage traversal + usage statistics
R
Receptor sensitivity. What kind of consumer is on the other end. A production model or a customer-facing report is a sensitive receptor; a scratch table is not. source · tags, domains, platform of the consuming asset
UF
Uncertainty. The counterintuitive one. Where evidence is missing, the score goes up. No column-level lineage adds 3. No usage data at all adds 3, because unused and unmeasured are not the same thing and we cannot tell them apart. Stale metadata adds 2. source · absence of metadata, not presence
Why uncertainty adds rather than multiplies. In safety assessment these factors normally compound, because each one covers a genuinely different gap in the evidence. Here they do not: all four are the same gap seen four ways — we cannot see what consumes this asset. Multiplying them would count the same ignorance four times over, and would swamp hazard and exposure entirely, turning the score back into a ranking of what we do not know. They are added, and capped at ten.

The argument, in one row pair

Same table, same break, three times the risk

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

Hhazard × dose 2/91.11
Esuccessors actively queried5.34
Rinternal dashboard2.5
UFno column lineage3.0

44.47

Monitor

naive 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

Hhazard × dose 2/91.11
E24 successors, almost none used0.89
Rinternal dashboard2.5
UFno lineage and no usage data6.0

14.83

No Action

naive 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.

0 · No Action22.5 · Monitor67.5 · Action135 · Critical

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

Twenty-two of twenty-seven findings score exactly zero

Run against DataHub's showcase-ecommerce catalog, the naive detector raises alerts on twenty-six assets. Paracelsus scores twenty-seven and returns this:

AssetPlatformNaiveRealRiskBand
ADDRESSESsnowflake#15#144.47Monitor
CUSTOMERSsnowflake#17#236.92Monitor
customersdbt#1#323.70Monitor
addressespostgres#2#414.83No Action
ORDER_DETAILSsnowflake#14#57.35No Action
22 further assets#6–270.00No 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

The judgment goes back into the graph

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:

Every write is read back and verified, because the API can report success on a write that silently did nothing.

DetectExposeAssessExplainWrite back

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

One machine, one catalog, no account anywhere

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.

  1. make preflightChecks Docker, Python 3.11, the DataHub CLI and uv. Names anything missing instead of failing later with an unrelated error.
  2. make venvCreates the project's own environment.
  3. make upStarts DataHub with token auth on, loads the showcase catalog, imports usage statistics, mints an access token.
  4. make statusVerifies the catalog actually finished indexing and the metadata the model needs is readable.
  5. make riskDetect, expose, assess. Produces the assessment for every finding.
  6. make writeWrites properties, tags and assessments back into DataHub, then reads them back to confirm.
  7. make boardServes the triage board: naive ranking against real ranking, with the factor breakdown behind every score.
Running 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

What is measured, and what is staged

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.

Known limits

Why it is worth having

What changes on Monday

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.