Graph Hacks 2026 · Track 03: Company Brain · Live demo · Code · Demo video
On 1 March 2018, someone on the IETF TLS mailing list wrote a message about draft-ietf-tls-tls13-24 and mentioned Section 4.2.1. Five people, six messages and three days later, the thread went quiet. Eventually the draft became RFC 8446, and Section 4.2.1 became the place where TLS 1.3 announces its version through a supported_versions extension instead of the version field every earlier TLS used.
If you join a team that ships TLS 1.3 today, you will wonder why it works that way. The reason is in that thread. Finding it takes a person with a good memory or an afternoon. Most companies have this problem in private, in a chat history nobody can search by meaning. We wanted to see whether a graph could do it, and we picked the one place where we could prove it: a mailing list that is completely public.
This is the story of how Ratified follows that one email to the RFC, and the mistakes we made on the way.
Rule one: no link, no answer
Before any code, we set one rule. A company brain that can't be checked is just a confident guesser, so every answer has to end in a link a stranger can open. That rule decided most of the design:
- Answers come from graph traversals, not from a model's memory.
- No model writes the Cypher. A deterministic router picks one hand-written query per question.
- If the traversal returns nothing, the card says "the graph does not know".
Step 1: the clue is a message
Everything starts with a Message. We pulled 3,078 messages from the TLS list, 1,324 of them with their full text, plus 633 threads and 403 people. Each message links to its author and its thread.
The first mistake was assuming "Section 4.2.9" in a message means Section 4.2.9 of the RFC. It doesn't. Section numbers moved between drafts. A message about draft -21 that says "4.2.9" means -21's 4.2.9, which became RFC §4.2.10.
So a message can't point at the RFC directly. It points at a draft revision, and the revision points at the section.
Step 2: the trail runs through draft revisions
We loaded 113 drafts and their 839 revisions from the datatracker, chained with SUPERSEDED, and the 66 RFCs they became. Then came the part that took the longest: ALIGNS_SECTION, which maps each section of each revision to the RFC section it turned into. It matches by heading text, because numbers lie and headings mostly don't. It also records whether the text was still changing, meaning less than 98% similar to what shipped.
Now a citation can be resolved: message → named revision → aligned RFC section. A citation that can't be resolved is dropped rather than guessed.
The whole trail is one query, and the last hop is a variable-length walk:
MATCH p=(dr)-[:SUPERSEDED*0..]->(last:DraftRevision)
MATCH (last)-[:BECAME]->(rfc:RFC)-[:HAS_SECTION]->(s)
RETURN [n IN nodes(p) | n.id] AS chain, rfc.id AS rfc, s.number AS number
For §4.2.1 it returns the chain message → thread → draft -24 → -25 → -26 → -27 → -28 → RFC 8446 → §4.2.1. Every node on that path is a link to the mail archive, the datatracker or the RFC Editor.
Step 3: only count people who were there while it was moving
Our first version had a flaw we only saw by reading the output. A name called "RFC Errata System" showed up as someone who "discussed" §4.2.10 before it shipped. Errata reports name draft -28, but years after the fact.
The fix was a rule in the query: a message counts only if it was written while the revision it names was current, before the next revision or before the RFC's publication month. After that, §4.2.1 had a clean answer: 6 messages, 5 people, March 2018, written while the clause still differed from what shipped.
We also stopped saying people "argued against" it. The edges show a message cited a clause while it was changing, and some of those people were the draft's own editors. The card now says discussed while still changing, and, further down, connected, not caused.
Step 4: now find the humans
Once you have the thread, the next question is who to ask. The card lists the five people from the thread, plus the current chairs and area directors, and drafts a question with the sources attached. It never sends it.
There's also a "who knows what" ranking. We built a CO_DISCUSSED projection (two people who wrote in the same thread) with Cypher at ingest, then ran algo.pageRank and algo.betweenness over it. The draft's editor ranks first on both, with one of the chairs second.
The trap here: PageRank follows edge direction, and a pair of people has no direction. With one edge per pair, the ranking came out in alphabetical order. Write the projection edges in both directions.
Nobody types "section 4.2.10"
People ask "why does early data work like this?" A full-text index on section headings turns that into §4.2.10 Early Data Indication, shows which words matched, and offers the runner-ups as one-click alternatives.
Two things we learned only by testing against the server. Space-separated terms are ANDed, so a casual question with one extra word matched nothing until we OR-ed them. And indexing the section text made results worse, because broad overview sections outranked exact headings. We index headings only.
Every answer shows its homework
Each card has a panel listing the Cypher FalkorDB ran, its parameters, row counts and the database's own run_time_ms. For the §4.2.1 question that's 4 queries, under a millisecond in total on a laptop. We'd copy this into any graph product. It turns "trust me" into "here is the query".
Asking what the graph doesn't know
Ask about IESG ballots or meeting minutes, which we never ingested, and you get a grey card that says the graph doesn't know. That is the feature we trust most.
Did it beat plain search?
We wrote 13 questions and derived the answer keys with a separate parser from the primary record. The graph answers 13/13. A BM25 plus bge-small retriever over the same text gets 1/10 exact and 3/10 in its top 10, with three count questions it can't score. That headline flatters us, so here is the fair version: on the three questions whose evidence sits in text a retriever can see, it's graph 3/3, retrieval 1/3. On revision lists and counts the answer isn't in any text, it's structure, which is the case for a graph but not really a retrieval failure.
The router was tuned on those 13, so we had a second model write 40 new questions without seeing it. First pass, untouched: 30/40, with 5 confidently wrong routes. The scorer first said 34. Reading the rows showed four questions that took "TLS 1.3" as section 1.3. We fixed it, so that set is no longer held out.
Other things that went wrong and got caught: every revision link we rendered was a 404 until a click-check script found it, one person appeared twice under two email addresses, and a first bench run credited retrieval with a count because the digits appeared in a chunk.
Where it stops
The graph can show a thread and a changed clause are connected. It can't show one caused the other. Meetings, GitHub issues and IESG ballots are outside the record. 47 of RFC 8446's 84 sections have a traced discussion, and the rest are answered as partial. We proved the idea on one working group, not on a company's private chat.
Try it
- Live: http://ratified.webbywasp.com. Click Try an example, then open Show how this was computed.
- Code: https://github.com/c-varun14/ratified
-
Run it yourself:
docker compose up --build, then http://127.0.0.1:8000. It rebuilds the graph offline from the committed record, with no keys.
We used AI coding assistants while building this and documented how in AI_USE.md.






Top comments (0)