Inspiration

Target leakage is the failure mode nobody catches in review. A feature encodes something knowable only after the prediction point, the model scores beautifully in validation, and it degrades the moment it meets production. By the time anyone notices, it has been making decisions for months.

The information needed to catch it is already sitting in the catalog: what the label is computed from, what each feature reads, how the windows are anchored. Nobody reads it, because reading it across dozens of models is tedious and nobody has the time.

That is a job for an agent.

What it does

leakwatch audits deployed ML models for target leakage using metadata alone — no training data, no model artifacts, no code. It works in three layers, and each catches something the one before it cannot:

  • textual — what does this feature description actually state?
  • temporal — does a stated window cross the prediction boundary?
  • topological — does this feature read the label's column under another name?

The third layer is the one that justifies having a catalog. In the seeded scenario, fulfilment_exception_rate has a spotless description: 90-day window, backward, anchored to the reference date. The extractor agrees — it finds nothing wrong in the text.

But the label's column has ancestry. One hop upstream, the raw table carries the same column under the same name, and that raw table is what the feature reads. The label is defined on the curated table; someone built a feature from the raw one believing it unrelated. No description anywhere betrays it.

Reading documentation cannot find that. Walking the graph can.

Verdicts are written back into DataHub as tags plus an audit trail on the model, so findings live in the catalog instead of a terminal.

How we built it

The graph is walked in code, not by the LLM. Handing an agent a set of tools and letting it wander costs a round trip per hop, varies between runs, and cannot be tested. Traversal is a solved problem — judgement is what deserves the tokens.

So the LLM is asked only what a description states: window length, direction, anchor, whether it closes before scoring. It never returns a verdict. Python applies the rules, and those rules can be read and unit-tested without an API key.

Column ancestry is resolved separately, in the graph. That layer is deterministic by construction and returns the same answer every run.

Challenges we ran into

Two rule sets that fired on missing evidence. The first version asked the LLM whether a description mentioned the label's column — a judgement dressed as a fact. It oscillated, and one run in five turned a documentation gap into a leak accusation. Replacing it with a graph fact fixed that field, but the rule still read closes_before_scoring != "yes", which includes unstated. The clean control got flagged with high confidence.

Both are the same mistake: a violation resting on the absence of counter-evidence rather than on positive evidence. Every rule now requires the metadata to state the problem. None fires on silence.

A single misread sentence was still enough. Across five runs, a feature whose description explicitly says its window closes before the reference date produced three different outcomes, including one false violation. That is what pushed us to majority voting: three extractions per model, fields without a strict majority collapse to unstated, which degrades to a documentation gap rather than an accusation.

Self-referential lineage in the sample data. analytics.order_history lists itself among its own upstreams. Any recursive walk without a visited set hangs on the first call.

Accomplishments we're proud of

The agent found a specification error we had written ourselves. A feature was described as covering "campaigns that ended before the reference date and drew an order within 14 days of campaign end". It flagged it, and we assumed a false positive.

It wasn't. A campaign ending three days before the reference date has a response window reaching eleven days past it. The condition closed the campaign, not the window. Written and reviewed by a human who believed it was correct, caught by an agent reading only metadata.

We also measured reliability instead of claiming it, on one model at temperature zero with architecture as the only variable:

architecture clean control, 5 runs
judgement in one prompt 3 inconclusive, 2 clean
extraction + rules + voting 5 inconclusive

What we learned

Give an agent one output channel for "something is off" and every uncertainty becomes an accusation. Separating findings from documentation gaps changed behaviour more than any prompt revision did. A model whose metadata cannot be audited has a documentation problem, and reporting that as leakage would be a lie.

Extraction variance measures the ambiguity of the text, not noise in the model. Where metadata is precise the extractor is deterministic; where it is vague it splits — which is what a human reviewer does reading the same sentence twice. Voting turns that split into a signal about catalog quality.

Temperature zero is not determinism. If a verdict is going to be persisted and re-read, it cannot come from an inference. The model reads the language; the code makes the decision.

What's next for leakwatch

  • Open incidents on the model rather than only applying tags, so findings enter a workflow someone has to close.
  • Contribute the ML datapack upstream: showcase-ecommerce ships no mlModel, mlFeature or mlModelDeployment entities, which is a real barrier for anyone building in this category.
  • Extend beyond temporal and topological leakage — group leakage and train/test contamination need signals the catalog does not currently carry.

Built With

Share this project:

Updates