ARIADNE
lineage-grounded root cause for production ML

Which data change
moved the model?

Model monitoring tells you a model's behaviour changed. It cannot tell you why, because it has never been told your warehouse exists. Ariadne answers the next question: which column, which file, which commit, how many hops away. It stands on the DataHub lineage graph and writes what it finds back into DataHub as an incident.

The metric said ship it. The graph said look again.

+0.0355model accuracy, up
+0.0394disability recoverable
12×the measured noise
0other alarms fired
what happened

One column, added for a good reason, by someone who was right to add it

Benefits administration asked for a coverage flag so onboarding could skip a prompt for people who already had cover. One line of dbt. No health detail, just a flag. The model retrained and accuracy improved by three and a half points, so every dashboard, every model monitor and every approval gate read the change as an improvement.

What also went up was how much the model could tell about who is disabled, by 12 times the measured noise. Nothing drifted. Nothing degraded. There was no distribution shift to detect, because a column arrived. The only artefact in the building that could see both halves at once was the lineage graph.

before the change lands

Blast radius, at the pull request

Root cause is the wrong half to reach for first. By the time you run 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.

It separates what is deployed from what is merely registered, because a change reaching forty archived models and no live one is a different change entirely.

See both agents
python tools/blast.py analytics_marts.dim_person \
    --column public_coverage_flag --policy eu_ai_act
  examine      in scope because it carries personal_data

  reaches 1 deployed model
    workforce-classifier_3     mlflow_production, 5 hops

  and 1 registered but not serving
    workforce-classifier_1     mlflow_archived
after it did

Root cause, in one sentence with a file in it

It starts from the measurement rather than the diff, because plenty of columns get added that change nothing. Then it walks the graph back to where the column entered.

Note the last line. The column is called something else where it enters, so nobody searching the warehouse by name would ever have found it.

Watch it run
python tools/rootcause.py --model workforce-classifier --via mcp
  accuracy went up, 0.6955 to 0.7310 (+0.0355)
  so nothing that watches model quality had a reason to object

    public_coverage_flag
      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/workforce_features.sql

  cause: public_coverage_flag
where the finding goes

Back into the catalog, not into a report

A governance tool that prints to a terminal gets run once. Every finding is written back into DataHub as an incident on the table it concerns, so it sits next to that table in the catalog the data team already opens.

They come back on their own, too. A later lineage walk sees them as a health signal without being told they exist.

GET /dataset/.../Incidents
workforce_features    6 active
  HIGH      disability became easier to rebuild
            from workforce-classifier
  CRITICAL  age reaches workforce-classifier_3
  LOW       marital_status_code reaches ...

income_features       12 active
  HIGH      race (Black alone) became easier ...
  CRITICAL  race_code reaches income-classifier_3
if you own the risk rather than the pipeline

The same warehouse, judged four ways

Which columns count as restricted is declared in a policy pack, not written into the checks. The same graph and the same tags reach different verdicts depending only on which statute is named, which is what makes this portable to a warehouse governed by a rule nobody here has read.

employment_us

Title VII, the ADA and the ADEA. Watches columns tagged as protected attributes.

coverage flag: nothing to report

ecoa

Fair lending. Watches the protected bases, with prohibited and conditional tiers.

coverage flag: nothing to report

canada

Canadian Human Rights Act section 3, for federally regulated banks, telecoms and transport. No age carve out, so it is stricter than ECOA on the same column.

coverage flag: nothing to report

eu_ai_act

Regulation (EU) 2024/1689, Annex III. Asks for examination of bias affecting fundamental rights, so it watches personal data too.

coverage flag: examine, Article 10(2)(f)

Two of the three are correct to stay quiet: insurance coverage genuinely is not a protected characteristic in US employment law. The difference between silence and a finding is a line of YAML, not a code change.

How the policy packs work How a proxy gets proven
if you build the platform

Every surface, wired and answering

Every claim Ariadne makes is a claim about the DataHub graph, read through DataHub's own agent surfaces and written back through DataHub's own APIs. Nothing in the lineage was hand emitted, because a graph you wrote yourself proves nothing.

context graph

Lineage

Column level, back to the raw extract and forward to whatever is serving.

7 hops, 3 connectors
mcp server

MCP

The official server as a child process over stdio, tools discovered at the handshake.

6 tools discovered
agent context kit

Agents

Both agents run on the kit, over either transport, and the two are checked against each other.

3 of 3 walks agree
datahub skills

Skill

An ML impact skill in the registry format, covering what the existing five do not.

4 failure modes encoded
incidents api

Writeback

Findings filed on the entity they concern, and read back through the API.

18 raised, two regimes
everything

All eight

The full table, including the Analytics Agent pattern and what each one returned.

8 surfaces
back to the project

Four things that were not in the documentation

Each one fails silently, which is why they were worth writing up rather than working around quietly. A loud failure teaches you something the first time. These teach you nothing until much later.

whatwhy it matterswhere
The self-hosted MCP server stalls before answering initialize Its usage ping is sent before the handshake, so on a restricted host the client sees no error and no response at all datahub#18684
Incidents accept seven entity types, and no ML entity The docs said "dataset, dashboard, chart, dataFlow, etc" and that "etc" is not open ended datahub#18685
An ML impact skill for the registry The existing five do not cover whether a warehouse change reaches something that is serving datahub-skills#66
The Agent Context Kit cannot import its own registration module It fails against the dependency version it pins, so the LangChain path breaks on a clean install datahub#18686

Start with the demo. Five steps, and every figure in it was recorded.

Real data throughout: 378,817 US Census records for California 2018, pulled live through folktables. Two models registered in MLflow and promoted to Production. The failure mode is native to the dataset, not injected for a demonstration.