M01 Β· Knowledge Graph Fundamentals
Every tool in this course β Graphify, CodeGraph, okf-rs, even OKF's humble Markdown links β is built from the same seven primitives: nodes, typed edges, traversal, multi-hop paths, communities, centrality, and provenance. In this module you build all seven by hand, in ~40 lines of dependency-free code, over the orderflow repo. When a tool's README later says "Leiden community detection identified 13 subsystems," you'll know exactly what happened.
Learning objectives
By the end of this module you will be able to:
- Model a codebase as a directed graph of typed edges and explain why "code is a social network, not a book."
- Contrast deterministic traversal with probabilistic search, including the cost model of each.
- Explain what communities and god nodes reveal about software architecture β and what computing them costs at scale.
- Compute a blast radius (transitive impact set) with a breadth-first search you wrote yourself.
- Apply the EXTRACTED / INFERRED / AMBIGUOUS provenance discipline that separates honest graphs from confident guessers.
Nodes, edges, and why the types matter
Before: think of how you read a novel β front to back, page by page, because a book's structure IS its reading order. Now think about how you actually navigate a city: nobody reads a city front to back. You use a map of places and the roads between them, and you care what KIND of road each one is β footpath, one-way street, highway.
The pain: your IDE β and a mapless AI agent β treats code like the novel: files and line numbers, read in order. But nobody experiences a codebase that way. You experience it as "this function calls that one," "this class inherits from that one," "this module imports those three." Reading files front-to-back to answer a relationship question is like reading every street sign in a city to find one address.
The mapping: a knowledge graph is the city map for code. Functions, classes, and modules are the places (nodes). Calls, imports, and inheritance are the roads (edges) β and each road is labeled with its kind and its direction. One article in this course's corpus put it perfectly: your code isn't a book; it's a social network of narcissistic objects all trying to talk over each other. A graph is just the honest picture of who talks to whom.
A graphA data structure made of nodes (things) and edges (relationships between pairs of things). Nothing more exotic than that. is a set of nodes and a set of edges connecting pairs of nodes. For code, the nodes are program entities: modules, classes, functions, methods β and in richer graphs, database tables, API routes, documents, even diagrams.
The edges are what elevate this above a file listing, and two properties do the work. First, edges are directed: verify_token β decode_jwt means verify_token calls decode_jwt, and emphatically not the reverse β direction is the difference between "what do I depend on?" and "who depends on me?", two completely different questions. Second, edges are typed: calls, imports, inherits, implements, publishes_to. A typed edge is a fact with a precise meaning; an untyped line between two boxes is just decoration.
Here is what one node and one edge actually look like in the machine-readable form every tool in this course emits β this exact shape comes from the course's own extractor running on orderflow:
{
"nodes": [
{ "id": "function:shared.auth.decode_jwt",
"kind": "function",
"name": "decode_jwt",
"file": "shared/auth.py", "line": 16 }
],
"edges": [
{ "source": "function:shared.auth.verify_token",
"target": "function:shared.auth.decode_jwt",
"type": "calls",
"provenance": "EXTRACTED" }
]
}Read that edge aloud and you get a sentence: "verify_token calls decode_jwt β and we extracted that fact directly from the source." A graph is a database of such sentences. Everything else in this course is machinery for producing, storing, serving, and trusting them.
"A knowledge graph needs a graph database." β No. Every tool you'll meet stores its graph in ordinary things: a JSON file (Graphify), a SQLite file (CodeGraph), a folder of Markdown files with links (OKF, okf-rs). The graph is the shape of the data, not a product you install.
"More edges = better graph." β The opposite, usually. An edge is a claim; false or noisy claims poison every query that touches them. That's why provenance tags (below) exist, and why messy code produces graphs "like a ball of yarn after a cat attack."
"The folder tree is already a graph." β It is β but of the wrong relationship. Directories encode where files sit, not who talks to whom. orderflow's billing and notifications folders are siblings, yet at runtime they're intimate: billing publishes the very events notifications consumes. Only an edge can say that.
Traversal vs search β the determinism dividend
A map is only useful if following it is cheaper than wandering. Here's the precise sense in which it is.
To answer "who calls decode_jwt?" without a graph, an agent searches: grep for the name (getting every mention β imports, comments, strings), open each candidate file, and read enough surrounding code to judge which hits are real call sites. The cost is proportional to the number and size of files touched, and the result is a judgment call. Run it twice, phrase the question differently, and you may get different answers.
With a graph, the agent traverses: look up the node, follow its incoming calls edges, done. The cost is proportional to the number of edges followed β usually a handful β and the answer is a fact retrieved, not a text interpreted. Same question, every time, same answer. That repeatability is what the course means by deterministic: no similarity thresholds, no ranking, no luck.
This is also the moment to name the complexity honestly. Search costs O(files Γ file size) in tokens per query, paid every query. Traversal costs O(edges followed) β after a one-time extraction cost paid when the graph is built. Extraction moved the expensive work out of the query path. You'll hear this called "compile once, query forever," and it's the same economic trick a database index plays.
A real measured example from the corpus (you'll rebuild it in M09): answering "who calls cmd_generate?" by hand meant opening a 672-line, ~24 KB file and reading enough of it to find the caller β roughly 6,000 tokens. The equivalent graph query returned one line: about 15 tokens. That's ~400Γ for a single question β and it compounds, because the agent never re-opens that file after context compaction either. Per query and per session, traversal's cost stays flat while search's cost grows.
Multi-hop reasoning β where flat retrieval structurally fails
Some questions cannot be answered from any single file, no matter how cleverly you retrieve it. Consider: "What breaks in the API layer if I alter the schema in db/migrations/04_users.sql?" The answer requires chaining facts: the migration alters a table β a model class maps that table β a repository uses the model β three route handlers call the repository. Four files. The connecting facts never co-occur in one place; each hop lives in a different file, and some hops (the modelβtable mapping) are implicit.
A graph answers this by construction: it's a path. Start at the table node, walk maps_to β used_by β called_by edges, and the four-file chain falls out as a sequence of lookups. In orderflow miniature: change decode_jwt's claims format, and the graph walks decode_jwt β verify_token β post_invoice β two hops from an auth helper to a billing route that never mentions JWTs at all.
Hold this example: in M02 you'll see exactly why chunk-based retrieval cannot make this chain (the hops are never co-located in a chunk), and in M10 "multi-hop reasoning failure" becomes the first of two formal triggers that justify adding a structural graph to a production stack.
Communities β subsystems the graph discovers by itself
Before: at a wedding reception with 100 guests, nobody hands you a seating chart of who knows whom. Yet watch the room for ten minutes and the structure is obvious β clusters form: the bride's university friends, the groom's coworkers, the extended family. Nobody assigned those clusters; they emerge from who actually talks to whom.
The pain: codebases have the same latent structure β the billing code all calls other billing code β but nothing in the file system enforces that the folders match the conversation patterns. In old codebases they often don't: the "utils" folder talks to everyone, and a "microservices" architecture turns out to be, as the corpus memorably put it, a secret monolith in a trench coat.
The mapping: community detection is the algorithm that watches the room. It clusters nodes that are densely connected to each other and sparsely connected to everyone else. The clusters it finds are your actual subsystems β the architecture as practiced, not as drawn on the wiki.
Community detection algorithms maximize modularityA score measuring how much denser connections are inside clusters than you'd expect by random chance. High modularity = crisp subsystems. β the degree to which edges concentrate inside clusters rather than between them. Two algorithms matter for this course. Leiden (used by Graphify) is a refinement of the classic Louvain method: fast, and guarantees well-connected communities. ClausetβNewmanβMoore (used by okf-rs) is an agglomerative modularity maximizer β okf-rs's own dogfooding showed it correctly splitting an 18-crate workspace that naive connected-components analysis collapsed into one blob.
That last contrast deserves one more sentence, because it's a common trap: connected components only asks "is there ANY path between these nodes?" β and in real code, everything reaches everything through shared utilities, so you get one giant component. Modularity-based methods ask the sharper question: "are these nodes unusually densely connected?" β which is what separates billing from notifications even though both touch the database pool.
On orderflow, community detection finds three clusters β billing, orders, notifications β matching the service boundaries. On a healthy codebase that's typical. On an unhealthy one, the mismatch IS the finding: a "shared" module that community detection assigns to billing is a module that only billing really uses.
In legacy enterprise monoliths, community detection often clusters unrelated modules together merely because they all reference shared infrastructure β logging singletons, telemetry helpers. The wedding-party equivalent: everyone talks to the bartender, but that doesn't make the whole room one friend group. Treat detected communities as a strong hint, not gospel, and expect noise proportional to how tangled the codebase already is.
God nodes and the price of centrality
Watch the animation's final frames: DatabasePool is reached by billing and orders, while publish is the one that actually touches all three communities. Which of them is "the god node" depends entirely on which measure you ask, and orderflow is small enough that you can check every answer by hand.
| Measure | What it asks | Winner on orderflow |
|---|---|---|
| in-degree | who is depended on most? | shared/auth.py (6 in-edges), then db.py 5, events.py 4 |
| services reaching it | how far does it span? | shared/events.py — the only module all three services touch |
| betweenness | who sits on paths between others? | billing/webhooks.py and invoice.py (4.00); db.py scores 0.00 |
| blast radius | who breaks the most if changed? | shared/db.py — 7 transitive dependents, vs 4 for events and 3 for auth |
Graph tools call the winners god nodes: the main characters of your codebase, identified by centralityA family of graph measures scoring how "important" a node is β by how many edges it has (degree), or how many shortest paths run through it (betweenness). measures. The corpus definition is memorable and accurate: "the classes everything else depends on β we call these God Nodes because if they die, the universe ends."
Why you care, concretely: a god node is where refactoring risk, review scrutiny, and onboarding attention should concentrate. On orderflow that is shared/db.py β not because it is the most connected (it is not) but because it has the largest blast radius: change it and seven functions across billing and orders can change behaviour. A new engineer (or agent) reading the graph learns in seconds what might take weeks of tribal exposure: this is the file you don't touch casually.
shared/db.py scores zero betweenness β nothing routes through the persistence layer, because it calls nothing back; it is a sink. A tool that ranked orderflow by betweenness would put a billing file on top and never mention the database at all. "Most important" is not a property of the graph, it is a property of the question β and any tool handing you a single god-node list has already picked the question for you.Now the honest cost accounting. The strictest centrality measure β betweenness centrality, which counts how many shortest paths pass through each node β costs O(VΓE): nodes times edges. On orderflow (36 nodes, 22 edges) that's nothing. On a real monorepo it's a wall: one documented case in the corpus involved a 450,000-node, 690,000-edge graph β roughly 310 billion operations, single-threaded, because the algorithm doesn't parallelize cleanly. A full rebuild on a 96-core Xeon was killed after 114 minutes without completing, while a different (smaller) repo finished in ~10 minutes on a laptop. Remember this number in M11: it's why "just rebuild the graph on every commit" stops being a plan at scale, and why cheap proxies like plain degree count (which our lab uses) are often the right engineering choice.
Blast radius β impact analysis as a graph walk
The single most valuable query a code graph answers is the one that prevents outages: "if I change this, what could break?" The graph formulation is clean: the blast radius of a node is everything reachable by walking incoming call edges transitively β callers, callers-of-callers, and so on. It's a breadth-first searchBFS: explore a graph level by level from a starting node, using a queue and a visited-set so nothing is processed twice. over the reversed graph, about ten lines of code, and you write it below.
Mature tools score the result rather than just listing it. okf-rs's impact command, for instance, ranks every changed concept between two git refs by three factors: transitive-caller count (the raw radius), public-API membership (external consumers can't be refactored alongside you), and cycle participation (changes inside dependency cycles propagate unpredictably). Its review subcommand turns that into a PR comment with a --fail-on-risk CI gate β impact analysis promoted from a query to a policy.
The corpus example that sells the feature: a developer asks, "if I delete this 'deprecated' method, will the payment gateway explode?" β and the graph shows the 14 hidden dependencies that say Yes. Fourteen facts no one person remembered, retrieved in milliseconds.
Provenance β the honesty layer
One primitive remains, and it's the one that separates trustworthy graphs from confident guessers.
Not every edge is equally certain. When a parser sees decode_jwt(token) inside verify_token's body in the same module, the call edge is a fact. But when code calls execute(...) and three different classes define an execute method, the extractor must guess β or admit it can't. Serious tools label every edge:
| Tag | Meaning | Trust level |
|---|---|---|
EXTRACTED | Parsed directly from source; the relationship is explicit | Ground truth β act on it |
INFERRED | Resolved by heuristic (name matching, proximity, "this variable is named xyzAuth and gets passed to a validator, so they're probably best friends") | Probably right β verify before a destructive action |
AMBIGUOUS | Multiple candidate targets; all emitted, none confirmed | A question, not an answer |
One reviewer in the corpus called this labeling "the whole argument" for graph tools: it is the only approach in the codebase-context landscape that tells you which of its answers are facts and which are guesses. Grep doesn't. Vector search certainly doesn't β every RAG result arrives with the same confident tone regardless of quality (M02 makes this failure visceral). A machine-readable confidence label is what lets an agent act boldly on EXTRACTED edges and double-check INFERRED ones β exactly the policy you'll implement in the lab's final step.
Where do dynamic languages fit? Python and JavaScript β where, as the corpus jokes, types are merely suggestions β force more INFERRED edges because names can't be resolved statically. M03 shows the resolution ladder (heuristics β language servers β honest AMBIGUOUS labels). The discipline stays the same: never emit a guess dressed as a fact.
Walk it, step by step
Build the graph by hand now β nodes, then edges, then the provenance that decides whether an edge can be trusted. Watch how few edges a parser can prove outright.
Code walkthrough β the whole toolkit in 40 lines
Let's build every primitive from this module over orderflow's real graph. The extractor (labs/shared_tools/kg_extract.py) has already produced graph.json β 36 nodes, 22 edges, each edge provenance-tagged. We'll write three functions: a reverse index, a direct-callers query, and a provenance-aware blast radius.
Chunk 1 β the reverse index. Edges are stored sourceβtarget ("who do I call?"), but impact questions run the other way ("who calls me?"). Rather than scanning all edges per query, we index them once by target. This tiny dict IS the difference between search and traversal: after this line, "who calls X" is a hash lookup. Gotcha: filter to calls edges β in richer graphs, walking imports edges into a blast radius wildly overstates impact.
Chunk 2 β direct callers. Resolve a human-friendly short name ("decode_jwt") to node ids, then read the reverse index. Note the deliberate handling of misses: an empty list, not an exception β in M09 the same behavior becomes the graceful "not in graph, try grep" MCP response that lets agents fall back safely.
Chunk 3 β blast radius with a confidence gate. A textbook BFS with a queue and a visited-set (the visited-set matters: real graphs have cycles, and without it you loop forever). The one addition worth staring at: when min_confidence="EXTRACTED", the walk skips unverified edges β computing the radius you can prove versus the radius you suspect. Two numbers, and the gap between them is your uncertainty, made visible.
import json
from collections import defaultdict, deque
graph = json.load(open("graph.json", encoding="utf-8"))
short = lambda node_id: node_id.rsplit(".", 1)[-1]
# ---- Chunk 1: index edges by TARGET, once ----
reverse = defaultdict(list)
for edge in graph["edges"]:
if edge["type"] == "calls":
reverse[edge["target"]].append(edge)
# ---- Chunk 2: direct callers, with graceful misses ----
def callers(name):
targets = [n["id"] for n in graph["nodes"] if short(n["id"]) == name]
found = {short(e["source"]) for t in targets for e in reverse.get(t, [])}
return sorted(found) # unknown name -> [] (never a crash)
# ---- Chunk 3: BFS blast radius with a confidence gate ----
def blast_radius(name, min_confidence=None):
seeds = [n["id"] for n in graph["nodes"] if short(n["id"]) == name]
queue, visited, impacted = deque(seeds), set(seeds), set()
while queue:
for edge in reverse.get(queue.popleft(), []):
if min_confidence == "EXTRACTED" and edge["provenance"] != "EXTRACTED":
continue # skip guesses when proof is demanded
if edge["source"] not in visited:
visited.add(edge["source"])
impacted.add(short(edge["source"]))
queue.append(edge["source"])
return sorted(impacted)
print(callers("decode_jwt")) # ['verify_token']
print(blast_radius("decode_jwt")) # [... 'post_invoice', 'verify_token']
print(blast_radius("execute")) # everything touching the DB pool
print(blast_radius("execute", "EXTRACTED")) # ...the provable subsetimport { readFileSync } from "node:fs";
const graph = JSON.parse(readFileSync("graph.json", "utf-8"));
const short = (id) => id.split(".").at(-1);
// ---- Chunk 1: index edges by TARGET, once ----
const reverse = new Map();
for (const edge of graph.edges) {
if (edge.type !== "calls") continue;
if (!reverse.has(edge.target)) reverse.set(edge.target, []);
reverse.get(edge.target).push(edge);
}
// ---- Chunk 2: direct callers, with graceful misses ----
function callers(name) {
const targets = graph.nodes.filter((n) => short(n.id) === name).map((n) => n.id);
const found = new Set();
for (const t of targets) for (const e of reverse.get(t) ?? []) found.add(short(e.source));
return [...found].sort(); // unknown name -> [] (never a crash)
}
// ---- Chunk 3: BFS blast radius with a confidence gate ----
function blastRadius(name, minConfidence = null) {
const seeds = graph.nodes.filter((n) => short(n.id) === name).map((n) => n.id);
const queue = [...seeds], visited = new Set(seeds), impacted = new Set();
while (queue.length) {
for (const edge of reverse.get(queue.shift()) ?? []) {
if (minConfidence === "EXTRACTED" && edge.provenance !== "EXTRACTED") continue;
if (!visited.has(edge.source)) {
visited.add(edge.source);
impacted.add(short(edge.source));
queue.push(edge.source);
}
}
}
return [...impacted].sort();
}
console.log(callers("decode_jwt")); // ['verify_token']
console.log(blastRadius("decode_jwt")); // [..., 'post_invoice', 'verify_token']
console.log(blastRadius("execute", "EXTRACTED")); // the provable subsetYou built a queryable code intelligence system in ~40 lines: one dict inversion turned stored edges into instant reverse lookups; one BFS turned those lookups into transitive impact analysis; one if statement turned provenance metadata into a trust policy. Graphify, CodeGraph, and okf-rs are these three moves plus better extraction, storage, and serving. The primitives never change.
Hands-on lab β labs/M01-graph-fundamentals
π Get the files: labs/M01-graph-fundamentals on GitHub β or clone the whole course once: git clone https://github.com/varasrinivas/knowledge-graph-course.git
What you'll build: the file above, from a TODO skeleton, verified by automated checks. Time: 30β45 min. Files: starter/graph_basics.py (yours), solution/ (if stuck), expected_output/sample_output.txt.
- Step 1 β Load the graph.
cd labs/M01-graph-fundamentals/starter && python graph_basics.py. The loader runs the extractor for you. Expected:36 nodes, 22 edges loaded. β If you see an import error, run from insidestarter/. - Step 2 β TODO 1: the reverse index. Loop over
graph["edges"], append eachcallsedge under its target. - Step 3 β TODO 2: BFS blast radius. Seed a deque with matching node ids; walk the reverse index with a visited-set.
blast_radius("decode_jwt")must includepost_invoiceβ two hops away. - Step 4 β TODO 3: the confidence gate. Skip non-EXTRACTED edges when demanded; compare both radii for
execute. - Verify.
python graph_basics.py --checkβALL CHECKS PASSED. π
Troubleshooting: empty blast radius almost always means you walked edges forward (callerβcallee) instead of backward. KeyError: 'edges' means the loader was edited β diff against the solution.
Stretch goal: compute each node's in-degree from the reverse index and print the top 3. The winner should be orderflow's god node. You've just implemented (degree) centrality β the cheap cousin of the O(VΓE) betweenness measure from this module.
Knowledge check
execute(...) called, and three classes define an execute method. The honest output is:Module summary
Nodes & typed edges
Code as a social network: entities plus directed, typed, machine-readable claims ("A calls B"). The graph is a shape, not a product.Traversal
Follow edges instead of reading files: O(edges followed) per query after one-time extraction. 6,000 tokens vs 15 for "who calls X".Multi-hop
Impact chains span files that never co-occur in a chunk β paths answer what flat retrieval structurally cannot.Communities
Leiden / CNM clustering finds subsystems from actual call density β architecture as practiced, noise included.God nodes
Risk concentrators β but name the measure first: on orderflow in-degree picksshared/auth.py, reach picks events.py, blast radius picks db.py. Betweenness costs O(VΓE) β the scale wall M11 revisits.Provenance
EXTRACTED / INFERRED / AMBIGUOUS: the only context layer that labels facts vs guesses. Never emit a guess dressed as a fact.What we built on orderflow: a 36-node, 22-edge graph plus reverse index, callers query, and provenance-gated blast radius β the exact primitives every later module refines.
Next β M02: The RAG Baseline and Where It Breaks. Before adopting graphs everywhere, you need the disease in your hands: you'll build a retrieval pipeline over orderflow's docs and watch it confidently return a stale metric definition β nothing crashing, everything wrong.
References
- Course extractor:
labs/shared_tools/kg_extract.pyΒ· ground-truth edges:labs/sample-project/README.md - Leiden algorithm (Traag, Waltman & van Eck) β used by Graphify for community detection
- ClausetβNewmanβMoore modularity clustering β used by okf-rs
graph communities - okf-rs impact analysis (
impact,review --fail-on-risk) β github.com/jyjeanne/okf-rs - Graphify god-node and community reporting β graphify.net Β· GRAPH_REPORT.md format (M04)