Cairn

The model

Five types. Two kinds of edge.

That's the whole schema, and its smallness is the point: every optional field is a field that stops getting filled in.

Anatomy of a node #

One file per node, in nodes/<id>.md: YAML frontmatter, then markdown prose. Never deleted, never renamed, never moved.

nodes/2026-07-05-mplastic-cheap-rung-falsified.md
---
id: 2026-07-05-mplastic-cheap-rung-falsified          1
type: experiment
title: "Cheap scoring rungs: MM/GBSA, short-MD, RFD3, …"
project: mplastic                                     2
status: dead                                            3
created: 2026-07-05
updated: 2026-07-28
repo: git@github.example/binders.git
commit: 4b3ee22                                       4
host: hpc-cluster
parents: [2026-06-24-mplastic-eabf-verifier]           5
history:                                               6
  - {date: 2026-07-05, status: running, note: four candidate cheap filters}
  - {date: 2026-07-28, status: dead, note: all four falsified …}
---

## Claim
Some cheap score can pre-filter candidates before the
expensive eABF verifier.

## Result
All four falsified. Recorded here so they are not         7
re-attempted.
- **Single-shot MM/GBSA.** Anti-correlated with
  pull-off force …
  • 1  The id is permanent and equals the filename stem. Never renamed, never reused, never deleted.
  • 2  One graph spans all projects. Cross-project edges are the hardest thing to reconstruct later, so they're never sharded away.
  • 3  status is a cache of history[-1]. Hand-editing it is the single most likely way this graph rots, so the validator rejects the mismatch.
  • 4  A SHA, never a branch. Branches get merged and deleted; the validator refuses anything that isn't a SHA.
  • 5  Lineage, not a folder. A cycle-checked DAG.
  • 6  history is an event log — which buys the time axis, the replay slider and the weekly digest for free, with no second thing to maintain.
  • 7  The body is prose. Claim / What I did / Result / Next is a convention, not a requirement.

Seven fields are required — id, type, title, project, status, created, updated — plus at least one history entry. Everything else is optional, and an experiment with no repo, commit or host is not a second-class node: that's what makes a bench experiment or a literature finding validate unchanged.

The five node types #

Shape means type, on the map and everywhere else. It never means anything else — colour is already carrying status, and a third visual axis would make the map undecodable.

experiment a claim direction a question that holds experiments seed a corridor idea, with contacts milestone a paper or deliverable task a commitment with a due date
typestatusesterminalextra fields
experiment planned running worked dead parked worked, dead repo commit host artifacts[]
direction open parked closed closed —
seed seed promoted killed promoted, killed origin contacts[]
milestone planned submitted accepted done accepted, done venue date url
task todo doing done dropped done, dropped due source

dead is not closed

An issue tracker has two states and throws away the distinction that matters here. dead means refuted — and it stays in the graph, findable, with the reason attached.

Two node types wanted extra fields

Milestones reached for repo and artifacts. They were made to use their bodies instead. One field has been added in the project's life — relates, below — and it took a written argument.

A literature finding is an experiment

There is no separate type for one, and reaching for a new type here is a mistake. A literature finding has a claim, a method and a result — the corpus is the instrument. status: dead on a claim the literature refuted means the same thing downstream as a job that failed.

Write the claim the finding settles, not a summary of what you read. “Can a reported final Tg be fitted as Tgp?” with status: dead is a node someone finds by asking that question. “Tg literature review” is not. See starting from a literature review.

Status is an event log #

status and history are the same fact, and storing a fact twice invites drift — so one is derived from the other. history is the source of truth; status is a cache of history[-1].status, kept because it's what you actually grep for and what reads well in a diff.

The validator enforces that they agree. Use cairn new_node --update, which moves status, updated and history atomically.

parked a pause — not terminal planned pre-registered running actually queued worked terminal dead terminal — and the most valuable node here terminal means the thread is finished, so the digest stops nagging about it.
planned is not terminal either — it exists so a pre-registration can be truthful. An experiment created with no status is running, which is what you want when logging work that already happened.

A claim written before the result is a pre-registration; written after, it's a story. That difference is what makes a dead end publishable — and it's the only reason planned is a status instead of a convention.

Two kinds of edge #

Cairn has two edge sets, doing two different jobs, and keeping them separate is the single most load-bearing decision in the schema.

parents is lineage: what this work grew out of. It is a DAG, it is cycle-checked, it drives the layout and the “isolate lineage” view, and it carries no meaning beyond descent — every arrow looks the same.

relates is meaning: typed, backlinked, and deliberately not cycle-checked.

parents — lineage a cycle-checked DAG. every arrow identical, and always forward in time direction experiment milestone time → “what did this grow out of?” relates — meaning typed, backlinked, may point backwards in time, not cycle-checked contradicts resolves the June panel the July panel task “which of these two do I believe?”
Same five nodes, two edge sets. Before relates existed, the July panel contradicting the June one was two identical grey arrows and a sentence of prose that only read correctly from one of the two nodes.

The four relation types

Four, and the bar for a fifth is a real node that needs it. An edge vocabulary nobody can keep in their head is one that gets used inconsistently, and an inconsistently-typed edge is worse than an untyped one.

supports strengthens that result, ideally by an independent route. Reads back as: supported by.
contradicts these two are in tension; a reader must see both. Reads back as: contradicted by.
supersedes this replaces that as the approach or answer to use. Reads back as: superseded by.
resolves this closes that open question, threat or gap. Reads back as: resolved by.

The relation hues sit deliberately off the status palette (green / red / blue / amber), so a typed edge never reads as a status.

Why it had to exist

Two scramble panels, same control, opposite verdict:

nodes/2026-07-31-mplastic-r1-scramble-panel.md
relates:
  - {to: 2026-06-25-mplastic-scramble-control,
     type: contradicts,
     note: "same control, opposite verdict: 3.1 sigma for order on
       crystalline PET, composition-only on the amorphous film.
       Surface disorder is the likely variable"}

Rules that differ from parents

  • Not cycle-checked. Two results really can contradict each other, and that is a fact about the research rather than a defect.
  • May point backwards in time. A result from August can contradict one from June.
  • Backlinked. The renderer shows the inverse phrasing on the target, so a contradiction is equally findable from either node — otherwise you only find it from whichever node happened to be written second, which is the node the reader hasn't got to yet.
  • A pair joined by both lineage and a relation draws one arrow, styled by the relation. The typed edge is the more specific claim.
  • note is optional but warned about. A typed edge with no reason is an arrow nobody can act on.

Adding an edge is not a state transition, so it appends no history entry and needs no status change:

cairn new_node --update 2026-07-31-mplastic-r1-scramble-panel \
  --relate "contradicts:2026-06-25-mplastic-scramble-control:composition, not order"

In a node body, [[2026-07-01-polyid-network-ablation]] refers to another node and renders as a link titled with that node's title. validate warns if the target doesn't exist.

Use it for the connections that aren't lineage and aren't one of the four relation types — “this builds on the wD-MPNN core”, “see the schema task first”. A cross-project dependency goes here, in the body, rather than in parents.

It is also Obsidian's own syntax, which is why nodes/ is already an Obsidian vault with backlinks and full-text search over the same files, with no export step. See the map.

Where a human reacts #

The agent writes the record; you read it later and have a reaction — a doubt, a correction, “the number here is wrong”, “this is the one that matters”. That goes under a ## Notes heading in the body, one dated bullet per note:

## Notes

- **2026-08-26** (jlaw) the spread looks bimodal — does it track the seed?

Written by cairn note <id> "…"; read back by standup, digest, build/report.md and the map. Three things about it are deliberate:

  • It is a body edit, not a status change. status, updated and history don't move, because a note isn't a claim about the state of the work. If it moved updated, the replay slider would show a change that never happened.
  • No new field, against the resist-new-fields rule. The line carries its own date, so it stays findable by reading the body — exactly how ## Next is already read.
  • “Unanswered” is derived. A note dated at or after the node's last history entry is one no later work has responded to; once the thread moves, it stops being reported. That's what keeps it off a nag list.

It works in the other direction too, which is the answer to “can the agent pick up on comments I left?” Yes — notes are in the node body, so they're in standup's resume packet and in the session-start context. A note is how you hand an instruction to next week's session without writing a prompt.

Papers, and the DOI as the join key #

Any node may cite papers by bare DOI in refs:. Each cited paper gets a page in papers/ with CrossRef-verified metadata:

doi: 10.1021/acs.jced.3c00123
title: Solvent effects on nucleophilic substitution rate constants
authors: [R. Okonkwo, M. Feld]
year: 2023
venue: J. Chem. Eng. Data
bibkey: okonkwo2023solvent          # what references.bib calls it
md: hpc-cluster:/projects/…/refs/10.1021__acs.jced.3c00123.md

The DOI is the only identifier common to Zotero, the references.bib Overleaf compiles, the converted full text on the compute host, and a node's refs:. bibkey and md are what make that join usable: one records what the citation is called, the other where the readable text lives. Full text never enters the repo.

Don't hand-write these — cairn add_paper <doi> fetches CrossRef and refuses to write a page it couldn't verify. A DOI recalled rather than resolved is worse than no DOI.

Paper pages are validated only for having a doi: — they're a lookup table, not part of the DAG. The renderer builds backlinks both ways, so a paper shows every node citing it, and a node shows what else cites the same paper.

What the validator checks #

It runs on every commit via a pre-commit hook. Silent schema drift is how this rots, so errors block the commit.

Errors

  • id matches the filename stem and the YYYY-MM-DD-slug format; no duplicates
  • type is known; status is valid for that type
  • required fields present; dates parse; updated >= created
  • history non-empty, chronological, and its last entry agrees with status
  • parents all resolve, nothing is its own parent, no cycles
  • relates entries have a known type and a resolving, non-self to — cycles are legal here
  • history entries carry only date / status / note — an extra key means an unquoted comma split the note and turned its tail into a bogus key
  • refs are bare DOIs; commit is a SHA; artifacts are host:/path

That comma check exists because it actually happened: note: past 1,000 systems silently became note: past 1 plus a junk key 000 systems, across 17 of the first 55 backfilled nodes.

Warnings — worth reading, but they don't block

  • an unknown frontmatter field — schema drift; add it properly or drop it
  • a relates entry with no note explaining why
  • a ref with no page in papers/
  • an open task with no due — it will never surface in a digest, which makes it invisible rather than pending
  • an id date prefix disagreeing with created