The graph, for people who build graphs
This project has been running for eight days. In that time it has become a knowledge graph of 3,038 nodes and 9,591 edges about one millimetre-long animal, built from 274 papers, with a simulation, a genome database and a public site hanging off it. Everything is in git and everything is open. This page is the engineering: what the graph actually is, what we used, what we wrote, and — more usefully — what broke and how we caught it.
What kind of graph it is
It is a property graph of scientific claims, stored as two JSONL files — one node per line, one edge per line — in an ordinary git repository. No database server. The whole thing is about 4 MB and you can download it. That choice is deliberate: a text file in git gives you line-level diffs, blame, and the ability to see when a claim changed and why, which is most of what provenance means in practice.
Four rules do the real work, and they are the part worth stealing:
| Rule | Why |
|---|---|
| Nothing enters without a source and a locator. | A claim cites a page, figure, table, supplementary file or database record — never just a paper. If only an abstract was read, the source says so and the claim's confidence drops. You can always get from an assertion back to the sentence it came from. |
| Contradictions are kept. | When two papers disagree we add both claims and a contradicts edge with a
note. We do not pick a winner and delete the loser. There are 161 of
these. A graph that resolves its conflicts silently is a graph that has lost its evidence. |
| Assumptions are first-class nodes. | Where the simulation needs a number no paper provides, that guess is a node with
evidence: assumed, and the rule that uses it points at it. One command lists every
guess in the system. Nothing is quietly hardcoded. |
| Failures stay in. | Every comparison with data is a test node recording seeds, date and a result
in words. Failing tests are not deleted. Several of our own published claims have been
corrected by later ones through a refines edge, with the original left readable. |
One schema decision changed the whole picture, and it generalises. Species membership started as a relation. That meant 2,477 of the graph's edges pointed at just 92 species nodes, and in a force layout those hubs became the entire image — every claim about Hypsibius exemplaris dragged into one knot, the actual structure invisible behind it.
We made it a node property instead. You can still filter by it; it exerts no pull. The rule we took away: a relation that is really a category should be an attribute. If most of your edges terminate on a handful of nodes, you have probably encoded a label as a link.
The bug that visualisation found and querying did not
We had been querying this graph for a week. Then we rendered it, and someone clicked the biggest
node in the picture. The inspector read
degree 48, degree_in 48, degree_out 0.
Counting that across the whole graph gave this:
| Node type | Outgoing | Incoming |
|---|---|---|
process | 0 | 907 |
species | 0 | 2,492 |
tissue | 6 | 158 |
structure | 224 | 1,931 |
Sixty-seven biological processes, 907 claims pointing at them, and not one relation leaving one. The graph could say that a paper made a claim about moulting. It could not say that moulting follows a stage, requires a gene, or produces a cuticle. All of that knowledge was sitting in claim prose that the schema had no relation to express — so it could not be queried, drawn, or contradicted.
No query we wrote found that, because you do not write a query for a relation you have not noticed is missing. A picture is a different query engine. It is a whole-population view that makes an absence visible as a shape, and it is the strongest argument we have for rendering your data even when you think you already understand it.
Adversarial by default
Science and security share a failure mode: it is easy to confirm what you already believe, and the work of not doing that has to be built into the process rather than left to good intentions. Four rules run on everything here.
| Rule | What it caught |
|---|---|
| Inventory before you ask for more. Before requesting or generating data, list what is already on disk. | We had 17 embryos and had used 2. The missing 15 held the most important result of that day. |
| Sweep every free parameter before believing a trend. Report the plateau, not one setting. | A domain-enrichment result survived a threshold sweep; a "peak time" result turned out to be an artefact of a 12-hour gap in the sampling, visible only when we drew the gaps. |
| Make every check symmetric. If you test whether something grew, test whether it shrank. | A gene "switching on" looked remarkable until the symmetric test showed 2% of the transcriptome does it. The identity was the finding; the statistic was not. |
| A confound test only rules out the confounds you thought of — and your own method is rarely on that list. | A null result we published turned out to be a property of the statistic, not the biology. A different measure on the same data reversed it. Both are in the write-up. |
A war story you will recognise
A dataset we use gives sample times in minutes. We established on day 2 — by matching it against an independent dataset — that those minutes are half of real time. We wrote that into four documents and two analysis scripts.
It never reached the other five scripts. For six days they divided the stored minutes by 60 and labelled the result "hours post laying". Nothing failed, because every statistic involved — rank correlations, permutation tests, embeddings — is invariant under a monotone rescaling of time. Re-running afterwards returned identical correlations, p-values and tree topology. Every number in the results was right. Every number in the discussion was half of what it said, in the graph, in a manuscript, and on this website, where curves were being drawn at half speed against stage markers that were in real hours.
The lesson is not the arithmetic. It is that a correction recorded in prose cannot reach a script. A note can go stale; code cannot. So the fix was three mechanisms and not a better note: one converter everything calls, an assertion inside the loader that refuses to return a series ending before the animal hatches, and a scanner that fails if any file computes hours from minutes by itself.
The scanner found two more tools immediately. Then it missed a browser-side copy of the same bug, because it only read Python — so it reads the web pages now too. And we checked it the only way worth checking a detection: we put the bug back and confirmed the check failed.
What we used, and what we wrote
| Used | For |
|---|---|
| Graphistry / PyGraphistry | GPU rendering of the whole graph, and GFQL for querying it. The explorer on this site is their renderer in an iframe. Their author suggested the embedding and pointed us at UMAP, which became a result. |
| HMMER, MAFFT, IQ-TREE, DIAMOND, BLAST, minimap2, samtools, seqkit | All from version-pinned BioContainers under unprivileged Docker. Which version produced a result is recorded rather than being whatever was on the machine that day. |
| igraph, NumPy, umap-learn, Playwright | Layouts, statistics, embeddings, and headless verification of our own pages — every visualisation on this site is screenshotted and checked by a script before it ships. |
| Written | Does |
|---|---|
tg.py | The graph itself: validate, query, show, list every assumption, export. validate must pass before any commit. |
graphistry_view.py | Nine slices of the graph, exported, laid out and uploaded; writes the embed manifest this site reads. |
connect.py | Finds relations that claim prose asserts and the graph does not carry — the fix for the degree_out 0 problem. |
family_tree.py | Takes a protein family and three genomes, and returns where an unknown gene sits in it, with bootstrap support and the placement read off the tree. |
check_timeaxis.py | The scanner from the war story above. |
supp_scan.py, future_work.py | Finds supplementary data we are missing, and sentences where a paper poses a question — those become nodes, and later work gets checked against them. |
All 101 tools are in the repository. Nothing here is a product and nothing is for sale.
Where code and science actually meet
The honest version: the bottleneck in this field is not measurement, it is that nobody has joined the data up. Most of what we have found came from putting two existing public datasets next to each other and asking a question neither paper asked. The genome was published in 2017. The developmental transcriptome in 2016. The result that a tardigrade builds its synapses last was sitting in both of them for eight years and required no new experiment — only somebody to ask.
That is a software problem more than a biology problem, and it is the reason a project like this can move in a week. If you write code and you have ever wondered whether it is useful anywhere outside your industry: this is where it is useful.
Written for BSides Canberra, September 2026. If you are reading this after finding it another way: the animal is a tardigrade, it is about a millimetre long, it survives being dried to 3% water and being fired into a vacuum, and there is a great deal more of it here.