I spent four days, solo, asking Kubernetes one simple question. Who actually owns this code? It turns out the answer isn't what its files say.
TL;DR
- Kubernetes lists 157 approvers in its OWNERS files. 48 of them haven't touched the main repo in a year.
- I built Drift on FalkorDB. It finds the right reviewer for a change 78% of the time, compared with 37% if you pick from the OWNERS files (tested on 400 real pull requests). It also drafts fixes to those files and tests them on a copy of the graph first.
- Along the way I hit a FalkorDB bug that made my first result look better than it was, and I dropped a claim that didn't hold up.
The problem
Every big engineering team writes down who owns what. GitHub has CODEOWNERS, and Kubernetes has an OWNERS file in almost every folder, listing the people who can approve changes there. That's how pull requests get routed, and it's how a newcomer figures out who to ask.
The trouble is that these files go stale. People change teams, take a break or just get busy, and nobody goes back to edit the file. Meanwhile someone new quietly becomes the person who actually knows that code, and they never get added. Nothing compares the record with what's really happening, so nobody notices.
I built Drift for Graph Hacks (WeMakeDevs × FalkorDB) to do exactly that comparison, on a real organisation. It's live at https://drift-egny.onrender.com and the code is at https://github.com/oscarosk/drift.
The first number
Once everything was in a graph, my first query was the obvious one. Which listed approvers have done nothing in the last year? No reviews, no /lgtm, no /approve, no merged pull requests in kubernetes/kubernetes.
The answer was 48 of 157, and every one of them is still listed. (I try to say "inactive here" rather than "gone", because plenty of them are probably busy in other Kubernetes repos.)
Everything else in this post is what I built on top of that number, and how I tried to make sure I wasn't fooling myself.
The data, and why it had to be a graph
All of it is public. I used the team list (sigs.yaml, 24 teams), every OWNERS and OWNERS_ALIASES file, about 680 enhancement proposals (the decision records Kubernetes calls KEPs), every merged pull request since October 2024 with the files it changed, and the reviews and /lgtm and /approve comments on those pull requests from the GitHub API. I only store GitHub handles. No names, emails or comment text.
The whole product is about comparing two sides of the same graph:
(:Person)-[:APPROVER_OF]->(:Dir) what the records say (:Person)-[:REVIEWED|LGTM|APPROVED_PR|AUTHORED_PR]->(:PR)-[:TOUCHES]->(:Dir) what people actually do (:Dir)-[:INSIDE]->(:Dir) OWNERS inheritance The reason it needs a graph is inheritance. If a folder has no approvers of its own, approval falls back to the nearest parent folder that has some, and so on up the tree. In Cypher, "everyone who reviewed anything at or below this folder" is a single pattern:
MATCH (p:Person)-[:REVIEWED|LGTM|APPROVED_PR]->(pr:PR)-[:TOUCHES]->(:Dir)-[:INSIDE*0..10]->(d:Dir {path: $p}) In SQL that's a recursive query I really wouldn't want to maintain.
The part people would actually use
Nobody wakes up wanting to know how stale ownership is. What they want to know is who should review their change.
So that's the main feature. You paste the files you changed, and Drift ranks people by their reviews and approvals in those folders over the last six months, preferring people who know more of your change. It also shows why it picked them (this person reviewed this pull request, which changed this folder), and draws the evidence as a graph, with the inactive listed approvers in plum.
[IMAGE: upload evidence-graph.png here]

Measuring it, and the bug
Screenshots can look convincing without proving anything, so I tested it against what actually happened.
I took 400 pull requests merged between July and October 2026. For each one, Drift only got to see activity from before that pull request (never including the author), and I checked whether its top three included someone who really typed /approve on it. The comparison is what a contributor does today, which is pick from the OWNERS files.
My first run said 86%. That felt too good, so I wrote a quick leak check. For a sample of pull requests, was any of the evidence from after the merge? Some of it was.
It took a while to find the cause, and it turned out to be FalkorDB itself. If you put a range index on a string property, a range filter ignores its upper bound:
CREATE INDEX FOR (p:PR) ON (p.s); MATCH (p:PR) WHERE p.s >= '2026-09-05' AND p.s < '2026-09-10' RETURN count(p) // 15, expected 5 The same query on an integer property is fine. I'd been filtering on date strings, so "before this pull request" was quietly letting the future in. I checked it on FalkorDB 4.18.3 and 4.22.1, wrote a short script that reproduces it, and switched every date filter to integers (PR.day = 20260910). There's a test now that fails if the leak ever comes back.
Here's the honest result:
| Who was in the top 3? | Right person first | Right person in top 3 |
|---|---|---|
| Drift | 53% | 78% |
| Three people from the OWNERS files | 13% | 37% |
| The three busiest approvers overall | n/a | 46% |
So it's about twice as good as the records. I kept the 86% story in the README on purpose. Honestly, the wrong number taught me more than the right one did.
Drafting the fix, and testing it on a copy
Pointing at problems only gets you so far, so Drift also drafts fixes. For the folders that have drifted most, it moves inactive approvers to emeritus_approvers (Kubernetes' own way of saying someone stepped back) and adds people who actually review there. It edits the real file text, keeps the comments, and exports a patch that passes git apply --check, with the evidence next to each line.
But removing approvers can change where approval requests end up, and I didn't want Drift suggesting something that quietly breaks a folder. This is where GRAPH.COPY became my favourite FalkorDB feature. Before you download anything, Drift copies the whole graph, applies the patch to the copy, runs the same "where does approval go?" query on both, compares them and deletes the copy. In code it's pretty small:
def with_copy(tools, edit): before = coverage(tools) # where does every approval go today? name = f"whatif_{uuid.uuid4().hex[:8]}" tools.g.copy(name) # GRAPH.COPY: a full, isolated copy try: copy = Tools(name) edit(copy) # e.g. move inactive approvers to emeritus after = coverage(copy) # the same query, on the copy finally: graph(name).delete() # the copy never outlives the request return diff(before, after) And coverage is one query that walks each folder up its ownership chain:
MATCH (d:Dir {repo: 'kubernetes'}) WHERE d.has_owners MATCH chain = (d)-[:INSIDE*0..10]->(o:Dir) OPTIONAL MATCH (p:Person)-[:APPROVER_OF]->(o) RETURN d.path, length(chain), o.path, o.no_parent_owners, collect(DISTINCT p.handle) For the 40 drafted fixes, 12 areas whose approval requests only reached inactive people get unstuck, and none get stuck. It takes a couple of seconds and never touches the real graph.
The same trick answers "what if?" questions. If one particular approver stepped back, 6 areas would be left waiting on inactive people. For 50 of the 60 busiest approvers, nothing would get stuck at all. Kubernetes turned out to be more resilient than I expected, with a few sharp edges.
The real teams, and a claim I dropped
The official labels say which team owns each folder. I wanted to know which folders are really worked on by the same group of people, so I linked two folders whenever the same people work in both, and ran FalkorDB's algo.labelPropagation on that.
It found 19 working groups, and 13 of them cross the official team lines. To check they weren't just noise, I built the groups from activity before July and tested them on later pull requests. 47% of folder pairs changed together in a pull request landed in the same group, compared with 15% by chance.
I also tested a second idea, that pull requests crossing groups need more approvers. The first time I ran it, it looked like three times as many. Then I re-ran it on a fresh download of the data and the effect disappeared. So I dropped it, and the README says so. It was the most exciting finding I had, which is exactly why I didn't trust it.
The agent never writes Cypher
Drift has an Ask box and an MCP server, so Claude Desktop or Cursor can use the same tools. I followed the approach from the organisers' guide on agents that reason over graphs. The model picks one of eight fixed queries and fills in the arguments, but never writes a query itself. Every folder, person or KEP it mentions is looked up in the graph before anything runs, so a typo gets "did you mean…" instead of a confident wrong answer. Answers are saved back into the graph as (:Finding) nodes, linked to what they're about, so next time Drift can tell you what's changed.
The piece I'd copy into any project is the check on the model's sentence. It's just a few regular expressions and set differences:
HANDLE = re.compile(r"@([A-Za-z0-9][A-Za-z0-9-]{0,38})") def check(text, answer): invented = {h.lower() for h in HANDLE.findall(text)} - known_handles(answer) if invented: return False, f"named people not in the result: {sorted(invented)}" # ...the same for folders, KEP numbers, and any number of two digits or more return True, "ok" If a sentence fails, the reader gets a plainer one built straight from the query results instead. I'd rather Drift sound a bit flat than say something wrong.
Things that went wrong (the less glamorous ones)
Not everything was a clever bug. My very first build failed with "connection refused" because Docker wasn't running. FalkorDB Cloud doesn't allow the KEYS command, which one of my tests used, so I switched it to GRAPH.LIST. My first deployment landed in a US region and felt slow from India, so I rebuilt it in Singapore, next to the database in Mumbai. And the CI badge was red for a whole day before I noticed that GitHub runs pytest slightly differently from how I ran it locally, so it couldn't even find my code. It was one missing line in pytest.ini.
What I learned
Compare against what people actually do today, not against a strawman. The honest question was never "graph vs RAG". It was "is this better than what a contributor already does?"
Be suspicious of good numbers. The 86% was a bug, and the leak check that caught it took about ten minutes to write.
Throw out results that don't reproduce, especially the exciting ones.
And graph databases earn their place through the shape of the problem as much as speed. Inheritance chains, two snapshots of the organisation in one database, and copy-edit-compare what-ifs are what made this a graph problem for me.
Five things you can reuse on your own graph project
- Keep the past as a second graph. FalkorDB holds many graphs in one instance, so a snapshot from a year ago can sit next to today's. Run the same queries on both and you can test predictions against what really happened.
- Copy, edit, compare.
GRAPH.COPYturns any "what if?" into the same query run twice. Delete the copy in afinallyblock. - Test with a time split, then try to break it. Only give the system what it would have known at the time, and write a leak check. Mine caught a real bug.
- Filter date ranges on integers. Until the string-index bug is fixed, store
20260910rather than"2026-09-10"for anything you filter by range. - Let the model choose queries, never write them. Use fixed, parameterised queries, look names up in the graph first, and check every sentence against the result.
Try it
- Live: https://drift-egny.onrender.com (it's on free hosting, so the first load can take a minute). Click Find a reviewer, then Try an example, or type
What if @deads2k stepped back?into the Ask box. - Code: https://github.com/oscarosk/drift. It runs locally with Docker, and the README has the full graph model and every Cypher query.
If you look after an OWNERS or CODEOWNERS file, I'd love to know whether yours has drifted too. Let me know in the comments.
Built for Graph Hacks: Context for AI Agents by WeMakeDevs and FalkorDB. I used Claude as an AI coding assistant while building it; there are details in the README.




