Overview / Evidence
Recorded from DataHub Core

Artifacts

What Hindsight actually produces

Not a description of what it would write — the real files, read from disk. Anyone can claim a tool writes evidence. This is the evidence.

Integration

Which parts of DataHub this uses

5 of 6 surfaces
SurfaceUsedHow
Context graphyes Column-level lineage, ML entities, schema, profiles
MCP Serveryes Discovery, lineage reads, governed mutations
Agent Context Kityes get_lineage_paths_between — the column-level directional path query this whole audit rests on
DataHub Skillsyes datahub-ml-release-audit, proposed upstream as datahub-skills#68
DataHub Actionsyes Audits a model the moment it appears, with nobody triggering it
Analytics Agentn/a Not applicable; this is not a text-to-SQL product

It can watch instead of waiting. The release gate subscribes to DataHub's event stream and audits a model on creation. It may raise an incident to notify — it never publishes the evidence record itself, because a process that silently rewrites governed metadata is the thing this project argues against.

datahub actions -c docker/hindsight-action.yml

Proven end to end against a live instance: a model was emitted, DataHub published the change to Kafka, the event was consumed off the bus, and the gate audited it and returned block. Full record in evidence/integrations/.

Written to DataHub

Four things, after a person approves

each re-read
KindIdentifierWhy it matters
Field tag hindsight:leakage-confirmed Marks the exact column that broke the rule, not just the table.
Structured property hindsight.auditVerdict The verdict as typed, queryable metadata - not a free-text note.
Document hindsight-audit-<case> The full evidence path and safe-control results, linked to the asset.
Incident ML_LEAKAGE Open and actionable. Reused on retry rather than duplicated.

A write that reports success but cannot be read back afterwards is treated as a failure.

Sample output

The audit document published to the catalog

examples/audit_document.md

This is what the next engineer or agent finds attached to the asset, instead of rediscovering the problem themselves.

# Hindsight audit: credit-default-leaked-payment-event

**Release decision:** BLOCK<br>
**Verdict:** `confirmed`<br>
**Catalog write-back:** awaiting human approval

## Evidence path

`payment_events_after_decision.payment_recorded_at`
-> `feature_pipeline_leaky.days_since_last_payment`

The transformation reads the configured post-outcome source without an
`available_at <= prediction_time` cutoff.

## The ablation trap

| Feature | Ablation delta | Verdict |
|---|---:|---|
| Planted leaked feature | 0.21 | `confirmed` |
| Legitimate pre-cutoff control | **0.24** | `clear_for_release` |

The safe feature has the larger importance delta. An ablation-only detector would get this exactly backwards.

## Point-in-time validation

| Measure | Observed | Reconstructed |
|---|---:|---:|
| AUC | 1.000000 | 0.833630 |
| Advantage over baseline | 0.301712 | 0.135342 |

The planted leak is total by construction, so observed AUC `1.000000` is expected; real leakage can be subtler. The strict-majority loss boundary is a configurable demo policy, not a universal scientific constant.

The reconstruction excluded 4,000 post-cutoff records. Only 44.858% of the
observed advantage remained, satisfying the frozen majority-loss rule.

## False-positive control

The legitimate `prior_delinquencies` feature had a large 0.226554 ablation
delta, but its AUC remained 0.924842 after point-in-time reconstruction.
Hindsight returned `clear_for_release`, demonstrating that feature importance
alone cannot confirm leakage.

## Proposed remediation

Add the following predicate to the payment join:

```sql
payment.available_at <= application.prediction_time
```

The full proposal is in `examples/remediation.sql` and independently verifies
as safe. No change has been applied automatically.

## Planned approved write-back

- field tag
- `hindsight.auditVerdict` structured property
- native DataHub audit Document
- active `CUSTOM / ML_LEAKAGE` incident

Proposed repair

One line, verified independently

SQL verified

Before — blocked

SELECT
    application.application_id,
    date_diff(
        'day',
        max(payment.payment_recorded_at),
        application.prediction_time
    ) AS days_since_last_payment
FROM applications_at_decision_time AS application
LEFT JOIN payment_events_after_decision AS payment
    ON payment.customer_id = application.customer_id
GROUP BY application.application_id, application.prediction_time;

After — clears

SELECT
    application.application_id,
    date_diff(
        'day',
        max(payment.payment_recorded_at),
        application.prediction_time
    ) AS days_since_last_payment
FROM applications_at_decision_time AS application
LEFT JOIN payment_events_after_decision AS payment
    ON payment.customer_id = application.customer_id
   AND payment.available_at <= application.prediction_time
GROUP BY application.application_id, application.prediction_time;

The difference is the available_at <= prediction_time guard. Hindsight proposes it and checks it, but never merges or applies pipeline code itself.

Recorded metadata

Hashed, so replay cannot drift from the capture

credit-default-live-recording-v1

Captured from DataHub Core v1.5.0.6 via MCP server 0.6.0 on 2026-07-27. Re-prove it against your own instance with hindsight verify-fixture-live.

FileSHA-256Present
ground_truth.json ccddce80695e… yes
remediation.sql 6069a18533a7… yes
responses/entity.json f5aa1fb4af11… yes
responses/lineage.json c48533f9ce81… yes
responses/validation.json cf6ea8809304… yes
transformation.sql 2f93b7311113… yes

Committed to the repository

Records of real runs

Live DataHub end-to-end

A full read/write/re-read cycle against DataHub Core.

evidence/live/2026-07-27.md · 62 lines

Feasibility proof

Fine-grained lineage and every write-back type, verified.

evidence/phase0/2026-07-27.md · 88 lines

Write-back proof

Each mutation re-read to confirm it persisted.

evidence/writeback/2026-07-27.md · 35 lines

Fixture capture

How the recorded metadata was produced.

evidence/fixtures/2026-07-27.md · 40 lines

Skill contract

The reusable DataHub Skill, tested.

evidence/skill/2026-07-27.md · 24 lines

Console

What the interface renders.

evidence/ui/2026-07-27.md · 32 lines

Raw machine reports stay ignored as *.local.json; these are the sanitized summaries, versioned with the code that produced them.

Self-verification

How this project checks its own claims

  • A high-correlation safe control must clear on every run, despite having a larger ablation delta than the planted leak.
  • One scenario is expected to pass. A gate that can only say no is indistinguishable from one that is not looking.
  • The generator is frozen; measured results are published rather than tuned.
  • The collapse threshold and its measured margin are disclosed, not hidden.
  • An LLM may explain evidence. It can never promote a verdict.
  • This console claims anyone can read it, so CI checks that: every route, both themes, both reading levels, against WCAG 2.1 AA contrast plus accessible names, labels, heading order and landmarks.

scripts/check_accessibility.py — it found three real defects the first time it ran, including a subtle text colour that failed AA on most surfaces in both themes.

Measured, not asserted

What happens as the defect gets subtler

21 cases · 0 false positives

The same planted defect, swept from reaching every record down to reaching 2% of them, with a matched clean query for every case. Each bar is one band. Every bar is full — the defect was caught every time. What changes is which route caught it.

100% caught by statistics
100% 0.177
100% caught by statistics
70% 0.158
100% caught by the deterministic proof only
40% 0.109
100% caught by the deterministic proof only
25% 0.075
100% caught by the deterministic proof only
15% 0.045
100% caught by the deterministic proof only
8% 0.026
100% caught by the deterministic proof only
2% 0.007
Defect reaches this share of records mean AUC delta, first and last shown
Caught by retraining and comparing Caught only by reading the code
Below 40% reach the statistical route stops firing entirely. At 2% the performance difference is 0.0066 of AUC — invisible to any threshold anyone would realistically set — and the deterministic proof still catches it. That is the argument for two routes, measured rather than claimed.
Table view of every band
ReachCases Caught by statistics Caught by code only MissedMean AUC delta
100% 3 3 0 0 0.1768
70% 3 3 0 0 0.1578
40% 3 0 3 0 0.1086
25% 3 0 3 0 0.0752
15% 3 0 3 0 0.0451
8% 3 0 3 0 0.0262
2% 3 0 3 0 0.0066

Read the perfect score with care. Ground truth here is structural - a case is 'leaked' if its query joins a post-outcome source with no availability guard. The deterministic route reads that same query. So its perfect score on this benchmark is close to true by construction, and should not be read as field accuracy.

What is measured honestly: the statistical route's decay as the defect gets subtler, zero false positives across 21 guarded queries that could have been flagged, and the AUC delta shrinking toward invisibility.

The fair test

Does it work on data we did not create?

passed

Hindsight's own scenarios are generated by code in this repository, so they cannot show whether it works on data it did not create. This is a committed dataset in a different domain, with different column names and a different leak mechanism, audited by the same deterministic routes.

Looked this good 0.9932 AUC with the suspect feature
Actually was 0.8257 rebuilt as of the decision
Advantage retained 0.4% of 0.1681 apparent gain
Records excluded 664 did not exist at the decision

Subscription churn. plan_changes_to_date is a running counter, and the training table joined in every change ever recorded - including the downgrade a customer makes while cancelling. Before the decision it is close to noise; after it, nearly the label.

Every feature, ranked by what the cutoff removes

FeatureLooked this good Actually wasAdvantage lost Verdict
plan_changes_to_date 0.993154 0.825747 0.167407 flagged
support_tickets_snapshot 0.960126 0.960126 0.0 clean
logins_30d 0.82508 0.82508 0.0 clean
tenure_snapshot 0.825169 0.825169 0.0 clean
spend_snapshot 0.825169 0.825169 0.0 clean

Read the second row before the first. support_tickets_snapshot scores far above the baseline and loses nothing to the cutoff. Ranked by importance it would sit near the top; ranked by advantage lost it is correctly at zero. That is the difference between a leakage detector and an importance chart.

Ranking is triage, not a verdict. A feature high in this list had most of its advantage removed by the cutoff, which is where to look first. Confirming a defect still requires the deterministic routes, one feature at a time. Note that ranking by importance would put the legitimate control near the top instead.

Take the data and run it yourself

FileWhat it isSize
retention_decisions.csv One row per subscriber: the decision, the outcome, and what was knowable at the time 1201 lines
plan_change_history.csv A running count of plan changes, each row stamped with when it was recorded 3065 lines
feature_snapshots.csv The same facts as a wide feature table, one row per subscriber per as-of date 3065 lines
scenario.json Maps those column names onto the audit contract 63 lines
scenario_wide.json The wide-table variant, with the sweep candidates 73 lines
uv run hindsight validate-point-in-time --scenario examples/adapter/scenario.json
uv run hindsight validate-point-in-time --scenario examples/adapter/scenario_wide.json
uv run hindsight sweep-features --scenario examples/adapter/scenario_wide.json

Every number above is a recorded result, not a live one — a public demo should not retrain a model because someone opened a page. A test re-runs the real audit on every build and fails if these figures drift.

Prior art

Does this already exist?

Leakage detection is an active field. Everything below is real and worth using. The point is to be precise about the gap, not to dismiss any of it.

Prior workWhat it analyses Covers upstream temporal leakage?
Yang et al., ASE 2022 Static analysis of notebook code no — notebook scope
LeakageDetector, ICSME 2025 PyCharm plugin over Python ML code no — Overlap, Multi-test, Preprocessing
A Grammar of ML Workflows, 2026 Type system rejecting leakage at call time partly — one execution context
Feature stores (Feast, Tecton) As-of joins at retrieval time prevents it, if every feature goes through the store

The three leakage types current tooling detects are all notebook-internal. Overlap, Multi-test and Preprocessing are mistakes made while splitting and fitting. A feature whose values came from records that did not exist at the decision is not in that taxonomy. The 2026 grammar paper comes closest and names the boundary itself — it lists “manual feature derivation before split” among its enforcement gaps.

And a feature store with correct as-of joins prevents this by construction, which is a better answer than detecting it. Hindsight is for the common case where features are assembled in SQL upstream with no such guarantee — and for the separate problem of proving, afterwards and to someone else, that a specific model was clean. That is why the output is an evidence record rather than a warning in an IDE.

Contributed back

Three pull requests to DataHub

1 merged

All three came out of building this rather than out of looking for something to contribute.

Pull requestWhatState
datahub#18705 Documents the required customType on CUSTOM incidents. Hindsight raises one during write-back, and the tutorial as written fails with customType is required. merged
datahub#18822 Stops docker quickstart raising UnicodeEncodeError on legacy Windows code pages. We hit it bringing DataHub up: the stack was already healthy, so a working install reported failure. open
datahub-skills#68 Adds the datahub-ml-release-audit skill: verdict lattice, workflow, evidence validator. open

Full record, including what went wrong on the first attempt, in evidence/contributions/.

Recorded

Recent runs

All runs
RunDecisionVerdictPublished
20260729T050250-693684 ALLOW clear_for_release no
20260729T050141-dc73ad BLOCK confirmed no
20260729T044318-0be937 BLOCK confirmed no
20260728T175344-81df34 BLOCK confirmed no
20260728T174522-89d397 BLOCK confirmed no