Cairn

The map

One file. No server.

build/graph.html is a single self-contained file — no server, no CDN, no build step beyond make build. You open it from disk, and it is a full interactive app.

Time-ordered, not force-directed #

x is the creation date, y is a project lane. Research history has a real time axis and a force layout throws it away. It also means node positions are stable between builds, so the map becomes memorable — you learn where things are.

polyid mplastic photopoly May Jun Jul Aug Sep x = created → shape = type fill = status ring = needs you dot = an unanswered note coloured arc = typed relation grey arrow = lineage
Marks sit on top of the glyph, never as changes to it — shape already means type and fill already means status, so a third axis has to be a decoration or the map stops being decodable.

The green/red ratio in a lane is the honest picture of a project, readable from across the room.

The whole graph #

The full Cairn map: nodes in twelve horizontal project lanes positioned left to right by creation date, mostly green for worked with red dead ends scattered through, and long coloured dashed arcs connecting nodes in different lanes.
Twelve project lanes. The long coloured curves are the cross-project edges — the part no single project repo can know about, and the hardest thing to reconstruct later.

Click a node #

The panel renders the body, resolves the commit to a clickable link, lists parents and children you can navigate, and links the markdown source. author is derived from git log --diff-filter=A, never stored in a field.

The node side panel for a dead experiment, showing a red status pill reading dead, the node id, its dates, derived author, repo, a clickable commit, host, and the rendered Claim, What I did and Result sections.
graph.html#2026-07-05-mplastic-cheap-rung-falsified

The dead end, six months later, still saying why

  • Four cheap scoring rungs, each falsified, each with the number that falsified it — including a correlation of 0.80 against the reference implementation, so the implementation was right and the idea was wrong.
  • That distinction is the whole value. It's the difference between “try MM/GBSA again” and “MM/GBSA is anti-correlated here; don't.”
  • This node is why the funnel has two rungs instead of three — and it's where a fifth attempt at that rung got recorded too, so the rung stays closed.

Isolate lineage #

The map collapsed to the eight nodes in one paper's lineage, with the milestone's panel open showing its scope, the contributing work, and the two nodes blocking it.
“8 in lineage of ‘PreKd paper: partition coefficient predic…’”

The view that makes a methods section writable

Click a milestone and the graph collapses to its full ancestry and descent: every experiment that fed the paper, dead ends included. No special edge type is involved — a milestone is just a node with high fan-in.

This one does something better than documentation. The panel names what blocks it: a simple descriptor baseline that beats the model on the compound split, which breaks the paper's central claim as currently written, and a pooling run that hasn't been scored on the split that would settle it.

That threat was found by an external review pass and would otherwise live in a chat log. Here it's a typed edge pointing at a paper, and it will still be pointing at it next month. Same mechanism, second use: annual reviews.

Replay the history #

Because history is an event log, replay is free: drag the “as of” slider back and nodes disappear, statuses revert, and the panel follows. No snapshots, no bookkeeping.

▶ plays it, which is the difference between a control and an animation: nobody drags a slider to watch a shape change, and the animation is what makes five months legible at once.

The same map replayed as of an earlier date: far fewer nodes, most of them blue for running, with only one red dead node.
graph.html?since=2026-07-05 — the graph as it stood, with the statuses of that date

The graph knows what was known when

Today's status next to a three-months-ago graph is how you misread history, so the panel shows the status of the slider's date — not the current one.

Second replay for free: because the whole graph is in git, checking out an old commit and re-rendering replays the research history a different way, with no feature required.

“Needs you” is evaluated against the slider's date too, so “needs you as of June” means what it meant in June.

The live demo #

This is Cairn's own design record — the real subgraph of the project that built it, with real dead ends, which no synthetic example can be. Click a node, drag the slider, press / to search.

23 nodes, one project, 191 KB, entirely self-contained · open full screen →
Generated by make site-demo from a scrubbed, derived export. The authoritative copies live in a private graph; if a claim here disagrees with one there, there wins.

Everything it does #

  • hover a node → a card with its status, date and latest history note
  • click → side panel: rendered body, clickable commit link, DOI links, parent/child links you can navigate, and a link to the node's markdown source
  • isolate lineage → collapse to one node's full ancestry and descent
  • typed relations → grey arrows are lineage; coloured dashed ones carry meaning, each with a one-line reason, backlinked so a contradiction is findable from both ends
  • copy link → deep link to a node (graph.html#<node-id>)
  • needs you → collapse to what is actually waiting: a thread whose status asserts activity but which has gone quiet, or a node carrying a note nobody has answered. Same rule as cairn standup
  • leave a note → type it in the panel and copy the exact cairn note command to your clipboard
  • filter by project / type / status / author, search, pan, wheel-zoom
  • URL params: ?since=YYYY-MM-DD, ?project=<key>, #<node-id>

Keyboard

← ↑ → ↓walk the layout — x is time, y is a project lane
p / cwalk the edges — parents, children
iisolate lineage
nwrite a note
wtoggle needs-you
spaceplay the replay
/search
Escclose

Why the note button copies a command instead of saving. The page is file:// with no server by design, so it cannot write to the repo — and a comment living in one browser's localStorage, invisible from the other machine, is the exact failure this whole project exists to avoid. Two keystrokes at a terminal and the note is in the node body, in git, on both machines, where standup and the next agent session both find it.

Two other ways to read it #

nodes/ is already an Obsidian vault, and nothing had to be built for that. Node bodies refer to each other as [[<node-id>]], which is Obsidian's own wikilink syntax — so pointing Obsidian at the graph gives you backlinks, a local graph view, full-text search and a mobile app, over the same files, in the same git remote, with no export step.

Use Obsidian when you want to write; use the map when you want the time axis, the replay and the lineage view, which Obsidian knows nothing about.

build/graph.md is a Mermaid fallback that renders in GitHub, an editor, or a terminal.