Both are built on the DataHub Agent Context Kit and can run over either transport. Neither has a language model in its decision path, and that is a choice rather than an omission: the answers are traversals and measurements, so they are reproducible, and a person who disagrees with one can check it rather than argue with it.
Root cause is the glamorous half and the wrong half to reach for first. By the time anyone runs it the model is already serving decisions off the thing nobody meant to add. The cheap moment is the pull request, when the change is still a diff and the only cost of knowing is a comment.
python tools/blast.py analytics_marts.dim_person --column public_coverage_flag --policy eu_ai_act
== blast radius of a change to analytics_marts.dim_person.public_coverage_flag ==
read through kit, under Regulation (EU) 2024/1689, Annex III high risk systems
examine in scope because it carries personal_data, and EU AI Act watches that tag.
under Article 10(2)(f), Article 10(2)(g), Article 9, Annex IV
reaches 1 deployed model
workforce-classifier_3 mlflow_production, 5 hops
and 1 registered but not serving
workforce-classifier_1 mlflow_archived
through 2 downstream tables
1 hop [dbt] workforce_features carrying public_coverage_flag
2 hop [postgres] workforce_features carrying public_coverage_flag
and 8 training runs that consumed it
The split between deployed and registered but not serving is the part that makes this readable. A change reaching forty registered models and no live one is a different change from one reaching a single live model, and a plain downstream count cannot tell them apart.
python tools/blast.py analytics_marts.dim_person --column public_coverage_flag --policy eu_ai_act --comment
**Blast radius: `analytics_marts.dim_person.public_coverage_flag`**
> **examine**: in scope because it carries personal_data, and EU AI Act
> watches that tag. Under Article 10(2)(f), Article 10(2)(g), Article 9, Annex IV.
Reaches **1 deployed model**:
| model | stage | hops |
| --- | --- | --- |
| `workforce-classifier_3` | mlflow_production | 5 |
<details><summary>2 downstream tables</summary>
...
</details>
<sub>Ariadne, from DataHub lineage, under Regulation (EU) 2024/1689.</sub>
It exits non zero only when a prohibited column reaches something that is serving. A change that merely touches a live model is a thing a person should read, not a build failure, and a check that blocks on everything gets switched off within a week.
Every ML observability tool can tell you a model changed. That is the easy half and it is where they stop, because a distribution plot has no way to reach the pull request that caused it. Ariadne holds both ends: the measurement knows the number moved and by how much against its own noise, and the graph knows which columns the deployed model gained between those two moments and where each came from.
python tools/rootcause.py --model workforce-classifier --policy eu_ai_act --via mcp
== why did workforce-classifier move ==
read through mcp, under Regulation (EU) 2024/1689, Annex III high risk systems
comparing version 2 (2026-07-27T13:30:31) with version 3 (2026-07-28T08:34:06)
accuracy went up, 0.6955 to 0.7310 (+0.0355)
so nothing that watches model quality had a reason to object
1 measurement moved beyond noise
disability (With a disability against Without a disability) 0.7489 to 0.7883 +0.0394, 12x the noise
the deployed model gained 1 column between those two moments
public_coverage_flag
examine, in scope by tag, Article 10(2)(f), Article 10(2)(g), Article 9, Annex IV
enters at postgres:raw_person, 7 hops back
called pubcov where it enters, so a search by name would not have found it
defined in models/marts/dim_person.sql
defined in models/marts/workforce_features.sql
defined in models/staging/sources.yml
defined in models/staging/stg_person.sql
cause: public_coverage_flag
It starts from the measurement, not from the diff. Plenty of columns get added that change nothing, and reporting them all would be a changelog rather than a cause. And it will not rank causes by plausibility when it cannot tell them apart: if two columns arrived together it names both and says the graph cannot choose. A confident single answer would read better and would be invented.
public_coverage_flag from
staging onward and pubcov in the raw extract. The filter dropped the
origin and left a shorter answer that read as complete: five hops to
stg_person instead of seven to raw_person. DataHub had
already scoped the walk, so the name check could only lose information.
Both are written into the skill, so an agent following it gets the answer rather than the plausible version of it.
DataHub's analytics-agent is a full application with a backend, a frontend and a helm chart, and embedding it here would have been theatre. Its own docs call it a reference implementation, so the useful thing to take was the shape rather than the code: a context platform abstraction in front of each way of reaching a catalog, built by a factory rather than chosen at the call site.
The part worth copying most was the smallest. It discovers what tools a platform offers instead of assuming, so asking for something a deployment does not have fails at the point of asking with the real list. Against this instance the MCP server advertises six tools and keeps three groups switched off, so the difference is not hypothetical.
| analytics-agent | ariadne | same idea |
|---|---|---|
ContextPlatform | Context | one interface, many transports |
MCPContextPlatform | MCPContext | stdio child process |
native_datahub | KitContext | in process SDK |
build_platform() | open_context() | factory, not a call site choice |
tools via tools/list | tools via tools/list | discovered, never hardcoded |