Build with DataHub Hackathon, Judging in Progress
Janus, The Missing CI for ML Supply Chains

- Role
- Co-Architect, Evaluation, CI/CD & Documentation
- Timeline
- July – August 2026
- Duration
- 5-week hackathon (Devpost)
- Team
- Team: Ahmed Saad, Ghassen Naouar
Overview
Janus is a reliability agent for the boundary between a warehouse and the models trained on it, a boundary DataHub is the only catalog that holds both sides of but never joins. We built it over five weeks for "Build with DataHub: The Agent Hackathon," entered in the Production ML Agents category; judging is still open as of this writing. Janus reads DataHub's column-level lineage and its ML metadata, writes the join between them that no ingestion source produces on its own, and walks that join to catch what live monitoring structurally cannot see: a feature quietly leaking the label it's supposed to predict, a model still serving on a table that stopped refreshing, a schema that drifted between training and now. Every finding is written back into DataHub itself, as an incident, a trust score, and a guarding assertion, so the same failure is caught automatically the next time. None of it is a screenshot: there's a live DataHub instance seeded with a real failure and continuously watched, a self-contained reference-manual documentation site, and a 1,053-test suite standing behind all of it.
The problem
Software engineering solved "does this change break something" with CI: every change runs tests before it ships. The machine learning data supply chain has no equivalent. Data-quality tools stop at the warehouse edge, model monitoring starts at the model, and the dangerous middle, where a feature is derived from a table and a schema change three hops upstream reaches a live endpoint, belongs to neither. DataHub is the one catalog that holds both graphs at once, column-level lineage across the warehouse and ML metadata for the models, but out of the box nothing joins them: an MLflow ingest produces a model whose training run records no inputs at all, and a dbt ingest produces excellent lineage between tables with no edge reaching a model. A detector that wants to walk from a feature back to its source column has nowhere to start.
What I built
- 01
Wrote the join nothing else writes
`janus link` declares which warehouse column each of a model's features came from, which column carries the label, and what the input schema looked like at training time, inferred from what the graph already holds, imported from a Feast repo or a dbt manifest, or typed by hand. Once it exists, every check below is a graph traversal instead of a guess.
- 02
Seven detectors sharing one traversal engine
Target leakage, schema drift, and blast radius ask whether the data is correct; sensitive-source, deprecated-input, and proxy-candidate ask what the organization already decided elsewhere in DataHub that nothing joins back to the model. Each rests on a published result rather than a rule of thumb: target leakage follows Kaufman, Rosset & Perlich's KDD 2011 definition of leakage as illegitimate information about the target; schema drift follows Breck et al.'s MLSys 2019 training/serving contract; blast radius follows Sculley et al.'s NeurIPS 2015 notion of undeclared consumers.
- 03
A degraded mode for the common case, nobody has linked anything yet. Rather than staying silent, a scan says what it can still see at table level, quotes the 0.25 precision that answer is measured at, and never lets it outrank or be confused with a real column-level finding. `coverage`, `inventory`, and `finops` fold the same honesty into catalog-wide sweeps a platform lead can act on, including the one report that ever suggests deleting something.
- 04
Idempotent by construction
Every incident, tag, trust score, and guarding assertion is read-before-write and keyed, so a scan rerun on an unchanged graph writes nothing new, measured by reading the graph back after a rerun rather than asserted.
- 05
A CI gate with an honest third exit code
`janus gate` runs the same detectors against a policy and answers 0 shippable, 1 policy violated, or 2 couldn't reach a verdict, because a gate that reports "DataHub was unreachable" as a violation teaches a team to wave through every red build. Bundled as a GitHub Action that posts findings straight to the pull request's own job summary.
- 06
Generated the paperwork instead of maintaining it by hand
A model card, an EU AI Act evidence pack, a per-feature Data Card, and a NIST AI RMF crosswalk are all regenerated from the graph on request, so they're current by construction instead of accurate-until-the-model-next-changes. The evidence pack's first heading says what it isn't, "This is not a compliance certification," followed immediately by what it could not establish.
- 07
Built Argos, a desktop watchdog
A 32×32-pixel dog patrols the corner of the screen, barks with a red collar the moment a finding lands, turns into a translucent ghost when it can't reach DataHub, and walks a finding's blast radius across the desktop, one lineage hop at a time, when you double-click it.
- 08
Measured it against ourselves, not just asserted it
Janus-Bench scores every detector against a live DataHub, compares column-level lineage to a table-level baseline on the identical graph, applies every finding's counterfactual fix and re-checks that it actually clears, and runs mutation testing against the detection code itself.
- 09
Hit DataHub's own limits and reported them instead of quietly working around them. Incidents can't attach to an mlModel (GMS answers 500), so model risk lives in structured properties instead, filed upstream as an RFC. Smart assertions are DataHub Cloud-only, so Janus renders its own portable open-assertions YAML. Sixteen reproducible bugs went back in total, plus a datahub-ml-guard skill and an incident-write MCP tool DataHub's own MCP server didn't have.
Architecture
Janus is a read, reason, write-back loop. A trigger, the CLI, a training script's API, an MCP client, or `watch`, starts a run. Detection queries DataHub's column-level and ML lineage and returns typed findings, with no LLM anywhere in that decision. Orchestration sequences the work and, on the human-approval path, pauses for consent. Reasoning asks a language model only to explain and rank what detection already found, never to decide anything. Write-back commits the result to DataHub idempotently, and Argos turns the same run's event stream into a desktop watchdog.
Tech stack
Deterministic detection engine
Seven typed detectors in `janus/detect/`, target leakage, schema drift, blast radius, sensitive-source, deprecated-input, proxy-candidate, and a degraded table-level mode, that never call an LLM and never write; a scan is byte-identical whether or not a model provider is configured.
`janus link`, the missing join
Declares which warehouse column each feature came from and which column carries the label, inferred from the graph, imported from a Feast repo or dbt manifest, or typed by hand, the one edge DataHub's own ingestion sources don't produce.
LangGraph orchestration
`agent/pipeline.py` runs the shared detect-reason-write core every trigger calls; `agent/graph.py`'s StateGraph adds a real `interrupt()` behind `scan --review` so nothing writes without a human's yes.
Idempotent write-back
Sixteen kinds of DataHub aspect writes, incidents, tags, glossary terms, structured properties, documents, guarding assertions, all read-before-write and keyed, measured at 0 duplicates after rerunning a scan that had written 31 incidents.
CI gate + GitHub Action
`janus gate` judges a dry-run scan against a policy and exits 0, 1, or 2 (shippable, violated, couldn't verify), bundled as `action.yml` and posting findings to the pull request's own job summary.
Argos, the desktop watchdog
A Tauri v2 + Rust binary drawing a 32×32-pixel dog with twelve states over its own event protocol; it patrols, sniffs, barks with a red collar, turns into a translucent ghost when DataHub is unreachable, and walks a finding's blast radius across the screen.
MCP server + companion skill
`janus-mcp` serves three read-only tools (check_leakage, check_freshness, check_gate) meant to run beside DataHub's own mcp-server-datahub; the datahub-ml-guard skill wraps the same detection engine for DataHub's skill registry.
Janus-Bench
The evaluation harness in `benchmarks/`, precision and recall against a live DataHub, a table-level lineage baseline scored on the identical graph, counterfactual remedies applied and re-checked, and mutation testing over the detectors themselves.
Kubernetes + Docker packaging
`charts/janus-watch` deploys the one long-running entry point (`watch`) as a Helm chart with a `link --all` CronJob; `docker-compose.yml` adds Janus onto the Docker network DataHub's own Quickstart already creates rather than reimplementing it.
Trust score + generated governance docs
A weighted 0-100 rollup banded healthy/watch/at-risk, capped at watch whenever the worst finding is critical or high regardless of point total, and versioned so a scoring change can never masquerade as a regression. Feeds a model card (Mitchell et al., FAT* 2019), an EU AI Act evidence pack, a per-feature Data Card (Pushkarna et al., FAccT 2022), and a NIST AI RMF crosswalk quoted verbatim from the Playbook.
Real platform bugs, found and reported
GMS returns 500 for an incident attached to an mlModel; get_aspect raises on the operation timeseries aspect instead of returning it; DataHub's search hides non-latest model versions Janus still has to see and write to. Each was found by running the product for real against a live instance, worked around, and filed as one of sixteen reproducible bugs upstream.
Results
1.00 precision, 1.00 recall
Across all 7 detectors, 31 trials scored against a live DataHub instance, never fixtures (Janus-Bench, benchmarks/RESULTS.md).
0.25 → 1.00 precision
Per-feature, against a table-level lineage baseline on the identical graph: table-level lineage can tell a model leaks but not which feature, and keeps alerting after the leak is fixed. Column-level lineage doesn't.
0 duplicate writes
31 incidents on the table after one scan, still 31 after an identical rerun, read back from DataHub rather than taken from the scan's own report.
76% mutation score
1,956 mutants generated against the detection code alone, 1,490 killed; every survivor triaged by hand as a real gap or a provably equivalent mutation.
1,053 tests
982 offline and 71 integration against a live DataHub, across 80 test modules and 8 CI jobs.
3 OSS contributions
A datahub-ml-guard skill submitted to DataHub's own skill registry, an incident-write MCP tool plus an RFC for its MCP server, and 16 reproducible bugs filed upstream.
16 aspects, 11 properties
Every kind of write Janus makes into the graph: incidents, tags, glossary terms, structured properties, documents, guarding assertions, and its own dataProcessInstance provenance, plus 11 structured properties on the model and one custom property per feature.
Live, judge-browsable demo
A real DataHub instance, seeded with a planted failure and continuously maintained by janus watch, browsable read-only at janus.ahmedxsaad.me (judge / Janus-ReadOnly-2026); an edit attempt is verified to return 403.
Full reference docs, self-hosted
A self-contained manual at docs.ahmedxsaad.me covering every command, flag, and the Python API with no build step, plus 16 engineering docs on the architecture, the detectors, the security model, and the design decisions behind them.
Moments





What I took away
- 01
The join was the entire bet. Column-level lineage and ML metadata already both lived in DataHub; the reason nothing used them together is that nothing wrote the edge between them. Writing that one join is what turned every detector into a graph traversal instead of a heuristic.
- 02
Deterministic detection is what made the LLM safe to add, not the other way around. Because the model only explains and ranks what a pure function already decided, a prompt injection sitting in catalog text can at worst affect wording, it's downstream of every finding, never upstream of one.
- 03
Idempotency isn't a nice-to-have when something runs forever. `watch` polls or streams DataHub's own change log continuously; without keyed, read-before-write mutations, a `watch` left running over a weekend would file the same incident a thousand times instead of once.
- 04
The benchmark had to be willing to fail to be worth trusting. The freshness sweep plants trials on either side of the SLA boundary, not just the obvious 30-hour case, and mutation testing is reported at 76%, not rounded up to a clean number, because a suite that can't fail hasn't been tested.
- 05
Argos taught us an honest status display has to be willing to look bad. A cheerful pet that keeps patrolling while it's actually disconnected from DataHub is the kind of lie that gets ambient status indicators switched off, so it turns into a translucent ghost the moment it can't reach the graph instead of pretending everything's fine.
- 06
Real platform limits are worth reporting, not just quietly working around. Finding that GMS 500s on an incident attached to an mlModel, or that search hides older model versions Janus still has to write to, could each have stayed a private workaround. Filing them as sixteen reproducible bugs, plus an RFC and a skill for DataHub's own ecosystem, is what turned five weeks of hitting a platform's edges into something the next team building on it doesn't have to rediscover.
- 07
Generated documentation earns trust by admitting its limits, not by asserting authority. The EU AI Act evidence pack's first heading is what it isn't, not what it is; a compliance document that implied certainty it didn't have would be worse than no document at all.
View the sourceRead the docsTry the live demoWatch the demo video