Graph Hacks · built on FalkorDB
Your docs go stale at the speed you ship. I built a graph that catches it.
How I built Staleproof on FalkorDB during Graph Hacks: the graph query behind it, the agent that remembers, what it found in 63 famous repositories, and why I published that my first version was wrong four times out of five.
$ npx -y staleproof lint
AI agent instructions
AGENTS.md
3:30 error `start` is not a script in package.json. [command-missing]
4:8 error `typecheck` is not a script in package.json. [command-missing]
5:50 error The link target `src/components/Button.jsx` does not exist. [link-broken]
README and contributing guides
README.md
9:1 error `start` is not a script in package.json. [command-missing]
14:41 error The link target `src/components/Button.jsx` does not exist. [link-broken]
5 errors in 2 files (3 files checked, 601 ms).My coding agent had just renamed two scripts and moved a component to TypeScript. The tests passed. CI was green.
And five lines of documentation were now wrong, two of them in the AGENTS.md file the same agent reads at the start
of every session.
That's a demo repo I made for this post, but the pattern isn't made up. It's my week. I ship fast, mostly with AI agents, and the docs don't keep up: the code moves, the README stays where it was, and nobody notices until a user copies the quickstart and it fails on line two. I've done this to my own users more than once. So for Graph Hacks I built the thing I wanted: Staleproof, an open-source agent that finds the docs a change makes false, proves it, and fixes them.
$ npx -y staleproof fix
fixed AGENTS.md:3:30 command-missing Rename to `npm run dev` (renamed in package.json)
fixed AGENTS.md:4:8 command-missing Rename to `npm run check` (renamed in package.json)
fixed AGENTS.md:5:50 link-broken Rename to `src/components/Button.tsx` (renamed in this change)
fixed README.md:9:1 command-missing Rename to `npm run dev` (renamed in package.json)
fixed README.md:14:41 link-broken Rename to `src/components/Button.tsx` (renamed in this change)
Fixed 5 findings in 2 files; 0 findings left.That part is a careful lint. The interesting part, and the reason this is a FalkorDB project, is the version of the problem a lint can't see.
Docs are prompts now
A year ago a stale README cost you a confused user. Today it also costs you a confused agent. AGENTS.md,
CLAUDE.md, Cursor rules and Copilot instructions are read on every session, and an agent follows them more literally
than any human would.
When I ran Staleproof on popular open-source projects (more on that below), this is the kind of thing it found:
huggingface_hub'sAGENTS.mdtells coding agents to updatedocs/sources/en/guides/cli.md. The folder isdocs/source/en/guides, no "s". Every agent that follows the instruction goes looking for a directory that doesn't exist.- The
AGENTS.mdtemplate thatcrewai createcopies into every new project importspersistfromcrewai.flow.flow, a module that doesn't export it. Every project starts with an agent file whose example raisesImportError.
Stale docs used to be a papercut. When your docs are your agent's instructions, they're a bug.
The hard version: the paragraph that breaks never names what you changed
Here's a real pull request on FalkorDB's Python client:
falkordb-py#262. It changed one internal function, Is_Cluster.
No doc mentions Is_Cluster. Search the docs for it and you get nothing. Code review sees a small fix to a private
helper. And yet the README's async example was affected, because of this chain:
Is_Clusterfalkordb-pyFalkorDB.__init__falkordb-pyREADME › Asynchronous Examplefalkordb-pyTypeError: Redis.__init__() got an unexpected keyword argument 'himport_registry'Is_Cluster, and the graph finds the paragraph anyway.The README never names the function. It calls FalkorDB(), whose constructor calls the function you changed. That's
the shape of almost every real docs break I've seen, and it gets worse with more than one repository. A devtools
company ships a client, an SDK that wraps the client, and a docs site that teaches the SDK. Change the client, and the
tutorial that breaks is two repositories away, written against a function three calls up from the one in your diff.
"Is this paragraph affected by this change?" isn't a text-similarity question. Embeddings won't answer it and grep can't. It's a reachability question: is there a path from the changed function, up through its callers, possibly in another repository, to a paragraph or a code example?
That's a graph query. So I put everything in a graph.
The graph
Staleproof indexes every repository of an organisation into one FalkorDB graph:
- Code. Python, TypeScript and Go functions, classes and methods become
Symbolnodes. Calls becomeCALLSedges, resolved through imports, package re-exports, attribute types and return annotations, soself._graph.query(...)inside the GraphRAG SDK resolves toAsyncGraph.queryin falkordb-py, another repository. - Docs. Markdown, MDX and Sphinx pages become
DocSectionnodes; every fenced example becomes aCodeExamplelinked to the symbols it calls (USES), resolved exactly like code. Prose mentions becomeMENTIONS; links between pages becomeLINKS_TO. - History and memory. Pull requests, example runs, the agent's verdicts, and maintainers' decisions are nodes too.
On FalkorDB's own three repositories (falkordb-py, GraphRAG-SDK and the docs site) that's 1,298 symbols, 3,138 doc sections, 1,835 code examples and 1,708 call edges, with 1,257 symbol-to-doc paths that cross a repository boundary.
The impact of a pull request is then one query:
MATCH (pr:PullRequest {url: $pr})<-[c:CHANGED_IN]-(s:Symbol)
WHERE c.kind <> 'added'
MATCH p = (s)<-[:CALLS*0..3]-(t:Symbol)<-[:MENTIONS|USES]-(d)
WHERE (d:DocSection OR d:CodeExample) AND d.status = 'live'
RETURN d.id, length(p) AS hops,
[n IN nodes(p) | coalesce(n.qualname, n.id)] AS evidenceTwo things I love about this. First, the path is the evidence. I don't just get "this section might be affected"; I get the chain of calls that connects it to the diff, and that chain is what the reviewer sees. Second, the absence of a path is evidence too. Changed code with no path to any doc is cleared: the agent can skip it with confidence, and the reviewer gets a grep command to double-check the claim.
Here's what that buys you on FalkorDB's own pull requests. For each one, I compared the sections the graph reaches with what a reviewer can do without it, which is search the docs for the names of the changed functions:
| Sections the graph reaches | …that grep also finds | grep matches that aren't affected | |
|---|---|---|---|
| 4 pull requests, total | 120 | 30 | 227 |
| the 3 falkordb-py pull requests | 47 | 0 | 8 |
Grep misses three quarters of what matters and buries the rest in noise. On the client's own pull requests it finds
none of it, because nobody writes Cluster_Conn or _query in a tutorial. They write db.select_graph(...) and
graph.query(...), which reach the changed code only through callers.
Take FalkorDB out and Staleproof is that grep. That's the test I used for "the graph has to do real work".
A path says "maybe". A run says "yes".
A path tells you a paragraph might be wrong. To prove it, Staleproof installs the repositories from source at the commit before the pull request and at the pull request, and runs the docs example at both, in a sandbox whose only reachable server is a scratch FalkorDB.
- Passes before, fails after: the pull request broke the docs.
- Fails before, passes after: the pull request fixed them. That's #262: the README's async example fails before
the PR with
TypeError: Redis.__init__() got an unexpected keyword argument 'himport_registry'and passes after it. The PR fixed a docs example nobody knew was broken, and only the graph could connect the two. - Fails both times: already broken. falkordb-py#267 fixed the
execution-plan parser; the path
ExecutionPlan._operation_tree ← ExecutionPlan.__init__ ← Graph.explaincrossed into the docs site'sGRAPH.EXPLAINpage, whose example fails as written on a fresh server, before and after the PR: it explains a query on a graph the page never creates. Not this PR's fault. Still worth knowing.
The biggest one, falkordb-py#276, changed four asyncio symbols that reach 43 doc sections, 41 of them in other repositories. Of the 21 reachable examples run at both commits, 10 pass both times, 6 were already broken, 3 can't even be installed together at those commits (GraphRAG-SDK 1.4.0 wants a newer falkordb-py than existed there), and 2 can't run as written.
An agent that can't make things up, and remembers
On top of the graph sits an agent. It gets eight tools, each a Cypher query (or a sandboxed run) over FalkorDB:
impact, clearance, read_section, run_example, decisions_for, rank_sections (PageRank over the docs' link
graph, via algo.pageRank, so the most-read pages come first), propose_edit and record_verdict. Every tool call in
the trace shows the Cypher it ran.
Three rules make it trustworthy:
- It can't edit a paragraph without a path.
propose_editrefuses any section the graph doesn't connect to the change. The model can't hallucinate a docs fix for code it never reached. - It isn't done until every affected section has a verdict. Stale (with the edit), ok, governed by a decision, or skipped, always with a reason.
- It remembers. Verdicts, runs and maintainers' decisions are written back to the graph. A
DecisionnodeGOVERNSa symbol or a section, and a decision made on one function covers the docs of everything that calls it (CALLS*0..2thenGOVERNS), withSUPERSEDESchains so the newest decision wins. When the agent checked #267 again, it calleddecisions_forbefore judging each section, and the graph answered: "Earlier: ok on #267, ok on #276, ok on #262." The next pull request starts from what the last one learned.
My favourite moment of the whole build came from rule 2. On #276, the docs' UDF example passed before the PR and failed
after it: a textbook "this PR breaks the docs" result. The agent's verdict disagreed. It called the failure "a
test-isolation artifact: the before run registered the 'StringUtils' UDF in the shared scratch FalkorDB." It was right.
FLUSHALL doesn't remove UDF libraries, so my sandbox wasn't as clean as I thought. The sandbox now also runs
GRAPH.UDF FLUSH, and the example passes before and after. The agent caught my infrastructure lying to it.
What FalkorDB taught me
Leaning this hard on variable-length traversal finds the edges of the engine. Three things worth knowing if you build on FalkorDB:
- A false proof of absence. On FalkorDB 4.18 (the version falkordblite bundles), the anonymous pattern
WHERE NOT (s)<-[:CALLS*0..3]-(…)missed callers reachable along two routes of different length, and "proved" a symbol undocumented when a doc was three calls away. Clearance now binds the path:OPTIONAL MATCH p = …andcount(p) = 0. It's fixed in 4.22, but local and cloud disagreed until I noticed. - An exact-length traversal that returns nothing. On FalkorDB Cloud (4.22),
<-[:R*2]-(t:Label)returned no rows. It's already on the tracker as FalkorDB#2682 (found there from aWHEREfilter; I hit it from a label alone). Every query now binds its path and starts ranges at 0 or 1, and a test keeps it that way. FLUSHALLkeeps UDFs. See above.GRAPH.UDF FLUSHis your friend.
And the things that made it a joy: Cypher paths that come back as evidence, millisecond answers (a whole graph-mode
check, five tools and thirteen queries, takes about a second on the free Cloud tier), algo.pageRank in the database,
and many graphs on one instance. The hosted service keeps one graph per GitHub owner and one per GitHub App
installation, and uses GRAPH.MEMORY USAGE to evict the least-recently-used ones inside the 100 MB free tier.
The day I learned my tool was wrong four times out of five
By the morning of the last day I had a tool I was proud of. I'd tested it on 22 repositories, seen no false alarms at error level, and said so.
Instead I did the thing I should have done first. I picked 63 popular repositories Staleproof had never been tuned on (31 Python, 27 TypeScript, 5 Go; LangChain, Hugging Face, Gradio, Prefect, TanStack, FastAPI and friends), pinned each to a commit, ran it on 54,937 files, and checked every warning and error against the repository at that commit.
Version 0.6.1 was right 19% of the time. 1,255 real drifts, and 5,260 false positives.
My 22-repository test had been a selection effect: repositories I'd picked, with docs I'd already learned to read. In the wild, docs sites serve pages in ways a repository doesn't show (their own heading slugs, site-root paths, generated API pages), TypeScript packages export through build output, and half of every big docs folder is release notes and migration guides that describe an API on purpose as it used to be.
A docs checker that cries wolf gets uninstalled. So 0.8.0 made precision the product:
- Rule maturity. A rule stays on by default only if it's right at least 80% of the time on those repositories. The rest still report, at info, but never fail a check or lower a score until a repository opts in.
- Guards for every class of false positive, 14 classes in all: 2,504 false positives fixed outright, 2,718 moved to info.
- A "False positive?" link on every finding, which opens a prefilled issue. Each report becomes a regression fixture.
Because the guards were written against those 63 repositories, re-measuring on them would flatter me. So I ran 18 more repositories that weren't used for any tuning, linted them only after the guards were written, and checked every finding. 33 of the 34 findings 0.8.0 shows by default were real drift (broken links 28 of 28, stale docstring parameters 5 of 5).
The whole record, false positives included, is public in the Drift Hall. Some of what's in there:
- 17 TanStack/table package READMEs link a
CONTRIBUTING.mdthat only exists at the repository root. - werkzeug documents
Headers.setlist(key, values)with:param value:. The parameter isvalues. - gemini-cli's docs link sections that were renamed when a page dropped its numbering.
- huggingface_hub's and crewAI's agent files, above.
Across the 63, there was real drift in 44. I've drafted a small fix for each project; none are sent yet, because a fix PR to someone else's repository deserves a human reading it first.
Publishing "my tool was 19% precise" during a hackathon felt terrible for about ten minutes. Then it felt like the most useful thing I'd built all week.
Built in public, in four days
Graph Hacks ran October 15 to 18. Staleproof went from an empty folder to this:
- Eight releases on npm in 36 hours, from 0.1.0 to 0.8.0. It was called Lockstep until day three.
- A GitHub App: install it once, and every pull request gets a check that marks the docs it makes wrong, with suggested changes and an Apply fixes button. No workflow file, no secrets.
- A GitHub Action that also runs your docs examples before and after each pull request, in your own CI.
- An MCP server at
staleproof.dev/api/mcp, so Claude, Cursor or VS Code can ask "which docs can this change make false?" before they commit. - A Claude Code plugin that tells Claude, after every edit, which docs and agent files it just made wrong, and asks it to fix them before it stops.
- A VS Code and Cursor extension, pre-commit hooks, and
npx staleproof. - A docs audit for any public repository, with a badge: paste one at staleproof.dev/#/audit.
Try it
In your own repository, no account, nothing leaves your machine:
npx -y staleproof lint
npx -y staleproof fixOn every pull request: install the GitHub App or add the Action from staleproof.dev/#/install.
In your coding agent:
claude mcp add --transport http staleproof https://staleproof.dev/api/mcpOr paste any public pull request at staleproof.dev and watch the trace: every tool call and the Cypher it ran on FalkorDB.
What I'd tell you if you're shipping fast
- Your docs are part of your build now. If an agent reads them, they're instructions. Lint them like code.
- The bug is rarely where the name is. The paragraph that breaks calls something that calls something you changed. That's a graph, so query it like one.
- A path is a claim; a run is a proof. Run the example before and after. "Already broken" is a finding too.
- Measure on code you didn't choose. My own test set said "no false alarms". Strangers' code said 19%. Only one of those numbers was true, and publishing it made the tool better in a day.
Staleproof is MIT-licensed and on GitHub at rajkaria/staleproof. If it flags something that isn't real in your repository, click "False positive?". That report becomes a test, and the next version is right where this one wasn't.
Built for Graph Hacks by WeMakeDevs × FalkorDB. I wrote Staleproof with Claude Code (Anthropic) as my coding assistant, including parallel agents for implementation, tests, docs and the first pass over the Drift Hall's findings, under my direction and review. Every number in this post comes from the repository's FACTS.md (generated from the graph) or the Drift Hall.