Skip to content
flamiinngoPublic

About

git diff for business logic — an agent that proves why two metrics disagree by executing the SQL, then writes the answer back to DataHub

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

RIFT

git diff for business logic. Two dashboards disagree about revenue. RIFT reads the SQL behind both, works out where the gap comes from, and proves it by running the queries. Then it writes the answer back into DataHub so nobody has to work it out again.

Built for Build with DataHub: The Agent Hackathon, Challenge 1.


The problem

Finance says monthly recurring revenue was $4,421,817.73. Growth says $4,832,076.02.

Both come from governed, documented, lineage-tracked tables. The catalog will show you the schemas, the owners and the upstreams for each, and none of that tells you which number belongs in the board deck.

So an analytics engineer gets the ticket, reads both models line by line, tests a few theories with throwaway queries, messages two owners, and comes back two days later with "trials, and something with currency." Nobody writes it down. It happens again next quarter.

What it does

$ rift explain revenue_finance revenue_growth

  reference  analytics.finance.revenue_finance
                              $4,421,817.73   owners: finance-analytics
  target     analytics.growth.revenue_growth
                              $4,832,076.02   owners: growth-analytics

  gap        $410,258.29   (9.28%)

  3 differences · 10 query executions

  CAUSE                       CONTRIBUTION    MEASURED ALONE   VERDICT
  ──────────────────────────────────────────────────────────────────────
  Aggregate expression         $175,635.14       $365,794.79   probable defect
  Join type                    $175,635.14       $365,794.79   probable defect
  Filter predicate              $58,988.01        $88,037.04   definitional choice
  ──────────────────────────────────────────────────────────────────────
  explained                    $410,258.29   100.00%
  residual                           $0.00

  ⚠ measuring each cause on its own and adding the results gives $819,626.62
    — 2.00× the real gap. The causes overlap, so that method double-counts
    $409,368.33 of shared rows.

  receipt eb3dbf152f9ed34c   (same inputs always produce this hash)

RIFT doesn't reason about what a clause is worth. It rewrites the query with that one clause changed, runs it, and reads the number off the result. If it can't turn a difference into a rewrite it can execute, it says so instead of guessing.

It shows you the rows

Totals are easy to argue with, so RIFT pulls the actual records behind each finding:

355 rows have no matching reference row, so they are counted
in their original units instead of being converted

  account_name   account_type  plan        currency  mrr_local  subscription_id
  account_00002  enterprise    enterprise  NZD       4677.15    3
  account_00019  smb           growth      AUD       238.62     21
  account_00021  enterprise    enterprise  AUD       3522.03    25
  … and 352 more

Subscription 3 bills NZ$4,677.15 a month, which is $2,853.06. Growth's model counts it as $4,677.15, because the exchange rate table has no New Zealand dollar row and COALESCE(rate, 1.0) treats a Kiwi dollar as a US dollar. An owner can check that against billing in ten seconds.

Why the obvious method is wrong

Measure each cause on its own, add up the results, and you get $819,626.62 for a gap that is really $410,258.29. Off by 2x.

The causes overlap. Trial accounts are concentrated in the same currencies that are missing from the FX table, so excluding trials and tightening the join both remove some of the same rows. Add the effects separately and those rows get counted twice.

RIFT runs the query under every combination of changes instead, and gives each change its average marginal contribution across all orderings. That's a Shapley value. It doesn't depend on what order you consider the causes in, and the parts add up to the whole because of how it's defined, not by luck.

It costs 2ⁿ executions, where n is how many ways the two queries differ. That's under six in practice.

Correlated causes split the credit

Look at the two $175,635.14 rows. The LEFT JOIN and its COALESCE(rate, 1.0) are one bug written twice, since the COALESCE only does anything because the join is permissive. Either one alone accounts for the same $365,794.79, so neither deserves full credit. Shapley splits it. A stepwise method gives 100% to whichever it tries first.

Quickstart

Two minutes, no cluster, no Docker.

git clone https://github.com/flamiinngo/rift && cd rift
pip install -e .
rift demo

If you'd rather not install anything, the only requirements are sqlglot, duckdb and jinja2:

pip install sqlglot duckdb jinja2
PYTHONPATH=src python -m rift.cli demo

rift demo builds a sample warehouse, seeds a DataHub-shaped catalog, scans for metric collisions, reconciles the top one, writes the result back and drops the artifacts in examples/.

rift scan                                        # find metric collisions
rift explain revenue_finance revenue_growth      # reconcile a pair
rift publish                                     # write the finding to the catalog
pytest                                           # 19 tests

Point it at a real DataHub:

export DATAHUB_GMS_URL=http://localhost:8080
export DATAHUB_GMS_TOKEN=...

How it uses DataHub

RIFT is an MCP client and an MCP server.

Reads, through DataHub's MCP server:

Tool Used for
search find modelled assets in scope
get_entities owners, tags, glossary terms, domains
list_schema_fields column sets for collision detection
get_lineage shared upstreams as a collision signal
get_dataset_queries transformation SQL, the input everything else runs on

get_dataset_queries is what makes this work on real deployments. When an asset has no stored view definition, observed query history supplies the logic, so plain tables work too.

Writes, all of it knowledge that didn't exist anywhere before:

Tool What RIFT writes
save_document the reconciliation, with evidence and reasoning
add_structured_properties machine-readable findings on both assets
add_tags a neutral marker that a reconciliation exists
add_tags + add_structured_properties a proposal for the owners to decide on

That last row is a compromise worth being explicit about. The open-source MCP server publishes no lifecycle-proposal tool, so the proposal is written as a tag an owner can filter on (rift:proposal-pending) alongside the properties describing it, including rift.decision = PENDING_HUMAN_REVIEW. RIFT still changes nothing about the asset itself.

Running it against a real instance:

DATAHUB_GMS_URL=http://localhost:8080 PYTHONPATH=src \
  python -m rift.cli demo --live

--live swaps the fixture for DataHubCatalog and everything downstream is unchanged. There's nothing to install: the MCP server is launched with uvx. scan, explain and publish take --live too, and writes need the server running with TOOLS_IS_MUTATION_ENABLED=true — without it RIFT raises MutationsDisabled rather than failing quietly. Setup notes are in docs/LIVE_DATAHUB.md.

What's verified where. The engine, differ, collision detection and write-back logic all run for real in the demo and under test. The MCP transport has been run end to end against a live DataHub v1.7.0 — reads, the reconciliation, and all seven writes — and separately tested against a stub returning payloads captured from that instance. tests/test_datahub_mcp.py pins the tool names, the argument shapes and the response parsing, including a test that fails if RIFT ever calls a tool the server doesn't publish. It hasn't been run against a production cluster at scale, and we're not going to imply otherwise.

Serves. RIFT exposes itself over MCP too (find_metric_collisions, reconcile_metrics, compare_sql, publish_reconciliation). Ask a normal agent what revenue was and it picks a table and is confidently wrong. An agent that can call reconcile_metrics first answers with the distinction and its size.

RIFT doesn't certify anything

Stamping one asset CANONICAL_TRUTH would be easy and wrong. Which definition of revenue a company uses is a business decision with someone's name on it, and an agent that quietly rewrites governance metadata is a liability.

So RIFT files a proposal and leaves it there for a human to accept or reject. On a DataHub that exposes a proposal workflow it goes through that; on open-source DataHub, which doesn't, it lands as a rift:proposal-pending tag and a PENDING_HUMAN_REVIEW decision property on the asset. Either way the asset is untouched and the decision is someone's to make. There's a test that fails if any write ever claims canonicity on RIFT's own authority.

The one call it does make is defect versus choice, and that comes from the SQL structure rather than an opinion. Replacing a missing joined rate with a constant inside an aggregate means unmatched rows get summed in the wrong units, and you can see that in the syntax tree. Excluding trial accounts isn't a mistake, so RIFT calls it a choice.

The taxonomy

Class What it does to the number
Filter predicate the models count different populations
Join type one model silently drops rows the other keeps
Aggregate expression the models add up different quantities
Grain the models report at different levels of detail
Date bound the models cover different time windows
Deduplication one model counts repeated rows more than once

Limitations

  • RIFT needs the transformation SQL, from a view definition, a dbt model, or query history. For a plain table with no observed queries it reports the collision and declines to explain it. The demo catalog has such a pair on purpose (active_users_daily vs active_users_weekly) so you can watch it refuse rather than guess.
  • Both queries have to run on the same engine. SQL is transpiled with sqlglot, and vendor-specific functions may not survive.
  • Attribution costs 2ⁿ executions. Fine for the handful of differences real models have, not for two queries with nothing in common.
  • Comparing arbitrary queries is undecidable in general. RIFT explains differences between comparable metric definitions. It doesn't prove equivalence.
  • The sample warehouse is synthetic, generated from a fixed seed. The engine isn't, and the numbers come from real execution.

Layout

catalog/     read and write DataHub over MCP, plus a fixture with identical shape
collide/     IDF-weighted collision detection
diff/        sqlglot normalisation and the six-class differ
ablate/      execute query variants, attribute with Shapley values, pull example rows
writeback/   defect-vs-choice assessment, reconciliation document, proposals
report/      single-file HTML with waterfall and highlighted SQL
mcp_server   RIFT exposed as MCP tools

Tests

pytest    # 19 passed

They pin the claims on this page rather than the happy path:

  • test_patches_fully_reconcile_the_queries — applying every difference turns one query into the other exactly, residual $0.00
  • test_naive_attribution_is_materially_wrong — fails if the fixture stops exercising interaction
  • test_attribution_is_order_independent — shuffle the differences, get identical numbers
  • test_correlated_causes_share_credit — the two halves of one bug split it evenly
  • test_every_summary_is_derived_from_the_sql — no hardcoded explanations
  • test_writeback_never_certifies_unilaterally — no write may claim canonicity
  • test_findings_carry_checkable_rows — findings must surface real rows, and the join finding must surface the unconverted currencies
  • test_only_real_tools_are_called — fails on any MCP tool DataHub doesn't publish

Sample output

License

Apache 2.0, see LICENSE.

About

git diff for business logic — an agent that proves why two metrics disagree by executing the SQL, then writes the answer back to DataHub

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages