Live DataHub end-to-end
A full read/write/re-read cycle against DataHub Core.
evidence/live/2026-07-27.md · 62 lines
Artifacts
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
| Surface | Used | How |
|---|---|---|
| Context graph | yes | Column-level lineage, ML entities, schema, profiles |
| MCP Server | yes | Discovery, lineage reads, governed mutations |
| Agent Context Kit | yes | get_lineage_paths_between — the column-level directional path query this whole audit rests on |
| DataHub Skills | yes | datahub-ml-release-audit, proposed upstream as datahub-skills#68 |
| DataHub Actions | yes | Audits a model the moment it appears, with nobody triggering it |
| Analytics Agent | n/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
| Kind | Identifier | Why 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
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
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
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.
| File | SHA-256 | Present |
|---|---|---|
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
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
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
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.
| Reach | Cases | Caught by statistics | Caught by code only | Missed | Mean 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
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.
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.
| Feature | Looked this good | Actually was | Advantage 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.
| File | What it is | Size |
|---|---|---|
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
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 work | What 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
All three came out of building this rather than out of looking for something to contribute.
| Pull request | What | State |
|---|---|---|
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
| Run | Decision | Verdict | Published |
|---|---|---|---|
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 |