<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: nightmoose</title>
    <description>The latest articles on DEV Community by nightmoose (@nightmoose).</description>
    <link>https://dev.to/nightmoose</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4191305%2F98948c69-c5f1-4a22-bf2c-fb678d29e118.jpg</url>
      <title>DEV Community: nightmoose</title>
      <link>https://dev.to/nightmoose</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nightmoose"/>
    <language>en</language>
    <item>
      <title>"What breaks if this column changes?" Building LineageGate on FalkorDB</title>
      <dc:creator>nightmoose</dc:creator>
      <pubDate>Sun, 18 Oct 2026 01:03:26 +0000</pubDate>
      <link>https://dev.to/nightmoose/what-breaks-if-this-column-changes-building-lineagegate-on-falkordb-3cia</link>
      <guid>https://dev.to/nightmoose/what-breaks-if-this-column-changes-building-lineagegate-on-falkordb-3cia</guid>
      <description>&lt;p&gt;&lt;em&gt;By Alex Suarez · written for the Graph Hacks: Context for AI Agents hackathon (WeMakeDevs × FalkorDB)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://lineagegate.fly.dev" rel="noopener noreferrer"&gt;https://lineagegate.fly.dev&lt;/a&gt; · &lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/nightmoose/lineagegate" rel="noopener noreferrer"&gt;https://github.com/nightmoose/lineagegate&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've been a data engineer since 1997. The tools have changed many times over: cron and shell scripts, then SSIS, then ETL suites, then Hadoop, then the cloud warehouse with dbt on top. One conversation hasn't changed in all that time:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Hey, we're changing &lt;code&gt;amount&lt;/code&gt; to cents in the orders feed. That's fine, right?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nobody can answer that with confidence. The column feeds a staging model, which feeds three intermediate models, which feed a fact table, a reconciliation mart, a customer lifetime value model and five dashboards. Two of those dashboards belong to people who've never heard of the orders feed. Somewhere in there is a data contract that says "USD dollars, not cents", written after the last time this exact thing happened.&lt;/p&gt;

&lt;p&gt;The second conversation is the 7 a.m. one: "The customer mart job failed. Why, and who do I page?" The error message names a failing test. The cause sits four hops upstream, in a CRM backfill. We've seen it before, but the write-up is in someone's old ticket and nobody remembers it.&lt;/p&gt;

&lt;p&gt;Both questions are about &lt;strong&gt;relationships&lt;/strong&gt;, not rows. That's why I built LineageGate on a graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a graph, specifically
&lt;/h2&gt;

&lt;p&gt;You &lt;em&gt;can&lt;/em&gt; answer "what's downstream of this column" with recursive SQL over an edge table. I've done it. It's painful to write, worse to maintain, and the answer comes back as a flat list of names. The flat list is the real problem: when I tell a finance lead their dashboard will break, the first thing they ask is "how?". I need the chain:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;stg_orders.amount → int_order_revenue.gross_revenue → fct_orders.order_amount → fct_revenue_daily.revenue → mart_finance_revenue.revenue_usd → finance.revenue_daily&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That chain is a path, and a graph database returns paths natively. Impact analysis is a variable-length traversal. Ownership is a multi-hop walk. "Have we seen this before?" is graph distance between today's failure and old incidents. Vector similarity can't give you a path, and a path is what earns trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data model
&lt;/h2&gt;

&lt;p&gt;LineageGate runs on a fully synthetic "Acme Analytics" warehouse: 111 nodes, 248 edges, 11 labels and 12 relationship types. It's small enough to read and big enough to be realistic. All lineage edges point &lt;strong&gt;upstream&lt;/strong&gt; ("depends on"), so impact analysis means walking incoming edges.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart RL
  Job&amp;gt;Job] --&amp;gt;|READS_FROM| Source([Source])
  Job --&amp;gt;|WRITES_TO| Table[Table]
  Job --&amp;gt;|REFRESHES| Model[Model]
  Table --&amp;gt;|HAS_COLUMN| Column[Column]
  Model --&amp;gt;|HAS_COLUMN| Column
  Model --&amp;gt;|READS_FROM| Table
  Model --&amp;gt;|DEPENDS_ON| Model
  Column --&amp;gt;|DEPENDS_ON| Column
  Dashboard[[Dashboard]] --&amp;gt;|DISPLAYS| Model
  Column --&amp;gt;|GOVERNED_BY| Contract{{Contract}}
  Test[/Test/] --&amp;gt;|GOVERNED_BY| Contract
  Test --&amp;gt;|READS_FROM| Column
  Incident((Incident)) --&amp;gt;|TRIGGERED_BY| Job
  Incident --&amp;gt;|VIOLATED| Contract
  Incident --&amp;gt;|RELATED_TO| Incident
  Model --&amp;gt;|OWNED_BY| Team[Team]
  Person[Person] --&amp;gt;|MEMBER_OF| Team&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Two design choices mattered most:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Column-level lineage is first-class.&lt;/strong&gt; &lt;code&gt;Column -[:DEPENDS_ON]-&amp;gt; Column&lt;/code&gt; edges let impact be
precise. Changing &lt;code&gt;stg_orders.amount&lt;/code&gt; doesn't flag the support dashboard just because it shares a model with &lt;code&gt;stg_orders&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incidents are nodes, not documents.&lt;/strong&gt; An incident links to the job that surfaced it, the contract
it violated, and related incidents. Memory is part of the graph, so it can be queried like the graph.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Three queries doing the real work
&lt;/h2&gt;

&lt;p&gt;Every tool the agent can call is a parameterised Cypher template. The model never writes Cypher itself. Here is the core of column impact, the path from a column to every dashboard it reaches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;c:&lt;/span&gt;&lt;span class="n"&gt;Column&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;fqn:&lt;/span&gt; &lt;span class="n"&gt;$fqn&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:DEPENDS_ON&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Column&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:HAS_COLUMN&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Model&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:DISPLAYS&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;d:&lt;/span&gt;&lt;span class="n"&gt;Dashboard&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;d.id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d.name&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;length&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hops&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
       &lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="nf"&gt;nodes&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;n.id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;label:&lt;/span&gt; &lt;span class="nf"&gt;labels&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="ss"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="ss"&gt;],&lt;/span&gt; &lt;span class="py"&gt;name:&lt;/span&gt; &lt;span class="nf"&gt;coalesce&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n.fqn&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n.name&lt;/span&gt;&lt;span class="ss"&gt;)}]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;pnodes&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
       &lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="nf"&gt;relationships&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;type:&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt; &lt;span class="py"&gt;src:&lt;/span&gt; &lt;span class="nf"&gt;startNode&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="n"&gt;.id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;dst:&lt;/span&gt; &lt;span class="nf"&gt;endNode&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="n"&gt;.id&lt;/span&gt;&lt;span class="ss"&gt;}]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;prels&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;hops&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two list comprehensions at the end matter more than they look. They project every path into plain node and edge maps on the server, so both the LLM and the UI receive the evidence in a form they can quote or draw.&lt;/p&gt;

&lt;p&gt;Failed-job triage is one pattern from the failed job up to every ingestion job that feeds it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Job&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;$id&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:REFRESHES&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Model&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:DEPENDS_ON&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Model&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
          &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:READS_FROM&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Table&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:WRITES_TO&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;uj:&lt;/span&gt;&lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;uj.id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uj.status&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pnodes&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prels&lt;/span&gt;   &lt;span class="c1"&gt;// same path projection as above&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For "which model would hurt most if it broke", I didn't write any graph math. I used FalkorDB's built-in algorithms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;algo.pageRank&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Model'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'DEPENDS_ON'&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;YIELD&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;node.id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PageRank and &lt;code&gt;algo.betweenness&lt;/code&gt; both put &lt;code&gt;dim_customer&lt;/code&gt; at the top. That's the hub I planted in the seed data, and it's the model that would hurt most in real life. LineageGate then attaches the dashboard paths that make it critical, so even an algorithm result comes with evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Grok picks the graph tools
&lt;/h2&gt;

&lt;p&gt;The agent loop is deliberately small: one Python module, no agent framework. It talks to xAI's OpenAI-compatible API and hands Grok seven tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;impact_from_column&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;owners_for_node&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;paths_explain&lt;/code&gt; (k shortest paths via &lt;code&gt;algo.SPpaths&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;triage_failed_job&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;similar_incidents&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;record_incident&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;critical_assets&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each tool has a one-paragraph description. The system prompt sets a few hard rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always call a tool before answering.&lt;/li&gt;
&lt;li&gt;Quote at least one returned path word for word.&lt;/li&gt;
&lt;li&gt;If a tool finds nothing, say the graph has no evidence and stop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I never route by keyword. Ask "a customer job failed last night, have we seen it before, who should I page?" and Grok calls &lt;code&gt;triage_failed_job&lt;/code&gt; with no arguments. That tool defaults to the most recent failure and already includes similar incidents. Ask about cents and dollars and it calls &lt;code&gt;impact_from_column(table="stg_orders", column="amount")&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Speed matters on camera. &lt;code&gt;grok-4.3&lt;/code&gt; gave great answers but took 5–26 seconds end to end. Switching the default to &lt;code&gt;grok-4.20-0309-non-reasoning&lt;/code&gt; cut that to roughly 3.6–6.4 seconds per question in my dry run against the live site. With no API key, a deterministic offline router picks the tool from the question instead, so the demo never depends on the network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory that survives the session
&lt;/h2&gt;

&lt;p&gt;Hackathon Track 02 is about agent memory, and I wanted memory to be more than a chat transcript. &lt;code&gt;record_incident&lt;/code&gt; &lt;code&gt;MERGE&lt;/code&gt;s a new &lt;code&gt;Incident&lt;/code&gt; node and links it into the lineage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;TRIGGERED_BY&lt;/code&gt; to the job&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VIOLATED&lt;/code&gt; to each broken contract&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RELATED_TO&lt;/code&gt; to its two most similar past incidents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;similar_incidents&lt;/code&gt; then ranks incidents by graph distance from whatever you're asking about, combined with keyword overlap. Ask about &lt;code&gt;stg_orders.amount&lt;/code&gt; in a brand-new session and the agent finds INC-0802 (the August cents-versus-dollars bug) through this path:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;(Column stg_orders.amount) -[:GOVERNED_BY]-&amp;gt; (Contract orders_amount) &amp;lt;-[:VIOLATED]- (Incident INC-0802)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Nobody tagged that incident with the column name. The graph connects them through the contract. That's the kind of recall I've wanted in every incident review I've ever sat through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Explainable by default, and honest when it can't answer
&lt;/h2&gt;

&lt;p&gt;The UI is one HTML file served by FastAPI. Chat is on the left. The evidence panel on the right lists every tool the agent called and every path in the result, drawn left-to-right with vis-network. Click a path and it lights up. A second tab renders the &lt;strong&gt;live schema&lt;/strong&gt; straight from FalkorDB (&lt;code&gt;db.labels()&lt;/code&gt;, &lt;code&gt;db.relationshipTypes()&lt;/code&gt; and a count of each label-type-label triple).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxmosf0e2qkox44xs3x70.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxmosf0e2qkox44xs3x70.png" alt="LineageGate UI: chat on the left, tool calls and evidence paths on the right" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fev6jdq7e4dauds6qohex.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fev6jdq7e4dauds6qohex.png" alt="The Data model tab, rendered live from FalkorDB" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I spent as much time on the "no" cases as on the "yes" cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Misspelled column:&lt;/strong&gt; the agent suggests close matches taken from the graph itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ambiguous name:&lt;/strong&gt; it lists the candidates instead of picking one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty graph:&lt;/strong&gt; it tells you to seed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unknown column:&lt;/strong&gt; ask about &lt;code&gt;stg_orders.amount_eur&lt;/code&gt; and it says "the graph has no evidence"
and offers &lt;code&gt;stg_orders.amount&lt;/code&gt;. It doesn't invent a plausible-sounding blast radius.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An opt-in test sends an &lt;code&gt;amount_eur&lt;/code&gt; question to the live model and asserts it abstains and prints no fabricated path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;

&lt;p&gt;It's one Fly.io machine running FalkorDB and the app together. On boot, it seeds the graph if it's empty, so a restart heals itself. Locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'.[llm,api,dev]'&lt;/span&gt;
lineagegate seed &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; lineagegate serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I'd do next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real ingestion:&lt;/strong&gt; load lineage from dbt &lt;code&gt;manifest.json&lt;/code&gt; and warehouse query logs instead of synthetic CSVs. The graph model wouldn't change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-merge checks:&lt;/strong&gt; run &lt;code&gt;impact_from_column&lt;/code&gt; against a pull request's changed columns and post the paths as a review comment &lt;em&gt;before&lt;/em&gt; the change ships.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent memory:&lt;/strong&gt; add a volume and let the triage flow propose &lt;code&gt;record_incident&lt;/code&gt; when an on-call engineer confirms a root cause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weighted paths:&lt;/strong&gt; score edges by query volume, so "critical" reflects actual usage, not just structure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thirty years in, my data quality questions are still the same ones. What's new is that an agent can answer them, as long as the answer is a path through a graph it can show you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built in four days with FalkorDB, Python, FastAPI and xAI Grok. AI coding assistants helped write the code; see &lt;code&gt;DISCLOSURE.md&lt;/code&gt; in the repo.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demo video:&lt;/strong&gt; &lt;a href="https://youtu.be/ARw7zSb5Jdo" rel="noopener noreferrer"&gt;https://youtu.be/ARw7zSb5Jdo&lt;/a&gt;&lt;/p&gt;

</description>
      <category>falkordb</category>
      <category>graphdatabase</category>
      <category>ai</category>
      <category>dataengineering</category>
    </item>
  </channel>
</rss>
