Mechanism 3 of 4 · The living graph

The map is drawn by the agent that reads the code.

The absence this makes loud: a file the map has never seen.

A code map that goes quietly out of date is worse than no map, because confidently-wrong structure is exactly what an agent will act on. So this one records freshness as a first-class state, and will tell you the difference between a file that went stale and a file it has never heard of.

Who builds it

Your coding agent already read the file. Graphban never has to.

The graph is not built by a parser Graphban runs over a checkout it doesn't have. Your external coding agent is the producer — it has the real repository in context, so it is the source of truth, and it calls describe_code as a byproduct of work it was doing anyway.

Graphban's own connected model is the consumer: semantic search over node summaries, neighbourhood reads, structural queries, and natural-language questions about the codebase answered from the graph without a checkout. Nodes carry a path, a kind (module · file · symbol · doc · config), a one-paragraph summary that gets embedded, a content hash, and a freshness flag. Edges are directed and typed — imports · calls · owns · tested_by · references — and an edge may point at a file nobody has described yet.

Docs and config are graph kinds too. Work that touches AGENTS.md or a deploy template has a blast radius, and a map that only knows about source files can't show it.

The key

Three channels that never borrow each other's meaning.

Everything you'll see on the map encodes exactly one of three things. Keeping them separate is what makes a glowing node mean one unambiguous thing.

node fill
what kind of thing
module
file · symbol
doc
config
edge stroke
what kind of relation
imports
calls
tested_by
references
cloud
whose agent holds it
you
predicted area
another person
contention

Hue is spent entirely on kind. Presence gets shape and opacity instead — which is why a held node still shows its own colour and freshness underneath, and the cloud never tints the node itself.

Staying true

The health check retires nothing. It is a read.

Re-describing a directory with prune=true does not delete the nodes it didn't see. It marks them stale. A partial describe should never destroy history — and a node that vanishes takes the meaning of its edges with it.

Describing code with prune marks unseen nodes stale rather than deleting them; the health endpoint reports fresh, stale, and never-described as three distinct states. describe_code(prune=true) the external agent is the producer unseen → stale, kept delete unseen GET /code/health fresh stale ever_described: false never described is not stale — and it is never printed as "clean" no sweeper, no periodic job. It retires nothing. It is a read.
The health check has no side effects, and it separates "went stale" from "never existed to the graph" — the second is the absence that reads clean everywhere else. A periodic job producing a report nobody reads is worse than a number you can ask for.

It is also honest about its own limits. With no checkout, the server cannot distinguish a deleted file from one nobody has described — so it only flags what is decidable from the path string, and refuses to guess further. And when nothing has ever been described, it says so explicitly rather than reporting zero stale nodes and looking healthy.

Queries

Inbound edges answer "what breaks if I change this".

hubs

Ranks by inbound degree. A file that imports forty things must not outrank one that forty things import — the first is cheap to change, the second is your blast radius.

components

Connected groups, largest first, each with an anchor to name it. This is what the galaxy view collapses to when a map exceeds the detail budget — when it collapses at all.

path

Shortest route between two files, walked undirected, with each hop reporting which way the edge actually points.

All three are deterministic and read-only. An unknown edge type is refused, not silently narrowed — quietly dropping a filter you asked for returns a smaller answer that looks like a complete one.

Scale

The galaxy view refuses to collapse what it cannot explain.

Past a certain size a force-directed graph stops being a picture and starts being a hairball. The usual answer is to draw the same thing slower. Above its detail budget Graphban instead changes what it draws: connected components collapse into super-nodes, each labelled by its highest-degree member, and clicking one enters it with a breadcrumb back.

Search still sees through it. Pressing / matches across collapsed components and enters the one holding the hit — a find that can't see what the view is hiding is a find that reports zero results for a node sitting right there. And zero genuine hits dims the graph and shows a literal 0, rather than leaving the map looking like nobody searched.

Collapsing on component count produces one huge super-node beside five specks; collapsing only when the distribution justifies it leaves the flat graph visible instead. collapse when count > 1 146 nodes 3 · 1 · 1 · 1 · 1 six components, so it collapses — and explains nothing collapse when the shape justifies it largest holds 95% → stay flat the honest mess is the more useful answer
A summary that hides the structure while claiming to summarise it is worse than no summary. The guard checks the distribution, not the count: it refuses to collapse when the largest component holds more than 60% of the graph.

The numbers above are real. Running components() against this repo's own live graph during the acceptance walk returned [146, 3, 1, 1, 1, 1]95% of the graph in a single component, because almost everything is reachable from the models module. Six components clears a "more than one component" test easily, so the original guard would have rendered one enormous dot beside five specks and called it a summary.

The gap was in the test data, not the reasoning

That guard had tests, and they passed. They used ten equal components of ninety nodes each — the friendly case, invented rather than measured. The check was green while being wrong about the only shape of graph anyone would ever point it at. It took deploying it and reading the live map to find, and the regression test now uses the measured shape instead of a convenient one.

The rest of the view is ordinary care taken seriously: layout runs in a worker so the main thread stays responsive, positions are computed from the unfiltered edge set so toggling a filter never reshuffles the map underneath you, labels fade in by level of detail, and the whole thing is keyboard-navigable with one tab stop into the canvas and arrow keys between nodes.

Presence

A node glows because an agent holds a live lease — never because an item declared an intention.

The map shows you where your fleet actually is right now. The distinction between a lease and a declaration is the entire design, because only one of them can expire.

A declared intention keeps a node glowing after its agent dies, whereas a lease-backed cloud fades to nothing exactly at its expiry. an item declares an intention intent agent last seen 2,333s ago — still glowing a declaration has no expiry an agent holds a lease expires_at expires_at the glow is a function of expires_at and the clock — nothing has to notice the agent died.
Presence decays by construction. The 2,333-second example is real, from our own instance — it's the one lease that doesn't expire yet, and it's on the known-gaps list.

Presence is a surveillance surface with a person's name attached, so it is reachable only with a signed-in session — never with an agent key. And it uses a deliberately stricter path matcher than the collision divvy does: over-matching is safe when you're deciding what to block (you over-block, you never collide) and a lie when you're drawing a picture of where people are.

Contention

Overlap is normal. Overlap across two people is an alarm.

one person normal — no ring
two people, apart normal — no ring
two people, same file contention ring

The divvy exists so the third picture cannot happen. So when it does, one of exactly three things is wrong: a lease lapsed in a race, the path matcher has a gap, or somebody claimed work outside the divvy. When the glow is wrong, the fleet is wrong — which is why this is drawn as an alarm and not as a busy-looking file.

Off the map

15 of 100 touchpoints on our own backlog resolve to no node.

They are listed, not dropped. An area that matches nothing on the map goes into a tray with its holder and a reason — undescribed or stale — because a presence payload that silently omits what it couldn't resolve is an absence reading as a clean result.

Areas that resolve to no graph node are collected in an off-map tray with their reason and holder, rather than being discarded. the map not dropped off the map docs/deploy.md — undescribed AGENTS.md — undescribed .claude/settings.json — stale 15 of 100 touchpoints the count doubles as a visible measure of how much of the repo nobody has described yet.
What the map cannot place is listed, not dropped. Our own coverage is about 30% — 123 described nodes against 401 source files — and the tray is how you find that out without going looking for it.
← previousThe fleet All four mechanisms ↑ next →Memory & the learning loop

What's new

Every entry is work that merged. The full history is on GitHub.