Your Knowledge Graph Is Making Your Agent Dumber
What a code knowledge graph actually buys you, what it costs, and the one structural property of your codebase that decides whether it works at all.
Why this write-up exists
Graphify turns a folder into a knowledge graph — nodes for files, symbols, and concepts; edges for imports, calls, containment, and LLM-inferred relationships; community detection on top; and a query interface that traverses the graph to answer natural-language questions about the codebase.
The pitch is compelling: build once, query forever, and your agent stops burning context on grep. Persistent structural memory instead of repeated blind search.
I ran it head-to-head against ordinary ripgrep + Read exploration on a live TypeScript monorepo — a Bun/Turbo workspace with 605 files, ~445k words, four apps and four packages, under active daily development. Three real questions, measured on output quality, tokens consumed, and correctness.
The result is not “it’s bad.” The result is that graphify is two products in one binary, and they have opposite value:
The build artifacts (
GRAPH_REPORT.md, hyperedges, community labels) produced genuine insight I could not have gotten from grep.The query interface (
graphify query,graphify explain) lost torgon all three questions, and explain returned a confidently-wrong answer.
Understanding why the second half fails is the useful part, because the failure is structural, predictable, and tells you in advance whether it will happen to your repo too.
What the graph actually contains
Before judging output, look at the data. From graphify-out/graph.json:
nodes 1842
links 3722
hyperedges 17
communities 103
built_at_commit 098b8bfNode types:
code 1520 files, functions, classes, types extracted by AST
concept 164 LLM-extracted ideas from docs
document 110 markdown files, plan docs
rationale 48 LLM-extracted "why" statementsEdge relations, sorted by count:
contains 1278
imports 1092
imports_from 462
calls 421
references 282
conceptually_related_to 39
method 32
extends 28
rationale_for 21
shares_data_with 16
implements 16
semantically_similar_to 15
indirect_call 11
re_exports 9Note the shape of that table. 76% of all edges are contains + imports + imports_from. Those come from the AST — they are free, exact, and boring. The semantically interesting relations (shares_data_with, semantically_similar_to, conceptually_related_to, implements) total 86 edges, under 3% of the graph.
Extraction honesty is good, and this is a real strength: the report separates EXTRACTED (98%) from INFERRED (2%, 58 edges, average confidence 0.81) from AMBIGUOUS (0%). You can tell which claims are AST-grounded and which are the LLM guessing. Most tools in this space do not label their guesses.
Parsing gotcha The report says “provenance,” but the JSON field is
confidencewith values"EXTRACTED"/"INFERRED", plus a separate numericconfidence_score. There is noprovenancekey — all 3722 links returnNonefor it. Small thing, but it will silently break your first script.
Part 1 — Where graphify genuinely wins
1.1 Hyperedges over a plan directory: the standout feature
This repo keeps numbered plan files in plan/ and plan/done/ — one markdown file per feature, ~110 of them accumulated over months. Graphify’s hyperedges (group relationships spanning 3+ nodes) clustered them into named programs of work:
graphify-out/GRAPH_REPORT.md — Hyperedges, excerpted
**Notifications subsystem (trigger, send, audit, FCM/Resend delivery)**
plan_done_0041_trigger_notifications_from_run_lifecycle
plan_done_0042_web_fcm_token_registration
plan_done_0043_notification_audit_insert_failure
plan_done_0044_authorize_notifications_send
plan_done_0045_notification_audit_success_semantics
plan_done_0046_push_token_reassignment
plan_done_0047_notifications_db_access_layer
plan_done_0048_fcm_batch_and_token_indexes [EXTRACTED 1.00]
**Single-user-identity migration batch (uid to users.id)**
plan_done_0038_provision_user_on_first_auth
plan_done_0039_onboarding_profile_keyed_by_user
plan_done_0040_hired_workers_keyed_by_user
plan_done_0041_source_connections_keyed_by_user
plan_done_0042_enforce_single_user_identity_check [EXTRACTED 1.00]
**Onboarding Persistence Bug Chain**
plan_done_0061_onboarding_persistence
plan_done_0064_fix_onboarding_save_500
plan_done_0066_api_migrate_on_boot_dev
plan_done_0065_api_db_unreachable_graceful [EXTRACTED 1.00]
**Zoho credential-to-tool chain (vault, connector, OAuth provider, callback)**
plan_done_0060_oauth_source_connection
plan_done_0070_mcp_connector_pattern
plan_0090_zoho_books_oauth_provider
plan_0109_oauth_callback_redirect_web_origin
plan_done_0083_price_list_import_hire_setup [INFERRED 0.85]This is real value and grep cannot produce it. “Onboarding Persistence Bug Chain” links four plan files written weeks apart, with different titles, that nobody labeled as a group — the tool inferred that 0061 created a bug, 0064 fixed a symptom, and 0065/0066 fixed the underlying causes. Reconstructing that by hand means reading 110 plan files.
The Zoho chain in this project is the practically useful one: it correctly ties a plan that is still open (0109) to the four completed plans it depends on. If you are picking up that work cold, that hyperedge is the briefing.
Generalizable lesson Hyperedges work here because the corpus has many small, topically coherent, prose-heavy documents accumulated over time. Plan directories, ADR directories, RFC archives, incident postmortems, design docs. This is where an LLM-built graph earns its build cost. It does not need to be precise, because you are going to read the linked documents anyway — it only needs to be a good pointer.
1.2 Community labels as an onboarding map
103 communities, each given a human-readable label:
Community 0 — "Shared Zod API Schemas" (71 nodes)
Community 2 — "Zoho MCP Server" (36 nodes)
Community 5 — "Database Data Layer" (49 nodes)
Community 7 — "Architecture Layer Rules" (49 nodes)
Community 9 — "Mail and Push Notifications" (29 nodes)
Community 13 — "Notifications Subsystem Plans"For a newcomer, that list is a better table of contents than the directory tree, because it crosses directory boundaries. “Mail and Push Notifications” groups createMailer() and FcmPushChannel and MulticastSender — an interface and two implementations that live in different packages.
Read the cohesion numbers before trusting the labels, though. See §2.5.
1.3 Import cycle detection
## Import Cycles
- None detected.One line, genuinely useful, and a real answer to a real question. Worth the build on its own for a monorepo where layer discipline matters. This is a graph algorithm doing what graph algorithms are good at.
1.4 Query-time cost is effectively zero
Once built, queries are local BFS over an in-memory graph. No LLM call, no network:
graphify query "..." → 0.14s user, 0.04s system, 0.2s wallThere is no per-query API charge. Whatever criticism follows, it is not “queries are expensive to run.” They are expensive to read, which is a different problem (§2.6).
1.5 It records the commit it was built from
graph.json contains built_at_commit. Most tools in this space do not record this. It is the hook that makes staleness detectable — see §3.1 for the guard script this enables.
Part 2 — Where it fails, and why
2.1 query returns nodes, not answers
The docs frame graphify query as answering questions. It does not. It returns a ranked node dump. Here is a real, unedited response to a question about how OAuth providers are wired:
Test 1 — OAuth provider registration
$ graphify query "How is an OAuth source connection provider registered and used end to end?"
Traversal: BFS depth=2 | Start: ['SourceConnectionOauthService', 'provider', 'Connection',
'POST /notifications/send endpoint', 'OAuth callback redirects to web app origin',
'source-connections.ts', 'unusedLlm'] | 170 nodes found
NODE index.ts [src=packages/shared/src/index.ts loc=L1 community=0]
NODE app.ts [src=apps/api/src/app.ts loc=L1 community=6]
NODE index.ts [src=packages/db/src/index.ts loc=L1 community=5]
NODE createApp() [src=apps/api/src/app.ts loc=L215 community=6]
NODE page.tsx [src=apps/web/src/app/(app)/settings/page.tsx loc=L1 community=4]
NODE index.ts [src=packages/llm/src/index.ts loc=L1 community=26]
NODE workers.ts [src=apps/agents/src/workers.ts loc=L1 community=34]
...
NODE ObjectStorage [src=packages/storage/src/index.ts loc=L13 community=33]
NODE computeQuote() [src=packages/shared/src/quote.ts loc=L56 community=39]
NODE StripeBilling [src=apps/api/src/billing.ts loc=L9 community=44]
NODE LLMError [src=packages/llm/src/index.ts loc=L62 community=47]
NODE Firebase Cloud Messaging (FCM) [src=plan/done/0042-web-fcm-token-registration.md]
... (truncated — 97 more nodes cut by ~2000-token budget)170 nodes. The question was about OAuth. The top of the ranking is index.ts, app.ts, index.ts, createApp(), page.tsx — barrel files and the application entry point. Stripe billing, object storage, LLM errors, and Firebase Cloud Messaging all rank above most of the OAuth code. Roughly 8 of 170 nodes are on-topic.
The baseline, on the same question:
Baseline — ripgrep, 0.01s
$ rg -n "isSupportedConnectionProvider|ConnectionOauthService|provider" \
apps/api/src/source-connections.ts
13: // The providers that HAVE an OAuth implementation. The start route guards on
22: export function isSupportedConnectionProvider(provider: string): boolean {
103: export type SourceConnectionOauthService = {
179: export function createConnectionOauthService(input: {
187: const redirectUri = (provider: string) =>
188: `${input.publicBaseUrl}/connections/oauth/callback/${provider}`;
190: const providers = new Map<string, ProviderOauthConfig>();
192: providers.set(GOOGLE_SHEETS_PROVIDER, { ... });
203: providers.set(ZOHO_BOOKS_PROVIDER, { ... });
215: const configFor = (provider: string): ProviderOauthConfig => {0.01s, 35 lines, and the architecture is legible from the output alone: a registry keyed by provider string, populated conditionally on credentials, with a shared redirect URI shape. That is the answer to “how is a provider registered.”
The graph query is not competing with an answer engine. It is competing with rg, and on a targeted question rg returns strictly more information per token.
2.2 The core defect: hub poisoning in a barrel-export monorepo
This is the important section. The bad rankings above are not a tuning problem, they are a graph-topology problem, and it is predictable from your repo’s structure.
Top nodes by degree:
1. 176 index.ts packages/shared/src/index.ts
2. 170 app.ts apps/api/src/app.ts
3. 160 index.ts packages/db/src/index.ts
4. 101 api.ts apps/web/src/lib/api.ts
5. 80 createApp() apps/api/src/app.ts
6. 70 page.tsx apps/web/.../settings/page.tsx
8. 60 cn() apps/web/src/lib/utils.tsThose top three are barrel files. In a TypeScript monorepo, packages/shared/src/index.ts re-exports every schema in the package, and every consumer imports from the package root. The AST extractor faithfully records this: 176 edges through one node.
Now combine that with the traversal strategy — BFS depth=2. I measured the reachable set:
Depth-2 blast radius, measured from graph.json
depth-1 from createApp() → 80 nodes
depth-2 from createApp() → 275 nodes (15% of graph)
depth-1 from notifications.ts → 21 nodes
depth-2 from notifications.ts → 465 nodes (25% of graph)Start anywhere near a barrel file and two hops reach a quarter of the codebase. notifications.ts has only 21 direct neighbors, but one of them is a barrel, and the barrel opens onto everything.
This is textbook small-world topology: high-degree hubs collapse the graph diameter. Depth-2 BFS is a reasonable default on a sparse graph and a catastrophic default on a graph with degree-176 hubs. And then the ranking is degree-influenced, so the same hubs that flooded the result set also sort to the top of it.
The seeds are good. The traversal destroys them. Look at the third test — a query about a specific notification bug:
Test 3 — notification audit failure
$ graphify query "notification send audit failure" --budget 600
Traversal: BFS depth=2 | Start: ['notification_sends table', 'SendNotificationRequest',
'sendNotificationRequestSchema', 'auditLog', 'Plan: Fix notification audit insert failure',
'notifications.ts', '.send()'] | 130 nodes found
NODE index.ts [src=packages/shared/src/index.ts]
NODE app.ts [src=apps/api/src/app.ts]
NODE index.ts [src=packages/db/src/index.ts]
NODE createApp() [src=apps/api/src/app.ts]
NODE app.test.ts [src=apps/api/src/app.test.ts]
NODE workers.ts [src=apps/agents/src/workers.ts]
...
NODE dunning.ts [src=apps/agents/src/dunning.ts]
NODE einvoice.ts [src=apps/agents/src/einvoice.ts]The seed selection is excellent. notification_sends table, Plan: Fix notification audit insert failure, notifications.ts — those seven seeds are precisely the right seven things. If the tool had printed the seeds and stopped, it would have won the comparison outright.
Instead it expanded to 130 nodes, and einvoice.ts and dunning.ts — unrelated agent workers, reached through the shared db barrel — outrank the seeds in the final output.
Then --budget 600 truncates the list. But truncation happens after the bad ranking, so the budget flag cuts the relevant tail and keeps the irrelevant head. Lowering the budget makes results worse, not tighter.
Predictive rule If your repo has barrel files, a shared types package, a DI container, a God app-factory, or a
utils.tseveryone imports — import-graph traversal will produce this failure. Roughly: every TypeScript monorepo, every large Python package with a fat__init__.py, every Java project with a widecommonmodule. If your repo is genuinely modular with narrow interfaces between components, traversal will work far better.
2.3 Stale graphs report dead symbols with full confidence
The graph records built_at_commit, but nothing checks it at query time. On this repo:
graph @ 098b8bf HEAD @ a515c6e drift: 6 commits, 10 filesBlast radius of those 10 files, measured against the graph:
Staleness after one day
nodes living in changed files: 251 of 1842 (13.6%)
edges touching those nodes: 874 of 3722 (23.5%)Nearly a quarter of the edge set is suspect after one day of normal work. Now watch what that does to a lookup:
graphify explain — on a symbol that no longer exists
$ graphify explain "createGoogleConnectionOauthService()"
Node: createGoogleConnectionOauthService()
ID: apps_api_src_source_connections_creategoogleconnectionoauthservice
Source: apps/api/src/source-connections.ts L151
Type: code
Community: 38
Degree: 4
Connections (4):
<-- config.ts [imports] [EXTRACTED]
<-- source-connections.ts [contains] [EXTRACTED]
<-- loadConfig() [calls] [EXTRACTED]
--> createCredentialVault() [calls] [EXTRACTED]
$ rg -c "createGoogleConnectionOauthService" .
NOT FOUND in repoThat function does not exist. It was renamed to createConnectionOauthService (now at L179) when a second OAuth provider was added. The graph reports the dead symbol, a stale line number, and four relationships — all stamped EXTRACTED, the tool’s highest-confidence label.
The timing is the part worth sitting with:
Stale before it finished writing
graph.json written 2026-07-24 00:48:15
commit fb508fe landed 2026-07-24 00:48:55Forty seconds. That is not a pathological case, that is a normal working day — you kick off a build, keep coding, and the artifact is behind by the time it lands.
The failure mode is worse than “outdated.” An agent reading EXTRACTED treats it as AST-verified ground truth and will happily write code calling a function that no longer exists. Wrong data wearing a confidence badge is more dangerous than no data, because it removes the prompt to go look.
Mitigation exists — see §3.1 and §3.2 — but it is opt-in, and nothing in the default query path warns you.
2.4 Degree centrality mislabels utilities as “core abstractions”
The report’s God Nodes section is headed “most connected — your core abstractions”:
1. createApp() - 80 edges 6. Button() - 26 edges
2. cn() - 60 edges 7. Card() - 17 edges
3. useAuth() - 38 edges 8. quotePipeline() - 15 edges
4. react - 33 edges 9. loadConfig() - 14 edges
5. Db - 29 edges 10. CardContent() - 14 edgescn() is the four-line Tailwind classname joiner. Button() and Card() and CardContent() are shadcn primitives. react is a dependency. Six of the top ten are leaf utilities with high fan-in — the opposite of core abstractions. They are depended upon by everything and depend on nothing, which is exactly what a well-factored leaf looks like.
Degree centrality cannot distinguish “central to the design” from “imported a lot.” The metric that would — betweenness centrality, which measures how often a node lies on the shortest path between other nodes — would surface Db, createApp(), loadConfig(), and quotePipeline() while dropping cn() and Button() to the floor. Betweenness is O(V·E) and therefore slower, but on a 1842-node graph that is milliseconds.
Practical effect: the list is still useful, but read it as refactor blast radius (”changing cn()‘s signature touches 60 places”), never as architecture (”cn() is a core abstraction”). Those are different questions and the header answers the wrong one.
2.5 Community labels oversell weak clusters
Every community carries a cohesion score. Here are the first ten:
Community 0 — "Shared Zod API Schemas" cohesion 0.03 (71 nodes)
Community 1 — "Hire Flow UI" cohesion 0.06 (48 nodes)
Community 2 — "Zoho MCP Server" cohesion 0.05 (36 nodes)
Community 3 — "Infra and OAuth Provisioning Plans" cohesion 0.05 (56 nodes)
Community 4 — "Settings Page UI" cohesion 0.08 (38 nodes)
Community 5 — "Database Data Layer" cohesion 0.06 (49 nodes)
Community 6 — "API App and Routes" cohesion 0.08 (49 nodes)
Community 7 — "Architecture Layer Rules" cohesion 0.07 (49 nodes)
Community 8 — "Web API Client" cohesion 0.06 (45 nodes)
Community 9 — "Mail and Push Notifications" cohesion 0.08 (29 nodes)Cohesion 0.03–0.08 across the board. Whatever the exact normalization, a range that narrow and that close to zero means the partition is barely better than arbitrary — these clusters are not densely interconnected, they are thin slices of a hairball.
But the labels are excellent, because they are LLM-generated from the member node names. So you get confident, plausible, well-written names attached to statistically weak groupings. “Shared Zod API Schemas” reads like a discovered architectural boundary. It is 71 nodes at cohesion 0.03 — which mostly reflects that they all live in one file.
This is a general hazard of LLM-labeled clustering, not specific to graphify: the label quality is decoupled from the cluster quality, so bad clusters get good names and you cannot tell by reading. Always check the cohesion number before citing a community as evidence of anything.
Where the labels do earn their keep is navigation (§1.2) — a wrong-but-plausible label still points you at 40 files worth skimming. Just don’t cite it in a design doc.
2.6 The token economics are inverted for agent use
The pitch is that a graph saves agent context versus repeated grep. Measured, on this repo, it is the reverse:
approach wall tokens on-target
graphify query 0.20s ~2000 ~5%
graphify query --budget 900 0.20s ~900 ~13%
rg (targeted) 0.01s ~300 ~100%
ls packages/*/AGENTS.md 0.01s ~40 100%Read that as four rows of the same job done four ways. Each row down is faster, cheaper, and more accurate than the one above it.
Graphify’s query cost is not the API bill — it is the noise injected into the context window. 2000 tokens of mostly-irrelevant node names is worse than 300 tokens of exact matches, because the irrelevant nodes are plausible-looking file paths that invite follow-up reads.
The fourth row is the one that stings. The second test question was “what are the architecture layer rules and which packages may import which?” Graphify returned 30 nodes, of which 26 were web UI components — button.tsx, cn(), price-list.ts, house-rules.ts — and the 4 relevant ones ranked last:
NODE packages/shared Layer Rules (Contract Layer) [src=packages/shared/AGENTS.md]
NODE packages/llm Layer Rules [src=packages/llm/AGENTS.md]
NODE packages/db layer rules [src=packages/db/AGENTS.md]It named the files but included none of their content. Meanwhile:
$ ls packages/*/AGENTS.md apps/*/AGENTS.md
apps/agents/AGENTS.md packages/db/AGENTS.md
apps/api/AGENTS.md packages/llm/AGENTS.md
apps/mcp-zoho/AGENTS.md packages/shared/AGENTS.md
apps/web/AGENTS.md packages/storage/AGENTS.mdEight files, one call, and cat gives the actual rules. Which points at the deepest problem:
2.7 It reconstructs, imperfectly, what good repos already write down
This repo maintains eight hand-written AGENTS.md layer-rule files, a 63KB DOCS.md, a GATES.md, and 110 numbered plan documents. Those artifacts are authored, reviewed, versioned, and correct.
Graphify’s community #7 is literally named “Architecture Layer Rules” — it is a lossy, LLM-inferred, timestamped-and-decaying copy of files that already exist in the repo, are always current, and can be read directly.
Inferring structure is valuable exactly in proportion to how much structure is missing. On an undocumented legacy codebase, that is a lot. On a repo with a disciplined docs culture, the graph is competing with better source material and losing.
2.8 Build cost
{
"runs": [
{ "date": "2026-07-14T05:20:42Z", "input_tokens": 1030745, "files": 519 },
{ "date": "2026-07-23T20:49:06Z", "input_tokens": 163040, "files": 378 }
],
"total_input_tokens": 1193785
}1.19M input tokens across two builds. The second run was 6× cheaper than the first, so caching and incremental extraction clearly work. Still, the first build on a 605-file repo is a real charge, and the tool warns you about it up front, which is the honest thing to do:
## Corpus Check
- Large corpus: 605 files · ~445,122 words. Semantic extraction will be expensive
(many Claude tokens). Consider running on a subfolder.Amortized over months this is negligible. Amortized over a graph that goes stale in 40 seconds and is rebuilt weekly, less so. The economics depend entirely on whether you set up incremental updates (§3.2).
Part 3 — How to actually use it
None of the above says “don’t use it.” It says use the half that works and automate around the half that doesn’t.
3.1 Add a staleness guard before you trust anything
built_at_commit is in the JSON. Use it. This script is verified working on the repo above:
python3 -c "
import json, subprocess
built = json.load(open('graphify-out/graph.json'))['built_at_commit']
head = subprocess.check_output(['git','rev-parse','HEAD']).decode().strip()
n = subprocess.check_output(['git','rev-list','--count',f'{built}..{head}']).decode().strip()
files = subprocess.check_output(['git','diff','--name-only',built,head]).decode().split()
print(f'graph @ {built[:8]} HEAD @ {head[:8]} drift: {n} commits, {len(files)} files')
"Output on a one-day-old graph:
graph @ 098b8bfd HEAD @ a515c6e3 drift: 6 commits, 10 filesWire it into your agent’s instructions or a pre-query hook. A graph more than a handful of commits behind should be treated as a hint generator, never as ground truth — and explain should not be trusted on code symbols at all past that point.
3.2 Use incremental update, not full rebuild
The CLI exposes cheaper paths than the full pipeline:
update <path> re-extract code files and update the graph (no LLM needed)
watch <path> watch a folder and rebuild the graph on code changes
check-update <path> check needs_update flag and notify if semantic re-extraction
is pending (cron-safe)update is the important one — AST re-extraction with no LLM cost, which fixes exactly the failure in §2.3 (renamed and moved symbols) for free. A post-commit hook running graphify update . would have prevented the dead-symbol result entirely.
Disclosure I did not run
updateduring this evaluation, because it rewritesgraph.jsonin place and I was measuring the existing artifact. The claim that it is LLM-free is from the CLI help text, not from my own measurement. Verify the cost on your own repo before relying on it.
3.3 Treat GRAPH_REPORT.md as the deliverable, query as a seed-finder
The workflow that actually pays:
Build once. Read
GRAPH_REPORT.mdend to end. Hyperedges, communities, import cycles, god nodes. This is the onboarding artifact and it is genuinely good.Query to find entry points, not answers. The seed list at the top of the query output is the most valuable line in it. Read the
Start: [...]array, ignore the node dump below it.Switch to
rgand Read immediately once you have seeds. That is where precision lives.Never use
explainon a code symbol unless the staleness guard says drift is zero. Use it onconceptanddocumentnodes, which decay far slower than code.
Concretely, on the notification question, the correct extraction from a 130-node dump was the seven-item seed list, then:
rg -n "notification_sends" packages/db/src/schema.ts apps/api/src/notifications.ts3.4 Scope the build to the corpus that benefits
The corpus check warning is right. Do not point it at the whole monorepo by reflex. On this repo the split is stark:
plan/anddocs/— high value. Prose, many small documents, no other tool cross-links them, and staleness barely matters because completed plans do not change.apps/andpackages/TypeScript — low value. Barrel-file topology defeats traversal (§2.2), AST edges duplicate whatrgand the language server already give you exactly, and code churns daily.
Building the graph over plan/ docs/ alone would have cut the token cost by most of a million and lost almost none of the value I actually got.
Verdict: who should use this
Strong fit
Repos with a large accumulated corpus of prose — plan docs, ADRs, RFCs, postmortems, research notes — that nobody has cross-indexed. Hyperedges are the killer feature and there is no cheap substitute.
Undocumented legacy codebases where inferred structure competes against nothing.
One-time onboarding or architecture review on an unfamiliar codebase, where a 60%-accurate map beats no map.
Genuinely modular codebases with narrow interfaces, where BFS traversal is not defeated by hubs.
Mixed-media corpora (papers, transcripts, images) where there is no AST and no language server to fall back on.
Poor fit
Barrel-export TypeScript monorepos, or anything else with degree-100+ hub nodes. Traversal ranking is structurally broken there and no flag fixes it (§2.2).
Repos with a strong docs culture. You are paying tokens to infer a worse copy of files you already maintain (§2.7).
Fast-moving codebases without an automated update hook. A graph that decays in 40 seconds and answers with EXTRACTED confidence is a liability (§2.3).
Precise “where is X defined / what calls X” lookups. That is what ripgrep and LSP are for, and they are exact, instant, and never stale.
Graphify is an excellent document knowledge graph and a mediocre code knowledge graph, and the tooling ships both under one command. Point it at your prose, keep grep for your code, and put a staleness guard in front of anything it tells you about a symbol.
Method notes
Everything above is measured on one repository, three queries, over one session. Specifically:
Repo: private TypeScript monorepo, Bun + Turbo, 4 apps + 4 packages, 605 files, ~445k words, under daily active development.
Graph: 1842 nodes / 3722 links / 103 communities, built 2026-07-23, evaluated 2026-07-24 at 6 commits of drift.
Degree distributions, depth-2 reachability, and staleness blast radius were computed directly from
graph.jsonrather than taken from the report.The
rgbaselines are real commands with real timings, not estimates.graphify updateandgraphify watchwere not exercised; §3.2 reports the CLI’s own description of them.
Three queries is a small sample. The topology argument in §2.2 is the part that should generalize — it follows from degree distribution and traversal depth, both of which you can measure on your own graph in a few lines. The precision numbers should not be assumed to transfer.






